internal/controld: check if ipv4 is available before connect to API

Updates #53
This commit is contained in:
Cuong Manh Le
2023-02-10 01:19:53 +07:00
committed by Cuong Manh Le
parent 45f827a2c5
commit 83b551fb2d
8 changed files with 108 additions and 87 deletions

View File

@@ -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()

View File

@@ -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)),

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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)

View File

@@ -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,

86
internal/net/net.go Normal file
View File

@@ -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
}