From b6f55937d499e358d49de8a6c18616661d621246 Mon Sep 17 00:00:00 2001 From: jedisct1 <124872+jedisct1@users.noreply.github.com> Date: Tue, 29 Sep 2026 13:36:59 +0000 Subject: [PATCH] netprobe: accept multiple comma-separated addresses This allows probing both an IPv4 and an IPv6 address, so that network detection works on IPv4-only as well as IPv6-only networks. Connectivity is detected as soon as any of the addresses works. Single-address configurations behave exactly as before. --- dnscrypt-proxy/example-dnscrypt-proxy.toml | 3 +++ dnscrypt-proxy/netprobe.go | 29 ++++++++++++++++++++++ dnscrypt-proxy/netprobe_others.go | 13 +++++++--- dnscrypt-proxy/netprobe_test.go | 19 ++++++++++++++ dnscrypt-proxy/netprobe_windows.go | 17 ++++++++----- 5 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 dnscrypt-proxy/netprobe.go create mode 100644 dnscrypt-proxy/netprobe_test.go diff --git a/dnscrypt-proxy/example-dnscrypt-proxy.toml b/dnscrypt-proxy/example-dnscrypt-proxy.toml index b9e4b73cdc..6524540243 100644 --- a/dnscrypt-proxy/example-dnscrypt-proxy.toml +++ b/dnscrypt-proxy/example-dnscrypt-proxy.toml @@ -381,6 +381,9 @@ netprobe_timeout = 60 ## when the system starts. ## On other operating systems, the connection will be initialized ## but nothing will be sent at all. +## Multiple comma-separated addresses can be given, for example to support +## both IPv4-only and IPv6-only networks. Connectivity is detected as soon +## as any of them works: '9.9.9.9:53,[2620:fe::fe]:53' netprobe_address = '9.9.9.9:53' diff --git a/dnscrypt-proxy/netprobe.go b/dnscrypt-proxy/netprobe.go new file mode 100644 index 0000000000..1c9c93d339 --- /dev/null +++ b/dnscrypt-proxy/netprobe.go @@ -0,0 +1,29 @@ +package main + +import ( + "errors" + "net" + "strings" +) + +// resolveNetprobeAddresses parses a netprobe address specification. +// Multiple addresses (for example an IPv4 and an IPv6 address) can be +// separated by commas; connectivity is detected as soon as any of them works. +func resolveNetprobeAddresses(address string) ([]*net.UDPAddr, error) { + var addrs []*net.UDPAddr + for _, part := range strings.Split(address, ",") { + part = strings.TrimSpace(part) + if len(part) == 0 { + continue + } + addr, err := net.ResolveUDPAddr("udp", part) + if err != nil { + return nil, err + } + addrs = append(addrs, addr) + } + if len(addrs) == 0 { + return nil, errors.New("No valid netprobe address in [" + address + "]") + } + return addrs, nil +} diff --git a/dnscrypt-proxy/netprobe_others.go b/dnscrypt-proxy/netprobe_others.go index 81217f9655..ce727bb515 100644 --- a/dnscrypt-proxy/netprobe_others.go +++ b/dnscrypt-proxy/netprobe_others.go @@ -20,7 +20,7 @@ func NetProbe(proxy *Proxy, address string, timeout int) error { } else { dlog.Critical(err) } - remoteUDPAddr, err := net.ResolveUDPAddr("udp", address) + remoteUDPAddrs, err := resolveNetprobeAddresses(address) if err != nil { return err } @@ -31,7 +31,15 @@ func NetProbe(proxy *Proxy, address string, timeout int) error { timeout = Min(MaxTimeout, timeout) } for tries := timeout; tries > 0; tries-- { - pc, err := net.DialTimeout("udp", remoteUDPAddr.String(), proxy.timeout) + var err error + for _, remoteUDPAddr := range remoteUDPAddrs { + var pc net.Conn + pc, err = net.DialTimeout("udp", remoteUDPAddr.String(), proxy.timeout) + if err == nil { + pc.Close() + break + } + } if err != nil { if !retried { retried = true @@ -41,7 +49,6 @@ func NetProbe(proxy *Proxy, address string, timeout int) error { time.Sleep(1 * time.Second) continue } - pc.Close() dlog.Notice("Network connectivity detected") return nil } diff --git a/dnscrypt-proxy/netprobe_test.go b/dnscrypt-proxy/netprobe_test.go new file mode 100644 index 0000000000..e5dd944495 --- /dev/null +++ b/dnscrypt-proxy/netprobe_test.go @@ -0,0 +1,19 @@ +package main + +import "testing" + +func TestResolveNetprobeAddresses(t *testing.T) { + addrs, err := resolveNetprobeAddresses("9.9.9.9:53") + if err != nil || len(addrs) != 1 || addrs[0].String() != "9.9.9.9:53" { + t.Fatalf("single address: %v %v", addrs, err) + } + addrs, err = resolveNetprobeAddresses("9.9.9.9:53, [2620:fe::fe]:53") + if err != nil || len(addrs) != 2 || addrs[1].String() != "[2620:fe::fe]:53" { + t.Fatalf("multiple addresses: %v %v", addrs, err) + } + for _, bad := range []string{"9.9.9.9", "9.9.9.9:53,bogus", " , "} { + if _, err := resolveNetprobeAddresses(bad); err == nil { + t.Fatalf("expected error for %q", bad) + } + } +} diff --git a/dnscrypt-proxy/netprobe_windows.go b/dnscrypt-proxy/netprobe_windows.go index 5a63dfd2bf..b6a5ad8faa 100644 --- a/dnscrypt-proxy/netprobe_windows.go +++ b/dnscrypt-proxy/netprobe_windows.go @@ -18,7 +18,7 @@ func NetProbe(proxy *Proxy, address string, timeout int) error { } else { dlog.Critical(err) } - remoteUDPAddr, err := net.ResolveUDPAddr("udp", address) + remoteUDPAddrs, err := resolveNetprobeAddresses(address) if err != nil { return err } @@ -29,15 +29,21 @@ func NetProbe(proxy *Proxy, address string, timeout int) error { timeout = Min(MaxTimeout, timeout) } for tries := timeout; tries > 0; tries-- { - pc, err := net.DialTimeout("udp", remoteUDPAddr.String(), proxy.timeout) - if err == nil { + var err error + for _, remoteUDPAddr := range remoteUDPAddrs { + var pc net.Conn + pc, err = net.DialTimeout("udp", remoteUDPAddr.String(), proxy.timeout) + if err != nil { + continue + } // 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() + pc.Close() + if err == nil { + break } } if err != nil { @@ -49,7 +55,6 @@ func NetProbe(proxy *Proxy, address string, timeout int) error { time.Sleep(1 * time.Second) continue } - pc.Close() dlog.Notice("Network connectivity detected") return nil }