diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index cd18ee1..d09e777 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -24,6 +24,7 @@ import ( "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/controld" + ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" ) var ( @@ -105,7 +106,7 @@ func initCLI() { log.Fatalf("failed to unmarshal config: %v", err) } // Wait for network up. - if !netUp() { + if !ctrldnet.Up() { log.Fatal("network is not up yet") } processLogAndCacheFlags() diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 4cdfab0..0f3d085 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -15,6 +15,7 @@ import ( "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/dnscache" + ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" ) const staleTTL = 60 * time.Second @@ -55,7 +56,7 @@ func (p *prog) serveUDP(listenerNum string) error { // On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can // listen on ::1, then spawn a listener for receiving DNS requests. - if runtime.GOOS == "windows" && supportsIPv6ListenLocal() { + if runtime.GOOS == "windows" && ctrldnet.SupportsIPv6() { go func() { s := &dns.Server{ Addr: net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), diff --git a/cmd/ctrld/net.go b/cmd/ctrld/net.go deleted file mode 100644 index 595f03f..0000000 --- a/cmd/ctrld/net.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -import ( - "context" - "net" - "sync" - "time" - - "tailscale.com/logtail/backoff" - - "github.com/Control-D-Inc/ctrld/internal/controld" -) - -const ( - controldIPv6Test = "ipv6.controld.io" -) - -var ( - stackOnce sync.Once - ipv6Enabled bool - canListenIPv6Local bool - hasNetworkUp bool -) - -func probeStack() { - b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, time.Minute) - for { - if _, err := controld.Dialer.Dial("udp", net.JoinHostPort(bootstrapDNS, "53")); err == nil { - hasNetworkUp = true - break - } else { - b.BackOff(context.Background(), err) - } - } - if _, err := controld.Dialer.Dial("tcp6", net.JoinHostPort(controldIPv6Test, "80")); err == nil { - ipv6Enabled = true - } - if ln, err := net.Listen("tcp6", "[::1]:53"); err == nil { - ln.Close() - canListenIPv6Local = true - } -} - -func netUp() bool { - stackOnce.Do(probeStack) - return hasNetworkUp -} - -func supportsIPv6() bool { - stackOnce.Do(probeStack) - return ipv6Enabled -} - -func supportsIPv6ListenLocal() bool { - stackOnce.Do(probeStack) - return canListenIPv6Local -} - -// isIPv6 checks if the provided IP is v6. -// -//lint:ignore U1000 use in os_windows.go -func isIPv6(ip string) bool { - parsedIP := net.ParseIP(ip) - return parsedIP != nil && parsedIP.To4() == nil && parsedIP.To16() != nil -} diff --git a/cmd/ctrld/os_linux.go b/cmd/ctrld/os_linux.go index 50ff469..22a469e 100644 --- a/cmd/ctrld/os_linux.go +++ b/cmd/ctrld/os_linux.go @@ -19,6 +19,7 @@ import ( "tailscale.com/util/dnsname" "github.com/Control-D-Inc/ctrld/internal/dns" + ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" "github.com/Control-D-Inc/ctrld/internal/resolvconffile" ) @@ -111,7 +112,7 @@ func resetDNS(iface *net.Interface) error { } // TODO(cuonglm): handle DHCPv6 properly. - if supportsIPv6() { + if ctrldnet.SupportsIPv6() { c := client6.NewClient() conversation, err := c.Exchange(iface.Name) if err != nil { diff --git a/cmd/ctrld/os_windows.go b/cmd/ctrld/os_windows.go index 213c104..bb1631f 100644 --- a/cmd/ctrld/os_windows.go +++ b/cmd/ctrld/os_windows.go @@ -10,6 +10,8 @@ import ( "strconv" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" + + ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" ) // TODO(cuonglm): implement. @@ -39,7 +41,7 @@ func setDNS(iface *net.Interface, nameservers []string) error { // TODO(cuonglm): should we use system API? func resetDNS(iface *net.Interface) error { - if supportsIPv6ListenLocal() { + if ctrldnet.SupportsIPv6ListenLocal() { if output, err := netsh("interface", "ipv6", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp"); err != nil { mainLog.Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output)) } @@ -54,7 +56,7 @@ func resetDNS(iface *net.Interface) error { func setPrimaryDNS(iface *net.Interface, dns string) error { ipVer := "ipv4" - if isIPv6(dns) { + if ctrldnet.IsIPv6(dns) { ipVer = "ipv6" } idx := strconv.Itoa(iface.Index) @@ -73,7 +75,7 @@ func setPrimaryDNS(iface *net.Interface, dns string) error { func addSecondaryDNS(iface *net.Interface, dns string) error { ipVer := "ipv4" - if isIPv6(dns) { + if ctrldnet.IsIPv6(dns) { ipVer = "ipv6" } output, err := netsh("interface", ipVer, "add", "dns", strconv.Itoa(iface.Index), dns, "index=2") diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index b8e22bd..d99449c 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -13,6 +13,7 @@ import ( "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/dnscache" + ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" ) var errWindowsAddrInUse = syscall.Errno(0x2740) @@ -64,7 +65,7 @@ func (p *prog) run() { // resolve it manually and set the bootstrap ip c := new(dns.Client) for _, dnsType := range []uint16{dns.TypeAAAA, dns.TypeA} { - if !supportsIPv6() && dnsType == dns.TypeAAAA { + if !ctrldnet.SupportsIPv6() && dnsType == dns.TypeAAAA { continue } m := new(dns.Msg) diff --git a/internal/controld/config.go b/internal/controld/config.go index 88cfd97..4f16b88 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -8,6 +8,8 @@ import ( "net" "net/http" "time" + + ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" ) const ( @@ -15,20 +17,6 @@ const ( InvalidConfigCode = 40401 ) -const bootstrapDNS = "76.76.2.0:53" - -var Dialer = &net.Dialer{ - Resolver: &net.Resolver{ - PreferGo: true, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{ - Timeout: 10 * time.Second, - } - return d.DialContext(ctx, "udp", bootstrapDNS) - }, - }, -} - // ResolverConfig represents Control D resolver data. type ResolverConfig struct { DOH string `json:"doh"` @@ -70,7 +58,13 @@ func FetchResolverConfig(uid string) (*ResolverConfig, error) { req.Header.Add("Content-Type", "application/json") transport := http.DefaultTransport.(*http.Transport).Clone() transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - return Dialer.DialContext(ctx, "tcp4", addr) + // We experiment hanging in TLS handshake when connecting to ControlD API + // with ipv6. So prefer ipv4 if available. + network = "tcp6" + if ctrldnet.SupportsIPv4() { + network = "tcp4" + } + return ctrldnet.Dialer.DialContext(ctx, network, addr) } client := http.Client{ Timeout: 10 * time.Second, diff --git a/internal/net/net.go b/internal/net/net.go new file mode 100644 index 0000000..b553c13 --- /dev/null +++ b/internal/net/net.go @@ -0,0 +1,86 @@ +package net + +import ( + "context" + "net" + "sync" + "time" + + "tailscale.com/logtail/backoff" +) + +const ( + controldIPv6Test = "ipv6.controld.io" + controldIPv4Test = "ipv4.controld.io" + bootstrapDNS = "76.76.2.0:53" +) + +var Dialer = &net.Dialer{ + Resolver: &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{ + Timeout: 10 * time.Second, + } + return d.DialContext(ctx, "udp", bootstrapDNS) + }, + }, +} + +var ( + stackOnce sync.Once + ipv4Enabled bool + ipv6Enabled bool + canListenIPv6Local bool + hasNetworkUp bool +) + +func probeStack() { + b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, time.Minute) + for { + if _, err := Dialer.Dial("udp", net.JoinHostPort(bootstrapDNS, "53")); err == nil { + hasNetworkUp = true + break + } else { + b.BackOff(context.Background(), err) + } + } + if _, err := Dialer.Dial("tcp4", net.JoinHostPort(controldIPv4Test, "80")); err == nil { + ipv4Enabled = true + } + if _, err := Dialer.Dial("tcp6", net.JoinHostPort(controldIPv6Test, "80")); err == nil { + ipv6Enabled = true + } + if ln, err := net.Listen("tcp6", "[::1]:53"); err == nil { + ln.Close() + canListenIPv6Local = true + } +} + +func Up() bool { + stackOnce.Do(probeStack) + return hasNetworkUp +} + +func SupportsIPv4() bool { + stackOnce.Do(probeStack) + return ipv4Enabled +} + +func SupportsIPv6() bool { + stackOnce.Do(probeStack) + return ipv6Enabled +} + +func SupportsIPv6ListenLocal() bool { + stackOnce.Do(probeStack) + return canListenIPv6Local +} + +// IsIPv6 checks if the provided IP is v6. +// +//lint:ignore U1000 use in os_windows.go +func IsIPv6(ip string) bool { + parsedIP := net.ParseIP(ip) + return parsedIP != nil && parsedIP.To4() == nil && parsedIP.To16() != nil +}