diff --git a/client/grpc/dialer_generic.go b/client/grpc/dialer_generic.go index 479575996..b87b666fb 100644 --- a/client/grpc/dialer_generic.go +++ b/client/grpc/dialer_generic.go @@ -29,7 +29,9 @@ func WithCustomDialer(_ bool, _ string) grpc.DialOption { // the custom dialer requires root permissions which are not required for use cases run as non-root if currentUser.Uid != "0" { log.Debug("Not running as root, using standard dialer") - dialer := &net.Dialer{} + dialer := &net.Dialer{ + Resolver: nbnet.NewResolver(), + } return dialer.DialContext(ctx, "tcp", addr) } } diff --git a/client/internal/stdnet/stdnet.go b/client/internal/stdnet/stdnet.go index 381886ac6..b0e69b027 100644 --- a/client/internal/stdnet/stdnet.go +++ b/client/internal/stdnet/stdnet.go @@ -18,6 +18,7 @@ import ( "github.com/pion/transport/v3/stdnet" "github.com/netbirdio/netbird/client/iface/netstack" + nbnet "github.com/netbirdio/netbird/client/net" ) const ( @@ -42,6 +43,8 @@ type Net struct { // ctx is the context for network operations that supports cancellation ctx context.Context + + resolver *net.Resolver } // NewNetWithDiscover creates a new StdNet instance. @@ -52,6 +55,7 @@ func NewNetWithDiscover(ctx context.Context, iFaceDiscover ExternalIFaceDiscover n := &Net{ interfaceFilter: InterfaceFilter(disallowList), ctx: ctx, + resolver: nbnet.NewResolver(), } // current ExternalIFaceDiscover implement in android-client https://github.dev/netbirdio/android-client // so in android cli use pionDiscover @@ -72,6 +76,7 @@ func NewNet(ctx context.Context, disallowList []string) (*Net, error) { iFaceDiscover: pionDiscover{}, interfaceFilter: InterfaceFilter(disallowList), ctx: ctx, + resolver: nbnet.NewResolver(), } return n, n.UpdateInterfaces() } @@ -110,7 +115,7 @@ func (n *Net) resolveAddr(network, address string) (netip.AddrPort, error) { ctx, cancel := context.WithTimeout(n.ctx, dnsResolveTimeout) defer cancel() - addrs, err := net.DefaultResolver.LookupNetIP(ctx, ipNet, host) + addrs, err := n.resolver.LookupNetIP(ctx, ipNet, host) if err != nil { return netip.AddrPort{}, err } diff --git a/client/net/dialer.go b/client/net/dialer.go index 29bec05a7..4651345c6 100644 --- a/client/net/dialer.go +++ b/client/net/dialer.go @@ -13,7 +13,9 @@ type Dialer struct { // NewDialer returns a customized net.Dialer with overridden Control method func NewDialer() *Dialer { dialer := &Dialer{ - Dialer: &net.Dialer{}, + Dialer: &net.Dialer{ + Resolver: NewResolver(), + }, } dialer.init() return dialer diff --git a/client/net/resolver.go b/client/net/resolver.go new file mode 100644 index 000000000..03af553f4 --- /dev/null +++ b/client/net/resolver.go @@ -0,0 +1,43 @@ +package net + +import ( + "net" + "os" + "runtime" + "strings" +) + +const ( + // EnvResolver is the environment variable to control DNS resolver behavior + // Values: "system" (use system resolver), "go" (use pure Go resolver), empty (auto-detect) + EnvResolver = "NB_DNS_RESOLVER" +) + +// NewResolver creates a DNS resolver with appropriate settings based on platform and configuration. +// On Darwin (macOS), it defaults to the pure Go resolver to avoid getaddrinfo hangs after sleep/wake. +// This is particularly important for connections using this package's Dialer, which bypasses the NetBird +// overlay network for control plane traffic. Since these connections target external infrastructure +// (management, signal, relay servers), it is safe to ignore split DNS configurations that would +// normally be provided by the system resolver. +// On other platforms, it uses the system resolver (cgo). +// This behavior can be overridden using the NB_DNS_RESOLVER environment variable or GODEBUG. +func NewResolver() *net.Resolver { + if resolver := os.Getenv(EnvResolver); resolver != "" { + switch strings.ToLower(resolver) { + case "system": + return net.DefaultResolver + case "go": + return &net.Resolver{ + PreferGo: true, + } + } + } + + if runtime.GOOS == "darwin" { + return &net.Resolver{ + PreferGo: true, + } + } + + return net.DefaultResolver +}