Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
760d8e6
feat: added support for multiple netprobe addresses
BPplays Sep 20, 2026
ffea570
fix: add go.sum
BPplays Sep 20, 2026
a42c641
chore: add vendor folder
BPplays Sep 20, 2026
dfb5173
updated example config
BPplays Sep 20, 2026
dc09806
removed vendor directory from branch
BPplays Sep 20, 2026
772cbab
Merge branch 'master' into netprobe_multiple
BPplays Sep 20, 2026
dc3dfcb
added vendor directory
BPplays Sep 20, 2026
022b218
Revert "added vendor directory"
BPplays Sep 20, 2026
8346f31
more consistant logging
BPplays Sep 20, 2026
28c2c3a
used timer to make loop closer to seconds
BPplays Sep 20, 2026
3eaad8a
changed timeout to time.Duration
BPplays Sep 21, 2026
157cc7d
testing deadline stuff
BPplays Sep 21, 2026
6d54ac8
Revert "testing deadline stuff"
BPplays Sep 21, 2026
52fee16
better error logging
BPplays Sep 21, 2026
7b9f009
testing just ctx deadline
BPplays Sep 21, 2026
dd6bda8
merged windows and other; now fully deadline based for single
BPplays Sep 21, 2026
498f450
cleaned up and added more error checking
BPplays Sep 21, 2026
3a9f6a7
cleaned up
BPplays Sep 21, 2026
47bc136
cleaned up
BPplays Sep 21, 2026
71772fc
fixed comment
BPplays Sep 22, 2026
b47154b
added comment to DeadlineInterval
BPplays Sep 22, 2026
51ddf6c
cleaned up
BPplays Sep 22, 2026
f741406
moved netprobe proper to context based
BPplays Sep 22, 2026
7ad46ed
better handling of legacy warnings, and stop logging network not avai…
BPplays Sep 22, 2026
fa5ac21
set timeout to better say hour; changed default netprobe addresses to…
BPplays Sep 22, 2026
75546f0
changed back to string, could resolve fqdn before and changing would …
BPplays Sep 22, 2026
cd53f4d
made dialer try to get name every loop, better logging logic
BPplays Sep 26, 2026
8f9b286
cleanup test
BPplays Sep 26, 2026
4853976
fixed formatting change
BPplays Sep 27, 2026
7a0944a
better logging message
BPplays Sep 27, 2026
68a77d6
better logging message
BPplays Sep 27, 2026
ce8395a
better logging message
BPplays Sep 27, 2026
d75659f
formatting
BPplays Sep 27, 2026
b97a557
changed function name
BPplays Sep 28, 2026
0b095b1
made dns error logging log more stuff
BPplays Sep 29, 2026
6cebc82
better var name
BPplays Sep 29, 2026
9024a8e
added a list of netprobe addrs and the old option is now a fatal error
BPplays Sep 29, 2026
839164a
better wording in example config
BPplays Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions dnscrypt-proxy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import (
)

const (
MaxTimeout = 3600
DefaultNetprobeAddress = "9.9.9.9:53"
MaxTimeout = time.Hour
)

type Config struct {
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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",
}
}

67 changes: 55 additions & 12 deletions dnscrypt-proxy/config_loader.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
package main

import (
"context"
"errors"
"flag"
"fmt"
"net"
"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"
)

Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down
8 changes: 6 additions & 2 deletions dnscrypt-proxy/example-dnscrypt-proxy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
220 changes: 220 additions & 0 deletions dnscrypt-proxy/netprobe.go
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading