diff --git a/dnscrypt-proxy/config.go b/dnscrypt-proxy/config.go index 49dc6d07cd..757e14189f 100644 --- a/dnscrypt-proxy/config.go +++ b/dnscrypt-proxy/config.go @@ -18,8 +18,7 @@ import ( ) const ( - MaxTimeout = 3600 - DefaultNetprobeAddress = "9.9.9.9:53" + MaxTimeout = time.Hour ) type Config struct { @@ -95,7 +94,8 @@ type Config struct { TLSCipherSuite []uint16 `toml:"tls_cipher_suite"` TLSPreferRSA bool `toml:"tls_prefer_rsa"` TLSKeyLogFile string `toml:"tls_key_log_file"` - NetprobeAddress string `toml:"netprobe_address"` + NetprobeAddresses []string `toml:"netprobe_addresses"` + NetprobeAddressLegacy string `toml:"netprobe_address"` NetprobeTimeout int `toml:"netprobe_timeout"` OfflineMode bool `toml:"offline_mode"` HTTPProxyURL string `toml:"http_proxy"` @@ -777,3 +777,11 @@ func isIPAndPort(addrStr string) error { } return nil } + +func GetDefaultNetprobeAddresses() ([]string) { + return []string{ + "[2620:fe::fe]:53", + "9.9.9.9:53", + } +} + diff --git a/dnscrypt-proxy/config_loader.go b/dnscrypt-proxy/config_loader.go index 60e76746ee..b19062cb43 100644 --- a/dnscrypt-proxy/config_loader.go +++ b/dnscrypt-proxy/config_loader.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "flag" "fmt" @@ -8,12 +9,14 @@ import ( "net/http" "net/url" "os" + "slices" "strconv" "strings" "time" "github.com/jedisct1/dlog" stamps "github.com/jedisct1/go-dnsstamps" + "github.com/projectdiscovery/utils/slice" netproxy "golang.org/x/net/proxy" ) @@ -463,23 +466,56 @@ func configureSourceRestrictions(proxy *Proxy, flags *ConfigFlags, config *Confi proxy.SourceODoH = config.SourceODoH } -// determineNetprobeAddress - Determines the address to use for network probing -func determineNetprobeAddress(flags *ConfigFlags, config *Config) (string, int) { - netprobeTimeout := config.NetprobeTimeout +// determineNetprobeAddresses - Determines the addresses to use for network probing +func determineNetprobeAddresses( + flags *ConfigFlags, + config *Config, +) ([]string, time.Duration) { + netprobeTimeout := time.Duration(config.NetprobeTimeout) * time.Second flag.Visit(func(commandLineFlag *flag.Flag) { if commandLineFlag.Name == "netprobe-timeout" && flags.NetprobeTimeoutOverride != nil { - netprobeTimeout = *flags.NetprobeTimeoutOverride + netprobeTimeout = time.Duration(*flags.NetprobeTimeoutOverride) * time.Second } }) - netprobeAddress := DefaultNetprobeAddress - if len(config.NetprobeAddress) > 0 { - netprobeAddress = config.NetprobeAddress - } else if len(config.BootstrapResolvers) > 0 { - netprobeAddress = config.BootstrapResolvers[0] + if netprobeTimeout < 0 || netprobeTimeout > MaxTimeout { + netprobeTimeout = MaxTimeout } - return netprobeAddress, netprobeTimeout + netprobeAddresses := slices.Clone(config.NetprobeAddresses) + + if len(config.NetprobeAddressLegacy) > 0 { + if len(netprobeAddresses) <= 0 { + dlog.Warn( + "netprobe_address was changed to a netprobe_addresses, a list -- Please update your configuration", + ) + netprobeAddresses = append(netprobeAddresses, config.NetprobeAddressLegacy) + } else { + dlog.Fatal( + "Can't use a list of netprobe_addresses at the same time as a netprobe_address", + ) + + } + } + + if len(netprobeAddresses) <= 0 && len(config.BootstrapResolvers) > 0 { + netprobeAddresses = append( + netprobeAddresses, + config.BootstrapResolvers..., + ) + } + + if len(netprobeAddresses) <= 0 { + netprobeAddresses = append( + netprobeAddresses, + GetDefaultNetprobeAddresses()..., + ) + } + + netprobeAddresses = sliceutil.Dedupe(netprobeAddresses) + + + return netprobeAddresses, netprobeTimeout } // initializeNetworking - Initializes networking @@ -489,8 +525,15 @@ func initializeNetworking(proxy *Proxy, flags *ConfigFlags, config *Config) erro return nil } - netprobeAddress, netprobeTimeout := determineNetprobeAddress(flags, config) - if err := NetProbe(proxy, netprobeAddress, netprobeTimeout); err != nil { + netprobeAddresses, netprobeTimeout := determineNetprobeAddresses(flags, config) + + ctx, cancel := context.WithTimeout(context.Background(), netprobeTimeout) + defer cancel() + if err := NetProbe( + proxy, + netprobeAddresses, + ctx, + ); err != nil { return err } diff --git a/dnscrypt-proxy/example-dnscrypt-proxy.toml b/dnscrypt-proxy/example-dnscrypt-proxy.toml index b9e4b73cdc..30cc546b07 100644 --- a/dnscrypt-proxy/example-dnscrypt-proxy.toml +++ b/dnscrypt-proxy/example-dnscrypt-proxy.toml @@ -373,7 +373,7 @@ ignore_system_dns = true netprobe_timeout = 60 -## Address and port to try initializing a connection to, just to check +## Address and port pairs to try initializing a connection to, just to check ## if the network is up. It can be any address and any port, even if ## there is nothing answering these on the other side. Just don't use ## a local address, as the goal is to check for Internet connectivity. @@ -382,7 +382,11 @@ netprobe_timeout = 60 ## On other operating systems, the connection will be initialized ## but nothing will be sent at all. -netprobe_address = '9.9.9.9:53' + +netprobe_addresses = [ + '[2620:fe::fe]:53', + '9.9.9.9:53', +] ## Offline mode - Do not use any remote encrypted servers. diff --git a/dnscrypt-proxy/netprobe.go b/dnscrypt-proxy/netprobe.go new file mode 100644 index 0000000000..7e8cb19249 --- /dev/null +++ b/dnscrypt-proxy/netprobe.go @@ -0,0 +1,220 @@ +package main + +import ( + "context" + "errors" + "fmt" + "net" + "runtime" + "time" + + "github.com/jedisct1/dlog" +) + +// determineNetprobeIntervals - Determines an interval that should finish at least margin before deadline +// +// mostly useful with a context.Context deadline +func determineNetprobeIntervals( + ideal time.Duration, + deadline time.Time, + margin time.Duration, +) (interval time.Duration, count int, ok bool) { + remaining := time.Until(deadline) - margin + + if ideal <= 0 || remaining <= 0 { + return 0, 0, false + } + + // Smallest number of intervals that does not require + // an interval larger than the ideal. + count = int((remaining + ideal - 1) / ideal) + + interval = remaining / time.Duration(count) + + if interval <= 0 { + return 0, 0, false + } + + return interval, count, true +} + +func NetProbe( + proxy *Proxy, + hosts_port []string, + ctx context.Context, +) error { + if len(hosts_port) == 0 || ctx.Err() != nil { + return nil + } + if captivePortalHandler, err := ColdStart(proxy); err == nil { + if captivePortalHandler != nil { + defer captivePortalHandler.Stop() + } + } else { + dlog.Critical(err) + } + + + ctx, cancelDial := context.WithCancel(ctx) + defer cancelDial() + + type result struct { + host string + ok bool + err error + } + + results := make(chan result, len(hosts_port)) + + var probesPending int = 0 + for _, host := range hosts_port { + if len(host) <= 0 { + continue + } + + probesPending++ + go func(host string) { + ok, err := NetProbeSingle(proxy, host, ctx) + results <- result{ + host: host, + ok: ok, + err: err, + } + if ok { + cancelDial() + } + }(host) + } + if probesPending <= 0 { + dlog.Error( + "netprobe_addresses non-zero length but all addresses are invalid somehow", + ) + return nil + } + + for { + select { + case res := <-results: + if res.ok && res.err == nil { + dlog.Noticef( + "Network connectivity detected (%s)", + res.host, + ) + return nil + } else if !errors.Is(res.err, context.Canceled) && + !errors.Is(res.err, context.DeadlineExceeded) { + dlog.Noticef("(%s) %v", res.host, res.err) + } + + probesPending-- + if probesPending <= 0 { + dlog.Error("Timeout while waiting for network connectivity") + return nil + } + + } + } +} + +func NetProbeSingle( + proxy *Proxy, + host_port string, + ctx context.Context, +) (ok bool, err error) { + if len(host_port) <= 0 { + return false, nil + } + if ctx.Err() != nil { + return false, ctx.Err() + } + if _, _, err := net.SplitHostPort(host_port); err != nil { + return false, err + } + + loggedMessages := make(map[string]struct{}) + + dialer := net.Dialer{ + Timeout: proxy.timeout, + } + + deadline, deadlineOk := ctx.Deadline() + + interval := time.Second + + if deadlineOk { + if i, _, ok := determineNetprobeIntervals( + time.Second, + deadline, + 10*time.Millisecond, + ); ok { + interval = i + } + } + + for { + retryLimitTimer := time.NewTimer(interval) + + pc, err := dialer.DialContext( + ctx, + "udp", + host_port, + ) + if runtime.GOOS == "windows" && err == nil { + // Write at least 1 byte. This ensures that sockets are ready to use for writing. + // Windows specific: during the system startup, sockets can be created but the underlying buffers may not be + // set up yet. If this is the case Write fails with WSAENOBUFS: "An operation on a socket could not be + // performed because the system lacked sufficient buffer space or because a queue was full" + _, err = pc.Write([]byte{0}) + if err != nil { + pc.Close() + } + } + + if err != nil { + msg := "" + var dnsErr *net.DNSError + + switch { + case ctx.Err() != nil: + msg = "" + case errors.As(err, &dnsErr): + msg = fmt.Sprintf( + "(%s) Name resolution error: %v", + host_port, + dnsErr, + ) + default: + msg = fmt.Sprintf( + "(%s) Network not available yet -- waiting...", + host_port, + ) + } + + + if _, exists := loggedMessages[msg]; !exists && msg != "" { + dlog.Notice(msg) + loggedMessages[msg] = struct{}{} + } + + dlog.Debugf( + "(%s) %v", + host_port, + err, + ) + + select { + case <-ctx.Done(): + dlog.Debugf( + "(%s) context done", + host_port, + ) + return false, ctx.Err() + case <-retryLimitTimer.C: + } + + continue + } + pc.Close() + return true, nil + } +} diff --git a/dnscrypt-proxy/netprobe_others.go b/dnscrypt-proxy/netprobe_others.go deleted file mode 100644 index 81217f9655..0000000000 --- a/dnscrypt-proxy/netprobe_others.go +++ /dev/null @@ -1,50 +0,0 @@ -//go:build !windows - -package main - -import ( - "net" - "time" - - "github.com/jedisct1/dlog" -) - -func NetProbe(proxy *Proxy, address string, timeout int) error { - if len(address) <= 0 || timeout == 0 { - return nil - } - if captivePortalHandler, err := ColdStart(proxy); err == nil { - if captivePortalHandler != nil { - defer captivePortalHandler.Stop() - } - } else { - dlog.Critical(err) - } - remoteUDPAddr, err := net.ResolveUDPAddr("udp", address) - if err != nil { - return err - } - retried := false - if timeout < 0 { - timeout = MaxTimeout - } else { - timeout = Min(MaxTimeout, timeout) - } - for tries := timeout; tries > 0; tries-- { - pc, err := net.DialTimeout("udp", remoteUDPAddr.String(), proxy.timeout) - if err != nil { - if !retried { - retried = true - dlog.Notice("Network not available yet -- waiting...") - } - dlog.Debug(err) - time.Sleep(1 * time.Second) - continue - } - pc.Close() - dlog.Notice("Network connectivity detected") - return nil - } - dlog.Error("Timeout while waiting for network connectivity") - return nil -} diff --git a/dnscrypt-proxy/netprobe_windows.go b/dnscrypt-proxy/netprobe_windows.go deleted file mode 100644 index 5a63dfd2bf..0000000000 --- a/dnscrypt-proxy/netprobe_windows.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "net" - "time" - - "github.com/jedisct1/dlog" -) - -func NetProbe(proxy *Proxy, address string, timeout int) error { - if len(address) <= 0 || timeout == 0 { - return nil - } - if captivePortalHandler, err := ColdStart(proxy); err == nil { - if captivePortalHandler != nil { - defer captivePortalHandler.Stop() - } - } else { - dlog.Critical(err) - } - remoteUDPAddr, err := net.ResolveUDPAddr("udp", address) - if err != nil { - return err - } - retried := false - if timeout < 0 { - timeout = MaxTimeout - } else { - timeout = Min(MaxTimeout, timeout) - } - for tries := timeout; tries > 0; tries-- { - pc, err := net.DialTimeout("udp", remoteUDPAddr.String(), proxy.timeout) - if err == nil { - // Write at least 1 byte. This ensures that sockets are ready to use for writing. - // Windows specific: during the system startup, sockets can be created but the underlying buffers may not be - // set up yet. If this is the case Write fails with WSAENOBUFS: "An operation on a socket could not be - // performed because the system lacked sufficient buffer space or because a queue was full" - _, err = pc.Write([]byte{0}) - if err != nil { - pc.Close() - } - } - if err != nil { - if !retried { - retried = true - dlog.Notice("Network not available yet -- waiting...") - } - dlog.Debug(err) - time.Sleep(1 * time.Second) - continue - } - pc.Close() - dlog.Notice("Network connectivity detected") - return nil - } - dlog.Error("Timeout while waiting for network connectivity") - return nil -} diff --git a/go.mod b/go.mod index 407ede54d5..2f4d92ba7d 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,8 @@ require ( require ( github.com/hashicorp/go-syslog v1.0.0 // indirect + github.com/projectdiscovery/blackrock v0.0.2 // indirect + github.com/projectdiscovery/utils v0.11.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/quic-go/qpack v0.6.0 // indirect go.uber.org/mock v0.6.0 // indirect diff --git a/go.sum b/go.sum index b8877ef106..8c78e1eecf 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,10 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/powerman/check v1.13.0 h1:/uNADrvWRzsRKCN00LEN/Ph0XMlUmtOhZTw2Qr60nNQ= github.com/powerman/check v1.13.0/go.mod h1:h7WkuVUhfVN1yl/F6zm+zbZLeOUtx34+JpMA8aA2vR0= +github.com/projectdiscovery/blackrock v0.0.2 h1:mxXdu0uM8P2L2Qi210COlU8QiICPFW/Rxk5QUhlPO2k= +github.com/projectdiscovery/blackrock v0.0.2/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= +github.com/projectdiscovery/utils v0.11.2 h1:gfvXFBEHN4DgX8HBRzsoO1s2wH/2R+cH17cLpC865x4= +github.com/projectdiscovery/utils v0.11.2/go.mod h1:HMxhxLigsAr+M9Oa8n9Z0ROZcs8wAJHGsgR7UGb+oUE= github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=