diff --git a/.goreleaser-darwin.yaml b/.goreleaser-darwin.yaml index 57b84c0..5fa9605 100644 --- a/.goreleaser-darwin.yaml +++ b/.goreleaser-darwin.yaml @@ -9,6 +9,8 @@ builds: - -trimpath ldflags: - -s -w + - -X main.version={{.Version}} + - -X main.commit={{.Commit}} goos: - darwin goarch: diff --git a/.goreleaser-qf.yaml b/.goreleaser-qf.yaml index 9e72ed4..5fc359e 100644 --- a/.goreleaser-qf.yaml +++ b/.goreleaser-qf.yaml @@ -9,6 +9,8 @@ builds: - -trimpath ldflags: - -s -w + - -X main.version={{.Version}} + - -X main.commit={{.Commit}} goos: - darwin - linux diff --git a/.goreleaser.yaml b/.goreleaser.yaml index f010ff9..a08f8d1 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,6 +9,8 @@ builds: - -trimpath ldflags: - -s -w + - -X main.version={{.Version}} + - -X main.commit={{.Commit}} goos: - linux - freebsd diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index dc172d3..4123e28 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -15,10 +15,12 @@ import ( "runtime" "strconv" "strings" + "sync" "time" "github.com/fsnotify/fsnotify" + "github.com/cuonglm/osinfo" "github.com/go-playground/validator/v10" "github.com/kardianos/service" "github.com/miekg/dns" @@ -35,6 +37,11 @@ import ( const selfCheckFQDN = "verify.controld.com" +var ( + version = "dev" + commit = "none" +) + var ( v = viper.NewWithOptions(viper.KeyDelimiter("::")) defaultConfigWritten = false @@ -61,17 +68,28 @@ _/ ___\ __\_ __ \ | / __ | \/ dns forwarding proxy \/ ` +var rootCmd = &cobra.Command{ + Use: "ctrld", + Short: strings.TrimLeft(rootShortDesc, "\n"), + Version: curVersion(), +} + +func curVersion() string { + if version != "dev" { + version = "v" + version + } + if len(commit) > 7 { + commit = commit[:7] + } + return fmt.Sprintf("%s-%s", version, commit) +} + func initCLI() { // Enable opening via explorer.exe on Windows. // See: https://github.com/spf13/cobra/issues/844. cobra.MousetrapHelpText = "" cobra.EnableCommandSorting = false - rootCmd := &cobra.Command{ - Use: "ctrld", - Short: strings.TrimLeft(rootShortDesc, "\n"), - Version: "1.1.3", - } rootCmd.PersistentFlags().CountVarP( &verbose, "verbose", @@ -89,6 +107,35 @@ func initCLI() { if daemon && runtime.GOOS == "windows" { log.Fatal("Cannot run in daemon mode. Please install a Windows service.") } + + waitCh := make(chan struct{}) + stopCh := make(chan struct{}) + if !daemon { + // We need to call s.Run() as soon as possible to response to the OS manager, so it + // can see ctrld is running and don't mark ctrld as failed service. + go func() { + p := &prog{ + waitCh: waitCh, + stopCh: stopCh, + } + s, err := service.New(p, svcConfig) + if err != nil { + mainLog.Fatal().Err(err).Msg("failed create new service") + } + serviceLogger, err := s.Logger(nil) + if err != nil { + mainLog.Error().Err(err).Msg("failed to get service logger") + return + } + + if err := s.Run(); err != nil { + if sErr := serviceLogger.Error(err); sErr != nil { + mainLog.Error().Err(sErr).Msg("failed to write service log") + } + mainLog.Error().Err(err).Msg("failed to start service") + } + }() + } noConfigStart := isNoConfigStart(cmd) writeDefaultConfig := !noConfigStart && configBase64 == "" configs := []struct { @@ -112,7 +159,11 @@ func initCLI() { if err := v.Unmarshal(&cfg); err != nil { log.Fatalf("failed to unmarshal config: %v", err) } - fmt.Println("starting ctrld...") + + log.Printf("starting ctrld %s\n", curVersion()) + oi := osinfo.New() + log.Printf("os: %s\n", oi.String()) + // Wait for network up. if !ctrldnet.Up() { log.Fatal("network is not up yet") @@ -149,22 +200,8 @@ func initCLI() { os.Exit(0) } - s, err := service.New(&prog{}, svcConfig) - if err != nil { - mainLog.Fatal().Err(err).Msg("failed create new service") - } - serviceLogger, err := s.Logger(nil) - if err != nil { - mainLog.Error().Err(err).Msg("failed to get service logger") - return - } - - if err := s.Run(); err != nil { - if sErr := serviceLogger.Error(err); sErr != nil { - mainLog.Error().Err(sErr).Msg("failed to write service log") - } - mainLog.Error().Err(err).Msg("failed to start service") - } + close(waitCh) + <-stopCh }, } runCmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon") @@ -346,6 +383,10 @@ func initCLI() { } }, } + if runtime.GOOS == "darwin" { + // On darwin, running status command without privileges may return wrong information. + statusCmd.PreRun = checkHasElevatedPrivilege + } uninstallCmd := &cobra.Command{ PreRun: checkHasElevatedPrivilege, @@ -506,15 +547,8 @@ func readConfigFile(writeDefaultConfig bool) bool { // If err == nil, there's a config supplied via `--config`, no default config written. err := v.ReadInConfig() if err == nil { - fmt.Println("loading config file from:", v.ConfigFileUsed()) + log.Println("loading config file from:", v.ConfigFileUsed()) defaultConfigFile = v.ConfigFileUsed() - v.OnConfigChange(func(in fsnotify.Event) { - if err := v.UnmarshalKey("listener", &cfg.Listener); err != nil { - log.Printf("failed to unmarshal listener config: %v", err) - return - } - }) - v.WatchConfig() return true } @@ -527,7 +561,7 @@ func readConfigFile(writeDefaultConfig bool) bool { if err := writeConfigFile(); err != nil { log.Fatalf("failed to write default config file: %v", err) } else { - fmt.Println("writing default config file to: " + defaultConfigFile) + log.Println("writing default config file to: " + defaultConfigFile) } defaultConfigWritten = true return false @@ -559,18 +593,24 @@ func processNoConfigFlags(noConfigStart bool) { } processListenFlag() + endpointAndTyp := func(endpoint string) (string, string) { + typ := ctrld.ResolverTypeFromEndpoint(endpoint) + return strings.TrimPrefix(endpoint, "quic://"), typ + } + pEndpoint, pType := endpointAndTyp(primaryUpstream) upstream := map[string]*ctrld.UpstreamConfig{ "0": { - Name: primaryUpstream, - Endpoint: primaryUpstream, - Type: ctrld.ResolverTypeDOH, + Name: pEndpoint, + Endpoint: pEndpoint, + Type: pType, }, } if secondaryUpstream != "" { + sEndpoint, sType := endpointAndTyp(secondaryUpstream) upstream["1"] = &ctrld.UpstreamConfig{ - Name: secondaryUpstream, - Endpoint: secondaryUpstream, - Type: ctrld.ResolverTypeLegacy, + Name: sEndpoint, + Endpoint: sEndpoint, + Type: sType, } rules := make([]ctrld.Rule, 0, len(domains)) for _, domain := range domains { @@ -727,8 +767,26 @@ func selfCheckStatus(status service.Status) service.Status { err := errors.New("query failed") maxAttempts := 20 mainLog.Debug().Msg("Performing self-check") + var ( + lcChanged map[string]*ctrld.ListenerConfig + mu sync.Mutex + ) + v.OnConfigChange(func(in fsnotify.Event) { + mu.Lock() + defer mu.Unlock() + if err := v.UnmarshalKey("listener", &lcChanged); err != nil { + log.Printf("failed to unmarshal listener config: %v", err) + return + } + }) + v.WatchConfig() for i := 0; i < maxAttempts; i++ { lc := cfg.Listener["0"] + mu.Lock() + if lcChanged != nil { + lc = lcChanged["0"] + } + mu.Unlock() m := new(dns.Msg) m.SetQuestion(selfCheckFQDN+".", dns.TypeA) m.RecursionDesired = true diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 733ae63..5143659 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -56,7 +56,7 @@ func (p *prog) serveDNS(listenerNum string) error { } }) - g := new(errgroup.Group) + g, ctx := errgroup.WithContext(context.Background()) for _, proto := range []string{"udp", "tcp"} { proto := proto // On Windows, there's no easy way for disabling/removing IPv6 DNS resolver, so we check whether we can @@ -68,6 +68,10 @@ func (p *prog) serveDNS(listenerNum string) error { Net: proto, Handler: handler, } + go func() { + <-ctx.Done() + _ = s.Shutdown() + }() if err := s.ListenAndServe(); err != nil { mainLog.Error().Err(err).Msg("could not serving on ::1") } @@ -80,6 +84,10 @@ func (p *prog) serveDNS(listenerNum string) error { Net: proto, Handler: handler, } + go func() { + <-ctx.Done() + _ = s.Shutdown() + }() if err := s.ListenAndServe(); err != nil { mainLog.Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr) return err diff --git a/cmd/ctrld/main.go b/cmd/ctrld/main.go index e53f7fd..57ae02d 100644 --- a/cmd/ctrld/main.go +++ b/cmd/ctrld/main.go @@ -73,7 +73,6 @@ func initLogging() { } writers = append(writers, logFile) } - zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs consoleWriter := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) { w.TimeFormat = time.StampMilli }) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 52b9c85..0206402 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -28,6 +28,10 @@ var svcConfig = &service.Config{ } type prog struct { + mu sync.Mutex + waitCh chan struct{} + stopCh chan struct{} + cfg *ctrld.Config cache dnscache.Cacher } @@ -39,6 +43,8 @@ func (p *prog) Start(s service.Service) error { } func (p *prog) run() { + // Wait the caller to signal that we can do our logic. + <-p.waitCh p.preRun() if p.cfg.Service.CacheEnable { cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize) @@ -106,7 +112,9 @@ func (p *prog) run() { } else { mainLog.Info().Msg("writing config file to: " + defaultConfigFile) } + p.mu.Lock() p.cfg.Service.AllocateIP = true + p.mu.Unlock() p.preRun() mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, net.JoinHostPort(ip, strconv.Itoa(port))) if err := p.serveDNS(listenerNum); err != nil { @@ -128,10 +136,13 @@ func (p *prog) Stop(s service.Service) error { return err } mainLog.Info().Msg("Service stopped") + close(p.stopCh) return nil } func (p *prog) allocateIP(ip string) error { + p.mu.Lock() + defer p.mu.Unlock() if !p.cfg.Service.AllocateIP { return nil } @@ -139,6 +150,8 @@ func (p *prog) allocateIP(ip string) error { } func (p *prog) deAllocateIP() error { + p.mu.Lock() + defer p.mu.Unlock() if !p.cfg.Service.AllocateIP { return nil } diff --git a/config.go b/config.go index 47c2315..fb97901 100644 --- a/config.go +++ b/config.go @@ -155,6 +155,12 @@ func (uc *UpstreamConfig) Init() { // SetupBootstrapIP manually find all available IPs of the upstream. // The first usable IP will be used as bootstrap IP of the upstream. func (uc *UpstreamConfig) SetupBootstrapIP() { + uc.setupBootstrapIP(true) +} + +// SetupBootstrapIP manually find all available IPs of the upstream. +// The first usable IP will be used as bootstrap IP of the upstream. +func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) { bootstrapIP := func(record dns.RR) string { switch ar := record.(type) { case *dns.A: @@ -166,7 +172,9 @@ func (uc *UpstreamConfig) SetupBootstrapIP() { } resolver := &osResolver{nameservers: availableNameservers()} - resolver.nameservers = append([]string{net.JoinHostPort(bootstrapDNS, "53")}, resolver.nameservers...) + if withBootstrapDNS { + resolver.nameservers = append([]string{net.JoinHostPort(bootstrapDNS, "53")}, resolver.nameservers...) + } ProxyLog.Debug().Msgf("Resolving %q using bootstrap DNS %q", uc.Domain, resolver.nameservers) timeoutMs := 2000 if uc.Timeout > 0 && uc.Timeout < timeoutMs { @@ -228,9 +236,13 @@ func (uc *UpstreamConfig) ReBootstrap() { default: return } - _, _, _ = uc.g.Do("rebootstrap", func() (any, error) { + _, _, _ = uc.g.Do("ReBootstrap", func() (any, error) { ProxyLog.Debug().Msg("re-bootstrapping upstream ip") n := uint32(len(uc.bootstrapIPs)) + if n == 0 { + uc.SetupBootstrapIP() + uc.setupTransportWithoutPingUpstream() + } timeoutMs := 1000 if uc.Timeout > 0 && uc.Timeout < timeoutMs { @@ -368,3 +380,27 @@ func availableNameservers() []string { } return nss[:n] } + +// ResolverTypeFromEndpoint tries guessing the resolver type with a given endpoint +// using following rules: +// +// - If endpoint is an IP address -> ResolverTypeLegacy +// - If endpoint starts with "https://" -> ResolverTypeDOH +// - If endpoint starts with "quic://" -> ResolverTypeDOQ +// - For anything else -> ResolverTypeDOT +func ResolverTypeFromEndpoint(endpoint string) string { + switch { + case strings.HasPrefix(endpoint, "https://"): + return ResolverTypeDOH + case strings.HasPrefix(endpoint, "quic://"): + return ResolverTypeDOQ + } + host := endpoint + if strings.Contains(endpoint, ":") { + host, _, _ = net.SplitHostPort(host) + } + if ip := net.ParseIP(host); ip != nil { + return ResolverTypeLegacy + } + return ResolverTypeDOT +} diff --git a/config_internal_test.go b/config_internal_test.go new file mode 100644 index 0000000..0a457d3 --- /dev/null +++ b/config_internal_test.go @@ -0,0 +1,21 @@ +package ctrld + +import ( + "testing" +) + +func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) { + uc := &UpstreamConfig{ + Name: "test", + Type: ResolverTypeDOH, + Endpoint: "https://freedns.controld.com/p2", + Timeout: 5000, + } + uc.Init() + uc.setupBootstrapIP(false) + if uc.BootstrapIP == "" { + t.Log(availableNameservers()) + t.Fatal("could not bootstrap ip without bootstrap DNS") + } + t.Log(uc) +} diff --git a/go.mod b/go.mod index 7193372..1624260 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,18 @@ module github.com/Control-D-Inc/ctrld -go 1.19 +go 1.20 require ( github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 + github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19 github.com/frankban/quicktest v1.14.3 github.com/fsnotify/fsnotify v1.6.0 github.com/go-playground/validator/v10 v10.11.1 github.com/godbus/dbus/v5 v5.0.6 github.com/hashicorp/golang-lru/v2 v2.0.1 github.com/illarion/gonotify v1.0.1 - github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e + github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 + github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 github.com/kardianos/service v1.2.1 github.com/miekg/dns v1.1.50 github.com/pelletier/go-toml/v2 v2.0.6 @@ -19,10 +21,11 @@ require ( github.com/spf13/cobra v1.4.0 github.com/spf13/viper v1.14.0 github.com/stretchr/testify v1.8.1 + golang.org/x/net v0.7.0 golang.org/x/sync v0.1.0 golang.org/x/sys v0.5.0 golang.zx2c4.com/wireguard/windows v0.5.3 - tailscale.com v1.34.1 + tailscale.com v1.38.3 ) require ( @@ -36,7 +39,6 @@ require ( github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/josharian/native v1.0.0 // indirect github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect github.com/kr/pretty v0.3.0 // indirect github.com/kr/text v0.2.0 // indirect @@ -45,9 +47,9 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect - github.com/mdlayher/netlink v1.6.0 // indirect + github.com/mdlayher/netlink v1.7.1 // indirect github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect - github.com/mdlayher/socket v0.2.3 // indirect + github.com/mdlayher/socket v0.4.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/onsi/ginkgo/v2 v2.2.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect @@ -62,14 +64,13 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.1 // indirect - github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect + github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect - golang.org/x/crypto v0.4.0 // indirect + golang.org/x/crypto v0.6.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect - golang.org/x/mod v0.6.0 // indirect - golang.org/x/net v0.7.0 // indirect + golang.org/x/mod v0.7.0 // indirect golang.org/x/text v0.7.0 // indirect - golang.org/x/tools v0.2.0 // indirect + golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index a158354..c3cb4be 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,8 @@ github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27 github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19 h1:7P/f19Mr0oa3ug8BYt4JuRe/Zq3dF4Mrr4m8+Kw+Hcs= +github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19/go.mod h1:G45410zMgmnSjLVKCq4f6GpbYAzoP2plX9rPwgx6C24= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -163,10 +165,12 @@ github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwso github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e h1:IQpunlq7T+NiJJMO7ODYV2YWBiv/KnObR3gofX0mWOo= -github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= -github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= +github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg= +github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE= github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= +github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= @@ -202,14 +206,15 @@ github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= -github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0= github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= +github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTmg= +github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ= github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= -github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM= -github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY= +github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw= +github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -275,9 +280,8 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= -github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIzeqnPzQcsLqqvL6vEfTPinME= -github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= +github.com/u-root/uio v0.0.0-20221213070652-c3537552635f h1:dpx1PHxYqAnXzbryJrWP1NQLzEjwcVgFLhkknuFQ7ww= +github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -299,8 +303,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -337,8 +341,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -448,7 +452,6 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -456,7 +459,9 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -524,8 +529,8 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d h1:9ZNWAi4CYhNv60mXGgAncgq7SGc5qa7C8VZV8Tg7Ggs= +golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -645,5 +650,5 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -tailscale.com v1.34.1 h1:tqm9Ww4ltyYp3IPe7vCGch6tT6j5G/WXPQ6BrVZ6pdI= -tailscale.com v1.34.1/go.mod h1:ZsBP7rjzzB2rp+UCOumr9DAe0EQ6OPivwSXcz/BrekQ= +tailscale.com v1.38.3 h1:2aX3+u0Re8QcN6nq7zf9Aa4ZCR2Nf6Imv3isqdQrb58= +tailscale.com v1.38.3/go.mod h1:UWLQxcd8dz+lds2I+HpfXSruHrvXM1j4zd4zdx86t7w= diff --git a/internal/controld/config.go b/internal/controld/config.go index 3994837..f323f99 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -7,16 +7,26 @@ import ( "fmt" "net" "net/http" + "sync" "time" + "github.com/miekg/dns" + + "github.com/Control-D-Inc/ctrld" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" ) const ( + apiDomain = "api.controld.com" resolverDataURL = "https://api.controld.com/utility" InvalidConfigCode = 40401 ) +var ( + resolveAPIDomainOnce sync.Once + apiDomainIP string +) + // ResolverConfig represents Control D resolver data. type ResolverConfig struct { DOH string `json:"doh"` @@ -64,6 +74,44 @@ func FetchResolverConfig(uid string) (*ResolverConfig, error) { if ctrldnet.SupportsIPv4() { proto = "tcp4" } + resolveAPIDomainOnce.Do(func() { + r, err := ctrld.NewResolver(&ctrld.UpstreamConfig{Type: ctrld.ResolverTypeOS}) + if err != nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + msg := new(dns.Msg) + dnsType := dns.TypeAAAA + if proto == "tcp4" { + dnsType = dns.TypeA + } + msg.SetQuestion(apiDomain+".", dnsType) + msg.RecursionDesired = true + answer, err := r.Resolve(ctx, msg) + if err != nil { + return + } + if answer.Rcode != dns.RcodeSuccess || len(answer.Answer) == 0 { + return + } + for _, record := range answer.Answer { + switch ar := record.(type) { + case *dns.A: + apiDomainIP = ar.A.String() + return + case *dns.AAAA: + apiDomainIP = ar.AAAA.String() + return + } + } + }) + if apiDomainIP != "" { + if _, port, _ := net.SplitHostPort(addr); port != "" { + return ctrldnet.Dialer.DialContext(ctx, proto, net.JoinHostPort(apiDomainIP, port)) + } + } return ctrldnet.Dialer.DialContext(ctx, proto, addr) } client := http.Client{ diff --git a/internal/dns/nm.go b/internal/dns/nm.go index 03e6f4a..a8ea923 100644 --- a/internal/dns/nm.go +++ b/internal/dns/nm.go @@ -14,8 +14,8 @@ import ( "time" "github.com/godbus/dbus/v5" + "github.com/josharian/native" "tailscale.com/util/dnsname" - "tailscale.com/util/endian" ) const ( @@ -131,7 +131,7 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error { for _, ip := range config.Nameservers { b := ip.As16() if ip.Is4() { - dnsv4 = append(dnsv4, endian.Native.Uint32(b[12:])) + dnsv4 = append(dnsv4, native.Endian.Uint32(b[12:])) } else { dnsv6 = append(dnsv6, b[:]) } diff --git a/internal/dns/resolvconffile/resolvconffile.go b/internal/dns/resolvconffile/resolvconffile.go index 5572891..b91354d 100644 --- a/internal/dns/resolvconffile/resolvconffile.go +++ b/internal/dns/resolvconffile/resolvconffile.go @@ -15,7 +15,6 @@ import ( "strings" "tailscale.com/util/dnsname" - "tailscale.com/util/strs" ) // Path is the canonical location of resolv.conf. @@ -63,7 +62,7 @@ func Parse(r io.Reader) (*Config, error) { line, _, _ = strings.Cut(line, "#") // remove any comments line = strings.TrimSpace(line) - if s, ok := strs.CutPrefix(line, "nameserver"); ok { + if s, ok := strings.CutPrefix(line, "nameserver"); ok { nameserver := strings.TrimSpace(s) if len(nameserver) == len(s) { return nil, fmt.Errorf("missing space after \"nameserver\" in %q", line) @@ -76,7 +75,7 @@ func Parse(r io.Reader) (*Config, error) { continue } - if s, ok := strs.CutPrefix(line, "search"); ok { + if s, ok := strings.CutPrefix(line, "search"); ok { domains := strings.TrimSpace(s) if len(domains) == len(s) { // No leading space?! diff --git a/internal/net/net.go b/internal/net/net.go index 888a2d6..4e71206 100644 --- a/internal/net/net.go +++ b/internal/net/net.go @@ -66,7 +66,7 @@ func supportListenIPv6Local() bool { } func probeStack() { - b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, time.Minute) + b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, 5*time.Second) for { if _, err := probeStackDialer.Dial("udp", bootstrapDNS); err == nil { hasNetworkUp = true diff --git a/nameservers.go b/nameservers.go new file mode 100644 index 0000000..ce99a3b --- /dev/null +++ b/nameservers.go @@ -0,0 +1,29 @@ +package ctrld + +import "net" + +type dnsFn func() []string + +func nameservers() []string { + var dns []string + seen := make(map[string]bool) + ch := make(chan []string) + fns := dnsFns() + + for _, fn := range fns { + go func(fn dnsFn) { + ch <- fn() + }(fn) + } + for range fns { + for _, ns := range <-ch { + if seen[ns] { + continue + } + seen[ns] = true + dns = append(dns, net.JoinHostPort(ns, "53")) + } + } + + return dns +} diff --git a/nameservers_bsd.go b/nameservers_bsd.go new file mode 100644 index 0000000..2beebd0 --- /dev/null +++ b/nameservers_bsd.go @@ -0,0 +1,75 @@ +//go:build darwin || dragonfly || freebsd || netbsd || openbsd + +package ctrld + +import ( + "net" + "os/exec" + "runtime" + "strings" + "syscall" + + "golang.org/x/net/route" +) + +func dnsFns() []dnsFn { + return []dnsFn{dnsFromRIB, dnsFromIPConfig} +} + +func dnsFromRIB() []string { + var dns []string + rib, err := route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0) + if err != nil { + return nil + } + messages, err := route.ParseRIB(route.RIBTypeRoute, rib) + if err != nil { + return nil + } + for _, message := range messages { + message, ok := message.(*route.RouteMessage) + if !ok { + continue + } + addresses := message.Addrs + if len(addresses) < 2 { + continue + } + dst, gw := toNetIP(addresses[0]), toNetIP(addresses[1]) + if dst == nil || gw == nil { + continue + } + if gw.IsLoopback() { + continue + } + if dst.Equal(net.IPv4zero) || dst.Equal(net.IPv6zero) { + dns = append(dns, gw.String()) + } + } + return dns +} + +func dnsFromIPConfig() []string { + if runtime.GOOS != "darwin" { + return nil + } + cmd := exec.Command("ipconfig", "getoption", "", "domain_name_server") + out, _ := cmd.Output() + if ip := net.ParseIP(strings.TrimSpace(string(out))); ip != nil { + return []string{ip.String()} + } + return nil +} + +func toNetIP(addr route.Addr) net.IP { + switch t := addr.(type) { + case *route.Inet4Addr: + return net.IPv4(t.IP[0], t.IP[1], t.IP[2], t.IP[3]) + case *route.Inet6Addr: + ip := make(net.IP, net.IPv6len) + copy(ip, t.IP[:]) + return ip + default: + return nil + } +} diff --git a/nameservers_linux.go b/nameservers_linux.go new file mode 100644 index 0000000..8859ea5 --- /dev/null +++ b/nameservers_linux.go @@ -0,0 +1,97 @@ +package ctrld + +import ( + "bufio" + "bytes" + "encoding/hex" + "net" + "os" + + "github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile" +) + +const ( + v4RouteFile = "/proc/net/route" + v6RouteFile = "/proc/net/ipv6_route" +) + +func dnsFns() []dnsFn { + return []dnsFn{dns4, dns6, dnsFromSystemdResolver} +} + +func dns4() []string { + f, err := os.Open(v4RouteFile) + if err != nil { + return nil + } + defer f.Close() + + var dns []string + seen := make(map[string]bool) + s := bufio.NewScanner(f) + first := true + for s.Scan() { + if first { + first = false + continue + } + fields := bytes.Fields(s.Bytes()) + if len(fields) < 2 { + continue + } + + gw := make([]byte, net.IPv4len) + // Third fields is gateway. + if _, err := hex.Decode(gw, fields[2]); err != nil { + continue + } + ip := net.IPv4(gw[3], gw[2], gw[1], gw[0]) + if ip.Equal(net.IPv4zero) || seen[ip.String()] { + continue + } + seen[ip.String()] = true + dns = append(dns, ip.String()) + } + return dns +} + +func dns6() []string { + f, err := os.Open(v6RouteFile) + if err != nil { + return nil + } + defer f.Close() + + var dns []string + s := bufio.NewScanner(f) + for s.Scan() { + fields := bytes.Fields(s.Bytes()) + if len(fields) < 4 { + continue + } + + gw := make([]byte, net.IPv6len) + // Fifth fields is gateway. + if _, err := hex.Decode(gw, fields[4]); err != nil { + continue + } + ip := net.IP(gw) + if ip.Equal(net.IPv6zero) { + continue + } + dns = append(dns, ip.String()) + } + return dns +} + +func dnsFromSystemdResolver() []string { + c, err := resolvconffile.ParseFile("/run/systemd/resolve/resolv.conf") + if err != nil { + return nil + } + ns := make([]string, 0, len(c.Nameservers)) + for _, nameserver := range c.Nameservers { + ns = append(ns, nameserver.String()) + } + return ns +} diff --git a/nameservers_test.go b/nameservers_test.go new file mode 100644 index 0000000..166cced --- /dev/null +++ b/nameservers_test.go @@ -0,0 +1,11 @@ +package ctrld + +import "testing" + +func TestNameservers(t *testing.T) { + ns := nameservers() + if len(ns) == 0 { + t.Fatal("failed to get nameservers") + } + t.Log(ns) +} diff --git a/nameservers_unix.go b/nameservers_unix.go deleted file mode 100644 index 5c765d3..0000000 --- a/nameservers_unix.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !js && !windows - -package ctrld - -import ( - "github.com/Control-D-Inc/ctrld/internal/resolvconffile" -) - -func nameservers() []string { - return resolvconffile.NameServersWithPort() -} diff --git a/nameservers_windows.go b/nameservers_windows.go index 7812f2a..5cd7811 100644 --- a/nameservers_windows.go +++ b/nameservers_windows.go @@ -2,70 +2,59 @@ package ctrld import ( "net" - "os" "syscall" - "unsafe" + + "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "golang.org/x/sys/windows" ) -func nameservers() []string { - aas, err := adapterAddresses() +func dnsFns() []dnsFn { + return []dnsFn{dnsFromAdapter} +} + +func dnsFromAdapter() []string { + aas, err := winipcfg.GetAdaptersAddresses(syscall.AF_UNSPEC, winipcfg.GAAFlagIncludeGateways|winipcfg.GAAFlagIncludePrefix) if err != nil { return nil } - ns := make([]string, 0, len(aas)) + ns := make([]string, 0, len(aas)*2) + seen := make(map[string]bool) + do := func(addr windows.SocketAddress) { + sa, err := addr.Sockaddr.Sockaddr() + if err != nil { + return + } + var ip net.IP + switch sa := sa.(type) { + case *syscall.SockaddrInet4: + ip = net.IPv4(sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3]) + case *syscall.SockaddrInet6: + ip = make(net.IP, net.IPv6len) + copy(ip, sa.Addr[:]) + if ip[0] == 0xfe && ip[1] == 0xc0 { + // Ignore these fec0/10 ones. Windows seems to + // populate them as defaults on its misc rando + // interfaces. + return + } + default: + return + + } + if ip.IsLoopback() || seen[ip.String()] { + return + } + seen[ip.String()] = true + ns = append(ns, ip.String()) + } for _, aa := range aas { - for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next { - sa, err := dns.Address.Sockaddr.Sockaddr() - if err != nil { - continue - } - var ip net.IP - switch sa := sa.(type) { - case *syscall.SockaddrInet4: - ip = net.IPv4(sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3]) - case *syscall.SockaddrInet6: - ip = make(net.IP, net.IPv6len) - copy(ip, sa.Addr[:]) - if ip[0] == 0xfe && ip[1] == 0xc0 { - // Ignore these fec0/10 ones. Windows seems to - // populate them as defaults on its misc rando - // interfaces. - continue - } - default: - // Unexpected type. - continue - } - ns = append(ns, net.JoinHostPort(ip.String(), "53")) + for dns := aa.FirstDNSServerAddress; dns != nil; dns = dns.Next { + do(dns.Address) + } + for gw := aa.FirstGatewayAddress; gw != nil; gw = gw.Next { + do(gw.Address) } } return ns } - -func adapterAddresses() ([]*windows.IpAdapterAddresses, error) { - var b []byte - l := uint32(15000) // recommended initial size - for { - b = make([]byte, l) - err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l) - if err == nil { - if l == 0 { - return nil, nil - } - break - } - if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW { - return nil, os.NewSyscallError("getadaptersaddresses", err) - } - if l <= uint32(len(b)) { - return nil, os.NewSyscallError("getadaptersaddresses", err) - } - } - var aas []*windows.IpAdapterAddresses - for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next { - aas = append(aas, aa) - } - return aas, nil -} diff --git a/resolver.go b/resolver.go index a12c700..45537fa 100644 --- a/resolver.go +++ b/resolver.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "sync" "github.com/miekg/dns" ) @@ -69,8 +70,15 @@ func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error dnsClient := &dns.Client{Net: "udp"} ch := make(chan *osResolverResult, numServers) + var wg sync.WaitGroup + wg.Add(len(o.nameservers)) + go func() { + wg.Wait() + close(ch) + }() for _, server := range o.nameservers { go func(server string) { + defer wg.Done() answer, _, err := dnsClient.ExchangeContext(ctx, msg, server) ch <- &osResolverResult{answer: answer, err: err} }(server) diff --git a/resolver_test.go b/resolver_test.go new file mode 100644 index 0000000..531570b --- /dev/null +++ b/resolver_test.go @@ -0,0 +1,53 @@ +package ctrld + +import ( + "context" + "testing" + "time" + + "github.com/miekg/dns" +) + +func Test_osResolver_Resolve(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + defer cancel() + resolver := &osResolver{nameservers: []string{"127.0.0.127:5353"}} + m := new(dns.Msg) + m.SetQuestion("controld.com.", dns.TypeA) + m.RecursionDesired = true + _, _ = resolver.Resolve(context.Background(), m) + }() + + select { + case <-time.After(10 * time.Second): + t.Error("os resolver hangs") + case <-ctx.Done(): + } +} + +func Test_upstreamTypeFromEndpoint(t *testing.T) { + tests := []struct { + name string + endpoint string + resolverType string + }{ + {"doh", "https://freedns.controld.com/p2", ResolverTypeDOH}, + {"doq", "quic://p2.freedns.controld.com", ResolverTypeDOQ}, + {"dot", "p2.freedns.controld.com", ResolverTypeDOT}, + {"legacy", "8.8.8.8:53", ResolverTypeLegacy}, + {"legacy ipv6", "[2404:6800:4005:809::200e]:53", ResolverTypeLegacy}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if rt := ResolverTypeFromEndpoint(tc.endpoint); rt != tc.resolverType { + t.Errorf("mismatch, want: %s, got: %s", tc.resolverType, rt) + } + }) + } +}