Просмотр исходного кода

Use host's DNS resolver from /etc/resolv.conf
* DNS resolver is used for UDP transparent DNS forwarding
* Monitors for changes to resolv.conf
* Supports config param for platforms without resolv.conf

Rod Hynes 9 лет назад
Родитель
Сommit
a82c412834
5 измененных файлов с 228 добавлено и 31 удалено
  1. 14 12
      psiphon/server/config.go
  2. 195 0
      psiphon/server/dns.go
  3. 7 0
      psiphon/server/services.go
  4. 3 16
      psiphon/server/udp.go
  5. 9 3
      psiphon/utils.go

+ 14 - 12
psiphon/server/config.go

@@ -194,13 +194,15 @@ type Config struct {
 	// a udpgw server which clients may be port forwarding to. When
 	// specified, these TCP port forwards are intercepted and handled
 	// directly by this server, which parses the SSH channel using the
-	// udpgw protocol.
+	// udpgw protocol. Handling includes udpgw transparent DNS: tunneled
+	// UDP DNS packets are rerouted to the host's DNS server.
 	UDPInterceptUdpgwServerAddress string
 
-	// DNSServerAddress specifies the network address of a DNS server
-	// to which DNS UDP packets will be forwarded to. When set, any
-	// tunneled DNS UDP packets will be re-routed to this destination.
-	UDPForwardDNSServerAddress string
+	// DNSResolverIPAddress specifies the IP address of a DNS server
+	// to be used when "/etc/resolv.conf" doesn't exist or fails to
+	// parse. When blank, "/etc/resolv.conf" must contain a usable
+	// "nameserver" entry.
+	DNSResolverIPAddress string
 
 	// LoadMonitorPeriodSeconds indicates how frequently to log server
 	// load information (number of connected clients per tunnel protocol,
@@ -299,18 +301,18 @@ func LoadConfig(configJSON []byte) (*Config, error) {
 		return err
 	}
 
-	if config.UDPForwardDNSServerAddress != "" {
-		if err := validateNetworkAddress(config.UDPForwardDNSServerAddress); err != nil {
-			return nil, fmt.Errorf("UDPForwardDNSServerAddress is invalid: %s", err)
-		}
-	}
-
 	if config.UDPInterceptUdpgwServerAddress != "" {
 		if err := validateNetworkAddress(config.UDPInterceptUdpgwServerAddress); err != nil {
 			return nil, fmt.Errorf("UDPInterceptUdpgwServerAddress is invalid: %s", err)
 		}
 	}
 
+	if config.DNSResolverIPAddress != "" {
+		if net.ParseIP(config.DNSResolverIPAddress) == nil {
+			return nil, fmt.Errorf("DNSResolverIPAddress is invalid")
+		}
+	}
+
 	return &config, nil
 }
 
@@ -479,7 +481,7 @@ func GenerateConfig(params *GenerateConfigParams) ([]byte, []byte, []byte, error
 		SSHPassword:                    sshPassword,
 		ObfuscatedSSHKey:               obfuscatedSSHKey,
 		TunnelProtocolPorts:            params.TunnelProtocolPorts,
-		UDPForwardDNSServerAddress:     "8.8.8.8:53",
+		DNSResolverIPAddress:           "8.8.8.8",
 		UDPInterceptUdpgwServerAddress: "127.0.0.1:7300",
 		MeekCookieEncryptionPrivateKey: meekCookieEncryptionPrivateKey,
 		MeekObfuscatedKey:              meekObfuscatedKey,

+ 195 - 0
psiphon/server/dns.go

@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2016, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package server
+
+import (
+	"bufio"
+	"errors"
+	"net"
+	"os"
+	"strings"
+	"sync/atomic"
+	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
+)
+
+const (
+	DNS_SYSTEM_CONFIG_FILENAME      = "/etc/resolv.conf"
+	DNS_SYSTEM_CONFIG_RELOAD_PERIOD = 5 * time.Second
+	DNS_RESOLVER_PORT               = 53
+)
+
+// DNSResolver maintains a fresh DNS resolver value, monitoring
+// "/etc/resolv.conf" on platforms where it is available; and
+// otherwise using a default value.
+type DNSResolver struct {
+	psiphon.ReloadableFile
+	lastReloadTime int64
+	isReloading    int32
+	resolver       net.IP
+}
+
+// NewDNSResolver initializes a new DNSResolver, loading it with
+// a fresh resolver value. The load must succeed, so either
+// "/etc/resolv.conf" must contain a valid "nameserver" line with
+// a DNS server IP address, or a valid "defaultResolver" default
+// value must be provided.
+// On systems without "/etc/resolv.conf", "defaultResolver" is
+// required.
+//
+// The resolver is considered stale and reloaded if last checked
+// more than 5 seconds before the last Get(), which is similar to
+// frequencies in other implementations:
+//
+// - https://golang.org/src/net/dnsclient_unix.go,
+//   resolverConfig.tryUpdate: 5 seconds
+//
+// - https://github.com/ambrop72/badvpn/blob/master/udpgw/udpgw.c,
+//   maybe_update_dns: 2 seconds
+//
+func NewDNSResolver(defaultResolver string) (*DNSResolver, error) {
+
+	dns := &DNSResolver{
+		lastReloadTime: time.Now().Unix(),
+	}
+
+	dns.ReloadableFile = psiphon.NewReloadableFile(
+		DNS_SYSTEM_CONFIG_FILENAME,
+		func(filename string) error {
+
+			resolver, err := parseResolveConf(filename)
+			if err != nil {
+				// On error, state remains the same
+				return psiphon.ContextError(err)
+			}
+
+			dns.resolver = resolver
+
+			log.WithContextFields(
+				LogFields{
+					"resolver": resolver.String(),
+				}).Debug("loaded system DNS resolver")
+
+			return nil
+		})
+
+	_, err := dns.Reload()
+	if err != nil {
+		if defaultResolver == "" {
+			return nil, psiphon.ContextError(err)
+		}
+
+		log.WithContextFields(
+			LogFields{"err": err}).Info(
+			"failed to load system DNS resolver; using default")
+
+		resolver, err := parseResolver(defaultResolver)
+		if err != nil {
+			return nil, psiphon.ContextError(err)
+		}
+
+		dns.resolver = resolver
+	}
+
+	return dns, nil
+}
+
+// Get returns the cached resolver, first updating the cached
+// value if it's stale. If reloading fails, the previous value
+// is used.
+func (dns *DNSResolver) Get() net.IP {
+
+	// Every UDP DNS port forward frequently calls Get(), so this code
+	// is intended to minimize blocking. Most callers will hit just the
+	// atomic.LoadInt64 reload time check and the RLock (an atomic.AddInt32
+	// when no write lock is pending). An atomic.CompareAndSwapInt32 is
+	// used to ensure only one goroutine enters Reload() and blocks on
+	// its write lock. Finally, since since psiphon.ReloadableFile.Reload
+	// checks whether the underlying file has changed _before_ aquiring a
+	// write lock, we only incur write lock blocking when "/etc/resolv.conf"
+	// has actually changed.
+
+	lastReloadTime := atomic.LoadInt64(&dns.lastReloadTime)
+	stale := time.Unix(lastReloadTime, 0).Add(DNS_SYSTEM_CONFIG_RELOAD_PERIOD).Before(time.Now())
+
+	if stale {
+
+		isReloader := atomic.CompareAndSwapInt32(&dns.isReloading, 0, 1)
+
+		if isReloader {
+
+			// Unconditionally set last reload time. Even on failure only
+			// want to retry after another DNS_SYSTEM_CONFIG_RELOAD_PERIOD.
+			atomic.StoreInt64(&dns.lastReloadTime, time.Now().Unix())
+
+			_, err := dns.Reload()
+			if err != nil {
+				log.WithContextFields(
+					LogFields{"err": err}).Info(
+					"failed to reload system DNS resolver")
+			}
+
+			atomic.StoreInt32(&dns.isReloading, 0)
+		}
+	}
+
+	dns.ReloadableFile.RLock()
+	defer dns.ReloadableFile.RUnlock()
+
+	return dns.resolver
+}
+
+func parseResolveConf(filename string) (net.IP, error) {
+	file, err := os.Open(filename)
+	if err != nil {
+		return nil, psiphon.ContextError(err)
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
+			continue
+		}
+		fields := strings.Fields(line)
+		if len(fields) == 2 && fields[0] == "nameserver" {
+			// TODO: parseResolverAddress will fail when the nameserver
+			// is not an IP address. It may be a domain name. To support
+			// this case, should proceed to the next "nameserver" line.
+			return parseResolver(fields[1])
+		}
+	}
+	if err := scanner.Err(); err != nil {
+		return nil, psiphon.ContextError(err)
+	}
+	return nil, psiphon.ContextError(errors.New("nameserver not found"))
+}
+
+func parseResolver(resolver string) (net.IP, error) {
+
+	ipAddress := net.ParseIP(resolver)
+	if ipAddress == nil {
+		return nil, psiphon.ContextError(errors.New("invalid IP address"))
+	}
+
+	return ipAddress, nil
+}

+ 7 - 0
psiphon/server/services.go

@@ -181,6 +181,7 @@ type SupportServices struct {
 	TrafficRulesSet *TrafficRulesSet
 	PsinetDatabase  *psinet.Database
 	GeoIPService    *GeoIPService
+	DNSResolver     *DNSResolver
 }
 
 // NewSupportServices initializes a new SupportServices.
@@ -201,11 +202,17 @@ func NewSupportServices(config *Config) (*SupportServices, error) {
 		return nil, psiphon.ContextError(err)
 	}
 
+	dnsResolver, err := NewDNSResolver(config.DNSResolverIPAddress)
+	if err != nil {
+		return nil, psiphon.ContextError(err)
+	}
+
 	return &SupportServices{
 		Config:          config,
 		TrafficRulesSet: trafficRulesSet,
 		PsinetDatabase:  psinetDatabase,
 		GeoIPService:    geoIPService,
+		DNSResolver:     dnsResolver,
 	}, nil
 }
 

+ 3 - 16
psiphon/server/udp.go

@@ -25,7 +25,6 @@ import (
 	"fmt"
 	"io"
 	"net"
-	"strconv"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -85,8 +84,8 @@ type udpPortForwardMultiplexer struct {
 	sshChannel        ssh.Channel
 	portForwardsMutex sync.Mutex
 	portForwards      map[uint16]*udpPortForward
-	relayWaitGroup    *sync.WaitGroup
 	portForwardLRU    *psiphon.LRUConns
+	relayWaitGroup    *sync.WaitGroup
 }
 
 func (mux *udpPortForwardMultiplexer) run() {
@@ -178,7 +177,8 @@ func (mux *udpPortForwardMultiplexer) run() {
 
 			// Transparent DNS forwarding
 			if message.forwardDNS {
-				dialIP, dialPort = mux.transparentDNSAddress(dialIP, dialPort)
+				dialIP = mux.sshClient.sshServer.support.DNSResolver.Get()
+				dialPort = DNS_RESOLVER_PORT
 			}
 
 			log.WithContextFields(
@@ -253,19 +253,6 @@ func (mux *udpPortForwardMultiplexer) run() {
 	mux.relayWaitGroup.Wait()
 }
 
-func (mux *udpPortForwardMultiplexer) transparentDNSAddress(
-	dialIP net.IP, dialPort int) (net.IP, int) {
-
-	if mux.sshClient.sshServer.support.Config.UDPForwardDNSServerAddress != "" {
-		// Note: UDPForwardDNSServerAddress is validated in LoadConfig
-		host, portStr, _ := net.SplitHostPort(
-			mux.sshClient.sshServer.support.Config.UDPForwardDNSServerAddress)
-		dialIP = net.ParseIP(host)
-		dialPort, _ = strconv.Atoi(portStr)
-	}
-	return dialIP, dialPort
-}
-
 func (mux *udpPortForwardMultiplexer) removePortForward(connID uint16) {
 	mux.portForwardsMutex.Lock()
 	delete(mux.portForwards, connID)

+ 9 - 3
psiphon/utils.go

@@ -362,14 +362,15 @@ func (reloadable *ReloadableFile) WillReload() bool {
 // All data structure readers should be blocked by the ReloadableFile mutex.
 func (reloadable *ReloadableFile) Reload() (bool, error) {
 
-	reloadable.Lock()
-	defer reloadable.Unlock()
-
 	if !reloadable.WillReload() {
 		return false, nil
 	}
 
+	// Check whether the file has changed _before_ blocking readers
+
+	reloadable.RLock()
 	changedFileInfo, err := IsFileChanged(reloadable.fileName, reloadable.fileInfo)
+	reloadable.RUnlock()
 	if err != nil {
 		return false, ContextError(err)
 	}
@@ -378,6 +379,11 @@ func (reloadable *ReloadableFile) Reload() (bool, error) {
 		return false, nil
 	}
 
+	// ...now block readers
+
+	reloadable.Lock()
+	defer reloadable.Unlock()
+
 	err = reloadable.reloadAction(reloadable.fileName)
 	if err != nil {
 		return false, ContextError(err)