diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 074d713..3a989ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false matrix: os: ["windows-latest", "ubuntu-latest", "macOS-latest"] - go: ["1.21.x"] + go: ["1.23.x"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 @@ -21,6 +21,6 @@ jobs: - run: "go test -race ./..." - uses: dominikh/staticcheck-action@v1.2.0 with: - version: "2023.1.2" + version: "2024.1.1" install-go: false cache-key: ${{ matrix.go }} diff --git a/cmd/cli/ad_others.go b/cmd/cli/ad_others.go new file mode 100644 index 0000000..eb1b506 --- /dev/null +++ b/cmd/cli/ad_others.go @@ -0,0 +1,10 @@ +//go:build !windows + +package cli + +import ( + "github.com/Control-D-Inc/ctrld" +) + +// addExtraSplitDnsRule adds split DNS rule if present. +func addExtraSplitDnsRule(_ *ctrld.Config) {} diff --git a/cmd/cli/ad_windows.go b/cmd/cli/ad_windows.go new file mode 100644 index 0000000..a3e7917 --- /dev/null +++ b/cmd/cli/ad_windows.go @@ -0,0 +1,45 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/Control-D-Inc/ctrld" +) + +// addExtraSplitDnsRule adds split DNS rule for domain if it's part of active directory. +func addExtraSplitDnsRule(cfg *ctrld.Config) { + domain, err := getActiveDirectoryDomain() + if err != nil { + mainLog.Load().Debug().Msgf("unable to get active directory domain: %v", err) + return + } + if domain == "" { + mainLog.Load().Debug().Msg("no active directory domain found") + return + } + for n, lc := range cfg.Listener { + if lc.Policy == nil { + lc.Policy = &ctrld.ListenerPolicyConfig{} + } + domainRule := "*." + strings.TrimPrefix(domain, ".") + for _, rule := range lc.Policy.Rules { + if _, ok := rule[domainRule]; ok { + mainLog.Load().Debug().Msgf("domain rule already exist for listener.%s", n) + return + } + } + mainLog.Load().Debug().Msgf("adding active directory domain for listener.%s", n) + lc.Policy.Rules = append(lc.Policy.Rules, ctrld.Rule{domainRule: []string{}}) + } +} + +// getActiveDirectoryDomain returns AD domain name of this computer. +func getActiveDirectoryDomain() (string, error) { + cmd := "$obj = Get-WmiObject Win32_ComputerSystem; if ($obj.PartOfDomain) { $obj.Domain }" + output, err := powershell(cmd) + if err != nil { + return "", fmt.Errorf("failed to get domain name: %w, output:\n\n%s", err, string(output)) + } + return string(output), nil +} diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 4b5fbdf..1e9c541 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -37,7 +37,7 @@ import ( "github.com/spf13/pflag" "github.com/spf13/viper" "tailscale.com/logtail/backoff" - "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/clientinfo" @@ -52,6 +52,7 @@ const ( windowsForwardersFilename = ".forwarders.txt" oldBinSuffix = "_previous" oldLogSuffix = ".1" + msgExit = "$$EXIT$$" ) var ( @@ -146,6 +147,7 @@ func initCLI() { runCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items") runCmd.Flags().StringVarP(&cdUID, cdUidFlagName, "", "", "Control D resolver uid") runCmd.Flags().StringVarP(&cdOrg, cdOrgFlagName, "", "", "Control D provision token") + runCmd.Flags().StringVarP(&customHostname, customHostnameFlagName, "", "", "Custom hostname passed to ControlD API") runCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain") _ = runCmd.Flags().MarkHidden("dev") runCmd.Flags().StringVarP(&homedir, "homedir", "", "", "") @@ -194,91 +196,46 @@ NOTE: running "ctrld start" without any arguments will start already installed c isCtrldRunning := status == service.StatusRunning isCtrldInstalled := !errors.Is(err, service.ErrNotInstalled) + // Get current running iface, if any. + var currentIface string + // If pin code was set, do not allow running start command. if isCtrldRunning { if err := checkDeactivationPin(s, nil); isCheckDeactivationPinErr(err) { os.Exit(deactivationPinInvalidExitCode) } + currentIface = runningIface(s) } - if !startOnly { - startOnly = len(osArgs) == 0 - } - // If user run "ctrld start" and ctrld is already installed, starting existing service. - if startOnly && isCtrldInstalled { - tryReadingConfigWithNotice(false, true) - if err := v.Unmarshal(&cfg); err != nil { - mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err) - } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - initLogging() - tasks := []task{ - resetDnsTask(p, s), - {s.Stop, false}, - {func() error { - // Save current DNS so we can restore later. - withEachPhysicalInterfaces("", "save DNS settings", func(i *net.Interface) error { - return saveCurrentStaticDNS(i) - }) - return nil - }, false}, - {s.Start, true}, - {noticeWritingControlDConfig, false}, - } - mainLog.Load().Notice().Msg("Starting existing ctrld service") - if doTasks(tasks) { - mainLog.Load().Notice().Msg("Service started") - sockDir, err := socketDir() - if err != nil { - mainLog.Load().Warn().Err(err).Msg("Failed to get socket directory") - os.Exit(1) - } - if cc := newSocketControlClient(s, sockDir); cc != nil { - if resp, _ := cc.post(ifacePath, nil); resp != nil && resp.StatusCode == http.StatusOK { - if iface == "auto" { - iface = defaultIfaceName() - } - logger := mainLog.Load().With().Str("iface", iface).Logger() - logger.Debug().Msg("setting DNS successfully") + reportSetDnsOk := func(sockDir string) { + if cc := newSocketControlClient(ctx, s, sockDir); cc != nil { + if resp, _ := cc.post(ifacePath, nil); resp != nil && resp.StatusCode == http.StatusOK { + if iface == "auto" { + iface = defaultIfaceName() } + logger := mainLog.Load().With().Str("iface", iface).Logger() + logger.Debug().Msg("setting DNS successfully") } - } else { - mainLog.Load().Error().Err(err).Msg("Failed to start existing ctrld service") - os.Exit(1) } - return - } - - if cdUID != "" { - doValidateCdRemoteConfig(cdUID) - } else if uid := cdUIDFromProvToken(); uid != "" { - cdUID = uid - mainLog.Load().Debug().Msg("using uid from provision token") - removeProvTokenFromArgs(sc) - // Pass --cd flag to "ctrld run" command, so the provision token takes no effect. - sc.Arguments = append(sc.Arguments, "--cd="+cdUID) - } - if cdUID != "" { - validateCdUpstreamProtocol() - } - - if err := p.router.ConfigureService(sc); err != nil { - mainLog.Load().Fatal().Err(err).Msg("failed to configure service on router") } // No config path, generating config in HOME directory. noConfigStart := isNoConfigStart(cmd) writeDefaultConfig := !noConfigStart && configBase64 == "" - if configPath != "" { - v.SetConfigFile(configPath) - } + logServerStarted := make(chan struct{}) // A buffer channel to gather log output from runCmd and report // to user in case self-check process failed. runCmdLogCh := make(chan string, 256) ud, err := userHomeDir() sockDir := ud - if err == nil { + if err != nil { + mainLog.Load().Warn().Msg("log server did not start") + close(logServerStarted) + } else { setWorkingDirectory(sc, ud) if configPath == "" && writeDefaultConfig { defaultConfigFile = filepath.Join(ud, defaultConfigFile) @@ -294,6 +251,7 @@ NOTE: running "ctrld start" without any arguments will start already installed c close(runCmdLogCh) _ = os.Remove(sockPath) }() + close(logServerStarted) if conn := runLogServer(sockPath); conn != nil { // Enough buffer for log message, we don't produce // such long log message, but just in case. @@ -303,11 +261,80 @@ NOTE: running "ctrld start" without any arguments will start already installed c if err != nil { return } - runCmdLogCh <- string(buf[:n]) + msg := string(buf[:n]) + if _, _, found := strings.Cut(msg, msgExit); found { + cancel() + } + runCmdLogCh <- msg } } }() } + <-logServerStarted + + if !startOnly { + startOnly = len(osArgs) == 0 + } + // If user run "ctrld start" and ctrld is already installed, starting existing service. + if startOnly && isCtrldInstalled { + tryReadingConfigWithNotice(false, true) + if err := v.Unmarshal(&cfg); err != nil { + mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err) + } + + initLogging() + tasks := []task{ + {s.Stop, false}, + resetDnsTask(p, s, isCtrldInstalled, currentIface), + {func() error { + // Save current DNS so we can restore later. + withEachPhysicalInterfaces("", "", func(i *net.Interface) error { + if err := saveCurrentStaticDNS(i); !errors.Is(err, errSaveCurrentStaticDNSNotSupported) && err != nil { + return err + } + return nil + }) + return nil + }, false}, + {s.Start, true}, + {noticeWritingControlDConfig, false}, + } + mainLog.Load().Notice().Msg("Starting existing ctrld service") + if doTasks(tasks) { + mainLog.Load().Notice().Msg("Service started") + sockDir, err := socketDir() + if err != nil { + mainLog.Load().Warn().Err(err).Msg("Failed to get socket directory") + os.Exit(1) + } + reportSetDnsOk(sockDir) + } else { + mainLog.Load().Error().Err(err).Msg("Failed to start existing ctrld service") + os.Exit(1) + } + return + } + + if cdUID != "" { + doValidateCdRemoteConfig(cdUID) + } else if uid := cdUIDFromProvToken(); uid != "" { + cdUID = uid + mainLog.Load().Debug().Msg("using uid from provision token") + removeOrgFlagsFromArgs(sc) + // Pass --cd flag to "ctrld run" command, so the provision token takes no effect. + sc.Arguments = append(sc.Arguments, "--cd="+cdUID) + } + if cdUID != "" { + validateCdUpstreamProtocol() + } + + if err := p.router.ConfigureService(sc); err != nil { + mainLog.Load().Fatal().Err(err).Msg("failed to configure service on router") + } + + if configPath != "" { + v.SetConfigFile(configPath) + } tryReadingConfigWithNotice(writeDefaultConfig, true) @@ -334,14 +361,17 @@ NOTE: running "ctrld start" without any arguments will start already installed c } tasks := []task{ - resetDnsTask(p, s), {s.Stop, false}, {func() error { return doGenerateNextDNSConfig(nextdns) }, true}, {func() error { return ensureUninstall(s) }, false}, + resetDnsTask(p, s, isCtrldInstalled, currentIface), {func() error { // Save current DNS so we can restore later. - withEachPhysicalInterfaces("", "save DNS settings", func(i *net.Interface) error { - return saveCurrentStaticDNS(i) + withEachPhysicalInterfaces("", "", func(i *net.Interface) error { + if err := saveCurrentStaticDNS(i); !errors.Is(err, errSaveCurrentStaticDNSNotSupported) && err != nil { + return err + } + return nil }) return nil }, false}, @@ -358,19 +388,19 @@ NOTE: running "ctrld start" without any arguments will start already installed c return } - ok, status, err := selfCheckStatus(s, ud, sockDir) + ok, status, err := selfCheckStatus(ctx, s, sockDir) switch { case ok && status == service.StatusRunning: mainLog.Load().Notice().Msg("Service started") default: marker := bytes.Repeat([]byte("="), 32) // If ctrld service is not running, emitting log obtained from ctrld process. - if status != service.StatusRunning { + if status != service.StatusRunning || ctx.Err() != nil { mainLog.Load().Error().Msg("ctrld service may not have started due to an error or misconfiguration, service log:") _, _ = mainLog.Load().Write(marker) haveLog := false for msg := range runCmdLogCh { - _, _ = mainLog.Load().Write([]byte(msg)) + _, _ = mainLog.Load().Write([]byte(strings.ReplaceAll(msg, msgExit, ""))) haveLog = true } // If we're unable to get log from "ctrld run", notice users about it. @@ -396,15 +426,7 @@ NOTE: running "ctrld start" without any arguments will start already installed c uninstall(p, s) os.Exit(1) } - if cc := newSocketControlClient(s, sockDir); cc != nil { - if resp, _ := cc.post(ifacePath, nil); resp != nil && resp.StatusCode == http.StatusOK { - if iface == "auto" { - iface = defaultIfaceName() - } - logger := mainLog.Load().With().Str("iface", iface).Logger() - logger.Debug().Msg("setting DNS successfully") - } - } + reportSetDnsOk(sockDir) } }, } @@ -419,6 +441,7 @@ NOTE: running "ctrld start" without any arguments will start already installed c startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items") startCmd.Flags().StringVarP(&cdUID, cdUidFlagName, "", "", "Control D resolver uid") startCmd.Flags().StringVarP(&cdOrg, cdOrgFlagName, "", "", "Control D provision token") + startCmd.Flags().StringVarP(&customHostname, customHostnameFlagName, "", "", "Custom hostname passed to ControlD API") startCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain") _ = startCmd.Flags().MarkHidden("dev") startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) @@ -543,7 +566,7 @@ NOTE: running "ctrld start" without any arguments will start already installed c mainLog.Load().Warn().Err(err).Msg("Service was restarted, but could not ping the control server") return } - cc := newSocketControlClient(s, dir) + cc := newSocketControlClient(context.TODO(), s, dir) if cc == nil { mainLog.Load().Notice().Msg("Service was not restarted") os.Exit(1) @@ -730,7 +753,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, Short: "List network interfaces of the host", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { + err := netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) { fmt.Printf("Index : %d\n", i.Index) fmt.Printf("Name : %s\n", i.Name) addrs, _ := i.Addrs() @@ -1035,7 +1058,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, } if doTasks(tasks) { if dir, err := socketDir(); err == nil { - if cc := newSocketControlClient(s, dir); cc != nil { + if cc := newSocketControlClient(context.TODO(), s, dir); cc != nil { _, _ = cc.post(ifacePath, nil) return true } @@ -1141,6 +1164,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { reloadDoneCh: make(chan struct{}), dnsWatcherStopCh: make(chan struct{}), apiReloadCh: make(chan *ctrld.Config), + apiForceReloadCh: make(chan struct{}), cfg: &cfg, appCallback: appCallback, } @@ -1161,6 +1185,9 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { p.logConn = lc } } + notifyExitToLogServer := func() { + _, _ = p.logConn.Write([]byte(msgExit)) + } if daemon && runtime.GOOS == "windows" { mainLog.Load().Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.") @@ -1186,8 +1213,12 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { mainLog.Load().Fatal().Err(err).Msg("failed to read base64 config") } processNoConfigFlags(noConfigStart) + + // After s.Run() was called, if ctrld is going to be terminated for any reason, + // write msgExit to p.logConn so others (like "ctrld start") won't have to wait for timeout. p.mu.Lock() if err := v.Unmarshal(&cfg); err != nil { + notifyExitToLogServer() mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err) } p.mu.Unlock() @@ -1203,6 +1234,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { // Wait for network up. if !ctrldnet.Up() { + notifyExitToLogServer() mainLog.Load().Fatal().Msg("network is not up yet") } @@ -1217,6 +1249,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { // time for validating server certificate. Some routers need NTP synchronization // to set the current time, so this check must happen before processCDFlags. if err := p.router.PreRun(); err != nil { + notifyExitToLogServer() mainLog.Load().Fatal().Err(err).Msg("failed to perform router pre-run check") } @@ -1238,6 +1271,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { if errors.As(err, &uer) && uer.ErrorField.Code == controld.InvalidConfigCode { _ = uninstallInvalidCdUID(p, cdLogger, false) } + notifyExitToLogServer() cdLogger.Fatal().Err(err).Msg("failed to fetch resolver config") } } @@ -1250,6 +1284,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { if updated { if err := writeConfigFile(&cfg); err != nil { + notifyExitToLogServer() mainLog.Load().Fatal().Err(err).Msg("failed to write config file") } else { mainLog.Load().Info().Msg("writing config file to: " + defaultConfigFile) @@ -1271,6 +1306,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { } if err := validateConfig(&cfg); err != nil { + notifyExitToLogServer() os.Exit(1) } initCache() @@ -1279,11 +1315,13 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { exe, err := os.Executable() if err != nil { mainLog.Load().Error().Err(err).Msg("failed to find the binary") + notifyExitToLogServer() os.Exit(1) } curDir, err := os.Getwd() if err != nil { mainLog.Load().Error().Err(err).Msg("failed to get current working directory") + notifyExitToLogServer() os.Exit(1) } // If running as daemon, re-run the command in background, with daemon off. @@ -1291,6 +1329,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { cmd.Dir = curDir if err := cmd.Start(); err != nil { mainLog.Load().Error().Err(err).Msg("failed to start process as daemon") + notifyExitToLogServer() os.Exit(1) } mainLog.Load().Info().Int("pid", cmd.Process.Pid).Msg("DNS proxy started") @@ -1339,15 +1378,14 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { close(waitCh) <-stopCh - // Wait goroutines which watches/manipulates DNS settings terminated, - // ensuring that changes to DNS since here won't be reverted. - p.dnsWg.Wait() + p.stopDnsWatchers() for _, f := range p.onStopped { f() } } func writeConfigFile(cfg *ctrld.Config) error { + addExtraSplitDnsRule(cfg) if cfu := v.ConfigFileUsed(); cfu != "" { defaultConfigFile = cfu } else if configPath != "" { @@ -1473,25 +1511,31 @@ func processNoConfigFlags(noConfigStart bool) { endpointAndTyp := func(endpoint string) (string, string) { typ := ctrld.ResolverTypeFromEndpoint(endpoint) - return strings.TrimPrefix(endpoint, "quic://"), typ + endpoint = strings.TrimPrefix(endpoint, "quic://") + if after, found := strings.CutPrefix(endpoint, "h3://"); found { + endpoint = "https://" + after + } + return endpoint, typ } pEndpoint, pType := endpointAndTyp(primaryUpstream) - upstream := map[string]*ctrld.UpstreamConfig{ - "0": { - Name: pEndpoint, - Endpoint: pEndpoint, - Type: pType, - Timeout: 5000, - }, + puc := &ctrld.UpstreamConfig{ + Name: pEndpoint, + Endpoint: pEndpoint, + Type: pType, + Timeout: 5000, } + puc.Init() + upstream := map[string]*ctrld.UpstreamConfig{"0": puc} if secondaryUpstream != "" { sEndpoint, sType := endpointAndTyp(secondaryUpstream) - upstream["1"] = &ctrld.UpstreamConfig{ + suc := &ctrld.UpstreamConfig{ Name: sEndpoint, Endpoint: sEndpoint, Type: sType, Timeout: 5000, } + suc.Init() + upstream["1"] = suc rules := make([]ctrld.Rule, 0, len(domains)) for _, domain := range domains { rules = append(rules, ctrld.Rule{domain: []string{"upstream.1"}}) @@ -1662,7 +1706,7 @@ func netInterface(ifaceName string) (*net.Interface, error) { ifaceName = defaultIfaceName() } var iface *net.Interface - err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { + err := netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) { if i.Name == ifaceName { iface = i.Interface } @@ -1680,7 +1724,7 @@ func defaultIfaceName() string { if ifaceName := router.DefaultInterfaceName(); ifaceName != "" { return ifaceName } - dri, err := interfaces.DefaultRouteInterface() + dri, err := netmon.DefaultRouteInterface() if err != nil { // On WSL 1, the route table does not have any default route. But the fact that // it only uses /etc/resolv.conf for setup DNS, so we can use "lo" here. @@ -1702,7 +1746,7 @@ func defaultIfaceName() string { // - External testing, ensuring query could be sent from ctrld -> upstream. // // Self-check is considered success only if both tests are ok. -func selfCheckStatus(s service.Service, homedir, sockDir string) (bool, service.Status, error) { +func selfCheckStatus(ctx context.Context, s service.Service, sockDir string) (bool, service.Status, error) { status, err := s.Status() if err != nil { mainLog.Load().Warn().Err(err).Msg("could not get service status") @@ -1718,7 +1762,7 @@ func selfCheckStatus(s service.Service, homedir, sockDir string) (bool, service. } mainLog.Load().Debug().Msg("waiting for ctrld listener to be ready") - cc := newSocketControlClient(s, sockDir) + cc := newSocketControlClient(ctx, s, sockDir) if cc == nil { return false, status, errors.New("could not connect to control server") } @@ -1772,6 +1816,7 @@ func selfCheckResolveDomain(ctx context.Context, addr, scope string, domain stri lastErr error ) + oi := osinfo.New() for i := 0; i < maxAttempts; i++ { if domain == "" { return errors.New("empty test domain") @@ -1788,6 +1833,12 @@ func selfCheckResolveDomain(ctx context.Context, addr, scope string, domain stri if errConnectionRefused(exErr) { return exErr } + // Return early if this is MacOS 15.0 and error is timeout error. + var e net.Error + if oi.Name == "darwin" && oi.Version == "15.0" && errors.As(exErr, &e) && e.Timeout() { + mainLog.Load().Warn().Msg("MacOS 15.0 Sequoia has a bug with the firewall which may prevent ctrld from starting. Disable the MacOS firewall and try again") + return exErr + } lastAnswer = r lastErr = exErr bo.BackOff(ctx, fmt.Errorf("ExchangeContext: %w", exErr)) @@ -2314,17 +2365,30 @@ func cdUIDFromProvToken() string { if cdOrg == "" { return "" } - + // Validate custom hostname if provided. + if customHostname != "" && !validHostname(customHostname) { + mainLog.Load().Fatal().Msgf("invalid custom hostname: %q", customHostname) + } + req := &controld.UtilityOrgRequest{ProvToken: cdOrg, Hostname: customHostname} // Process provision token if provided. - resolverConfig, err := controld.FetchResolverUID(cdOrg, rootCmd.Version, cdDev) + resolverConfig, err := controld.FetchResolverUID(req, rootCmd.Version, cdDev) if err != nil { mainLog.Load().Fatal().Err(err).Msgf("failed to fetch resolver uid with provision token: %s", cdOrg) } return resolverConfig.UID } -// removeProvTokenFromArgs removes the --cd-org from command line arguments. -func removeProvTokenFromArgs(sc *service.Config) { +// removeOrgFlagsFromArgs removes organization flags from command line arguments. +// The flags are: +// +// - "--cd-org" +// - "--custom-hostname" +// +// This is necessary because "ctrld run" only need a valid UID, which could be fetched +// using "--cd-org". So if "ctrld start" have already been called with "--cd-org", we +// already have a valid UID to pass to "ctrld run", so we don't have to force "ctrld run" +// to re-do the already done job. +func removeOrgFlagsFromArgs(sc *service.Config) { a := sc.Arguments[:0] skip := false for _, x := range sc.Arguments { @@ -2332,13 +2396,14 @@ func removeProvTokenFromArgs(sc *service.Config) { skip = false continue } - // For "--cd-org XXX", skip it and mark next arg skipped. - if x == "--"+cdOrgFlagName { + // For "--cd-org XXX"/"--custom-hostname XXX", skip them and mark next arg skipped. + if x == "--"+cdOrgFlagName || x == "--"+customHostnameFlagName { skip = true continue } - // For "--cd-org=XXX", just skip it. - if strings.HasPrefix(x, "--"+cdOrgFlagName+"=") { + // For "--cd-org=XXX"/"--custom-hostname=XXX", just skip them. + if strings.HasPrefix(x, "--"+cdOrgFlagName+"=") || + strings.HasPrefix(x, "--"+customHostnameFlagName+"=") { continue } a = append(a, x) @@ -2347,14 +2412,13 @@ func removeProvTokenFromArgs(sc *service.Config) { } // newSocketControlClient returns new control client after control server was started. -func newSocketControlClient(s service.Service, dir string) *controlClient { +func newSocketControlClient(ctx context.Context, s service.Service, dir string) *controlClient { // Return early if service is not running. if status, err := s.Status(); err != nil || status != service.StatusRunning { return nil } bo := backoff.NewBackoff("self-check", logf, 10*time.Second) bo.LogLongerThan = 10 * time.Second - ctx := context.Background() cc := newControlClient(filepath.Join(dir, ctrldControlUnixSock)) timeout := time.NewTimer(30 * time.Second) @@ -2374,6 +2438,8 @@ func newSocketControlClient(s service.Service, dir string) *controlClient { select { case <-timeout.C: return nil + case <-ctx.Done(): + return nil default: } } @@ -2491,7 +2557,7 @@ func checkDeactivationPin(s service.Service, stopCh chan struct{}) error { if s == nil { cc = newSocketControlClientMobile(dir, stopCh) } else { - cc = newSocketControlClient(s, dir) + cc = newSocketControlClient(context.TODO(), s, dir) } if cc == nil { return nil // ctrld is not running. @@ -2575,7 +2641,7 @@ func runInCdMode() bool { func curCdUID() string { if s, _ := newService(&prog{}, svcConfig); s != nil { if dir, _ := socketDir(); dir != "" { - cc := newSocketControlClient(s, dir) + cc := newSocketControlClient(context.TODO(), s, dir) if cc != nil { resp, _ := cc.post(cdPath, nil) if resp != nil { @@ -2621,7 +2687,7 @@ func upgradeUrl(baseUrl string) string { // runningIface returns the value of the iface variable used by ctrld process which is running. func runningIface(s service.Service) string { if sockDir, err := socketDir(); err == nil { - if cc := newSocketControlClient(s, sockDir); cc != nil { + if cc := newSocketControlClient(context.TODO(), s, sockDir); cc != nil { resp, err := cc.post(ifacePath, nil) if err != nil { return "" @@ -2637,17 +2703,20 @@ func runningIface(s service.Service) string { // resetDnsNoLog performs resetting DNS with logging disable. func resetDnsNoLog(p *prog) { - lvl := zerolog.GlobalLevel() - zerolog.SetGlobalLevel(zerolog.Disabled) + // Normally, disable log to prevent annoying users. + if verbose < 3 { + lvl := zerolog.GlobalLevel() + zerolog.SetGlobalLevel(zerolog.Disabled) + p.resetDNS() + zerolog.SetGlobalLevel(lvl) + return + } + // For debugging purpose, still emit log. p.resetDNS() - zerolog.SetGlobalLevel(lvl) } // resetDnsTask returns a task which perform reset DNS operation. -func resetDnsTask(p *prog, s service.Service) task { - status, err := s.Status() - isCtrldInstalled := !errors.Is(err, service.ErrNotInstalled) - isCtrldRunning := status == service.StatusRunning +func resetDnsTask(p *prog, s service.Service, isCtrldInstalled bool, currentRunningIface string) task { return task{func() error { if iface == "" { return nil @@ -2657,11 +2726,14 @@ func resetDnsTask(p *prog, s service.Service) task { // process to reset what setDNS has done properly. oldIface := iface iface = "auto" - if isCtrldRunning { - iface = runningIface(s) + if currentRunningIface != "" { + iface = currentRunningIface } if isCtrldInstalled { mainLog.Load().Debug().Msg("restore system DNS settings") + if status, _ := s.Status(); status == service.StatusRunning { + mainLog.Load().Fatal().Msg("reset DNS while ctrld still running is not safe") + } resetDnsNoLog(p) } iface = oldIface diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index a7c62af..0da0b1d 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -9,6 +9,7 @@ import ( "net" "net/netip" "runtime" + "slices" "strconv" "strings" "sync" @@ -16,8 +17,7 @@ import ( "github.com/miekg/dns" "golang.org/x/sync/errgroup" - "tailscale.com/net/interfaces" - "tailscale.com/net/netaddr" + "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" "github.com/Control-D-Inc/ctrld" @@ -149,6 +149,7 @@ func (p *prog) serveDNS(listenerNum string) error { ufr: ur, }) go p.doSelfUninstall(pr.answer) + answer = pr.answer rtt := time.Since(t) ctrld.Log(ctx, mainLog.Load().Debug(), "received response of %d bytes in %s", answer.Len(), rtt) @@ -166,6 +167,7 @@ func (p *prog) serveDNS(listenerNum string) error { go func() { p.WithLabelValuesInc(statsQueriesCount, labelValues...) p.WithLabelValuesInc(statsClientQueriesCount, []string{ci.IP, ci.Mac, ci.Hostname}...) + p.forceFetchingAPI(domain) }() if err := w.WriteMsg(answer); err != nil { ctrld.Log(ctx, mainLog.Load().Error().Err(err), "serveDNS: failed to send DNS response to client") @@ -408,6 +410,19 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse { upstreams := req.ufr.upstreams serveStaleCache := p.cache != nil && p.cfg.Service.CacheServeStale upstreamConfigs := p.upstreamConfigsFromUpstreamNumbers(upstreams) + + leaked := false + // If ctrld is going to leak query to OS resolver, check remote upstream in background, + // so ctrld could be back to normal operation as long as the network is back online. + if len(upstreamConfigs) > 0 && p.leakingQuery.Load() { + for n, uc := range upstreamConfigs { + go p.checkUpstream(upstreams[n], uc) + } + upstreamConfigs = nil + leaked = true + ctrld.Log(ctx, mainLog.Load().Debug(), "%v is down, leaking query to OS resolver", upstreams) + } + if len(upstreamConfigs) == 0 { upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig} upstreams = []string{upstreamOS} @@ -423,7 +438,11 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse { // 4. Try remote upstream. isLanOrPtrQuery := false if req.ufr.matched { - ctrld.Log(ctx, mainLog.Load().Debug(), "%s, %s, %s -> %v", req.ufr.matchedPolicy, req.ufr.matchedNetwork, req.ufr.matchedRule, upstreams) + if leaked { + ctrld.Log(ctx, mainLog.Load().Debug(), "%s, %s, %s -> %v (leaked)", req.ufr.matchedPolicy, req.ufr.matchedNetwork, req.ufr.matchedRule, upstreams) + } else { + ctrld.Log(ctx, mainLog.Load().Debug(), "%s, %s, %s -> %v", req.ufr.matchedPolicy, req.ufr.matchedNetwork, req.ufr.matchedRule, upstreams) + } } else { switch { case isPrivatePtrLookup(req.msg): @@ -493,10 +512,11 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse { answer, err := resolve1(n, upstreamConfig, msg) if err != nil { ctrld.Log(ctx, mainLog.Load().Error().Err(err), "failed to resolve query") - if errNetworkError(err) { + isNetworkErr := errNetworkError(err) + if isNetworkErr { p.um.increaseFailureCount(upstreams[n]) if p.um.isDown(upstreams[n]) { - go p.um.checkUpstream(upstreams[n], upstreamConfig) + go p.checkUpstream(upstreams[n], upstreamConfig) } } // For timeout error (i.e: context deadline exceed), force re-bootstrapping. @@ -567,6 +587,14 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse { return res } ctrld.Log(ctx, mainLog.Load().Error(), "all %v endpoints failed", upstreams) + if cdUID != "" && p.leakOnUpstreamFailure() { + p.leakingQueryMu.Lock() + if !p.leakingQueryWasRun { + p.leakingQueryWasRun = true + go p.performLeakingQuery() + } + p.leakingQueryMu.Unlock() + } answer := new(dns.Msg) answer.SetRcode(req.msg, dns.RcodeServerFailure) res.answer = answer @@ -817,7 +845,7 @@ func (p *prog) getClientInfo(remoteIP string, msg *dns.Msg) *ctrld.ClientInfo { } else { ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac) } - ci.Self = queryFromSelf(ci.IP) + ci.Self = p.queryFromSelf(ci.IP) // If this is a query from self, but ci.IP is not loopback IP, // try using hostname mapping for lookback IP if presents. if ci.Self { @@ -887,29 +915,71 @@ func (p *prog) selfUninstallCoolOfPeriod() { p.selfUninstallMu.Unlock() } +// performLeakingQuery performs necessary works to leak queries to OS resolver. +func (p *prog) performLeakingQuery() { + mainLog.Load().Warn().Msg("leaking query to OS resolver") + // Signal dns watchers to stop, so changes made below won't be reverted. + p.leakingQuery.Store(true) + p.resetDNS() + ns := ctrld.InitializeOsResolver() + mainLog.Load().Debug().Msgf("re-initialized OS resolver with nameservers: %v", ns) + p.dnsWg.Wait() + p.setDNS() +} + +// forceFetchingAPI sends signal to force syncing API config if run in cd mode, +// and the domain == "cdUID.verify.controld.com" +func (p *prog) forceFetchingAPI(domain string) { + if cdUID == "" { + return + } + resolverID, parent, _ := strings.Cut(domain, ".") + if resolverID != cdUID { + return + } + switch { + case cdDev && parent == "verify.controld.dev": + // match ControlD dev + case parent == "verify.controld.com": + // match ControlD + default: + return + } + _ = p.apiForceReloadGroup.DoChan("force_sync_api", func() (interface{}, error) { + p.apiForceReloadCh <- struct{}{} + // Wait here to prevent abusing API if we are flooded. + time.Sleep(timeDurationOrDefault(p.cfg.Service.ForceRefetchWaitTime, 30) * time.Second) + return nil, nil + }) +} + +// timeDurationOrDefault returns time duration value from n if not nil. +// Otherwise, it returns time duration value defaultN. +func timeDurationOrDefault(n *int, defaultN int) time.Duration { + if n != nil && *n > 0 { + return time.Duration(*n) + } + return time.Duration(defaultN) +} + // queryFromSelf reports whether the input IP is from device running ctrld. -func queryFromSelf(ip string) bool { +func (p *prog) queryFromSelf(ip string) bool { + if val, ok := p.queryFromSelfMap.Load(ip); ok { + return val.(bool) + } netIP := netip.MustParseAddr(ip) - ifaces, err := interfaces.GetList() + regularIPs, loopbackIPs, err := netmon.LocalAddresses() if err != nil { - mainLog.Load().Warn().Err(err).Msg("could not get interfaces list") + mainLog.Load().Warn().Err(err).Msg("could not get local addresses") return false } - for _, iface := range ifaces { - addrs, err := iface.Addrs() - if err != nil { - mainLog.Load().Warn().Err(err).Msgf("could not get interfaces addresses: %s", iface.Name) - continue - } - for _, a := range addrs { - switch v := a.(type) { - case *net.IPNet: - if pfx, ok := netaddr.FromStdIPNet(v); ok && pfx.Addr().Compare(netIP) == 0 { - return true - } - } + for _, localIP := range slices.Concat(regularIPs, loopbackIPs) { + if localIP.Compare(netIP) == 0 { + p.queryFromSelfMap.Store(ip, true) + return true } } + p.queryFromSelfMap.Store(ip, false) return false } diff --git a/cmd/cli/hostname.go b/cmd/cli/hostname.go new file mode 100644 index 0000000..d28435d --- /dev/null +++ b/cmd/cli/hostname.go @@ -0,0 +1,14 @@ +package cli + +import "regexp" + +// validHostname reports whether hostname is a valid hostname. +// A valid hostname contains 3 -> 64 characters and conform to RFC1123. +func validHostname(hostname string) bool { + hostnameLen := len(hostname) + if hostnameLen < 3 || hostnameLen > 64 { + return false + } + validHostnameRfc1123 := regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`) + return validHostnameRfc1123.MatchString(hostname) +} diff --git a/cmd/cli/hostname_test.go b/cmd/cli/hostname_test.go new file mode 100644 index 0000000..f44b231 --- /dev/null +++ b/cmd/cli/hostname_test.go @@ -0,0 +1,35 @@ +package cli + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_validHostname(t *testing.T) { + tests := []struct { + name string + hostname string + valid bool + }{ + {"localhost", "localhost", true}, + {"localdomain", "localhost.localdomain", true}, + {"localhost6", "localhost6.localdomain6", true}, + {"ip6", "ip6-localhost", true}, + {"non-domain", "controld", true}, + {"domain", "controld.com", true}, + {"empty", "", false}, + {"min length", "fo", false}, + {"max length", strings.Repeat("a", 65), false}, + {"special char", "foo!", false}, + {"non-ascii", "fooΩ", false}, + } + for _, tc := range tests { + tc := tc + t.Run(tc.hostname, func(t *testing.T) { + t.Parallel() + assert.True(t, validHostname(tc.hostname) == tc.valid) + }) + } +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 146c58d..b8f6d8d 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -29,6 +29,7 @@ var ( silent bool cdUID string cdOrg string + customHostname string cdDev bool iface string ifaceStartStop string @@ -45,9 +46,10 @@ var ( ) const ( - cdUidFlagName = "cd" - cdOrgFlagName = "cd-org" - nextdnsFlagName = "nextdns" + cdUidFlagName = "cd" + cdOrgFlagName = "cd-org" + customHostnameFlagName = "custom-hostname" + nextdnsFlagName = "nextdns" ) func init() { diff --git a/cmd/cli/net.go b/cmd/cli/net.go deleted file mode 100644 index 80da827..0000000 --- a/cmd/cli/net.go +++ /dev/null @@ -1,34 +0,0 @@ -package cli - -import "strings" - -// Copied from https://gist.github.com/Ultraporing/fe52981f678be6831f747c206a4861cb - -// Mac Address parts to look for, and identify non-physical devices. There may be more, update me! -var macAddrPartsToFilter = []string{ - "00:03:FF", // Microsoft Hyper-V, Virtual Server, Virtual PC - "0A:00:27", // VirtualBox - "00:00:00:00:00", // Teredo Tunneling Pseudo-Interface - "00:50:56", // VMware ESX 3, Server, Workstation, Player - "00:1C:14", // VMware ESX 3, Server, Workstation, Player - "00:0C:29", // VMware ESX 3, Server, Workstation, Player - "00:05:69", // VMware ESX 3, Server, Workstation, Player - "00:1C:42", // Microsoft Hyper-V, Virtual Server, Virtual PC - "00:0F:4B", // Virtual Iron 4 - "00:16:3E", // Red Hat Xen, Oracle VM, XenSource, Novell Xen - "08:00:27", // Sun xVM VirtualBox - "7A:79", // Hamachi -} - -// Filters the possible physical interface address by comparing it to known popular VM Software addresses -// and Teredo Tunneling Pseudo-Interface. -// -//lint:ignore U1000 use in net_windows.go -func isPhysicalInterface(addr string) bool { - for _, macPart := range macAddrPartsToFilter { - if strings.HasPrefix(strings.ToLower(addr), strings.ToLower(macPart)) { - return false - } - } - return true -} diff --git a/cmd/cli/net_darwin.go b/cmd/cli/net_darwin.go index b58a0bf..ece1862 100644 --- a/cmd/cli/net_darwin.go +++ b/cmd/cli/net_darwin.go @@ -49,6 +49,7 @@ func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bo return ok } +// validInterfacesMap returns a set of all valid hardware ports. func validInterfacesMap() map[string]struct{} { b, err := exec.Command("networksetup", "-listallhardwareports").Output() if err != nil { diff --git a/cmd/cli/net_windows.go b/cmd/cli/net_windows.go index 8ec5a5f..dc13b08 100644 --- a/cmd/cli/net_windows.go +++ b/cmd/cli/net_windows.go @@ -1,7 +1,10 @@ package cli import ( + "bufio" + "bytes" "net" + "strings" ) func patchNetIfaceName(iface *net.Interface) error { @@ -11,13 +14,21 @@ func patchNetIfaceName(iface *net.Interface) error { // validInterface reports whether the *net.Interface is a valid one. // On Windows, only physical interfaces are considered valid. func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bool { - if iface == nil { - return false - } - if isPhysicalInterface(iface.HardwareAddr.String()) { - return true - } - return false + _, ok := validIfacesMap[iface.Name] + return ok } -func validInterfacesMap() map[string]struct{} { return nil } +// validInterfacesMap returns a set of all physical interfaces. +func validInterfacesMap() map[string]struct{} { + out, err := powershell("Get-NetAdapter -Physical | Select-Object -ExpandProperty Name") + if err != nil { + return nil + } + m := make(map[string]struct{}) + scanner := bufio.NewScanner(bytes.NewReader(out)) + for scanner.Scan() { + ifaceName := strings.TrimSpace(scanner.Text()) + m[ifaceName] = struct{}{} + } + return m +} diff --git a/cmd/cli/os_freebsd.go b/cmd/cli/os_freebsd.go index cc5ff92..e94012b 100644 --- a/cmd/cli/os_freebsd.go +++ b/cmd/cli/os_freebsd.go @@ -5,6 +5,8 @@ import ( "net/netip" "os/exec" + "tailscale.com/tsd" + "github.com/Control-D-Inc/ctrld/internal/dns" "github.com/Control-D-Inc/ctrld/internal/resolvconffile" ) @@ -36,7 +38,8 @@ func setDnsIgnoreUnusableInterface(iface *net.Interface, nameservers []string) e // set the dns server for the provided network interface func setDNS(iface *net.Interface, nameservers []string) error { - r, err := dns.NewOSConfigurator(logf, iface.Name) + sys := new(tsd.System) + r, err := dns.NewOSConfigurator(logf, sys.HealthTracker(), sys.ControlKnobs(), iface.Name) if err != nil { mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator") return err @@ -60,7 +63,8 @@ func resetDnsIgnoreUnusableInterface(iface *net.Interface) error { } func resetDNS(iface *net.Interface) error { - r, err := dns.NewOSConfigurator(logf, iface.Name) + sys := new(tsd.System) + r, err := dns.NewOSConfigurator(logf, sys.HealthTracker(), sys.ControlKnobs(), iface.Name) if err != nil { mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator") return err diff --git a/cmd/cli/os_linux.go b/cmd/cli/os_linux.go index eff5edf..502935e 100644 --- a/cmd/cli/os_linux.go +++ b/cmd/cli/os_linux.go @@ -14,6 +14,8 @@ import ( "syscall" "time" + "tailscale.com/tsd" + "github.com/insomniacslk/dhcp/dhcpv4/nclient4" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/client6" @@ -54,7 +56,8 @@ func setDnsIgnoreUnusableInterface(iface *net.Interface, nameservers []string) e } func setDNS(iface *net.Interface, nameservers []string) error { - r, err := dns.NewOSConfigurator(logf, iface.Name) + sys := new(tsd.System) + r, err := dns.NewOSConfigurator(logf, sys.HealthTracker(), sys.ControlKnobs(), iface.Name) if err != nil { mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator") return err @@ -136,7 +139,8 @@ func resetDNS(iface *net.Interface) (err error) { if exe, _ := exec.LookPath("/lib/systemd/systemd-networkd"); exe != "" { _ = exec.Command("systemctl", "start", "systemd-networkd").Run() } - if r, oerr := dns.NewOSConfigurator(logf, iface.Name); oerr == nil { + sys := new(tsd.System) + if r, oerr := dns.NewOSConfigurator(logf, sys.HealthTracker(), sys.ControlKnobs(), iface.Name); oerr == nil { _ = r.SetDNS(dns.OSConfig{}) if err := r.Close(); err != nil { mainLog.Load().Error().Err(err).Msg("failed to rollback DNS setting") diff --git a/cmd/cli/os_windows.go b/cmd/cli/os_windows.go index 234764f..b9412b6 100644 --- a/cmd/cli/os_windows.go +++ b/cmd/cli/os_windows.go @@ -119,6 +119,7 @@ func resetDNS(iface *net.Interface) error { if len(ns) == 0 { continue } + mainLog.Load().Debug().Msgf("setting static DNS for interface %q", iface.Name) if err := setDNS(iface, ns); err != nil { return err } diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 6018efb..2a2c59c 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -24,7 +24,8 @@ import ( "github.com/kardianos/service" "github.com/rs/zerolog" "github.com/spf13/viper" - "tailscale.com/net/interfaces" + "golang.org/x/sync/singleflight" + "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" "github.com/Control-D-Inc/ctrld" @@ -68,19 +69,21 @@ var svcConfig = &service.Config{ var useSystemdResolved = false type prog struct { - mu sync.Mutex - waitCh chan struct{} - stopCh chan struct{} - reloadCh chan struct{} // For Windows. - reloadDoneCh chan struct{} - apiReloadCh chan *ctrld.Config - logConn net.Conn - cs *controlServer - csSetDnsDone chan struct{} - csSetDnsOk bool - dnsWatchDogOnce sync.Once - dnsWg sync.WaitGroup - dnsWatcherStopCh chan struct{} + mu sync.Mutex + waitCh chan struct{} + stopCh chan struct{} + reloadCh chan struct{} // For Windows. + reloadDoneCh chan struct{} + apiReloadCh chan *ctrld.Config + apiForceReloadCh chan struct{} + apiForceReloadGroup singleflight.Group + logConn net.Conn + cs *controlServer + csSetDnsDone chan struct{} + csSetDnsOk bool + dnsWg sync.WaitGroup + dnsWatcherClosedOnce sync.Once + dnsWatcherStopCh chan struct{} cfg *ctrld.Config localUpstreams []string @@ -95,6 +98,7 @@ type prog struct { ptrLoopGuard *loopGuard lanLoopGuard *loopGuard metricsQueryStats atomic.Bool + queryFromSelfMap sync.Map selfUninstallMu sync.Mutex refusedQueryCount int @@ -104,6 +108,10 @@ type prog struct { loopMu sync.Mutex loop map[string]bool + leakingQueryMu sync.Mutex + leakingQueryWasRun bool + leakingQuery atomic.Bool + started chan struct{} onStartedDone chan struct{} onStarted []func() @@ -239,6 +247,8 @@ func (p *prog) postRun() { ns := ctrld.InitializeOsResolver() mainLog.Load().Debug().Msgf("initialized OS resolver with nameservers: %v", ns) p.setDNS() + p.csSetDnsDone <- struct{}{} + close(p.csSetDnsDone) } } @@ -248,47 +258,48 @@ func (p *prog) apiConfigReload() { return } - secs := 3600 - if p.cfg.Service.RefetchTime != nil && *p.cfg.Service.RefetchTime > 0 { - secs = *p.cfg.Service.RefetchTime - } - - ticker := time.NewTicker(time.Duration(secs) * time.Second) + ticker := time.NewTicker(timeDurationOrDefault(p.cfg.Service.RefetchTime, 3600) * time.Second) defer ticker.Stop() logger := mainLog.Load().With().Str("mode", "api-reload").Logger() logger.Debug().Msg("starting custom config reload timer") lastUpdated := time.Now().Unix() + + doReloadApiConfig := func(forced bool, logger zerolog.Logger) { + resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev) + selfUninstallCheck(err, p, logger) + if err != nil { + logger.Warn().Err(err).Msg("could not fetch resolver config") + return + } + + if resolverConfig.Ctrld.CustomConfig == "" { + return + } + + if resolverConfig.Ctrld.CustomLastUpdate > lastUpdated || forced { + lastUpdated = time.Now().Unix() + cfg := &ctrld.Config{} + if err := validateCdRemoteConfig(resolverConfig, cfg); err != nil { + logger.Warn().Err(err).Msg("skipping invalid custom config") + if _, err := controld.UpdateCustomLastFailed(cdUID, rootCmd.Version, cdDev, true); err != nil { + logger.Error().Err(err).Msg("could not mark custom last update failed") + } + return + } + setListenerDefaultValue(cfg) + logger.Debug().Msg("custom config changes detected, reloading...") + p.apiReloadCh <- cfg + } else { + logger.Debug().Msg("custom config does not change") + } + } for { select { + case <-p.apiForceReloadCh: + doReloadApiConfig(true, logger.With().Bool("forced", true).Logger()) case <-ticker.C: - resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev) - selfUninstallCheck(err, p, logger) - if err != nil { - logger.Warn().Err(err).Msg("could not fetch resolver config") - continue - } - - if resolverConfig.Ctrld.CustomConfig == "" { - continue - } - - if resolverConfig.Ctrld.CustomLastUpdate > lastUpdated { - lastUpdated = time.Now().Unix() - cfg := &ctrld.Config{} - if err := validateCdRemoteConfig(resolverConfig, cfg); err != nil { - logger.Warn().Err(err).Msg("skipping invalid custom config") - if _, err := controld.UpdateCustomLastFailed(cdUID, rootCmd.Version, cdDev, true); err != nil { - logger.Error().Err(err).Msg("could not mark custom last update failed") - } - break - } - setListenerDefaultValue(cfg) - logger.Debug().Msg("custom config changes detected, reloading...") - p.apiReloadCh <- cfg - } else { - logger.Debug().Msg("custom config does not change") - } + doReloadApiConfig(false, logger) case <-p.stopCh: return } @@ -301,7 +312,11 @@ func (p *prog) setupUpstream(cfg *ctrld.Config) { isControlDUpstream := false for n := range cfg.Upstream { uc := cfg.Upstream[n] + sdns := uc.Type == ctrld.ResolverTypeSDNS uc.Init() + if sdns { + mainLog.Load().Debug().Msgf("initialized DNS Stamps with endpoint: %s, type: %s", uc.Endpoint, uc.Type) + } isControlDUpstream = isControlDUpstream || uc.IsControlD() if uc.BootstrapIP == "" { uc.SetupBootstrapIP() @@ -497,6 +512,8 @@ func (p *prog) metricsEnabled() bool { } func (p *prog) Stop(s service.Service) error { + p.stopDnsWatchers() + mainLog.Load().Debug().Msg("dns watchers stopped") mainLog.Load().Info().Msg("Service stopped") close(p.stopCh) if err := p.deAllocateIP(); err != nil { @@ -506,6 +523,15 @@ func (p *prog) Stop(s service.Service) error { return nil } +func (p *prog) stopDnsWatchers() { + // Ensure all DNS watchers goroutine are terminated, + // so it won't mess up with other DNS changes. + p.dnsWatcherClosedOnce.Do(func() { + close(p.dnsWatcherStopCh) + }) + p.dnsWg.Wait() +} + func (p *prog) allocateIP(ip string) error { p.mu.Lock() defer p.mu.Unlock() @@ -533,8 +559,6 @@ func (p *prog) setDNS() { setDnsOK := false defer func() { p.csSetDnsOk = setDnsOK - p.csSetDnsDone <- struct{}{} - close(p.csSetDnsDone) }() if cfg.Listener == nil { @@ -598,6 +622,11 @@ func (p *prog) setDNS() { } setDnsOK = true logger.Debug().Msg("setting DNS successfully") + if allIfaces { + withEachPhysicalInterfaces(netIface.Name, "set DNS", func(i *net.Interface) error { + return setDnsIgnoreUnusableInterface(i, nameservers) + }) + } if shouldWatchResolvconf() { servers := make([]netip.Addr, len(nameservers)) for i := range nameservers { @@ -609,11 +638,6 @@ func (p *prog) setDNS() { p.watchResolvConf(netIface, servers, setResolvConf) }() } - if allIfaces { - withEachPhysicalInterfaces(netIface.Name, "set DNS", func(i *net.Interface) error { - return setDnsIgnoreUnusableInterface(i, nameservers) - }) - } if p.dnsWatchdogEnabled() { p.dnsWg.Add(1) go func() { @@ -648,41 +672,42 @@ func (p *prog) dnsWatchdog(iface *net.Interface, nameservers []string, allIfaces return } - p.dnsWatchDogOnce.Do(func() { - mainLog.Load().Debug().Msg("start DNS settings watchdog") - ns := nameservers - slices.Sort(ns) - ticker := time.NewTicker(p.dnsWatchdogDuration()) - logger := mainLog.Load().With().Str("iface", iface.Name).Logger() - for { - select { - case <-p.dnsWatcherStopCh: + mainLog.Load().Debug().Msg("start DNS settings watchdog") + ns := nameservers + slices.Sort(ns) + ticker := time.NewTicker(p.dnsWatchdogDuration()) + logger := mainLog.Load().With().Str("iface", iface.Name).Logger() + for { + select { + case <-p.dnsWatcherStopCh: + return + case <-p.stopCh: + mainLog.Load().Debug().Msg("stop dns watchdog") + return + case <-ticker.C: + if p.leakingQuery.Load() { return - case <-p.stopCh: - mainLog.Load().Debug().Msg("stop dns watchdog") - return - case <-ticker.C: - if dnsChanged(iface, ns) { - logger.Debug().Msg("DNS settings were changed, re-applying settings") - if err := setDNS(iface, ns); err != nil { - mainLog.Load().Error().Err(err).Str("iface", iface.Name).Msgf("could not re-apply DNS settings") - } - } - if allIfaces { - withEachPhysicalInterfaces(iface.Name, "", func(i *net.Interface) error { - if dnsChanged(i, ns) { - if err := setDnsIgnoreUnusableInterface(i, nameservers); err != nil { - mainLog.Load().Error().Err(err).Str("iface", i.Name).Msgf("could not re-apply DNS settings") - } else { - mainLog.Load().Debug().Msgf("re-applying DNS for interface %q successfully", i.Name) - } - } - return nil - }) + } + if dnsChanged(iface, ns) { + logger.Debug().Msg("DNS settings were changed, re-applying settings") + if err := setDNS(iface, ns); err != nil { + mainLog.Load().Error().Err(err).Str("iface", iface.Name).Msgf("could not re-apply DNS settings") } } + if allIfaces { + withEachPhysicalInterfaces(iface.Name, "", func(i *net.Interface) error { + if dnsChanged(i, ns) { + if err := setDnsIgnoreUnusableInterface(i, nameservers); err != nil { + mainLog.Load().Error().Err(err).Str("iface", i.Name).Msgf("could not re-apply DNS settings") + } else { + mainLog.Load().Debug().Msgf("re-applying DNS for interface %q successfully", i.Name) + } + } + return nil + }) + } } - }) + } } func (p *prog) resetDNS() { @@ -717,6 +742,18 @@ func (p *prog) resetDNS() { } } +// leakOnUpstreamFailure reports whether ctrld should leak query to OS resolver when failed to connect all upstreams. +func (p *prog) leakOnUpstreamFailure() bool { + if ptr := p.cfg.Service.LeakOnUpstreamFailure; ptr != nil { + return *ptr + } + // Default is false on routers, since this leaking is only useful for devices that move between networks. + if router.Name() != "" { + return false + } + return true +} + func randomLocalIP() string { n := rand.Intn(254-2) + 2 return fmt.Sprintf("127.0.0.%d", n) @@ -834,7 +871,7 @@ func ifaceFirstPrivateIP(iface *net.Interface) string { // defaultRouteIP returns private IP string of the default route if present, prefer IPv4 over IPv6. func defaultRouteIP() string { - dr, err := interfaces.DefaultRoute() + dr, err := netmon.DefaultRoute() if err != nil { return "" } @@ -854,7 +891,7 @@ func defaultRouteIP() string { // There could be multiple LAN interfaces with the same Mac address, so we find all private // IPs then using the smallest one. var addrs []netip.Addr - interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { + netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) { if i.Name == drNetIface.Name { return } @@ -894,7 +931,7 @@ func canBeLocalUpstream(addr string) bool { // log message when error happens. func withEachPhysicalInterfaces(excludeIfaceName, context string, f func(i *net.Interface) error) { validIfacesMap := validInterfacesMap() - interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { + netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) { // Skip loopback/virtual interface. if i.IsLoopback() || len(i.HardwareAddr) == 0 { return @@ -952,11 +989,13 @@ func saveCurrentStaticDNS(iface *net.Interface) error { if err := os.Remove(file); err != nil && !errors.Is(err, fs.ErrNotExist) { mainLog.Load().Warn().Err(err).Msg("could not remove old static DNS settings file") } - mainLog.Load().Debug().Msgf("DNS settings for %s is static, saving ...", iface.Name) - if err := os.WriteFile(file, []byte(strings.Join(ns, ",")), 0600); err != nil { + nss := strings.Join(ns, ",") + mainLog.Load().Debug().Msgf("DNS settings for %q is static: %v, saving ...", iface.Name, nss) + if err := os.WriteFile(file, []byte(nss), 0600); err != nil { mainLog.Load().Err(err).Msgf("could not save DNS settings for iface: %s", iface.Name) return err } + mainLog.Load().Debug().Msgf("save DNS settings for interface %q successfully", iface.Name) return nil } @@ -992,9 +1031,7 @@ func dnsChanged(iface *net.Interface, nameservers []string) bool { func selfUninstallCheck(uninstallErr error, p *prog, logger zerolog.Logger) { var uer *controld.UtilityErrorResponse if errors.As(uninstallErr, &uer) && uer.ErrorField.Code == controld.InvalidConfigCode { - // Ensure all DNS watchers goroutine are terminated, so it won't mess up with self-uninstall. - close(p.dnsWatcherStopCh) - p.dnsWg.Wait() + p.stopDnsWatchers() // Perform self-uninstall now. selfUninstall(p, logger) diff --git a/cmd/cli/prog_linux.go b/cmd/cli/prog_linux.go index 0af906d..a6963f1 100644 --- a/cmd/cli/prog_linux.go +++ b/cmd/cli/prog_linux.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "strings" + "tailscale.com/tsd" "github.com/kardianos/service" @@ -14,7 +15,8 @@ import ( ) func init() { - if r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo"); err == nil { + sys := new(tsd.System) + if r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, sys.HealthTracker(), sys.ControlKnobs(), "lo"); err == nil { useSystemdResolved = r.Mode() == "systemd-resolved" } // Disable quic-go's ECN support by default, see https://github.com/quic-go/quic-go/issues/3911 diff --git a/cmd/cli/resolvconf.go b/cmd/cli/resolvconf.go index 5be34fc..6df7be6 100644 --- a/cmd/cli/resolvconf.go +++ b/cmd/cli/resolvconf.go @@ -40,6 +40,9 @@ func (p *prog) watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn f mainLog.Load().Debug().Msgf("stopping watcher for %s", resolvConfPath) return case event, ok := <-watcher.Events: + if p.leakingQuery.Load() { + return + } if !ok { return } diff --git a/cmd/cli/resolvconf_not_darwin_unix.go b/cmd/cli/resolvconf_not_darwin_unix.go index b98496e..dada4e9 100644 --- a/cmd/cli/resolvconf_not_darwin_unix.go +++ b/cmd/cli/resolvconf_not_darwin_unix.go @@ -6,6 +6,7 @@ import ( "net" "net/netip" + "tailscale.com/tsd" "tailscale.com/util/dnsname" "github.com/Control-D-Inc/ctrld/internal/dns" @@ -13,7 +14,8 @@ import ( // setResolvConf sets the content of resolv.conf file using the given nameservers list. func setResolvConf(iface *net.Interface, ns []netip.Addr) error { - r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter. + sys := new(tsd.System) + r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, sys.HealthTracker(), sys.ControlKnobs(), "lo") // interface name does not matter. if err != nil { return err } @@ -27,7 +29,8 @@ func setResolvConf(iface *net.Interface, ns []netip.Addr) error { // shouldWatchResolvconf reports whether ctrld should watch changes to resolv.conf file with given OS configurator. func shouldWatchResolvconf() bool { - r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter. + sys := new(tsd.System) + r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, sys.HealthTracker(), sys.ControlKnobs(), "lo") // interface name does not matter. if err != nil { return false } diff --git a/cmd/cli/upstream_monitor.go b/cmd/cli/upstream_monitor.go index 67ae13d..b17cb32 100644 --- a/cmd/cli/upstream_monitor.go +++ b/cmd/cli/upstream_monitor.go @@ -71,19 +71,19 @@ func (um *upstreamMonitor) reset(upstream string) { // checkUpstream checks the given upstream status, periodically sending query to upstream // until successfully. An upstream status/counter will be reset once it becomes reachable. -func (um *upstreamMonitor) checkUpstream(upstream string, uc *ctrld.UpstreamConfig) { - um.mu.Lock() - isChecking := um.checking[upstream] +func (p *prog) checkUpstream(upstream string, uc *ctrld.UpstreamConfig) { + p.um.mu.Lock() + isChecking := p.um.checking[upstream] if isChecking { - um.mu.Unlock() + p.um.mu.Unlock() return } - um.checking[upstream] = true - um.mu.Unlock() + p.um.checking[upstream] = true + p.um.mu.Unlock() defer func() { - um.mu.Lock() - um.checking[upstream] = false - um.mu.Unlock() + p.um.mu.Lock() + p.um.checking[upstream] = false + p.um.mu.Unlock() }() resolver, err := ctrld.NewResolver(uc) @@ -104,7 +104,13 @@ func (um *upstreamMonitor) checkUpstream(upstream string, uc *ctrld.UpstreamConf for { if err := check(); err == nil { mainLog.Load().Debug().Msgf("upstream %q is online", uc.Endpoint) - um.reset(upstream) + p.um.reset(upstream) + if p.leakingQuery.CompareAndSwap(true, false) { + p.leakingQueryMu.Lock() + p.leakingQueryWasRun = false + p.leakingQueryMu.Unlock() + mainLog.Load().Warn().Msg("stop leaking query") + } return } time.Sleep(checkUpstreamBackoffSleep) diff --git a/config.go b/config.go index e09fdad..3f9b2f8 100644 --- a/config.go +++ b/config.go @@ -7,6 +7,7 @@ import ( "crypto/x509" "encoding/hex" "errors" + "fmt" "io" "math/rand" "net" @@ -22,6 +23,7 @@ import ( "sync/atomic" "time" + "github.com/ameshkov/dnsstamps" "github.com/go-playground/validator/v10" "github.com/miekg/dns" "github.com/spf13/viper" @@ -59,6 +61,11 @@ const ( controlDComDomain = "controld.com" controlDNetDomain = "controld.net" controlDDevDomain = "controld.dev" + + endpointPrefixHTTPS = "https://" + endpointPrefixQUIC = "quic://" + endpointPrefixH3 = "h3://" + endpointPrefixSdns = "sdns://" ) var ( @@ -211,6 +218,8 @@ type ServiceConfig struct { DnsWatchdogEnabled *bool `mapstructure:"dns_watchdog_enabled" toml:"dns_watchdog_enabled,omitempty"` DnsWatchdogInvterval *time.Duration `mapstructure:"dns_watchdog_interval" toml:"dns_watchdog_interval,omitempty"` RefetchTime *int `mapstructure:"refetch_time" toml:"refetch_time,omitempty"` + ForceRefetchWaitTime *int `mapstructure:"force_refetch_wait_time" toml:"force_refetch_wait_time,omitempty"` + LeakOnUpstreamFailure *bool `mapstructure:"leak_on_upstream_failure" toml:"leak_on_upstream_failure,omitempty"` Daemon bool `mapstructure:"-" toml:"-"` AllocateIP bool `mapstructure:"-" toml:"-"` } @@ -225,7 +234,7 @@ type NetworkConfig struct { // UpstreamConfig specifies configuration for upstreams that ctrld will forward requests to. type UpstreamConfig struct { Name string `mapstructure:"name" toml:"name,omitempty"` - Type string `mapstructure:"type" toml:"type,omitempty" validate:"oneof=doh doh3 dot doq os legacy"` + Type string `mapstructure:"type" toml:"type,omitempty" validate:"oneof=doh doh3 dot doq os legacy sdns ''"` Endpoint string `mapstructure:"endpoint" toml:"endpoint,omitempty"` BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip,omitempty"` Domain string `mapstructure:"-" toml:"-"` @@ -299,10 +308,13 @@ type Rule map[string][]string // Init initialized necessary values for an UpstreamConfig. func (uc *UpstreamConfig) Init() { + if err := uc.initDnsStamps(); err != nil { + ProxyLogger.Load().Fatal().Err(err).Msg("invalid DNS Stamps") + } uc.initDoHScheme() uc.uid = upstreamUID() if u, err := url.Parse(uc.Endpoint); err == nil { - uc.Domain = u.Host + uc.Domain = u.Hostname() switch uc.Type { case ResolverTypeDOH, ResolverTypeDOH3: uc.u = u @@ -676,16 +688,67 @@ func (uc *UpstreamConfig) netForDNSType(dnsType uint16) (string, string) { // initDoHScheme initializes the endpoint scheme for DoH/DoH3 upstream if not present. func (uc *UpstreamConfig) initDoHScheme() { + if strings.HasPrefix(uc.Endpoint, endpointPrefixH3) && uc.Type == "" { + uc.Type = ResolverTypeDOH3 + } switch uc.Type { - case ResolverTypeDOH, ResolverTypeDOH3: + case ResolverTypeDOH: + case ResolverTypeDOH3: + if after, found := strings.CutPrefix(uc.Endpoint, endpointPrefixH3); found { + uc.Endpoint = endpointPrefixHTTPS + after + } default: return } - if !strings.HasPrefix(uc.Endpoint, "https://") { - uc.Endpoint = "https://" + uc.Endpoint + if !strings.HasPrefix(uc.Endpoint, endpointPrefixHTTPS) { + uc.Endpoint = endpointPrefixHTTPS + uc.Endpoint } } +// initDnsStamps initializes upstream config based on encoded DNS Stamps Endpoint. +func (uc *UpstreamConfig) initDnsStamps() error { + if strings.HasPrefix(uc.Endpoint, endpointPrefixSdns) && uc.Type == "" { + uc.Type = ResolverTypeSDNS + } + if uc.Type != ResolverTypeSDNS { + return nil + } + sdns, err := dnsstamps.NewServerStampFromString(uc.Endpoint) + if err != nil { + return err + } + ip, port, _ := net.SplitHostPort(sdns.ServerAddrStr) + providerName, port2, _ := net.SplitHostPort(sdns.ProviderName) + if port2 != "" { + port = port2 + } + if providerName == "" { + providerName = sdns.ProviderName + } + switch sdns.Proto { + case dnsstamps.StampProtoTypeDoH: + uc.Type = ResolverTypeDOH + host := sdns.ProviderName + if port != "" && port != defaultPortFor(uc.Type) { + host = net.JoinHostPort(providerName, port) + } + uc.Endpoint = "https://" + host + sdns.Path + case dnsstamps.StampProtoTypeTLS: + uc.Type = ResolverTypeDOT + uc.Endpoint = net.JoinHostPort(providerName, port) + case dnsstamps.StampProtoTypeDoQ: + uc.Type = ResolverTypeDOQ + uc.Endpoint = net.JoinHostPort(providerName, port) + case dnsstamps.StampProtoTypePlain: + uc.Type = ResolverTypeLegacy + uc.Endpoint = sdns.ServerAddrStr + default: + return fmt.Errorf("unsupported stamp protocol %q", sdns.Proto) + } + uc.BootstrapIP = ip + return nil +} + // Init initialized necessary values for an ListenerConfig. func (lc *ListenerConfig) Init() { if lc.Policy != nil { @@ -738,6 +801,23 @@ func upstreamConfigStructLevelValidation(sl validator.StructLevel) { return } + // Empty type is ok only for endpoints starts with "h3://" and "sdns://". + if uc.Type == "" && !strings.HasPrefix(uc.Endpoint, endpointPrefixH3) && !strings.HasPrefix(uc.Endpoint, endpointPrefixSdns) { + sl.ReportError(uc.Endpoint, "type", "type", "oneof", "doh doh3 dot doq os legacy sdns") + return + } + + // initDoHScheme/initDnsStamps may change upstreams information, + // so restoring changed values after validation to keep original one. + defer func(ep, typ string) { + uc.Endpoint = ep + uc.Type = typ + }(uc.Endpoint, uc.Type) + + if err := uc.initDnsStamps(); err != nil { + sl.ReportError(uc.Endpoint, "endpoint", "Endpoint", "http_url", "") + return + } uc.initDoHScheme() // DoH/DoH3 requires endpoint is an HTTP url. if uc.Type == ResolverTypeDOH || uc.Type == ResolverTypeDOH3 { @@ -767,13 +847,19 @@ func defaultPortFor(typ string) string { // - If endpoint is an IP address -> ResolverTypeLegacy // - If endpoint starts with "https://" -> ResolverTypeDOH // - If endpoint starts with "quic://" -> ResolverTypeDOQ +// - If endpoint starts with "h3://" -> ResolverTypeDOH3 +// - If endpoint starts with "sdns://" -> ResolverTypeSDNS // - For anything else -> ResolverTypeDOT func ResolverTypeFromEndpoint(endpoint string) string { switch { - case strings.HasPrefix(endpoint, "https://"): + case strings.HasPrefix(endpoint, endpointPrefixHTTPS): return ResolverTypeDOH - case strings.HasPrefix(endpoint, "quic://"): + case strings.HasPrefix(endpoint, endpointPrefixQUIC): return ResolverTypeDOQ + case strings.HasPrefix(endpoint, endpointPrefixH3): + return ResolverTypeDOH3 + case strings.HasPrefix(endpoint, endpointPrefixSdns): + return ResolverTypeSDNS } host := endpoint if strings.Contains(endpoint, ":") { diff --git a/config_internal_test.go b/config_internal_test.go index 96beddc..6823686 100644 --- a/config_internal_test.go +++ b/config_internal_test.go @@ -17,7 +17,7 @@ func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) { uc.Init() uc.setupBootstrapIP(false) if len(uc.bootstrapIPs) == 0 { - t.Log(nameservers()) + t.Log(defaultNameservers()) t.Fatal("could not bootstrap ip without bootstrap DNS") } t.Log(uc) @@ -26,6 +26,7 @@ func TestUpstreamConfig_SetupBootstrapIP(t *testing.T) { func TestUpstreamConfig_Init(t *testing.T) { u1, _ := url.Parse("https://example.com") u2, _ := url.Parse("https://example.com?k=v") + u3, _ := url.Parse("https://freedns.controld.com/p1") tests := []struct { name string uc *UpstreamConfig @@ -178,6 +179,152 @@ func TestUpstreamConfig_Init(t *testing.T) { u: u2, }, }, + { + "h3", + &UpstreamConfig{ + Name: "doh3", + Type: "doh3", + Endpoint: "h3://example.com", + BootstrapIP: "", + Domain: "", + Timeout: 0, + }, + &UpstreamConfig{ + Name: "doh3", + Type: "doh3", + Endpoint: "https://example.com", + BootstrapIP: "", + Domain: "example.com", + Timeout: 0, + IPStack: IpStackBoth, + u: u1, + }, + }, + { + "h3 without type", + &UpstreamConfig{ + Name: "doh3", + Endpoint: "h3://example.com", + BootstrapIP: "", + Domain: "", + Timeout: 0, + }, + &UpstreamConfig{ + Name: "doh3", + Type: "doh3", + Endpoint: "https://example.com", + BootstrapIP: "", + Domain: "example.com", + Timeout: 0, + IPStack: IpStackBoth, + u: u1, + }, + }, + { + "sdns -> doh", + &UpstreamConfig{ + Name: "sdns", + Type: "sdns", + Endpoint: "sdns://AgMAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29tAy9wMQ", + BootstrapIP: "", + Domain: "", + Timeout: 0, + IPStack: IpStackBoth, + }, + &UpstreamConfig{ + Name: "sdns", + Type: "doh", + Endpoint: "https://freedns.controld.com/p1", + BootstrapIP: "76.76.2.11", + Domain: "freedns.controld.com", + Timeout: 0, + IPStack: IpStackBoth, + u: u3, + }, + }, + { + "sdns -> dot", + &UpstreamConfig{ + Name: "sdns", + Type: "sdns", + Endpoint: "sdns://AwcAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29t", + BootstrapIP: "", + Domain: "", + Timeout: 0, + IPStack: IpStackBoth, + }, + &UpstreamConfig{ + Name: "sdns", + Type: "dot", + Endpoint: "freedns.controld.com:843", + BootstrapIP: "76.76.2.11", + Domain: "freedns.controld.com", + Timeout: 0, + IPStack: IpStackBoth, + }, + }, + { + "sdns -> doq", + &UpstreamConfig{ + Name: "sdns", + Type: "sdns", + Endpoint: "sdns://BAcAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29t", + BootstrapIP: "", + Domain: "", + Timeout: 0, + IPStack: IpStackBoth, + }, + &UpstreamConfig{ + Name: "sdns", + Type: "doq", + Endpoint: "freedns.controld.com:784", + BootstrapIP: "76.76.2.11", + Domain: "freedns.controld.com", + Timeout: 0, + IPStack: IpStackBoth, + }, + }, + { + "sdns -> legacy", + &UpstreamConfig{ + Name: "sdns", + Type: "sdns", + Endpoint: "sdns://AAcAAAAAAAAACjc2Ljc2LjIuMTE", + BootstrapIP: "", + Domain: "", + Timeout: 0, + IPStack: IpStackBoth, + }, + &UpstreamConfig{ + Name: "sdns", + Type: "legacy", + Endpoint: "76.76.2.11:53", + BootstrapIP: "76.76.2.11", + Domain: "76.76.2.11", + Timeout: 0, + IPStack: IpStackBoth, + }, + }, + { + "sdns without type", + &UpstreamConfig{ + Name: "sdns", + Endpoint: "sdns://AAcAAAAAAAAACjc2Ljc2LjIuMTE", + BootstrapIP: "", + Domain: "", + Timeout: 0, + IPStack: IpStackBoth, + }, + &UpstreamConfig{ + Name: "sdns", + Type: "legacy", + Endpoint: "76.76.2.11:53", + BootstrapIP: "76.76.2.11", + Domain: "76.76.2.11", + Timeout: 0, + IPStack: IpStackBoth, + }, + }, } for _, tc := range tests { diff --git a/config_test.go b/config_test.go index 03a1a3f..a20b33c 100644 --- a/config_test.go +++ b/config_test.go @@ -107,6 +107,9 @@ func TestConfigValidation(t *testing.T) { {"invalid doh/doh3 endpoint", configWithInvalidDoHEndpoint(t), true}, {"invalid client id pref", configWithInvalidClientIDPref(t), true}, {"doh endpoint without scheme", dohUpstreamEndpointWithoutScheme(t), false}, + {"doh endpoint without type", dohUpstreamEndpointWithoutType(t), true}, + {"doh3 endpoint without type", doh3UpstreamEndpointWithoutType(t), false}, + {"sdns endpoint without type", sdnsUpstreamEndpointWithoutType(t), false}, {"maximum number of flush cache domains", configWithInvalidFlushCacheDomain(t), true}, } @@ -127,6 +130,21 @@ func TestConfigValidation(t *testing.T) { } } +func TestConfigValidationDoNotChangeEndpoint(t *testing.T) { + cfg := configWithInvalidDoHEndpoint(t) + endpointMap := map[string]struct{}{} + for _, uc := range cfg.Upstream { + endpointMap[uc.Endpoint] = struct{}{} + } + validate := validator.New() + _ = ctrld.ValidateConfig(validate, cfg) + for _, uc := range cfg.Upstream { + if _, ok := endpointMap[uc.Endpoint]; !ok { + t.Fatalf("expected endpoint '%s' to exist", uc.Endpoint) + } + } +} + func TestConfigDiscoverOverride(t *testing.T) { v := viper.NewWithOptions(viper.KeyDelimiter("::")) ctrld.InitConfig(v, "test_config_discover_override") @@ -179,6 +197,27 @@ func dohUpstreamEndpointWithoutScheme(t *testing.T) *ctrld.Config { return cfg } +func dohUpstreamEndpointWithoutType(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Upstream["0"].Endpoint = "https://freedns.controld.com/p1" + cfg.Upstream["0"].Type = "" + return cfg +} + +func doh3UpstreamEndpointWithoutType(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Upstream["0"].Endpoint = "h3://freedns.controld.com/p1" + cfg.Upstream["0"].Type = "" + return cfg +} + +func sdnsUpstreamEndpointWithoutType(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Upstream["0"].Endpoint = "sdns://AgMAAAAAAAAACjc2Ljc2LjIuMTEAFGZyZWVkbnMuY29udHJvbGQuY29tAy9wMQ" + cfg.Upstream["0"].Type = "" + return cfg +} + func invalidUpstreamTimeout(t *testing.T) *ctrld.Config { cfg := defaultConfig(t) cfg.Upstream["0"].Timeout = -1 diff --git a/docs/config.md b/docs/config.md index 8c216ec..136cb04 100644 --- a/docs/config.md +++ b/docs/config.md @@ -281,6 +281,13 @@ The value must be a positive number, any invalid value will be ignored and defau - Required: no - Default: 3600 +### leak_on_upstream_failure +Once ctrld is "offline", mean ctrld could not connect to any upstream, next queries will be leaked to OS resolver. + +- Type: boolean +- Required: no +- Default: true on Windows, MacOS and non-router Linux. + ## Upstream The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to. diff --git a/go.mod b/go.mod index bfe6060b..1dc51e0 100644 --- a/go.mod +++ b/go.mod @@ -1,95 +1,126 @@ module github.com/Control-D-Inc/ctrld -go 1.21 +go 1.23 + +toolchain go1.23.1 require ( github.com/Masterminds/semver v1.5.0 + github.com/ameshkov/dnsstamps v1.0.3 github.com/coreos/go-systemd/v22 v22.5.0 github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf - github.com/frankban/quicktest v1.14.5 - github.com/fsnotify/fsnotify v1.6.0 + github.com/frankban/quicktest v1.14.6 + github.com/fsnotify/fsnotify v1.7.0 github.com/go-playground/validator/v10 v10.11.1 - github.com/godbus/dbus/v5 v5.1.0 + github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 github.com/hashicorp/golang-lru/v2 v2.0.1 - github.com/illarion/gonotify v1.0.1 - github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 + github.com/illarion/gonotify/v2 v2.0.3 + github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 github.com/kardianos/service v1.2.1 github.com/mdlayher/ndp v1.0.1 - github.com/miekg/dns v1.1.55 + github.com/miekg/dns v1.1.58 github.com/minio/selfupdate v0.6.0 github.com/olekukonko/tablewriter v0.0.5 github.com/pelletier/go-toml/v2 v2.0.8 - github.com/prometheus/client_golang v1.15.1 - github.com/prometheus/client_model v0.4.0 + github.com/prometheus/client_golang v1.19.1 + github.com/prometheus/client_model v0.5.0 github.com/prometheus/prom2json v1.3.3 github.com/quic-go/quic-go v0.42.0 github.com/rs/zerolog v1.28.0 - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 - github.com/stretchr/testify v1.8.3 + github.com/stretchr/testify v1.9.0 github.com/vishvananda/netlink v1.2.1-beta.2 - golang.org/x/net v0.23.0 - golang.org/x/sync v0.2.0 - golang.org/x/sys v0.18.0 + golang.org/x/net v0.27.0 + golang.org/x/sync v0.7.0 + golang.org/x/sys v0.22.0 golang.zx2c4.com/wireguard/windows v0.5.3 - tailscale.com v1.44.0 + tailscale.com v1.74.0 ) require ( aead.dev/minisign v0.2.0 // indirect - github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/akutz/memconn v0.1.0 // indirect + github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/coder/websocket v1.8.12 // indirect + github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect + github.com/fxamacker/cbor/v2 v2.6.0 // indirect + github.com/gaissmai/bart v0.11.1 // indirect + github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect + github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jsimonetti/rtnetlink v1.3.2 // indirect + github.com/jsimonetti/rtnetlink v1.4.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect + github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect - github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect - github.com/mdlayher/socket v0.4.1 // indirect + github.com/mdlayher/packet v1.1.2 // indirect + github.com/mdlayher/socket v0.5.0 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect - github.com/pierrec/lz4/v4 v4.1.17 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/common v0.44.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/spf13/afero v1.9.5 // indirect - github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect - github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect + github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect + github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect + github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect + github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect + github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc // indirect + github.com/tcnksm/go-httpstat v0.2.0 // indirect + github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect github.com/vishvananda/netns v0.0.4 // indirect + github.com/x448/float16 v0.8.4 // indirect go.uber.org/mock v0.4.0 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect - golang.org/x/mod v0.11.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.9.1 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.23.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect ) replace github.com/mr-karan/doggo => github.com/Windscribe/doggo v0.0.0-20220919152748-2c118fc391f8 diff --git a/go.sum b/go.sum index 22f00e9..5c560e9 100644 --- a/go.sum +++ b/go.sum @@ -38,52 +38,79 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= +filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be h1:qBKVRi7Mom5heOkyZ+NCIu9HZBiNCsRqrRe5t9pooik= github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= -github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= -github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= +github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= +github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/ebpf v0.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ= -github.com/cilium/ebpf v0.10.0/go.mod h1:DPiVdY/kT534dgc9ERmvP8mWA+9gvwgKfRvk4nNWnoE= +github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= +github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf h1:40DHYsri+d1bnroFDU2FQAeq68f3kAlOzlQ93kCf26Q= github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf/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= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= +github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= +github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= +github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= -github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= +github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= +github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= @@ -95,12 +122,14 @@ github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= +github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -122,11 +151,12 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -136,12 +166,14 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -152,8 +184,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -165,28 +197,32 @@ github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnx github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio= -github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= +github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A= +github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E= -github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c h1:kbTQ8oGf+BVFvt/fM+ECI+NbZDCqoi0vtZTfB2p2hrI= github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c/go.mod h1:k6+89xKz7BSMJ+DzIerBdtpEUeTlBMugO/hcVSzahog= 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 v1.3.2 h1:dcn0uWkfxycEEyNy0IGfx3GrhQ38LH7odjxAghimsVI= -github.com/jsimonetti/rtnetlink v1.3.2/go.mod h1:BBu4jZCpTjP6Gk0/wfrO8qcqymnN3g0hoFqObRmUo6U= +github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= +github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kardianos/service v1.2.1 h1:AYndMsehS+ywIS6RB9KOlcXzteWUzxgMgBymJD7+BYk= github.com/kardianos/service v1.2.1/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -206,28 +242,29 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= -github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= +github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/ndp v1.0.1 h1:+yAD79/BWyFlvAoeG5ncPS0ItlHP/eVbH7bQ6/+LVA4= github.com/mdlayher/ndp v1.0.1/go.mod h1:rf3wKaWhAYJEXFKpgF8kQ2AxypxVbfNcZbqoAo6fVzk= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= -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.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= -github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= -github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= -github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= +github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= +github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= +github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU= github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -239,22 +276,23 @@ github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+q github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= -github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= -github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo= github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= @@ -268,16 +306,16 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -286,8 +324,9 @@ github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -295,17 +334,32 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg= -github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= +github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= +github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= +github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= +github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc h1:cezaQN9pvKVaw56Ma5qr/G646uKIYP0yQf+OyWN/okc= +github.com/tailscale/wireguard-go v0.0.0-20240905161824-799c1978fafc/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= +github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= +github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw= +github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 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= @@ -320,6 +374,8 @@ go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8= go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= +go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -330,8 +386,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 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= @@ -342,8 +398,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= -golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -367,15 +423,14 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -402,8 +457,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -423,16 +478,15 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -472,13 +526,16 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/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-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -487,8 +544,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -541,12 +598,14 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 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= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -637,8 +696,6 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -653,6 +710,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -660,8 +719,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 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.44.0 h1:MPos9n30kJvdyfL52045gVFyNg93K+bwgDsr8gqKq2o= -tailscale.com v1.44.0/go.mod h1:+iYwTdeHyVJuNDu42Zafwihq1Uqfh+pW7pRaY1GD328= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +tailscale.com v1.74.0 h1:J+vRN9o3D4wCqZBiwvDg9kZpQag2mG4Xz5RXNpmV3KE= +tailscale.com v1.74.0/go.mod h1:3iACpCONQ4lauDXvwfoGlwNCpfbVxjdc2j6G9EuFOW8= diff --git a/internal/clientinfo/dhcp.go b/internal/clientinfo/dhcp.go index 147ad29..5d11d5e 100644 --- a/internal/clientinfo/dhcp.go +++ b/internal/clientinfo/dhcp.go @@ -13,8 +13,9 @@ import ( "strings" "sync" + "tailscale.com/net/netmon" + "github.com/fsnotify/fsnotify" - "tailscale.com/net/interfaces" "tailscale.com/util/lineread" "github.com/Control-D-Inc/ctrld" @@ -356,7 +357,7 @@ func (d *dhcp) addSelf() { d.ip2name.Store(ipV4Loopback, hostname) d.ip2name.Store(ipv6Loopback, hostname) found := false - interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { + netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) { mac := i.HardwareAddr.String() // Skip loopback interfaces, info was stored above. if mac == "" { diff --git a/internal/controld/config.go b/internal/controld/config.go index 01e114b..1bc2512 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -5,6 +5,7 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" "io" "net" @@ -64,7 +65,8 @@ type utilityRequest struct { ClientID string `json:"client_id,omitempty"` } -type utilityOrgRequest struct { +// UtilityOrgRequest contains request data for calling Org API. +type UtilityOrgRequest struct { ProvToken string `json:"prov_token"` Hostname string `json:"hostname"` } @@ -81,9 +83,15 @@ func FetchResolverConfig(rawUID, version string, cdDev bool) (*ResolverConfig, e } // FetchResolverUID fetch resolver uid from provision token. -func FetchResolverUID(pt, version string, cdDev bool) (*ResolverConfig, error) { - hostname, _ := os.Hostname() - body, _ := json.Marshal(utilityOrgRequest{ProvToken: pt, Hostname: hostname}) +func FetchResolverUID(req *UtilityOrgRequest, version string, cdDev bool) (*ResolverConfig, error) { + if req == nil { + return nil, errors.New("invalid request") + } + hostname := req.Hostname + if hostname == "" { + hostname, _ = os.Hostname() + } + body, _ := json.Marshal(UtilityOrgRequest{ProvToken: req.ProvToken, Hostname: hostname}) return postUtilityAPI(version, cdDev, false, bytes.NewReader(body)) } diff --git a/internal/dns/README.md b/internal/dns/README.md index aadc3a5..3dd85fc 100644 --- a/internal/dns/README.md +++ b/internal/dns/README.md @@ -1,2 +1,2 @@ -This is a fork of https://pkg.go.dev/tailscale.com@v1.34.2/net/dns with modification +This is a fork of https://pkg.go.dev/tailscale.com@v1.74.0/net/dns with modification to fit ctrld use case. \ No newline at end of file diff --git a/internal/dns/debian_resolvconf.go b/internal/dns/debian_resolvconf.go index f3d736d..ec0e146 100644 --- a/internal/dns/debian_resolvconf.go +++ b/internal/dns/debian_resolvconf.go @@ -1,12 +1,12 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause //go:build linux || freebsd || openbsd package dns import ( + "bufio" "bytes" _ "embed" "fmt" @@ -33,7 +33,7 @@ var workaroundScript []byte // resolvconf implementations encourage adding a suffix roughly // indicating where the config came from, and "inet" is the "none of // the above" value (rather than, say, "ppp" or "dhcp"). -const resolvconfConfigName = "ctrld.inet" +const resolvconfConfigName = "tun-ctrld.inet" // resolvconfLibcHookPath is the directory containing libc update // scripts, which are run by Debian resolvconf when /etc/resolv.conf @@ -53,8 +53,6 @@ type resolvconfManager struct { scriptInstalled bool // libc update script has been installed } -var _ OSConfigurator = (*resolvconfManager)(nil) - func newDebianResolvconfManager(logf logger.Logf) (*resolvconfManager, error) { ret := &resolvconfManager{ logf: logf, @@ -135,6 +133,43 @@ func (m *resolvconfManager) SetDNS(config OSConfig) error { return nil } +func (m *resolvconfManager) SupportsSplitDNS() bool { + return false +} + +func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) { + var bs bytes.Buffer + + cmd := exec.Command(m.listRecordsPath) + // list-records assumes it's being run with CWD set to the + // interfaces runtime dir, and returns nonsense otherwise. + cmd.Dir = m.interfacesDir + cmd.Stdout = &bs + if err := cmd.Run(); err != nil { + return OSConfig{}, err + } + + var conf bytes.Buffer + sc := bufio.NewScanner(&bs) + for sc.Scan() { + if sc.Text() == resolvconfConfigName { + continue + } + bs, err := os.ReadFile(filepath.Join(m.interfacesDir, sc.Text())) + if err != nil { + if os.IsNotExist(err) { + // Probably raced with a deletion, that's okay. + continue + } + return OSConfig{}, err + } + conf.Write(bs) + conf.WriteByte('\n') + } + + return readResolv(&conf) +} + func (m *resolvconfManager) Close() error { if err := m.deleteCtrldConfig(); err != nil { return err diff --git a/internal/dns/direct.go b/internal/dns/direct.go index a825e6d..723543b 100644 --- a/internal/dns/direct.go +++ b/internal/dns/direct.go @@ -1,9 +1,5 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//lint:file-ignore U1000 Ignore, this file is forked from upstream code. -//lint:file-ignore ST1005 Ignore, this file is forked from upstream code. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause package dns @@ -20,11 +16,13 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strings" "sync" "time" "tailscale.com/health" + "tailscale.com/net/tsaddr" "tailscale.com/types/logger" "tailscale.com/util/dnsname" "tailscale.com/version/distro" @@ -32,11 +30,6 @@ import ( "github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile" ) -const ( - backupConf = "/etc/resolv.pre-ctrld-backup.conf" - resolvConf = "/etc/resolv.conf" -) - // writeResolvConf writes DNS configuration in resolv.conf format to the given writer. func writeResolvConf(w io.Writer, servers []netip.Addr, domains []dnsname.FQDN) error { c := &resolvconffile.Config{ @@ -60,6 +53,8 @@ func readResolv(r io.Reader) (OSConfig, error) { // resolvOwner returns the apparent owner of the resolv.conf // configuration in bs - one of "resolvconf", "systemd-resolved" or // "NetworkManager", or "" if no known owner was found. +// +//lint:ignore U1000 used in linux and freebsd code func resolvOwner(bs []byte) string { likely := "" b := bytes.NewBuffer(bs) @@ -123,8 +118,9 @@ func restartResolved() error { // The caller must call Down before program shutdown // or as cleanup if the program terminates unexpectedly. type directManager struct { - logf logger.Logf - fs wholeFileFS + logf logger.Logf + health *health.Tracker + fs wholeFileFS // renameBroken is set if fs.Rename to or from /etc/resolv.conf // fails. This can happen in some container runtimes, where // /etc/resolv.conf is bind-mounted from outside the container, @@ -140,19 +136,22 @@ type directManager struct { ctx context.Context // valid until Close ctxClose context.CancelFunc // closes ctx - mu sync.Mutex - wantResolvConf []byte // if non-nil, what we expect /etc/resolv.conf to contain + mu sync.Mutex + wantResolvConf []byte // if non-nil, what we expect /etc/resolv.conf to contain + //lint:ignore U1000 used in direct_linux.go lastWarnContents []byte // last resolv.conf contents that we warned about } -func newDirectManager(logf logger.Logf) *directManager { - return newDirectManagerOnFS(logf, directFS{}) +//lint:ignore U1000 used in manager_{freebsd,openbsd}.go +func newDirectManager(logf logger.Logf, health *health.Tracker) *directManager { + return newDirectManagerOnFS(logf, health, directFS{}) } -func newDirectManagerOnFS(logf logger.Logf, fs wholeFileFS) *directManager { +func newDirectManagerOnFS(logf logger.Logf, health *health.Tracker, fs wholeFileFS) *directManager { ctx, cancel := context.WithCancel(context.Background()) m := &directManager{ logf: logf, + health: health, fs: fs, ctx: ctx, ctxClose: cancel, @@ -193,13 +192,13 @@ func (m *directManager) ownedByCtrld() (bool, error) { } // backupConfig creates or updates a backup of /etc/resolv.conf, if -// resolv.conf does not currently contain a Tailscale-managed config. +// resolv.conf does not currently contain a ctrld-managed config. func (m *directManager) backupConfig() error { if _, err := m.fs.Stat(resolvConf); err != nil { if os.IsNotExist(err) { // No resolv.conf, nothing to back up. Also get rid of any // existing backup file, to avoid restoring something old. - _ = m.fs.Remove(backupConf) + m.fs.Remove(backupConf) return nil } return err @@ -237,7 +236,7 @@ func (m *directManager) restoreBackup() (restored bool, err error) { if resolvConfExists && !owned { // There's already a non-ctrld config in place, get rid of // our backup. - _ = m.fs.Remove(backupConf) + m.fs.Remove(backupConf) return false, nil } @@ -278,6 +277,14 @@ func (m *directManager) rename(old, new string) error { return fmt.Errorf("writing to %q in rename of %q: %w", new, old, err) } + // Explicitly set the permissions on the new file. This ensures that + // if we have a umask set which prevents creating world-readable files, + // the file will still have the correct permissions once it's renamed + // into place. See #12609. + if err := m.fs.Chmod(new, 0644); err != nil { + return fmt.Errorf("chmod %q in rename of %q: %w", new, old, err) + } + if err := m.fs.Remove(old); err != nil { err2 := m.fs.Truncate(old) if err2 != nil { @@ -298,53 +305,6 @@ func (m *directManager) setWant(want []byte) { m.wantResolvConf = want } -var warnTrample = health.NewWarnable() - -// checkForFileTrample checks whether /etc/resolv.conf has been trampled -// by another program on the system. (e.g. a DHCP client) -func (m *directManager) checkForFileTrample() { - m.mu.Lock() - want := m.wantResolvConf - lastWarn := m.lastWarnContents - m.mu.Unlock() - - if want == nil { - return - } - - cur, err := m.fs.ReadFile(resolvConf) - if err != nil { - m.logf("trample: read error: %v", err) - return - } - if bytes.Equal(cur, want) { - warnTrample.Set(nil) - if lastWarn != nil { - m.mu.Lock() - m.lastWarnContents = nil - m.mu.Unlock() - m.logf("trample: resolv.conf again matches expected content") - } - return - } - if bytes.Equal(cur, lastWarn) { - // We already logged about this, so not worth doing it again. - return - } - - m.mu.Lock() - m.lastWarnContents = cur - m.mu.Unlock() - - show := cur - if len(show) > 1024 { - show = show[:1024] - } - m.logf("trample: resolv.conf changed from what we expected. did some other program interfere? current contents: %q", show) - //lint:ignore ST1005 This error is for human. - warnTrample.Set(errors.New("Linux DNS config not ideal. /etc/resolv.conf overwritten. See https://tailscale.com/s/dns-fight")) -} - func (m *directManager) SetDNS(config OSConfig) (err error) { defer func() { if err != nil && errors.Is(err, fs.ErrPermission) && runtime.GOOS == "linux" && @@ -370,7 +330,7 @@ func (m *directManager) SetDNS(config OSConfig) (err error) { } buf := new(bytes.Buffer) - _ = writeResolvConf(buf, config.Nameservers, config.SearchDomains) + writeResolvConf(buf, config.Nameservers, config.SearchDomains) if err := m.atomicWriteFile(m.fs, resolvConf, buf.Bytes(), 0644); err != nil { return err } @@ -411,12 +371,57 @@ func (m *directManager) SetDNS(config OSConfig) (err error) { return nil } +func (m *directManager) SupportsSplitDNS() bool { + return false +} + +func (m *directManager) GetBaseConfig() (OSConfig, error) { + owned, err := m.ownedByCtrld() + if err != nil { + return OSConfig{}, err + } + fileToRead := resolvConf + if owned { + fileToRead = backupConf + } + + oscfg, err := m.readResolvFile(fileToRead) + if err != nil { + return OSConfig{}, err + } + + // On some systems, the backup configuration file is actually a + // symbolic link to something owned by another DNS service (commonly, + // resolved). Thus, it can be updated out from underneath us to contain + // the Tailscale service IP, which results in an infinite loop of us + // trying to send traffic to resolved, which sends back to us, and so + // on. To solve this, drop the Tailscale service IP from the base + // configuration; we do this in all situations since there's + // essentially no world where we want to forward to ourselves. + // + // See: https://github.com/tailscale/tailscale/issues/7816 + var removed bool + oscfg.Nameservers = slices.DeleteFunc(oscfg.Nameservers, func(ip netip.Addr) bool { + if ip == tsaddr.TailscaleServiceIP() || ip == tsaddr.TailscaleServiceIPv6() { + removed = true + return true + } + return false + }) + if removed { + m.logf("[v1] dropped Tailscale IP from base config that was a symlink") + } + return oscfg, nil +} + func (m *directManager) Close() error { - // We used to keep a file for the ctrld config and symlinked + m.ctxClose() + + // We used to keep a file for the tailscale config and symlinked // to it, but then we stopped because /etc/resolv.conf being a // symlink to surprising places breaks snaps and other sandboxing // things. Clean it up if it's still there. - _ = m.fs.Remove("/etc/resolv.ctrld.conf") + m.fs.Remove("/etc/resolv.ctrld.conf") if _, err := m.fs.Stat(backupConf); err != nil { if os.IsNotExist(err) { @@ -436,9 +441,9 @@ func (m *directManager) Close() error { resolvConfExists := !os.IsNotExist(err) if resolvConfExists && !owned { - // There's already a non-ctrld config in place, get rid of + // There's already a non-tailscale config in place, get rid of // our backup. - _ = m.fs.Remove(backupConf) + m.fs.Remove(backupConf) return nil } @@ -475,6 +480,14 @@ func (m *directManager) atomicWriteFile(fs wholeFileFS, filename string, data [] if err := fs.WriteFile(tmpName, data, perm); err != nil { return fmt.Errorf("atomicWriteFile: %w", err) } + // Explicitly set the permissions on the temporary file before renaming + // it. This ensures that if we have a umask set which prevents creating + // world-readable files, the file will still have the correct + // permissions once it's renamed into place. See #12609. + if err := fs.Chmod(tmpName, perm); err != nil { + return fmt.Errorf("atomicWriteFile: Chmod: %w", err) + } + return m.rename(tmpName, filename) } @@ -483,10 +496,11 @@ func (m *directManager) atomicWriteFile(fs wholeFileFS, filename string, data [] // // All name parameters are absolute paths. type wholeFileFS interface { - Stat(name string) (isRegular bool, err error) - Rename(oldName, newName string) error - Remove(name string) error + Chmod(name string, mode os.FileMode) error ReadFile(name string) ([]byte, error) + Remove(name string) error + Rename(oldName, newName string) error + Stat(name string) (isRegular bool, err error) Truncate(name string) error WriteFile(name string, contents []byte, perm os.FileMode) error } @@ -510,6 +524,10 @@ func (fs directFS) Stat(name string) (isRegular bool, err error) { return fi.Mode().IsRegular(), nil } +func (fs directFS) Chmod(name string, mode os.FileMode) error { + return os.Chmod(fs.path(name), mode) +} + func (fs directFS) Rename(oldName, newName string) error { return os.Rename(fs.path(oldName), fs.path(newName)) } diff --git a/internal/dns/direct_linux.go b/internal/dns/direct_linux.go index 565c227..57615bb 100644 --- a/internal/dns/direct_linux.go +++ b/internal/dns/direct_linux.go @@ -1,26 +1,26 @@ -// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause package dns import ( + "bytes" "context" - "github.com/illarion/gonotify" + "github.com/illarion/gonotify/v2" + "tailscale.com/health" ) func (m *directManager) runFileWatcher() { - in, err := gonotify.NewInotify() + ctx, cancel := context.WithCancel(m.ctx) + defer cancel() + in, err := gonotify.NewInotify(ctx) if err != nil { // Oh well, we tried. This is all best effort for now, to // surface warnings to users. m.logf("dns: inotify new: %v", err) return } - ctx, cancel := context.WithCancel(m.ctx) - defer cancel() - go m.closeInotifyOnDone(ctx, in) const events = gonotify.IN_ATTRIB | gonotify.IN_CLOSE_WRITE | @@ -56,7 +56,53 @@ func (m *directManager) runFileWatcher() { } } -func (m *directManager) closeInotifyOnDone(ctx context.Context, in *gonotify.Inotify) { - <-ctx.Done() - _ = in.Close() +var resolvTrampleWarnable = health.Register(&health.Warnable{ + Code: "ctrld-resolv-conf-overwritten", + Severity: health.SeverityMedium, + Title: "Linux DNS configuration issue", + Text: health.StaticMessage("Linux DNS config not ideal. /etc/resolv.conf overwritten. See https://tailscale.com/s/dns-fight"), +}) + +// checkForFileTrample checks whether /etc/resolv.conf has been trampled +// by another program on the system. (e.g. a DHCP client) +func (m *directManager) checkForFileTrample() { + m.mu.Lock() + want := m.wantResolvConf + lastWarn := m.lastWarnContents + m.mu.Unlock() + + if want == nil { + return + } + + cur, err := m.fs.ReadFile(resolvConf) + if err != nil { + m.logf("trample: read error: %v", err) + return + } + if bytes.Equal(cur, want) { + m.health.SetHealthy(resolvTrampleWarnable) + if lastWarn != nil { + m.mu.Lock() + m.lastWarnContents = nil + m.mu.Unlock() + m.logf("trample: resolv.conf again matches expected content") + } + return + } + if bytes.Equal(cur, lastWarn) { + // We already logged about this, so not worth doing it again. + return + } + + m.mu.Lock() + m.lastWarnContents = cur + m.mu.Unlock() + + show := cur + if len(show) > 1024 { + show = show[:1024] + } + m.logf("trample: resolv.conf changed from what we expected. did some other program interfere? current contents: %q", show) + m.health.SetUnhealthy(resolvTrampleWarnable, nil) } diff --git a/internal/dns/direct_notlinux.go b/internal/dns/direct_notlinux.go index 5563586..c221ca1 100644 --- a/internal/dns/direct_notlinux.go +++ b/internal/dns/direct_notlinux.go @@ -1,6 +1,5 @@ -// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause //go:build !linux diff --git a/internal/dns/direct_test.go b/internal/dns/direct_test.go index 57962dd..5a68403 100644 --- a/internal/dns/direct_test.go +++ b/internal/dns/direct_test.go @@ -1,10 +1,10 @@ -// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause package dns import ( + "context" "errors" "fmt" "io/fs" @@ -79,7 +79,10 @@ func testDirect(t *testing.T, fs wholeFileFS) { } } - m := directManager{logf: t.Logf, fs: fs} + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + m := directManager{logf: t.Logf, fs: fs, ctx: ctx, ctxClose: cancel} if err := m.SetDNS(OSConfig{ Nameservers: []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("8.8.4.4")}, SearchDomains: []dnsname.FQDN{"controld.com."}, @@ -121,7 +124,7 @@ type brokenRemoveFS struct { directFS } -func (b brokenRemoveFS) Rename(_, _ string) error { +func (b brokenRemoveFS) Rename(old, new string) error { return errors.New("nyaaah I'm a silly container!") } @@ -178,12 +181,12 @@ func TestReadResolve(t *testing.T) { SearchDomains: []dnsname.FQDN{"controld.com."}, }, }, - {in: `search controld.com # typo`, + {in: `search controld.com # comment`, want: OSConfig{ SearchDomains: []dnsname.FQDN{"controld.com."}, }, }, - {in: `searchcontrold.com`, wantErr: true}, + {in: `searchctrld.com`, wantErr: true}, {in: `search`, wantErr: true}, } diff --git a/internal/dns/manager_freebsd.go b/internal/dns/manager_freebsd.go index 27a4e7f..1ec9ea8 100644 --- a/internal/dns/manager_freebsd.go +++ b/internal/dns/manager_freebsd.go @@ -1,6 +1,5 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause package dns @@ -8,13 +7,18 @@ import ( "fmt" "os" + "tailscale.com/control/controlknobs" + "tailscale.com/health" "tailscale.com/types/logger" ) -func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) { +// NewOSConfigurator creates a new OS configurator. +// +// The health tracker may be nil; the knobs may be nil and are ignored on this platform. +func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, _ string) (OSConfigurator, error) { bs, err := os.ReadFile("/etc/resolv.conf") if os.IsNotExist(err) { - return newDirectManager(logf), nil + return newDirectManager(logf, health), nil } if err != nil { return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err) @@ -24,16 +28,16 @@ func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) { case "resolvconf": switch resolvconfStyle() { case "": - return newDirectManager(logf), nil + return newDirectManager(logf, health), nil case "debian": return newDebianResolvconfManager(logf) case "openresolv": - return newOpenresolvManager() + return newOpenresolvManager(logf) default: logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", resolvconfStyle()) - return newDirectManager(logf), nil + return newDirectManager(logf, health), nil } default: - return newDirectManager(logf), nil + return newDirectManager(logf, health), nil } } diff --git a/internal/dns/manager_linux.go b/internal/dns/manager_linux.go index 2886090..1d46ab4 100644 --- a/internal/dns/manager_linux.go +++ b/internal/dns/manager_linux.go @@ -1,8 +1,5 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//lint:file-ignore U1000 Ignore this file, it's a copy. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause package dns @@ -17,6 +14,7 @@ import ( "time" "github.com/godbus/dbus/v5" + "tailscale.com/control/controlknobs" "tailscale.com/health" "tailscale.com/net/netaddr" "tailscale.com/types/logger" @@ -38,7 +36,10 @@ func (kv kv) String() string { var publishOnce sync.Once -func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurator, err error) { +// NewOSConfigurator created a new OS configurator. +// +// The health tracker may be nil; the knobs may be nil and are ignored on this platform. +func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ *controlknobs.Knobs, interfaceName string) (ret OSConfigurator, err error) { env := newOSConfigEnv{ fs: directFS{}, dbusPing: dbusPing, @@ -47,7 +48,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat nmVersionBetween: nmVersionBetween, resolvconfStyle: resolvconfStyle, } - mode, err := dnsMode(logf, env) + mode, err := dnsMode(logf, health, env) if err != nil { return nil, err } @@ -59,18 +60,18 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat logf("dns: using %q mode", mode) switch mode { case "direct": - return newDirectManagerOnFS(logf, env.fs), nil + return newDirectManagerOnFS(logf, health, env.fs), nil case "systemd-resolved": - return newResolvedManager(logf, interfaceName) + return newResolvedManager(logf, health, interfaceName) case "network-manager": return newNMManager(interfaceName) case "debian-resolvconf": return newDebianResolvconfManager(logf) case "openresolv": - return newOpenresolvManager() + return newOpenresolvManager(logf) default: logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode) - return newDirectManagerOnFS(logf, env.fs), nil + return newDirectManagerOnFS(logf, health, env.fs), nil } } @@ -84,7 +85,7 @@ type newOSConfigEnv struct { resolvconfStyle func() string } -func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) { +func dnsMode(logf logger.Logf, health *health.Tracker, env newOSConfigEnv) (ret string, err error) { var debug []kv dbg := func(k, v string) { debug = append(debug, kv{k, v}) @@ -145,7 +146,7 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) { // header, but doesn't actually point to resolved. We mustn't // try to program resolved in that case. // https://github.com/tailscale/tailscale/issues/2136 - if err := resolvedIsActuallyResolver(bs); err != nil { + if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil { logf("dns: resolvedIsActuallyResolver error: %v", err) dbg("resolved", "not-in-use") return "direct", nil @@ -231,7 +232,7 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) { dbg("rc", "nm") // Sometimes, NetworkManager owns the configuration but points // it at systemd-resolved. - if err := resolvedIsActuallyResolver(bs); err != nil { + if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil { logf("dns: resolvedIsActuallyResolver error: %v", err) dbg("resolved", "not-in-use") // You'd think we would use newNMManager here. However, as @@ -271,6 +272,14 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) { dbg("nm-safe", "yes") return "network-manager", nil } + if err := env.nmIsUsingResolved(); err != nil { + // If systemd-resolved is not running at all, then we don't have any + // other choice: we take direct control of DNS. + dbg("nm-resolved", "no") + return "direct", nil + } + + //lint:ignore SA1019 upstream code still use it. health.SetDNSManagerHealth(errors.New("systemd-resolved and NetworkManager are wired together incorrectly; MagicDNS will probably not work. For more info, see https://tailscale.com/s/resolved-nm")) dbg("nm-safe", "no") return "systemd-resolved", nil @@ -324,14 +333,23 @@ func nmIsUsingResolved() error { return nil } -// resolvedIsActuallyResolver reports whether the given resolv.conf -// bytes describe a configuration where systemd-resolved (127.0.0.53) -// is the only configured nameserver. +// resolvedIsActuallyResolver reports whether the system is using +// systemd-resolved as the resolver. There are two different ways to +// use systemd-resolved: +// - libnss_resolve, which requires adding `resolve` to the "hosts:" +// line in /etc/nsswitch.conf +// - setting the only nameserver configured in `resolv.conf` to +// systemd-resolved IP (127.0.0.53) // // Returns an error if the configuration is something other than // exclusively systemd-resolved, or nil if the config is only // systemd-resolved. -func resolvedIsActuallyResolver(bs []byte) error { +func resolvedIsActuallyResolver(logf logger.Logf, env newOSConfigEnv, dbg func(k, v string), bs []byte) error { + if err := isLibnssResolveUsed(env); err == nil { + dbg("resolved", "nss") + return nil + } + cfg, err := readResolv(bytes.NewBuffer(bs)) if err != nil { return err @@ -348,9 +366,34 @@ func resolvedIsActuallyResolver(bs []byte) error { return fmt.Errorf("resolv.conf doesn't point to systemd-resolved; points to %v", cfg.Nameservers) } } + dbg("resolved", "file") return nil } +// isLibnssResolveUsed reports whether libnss_resolve is used +// for resolving names. Returns nil if it is, and an error otherwise. +func isLibnssResolveUsed(env newOSConfigEnv) error { + bs, err := env.fs.ReadFile("/etc/nsswitch.conf") + if err != nil { + return fmt.Errorf("reading /etc/resolv.conf: %w", err) + } + for _, line := range strings.Split(string(bs), "\n") { + fields := strings.Fields(line) + if len(fields) < 2 || fields[0] != "hosts:" { + continue + } + for _, module := range fields[1:] { + if module == "dns" { + return fmt.Errorf("dns with a higher priority than libnss_resolve") + } + if module == "resolve" { + return nil + } + } + } + return fmt.Errorf("libnss_resolve not used") +} + func dbusPing(name, objectPath string) error { conn, err := dbus.SystemBus() if err != nil { diff --git a/internal/dns/manager_linux_test.go b/internal/dns/manager_linux_test.go index 70a2be4..605344c 100644 --- a/internal/dns/manager_linux_test.go +++ b/internal/dns/manager_linux_test.go @@ -1,6 +1,5 @@ -// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause package dns @@ -71,7 +70,7 @@ func TestLinuxDNSMode(t *testing.T) { { name: "resolved_alone_without_ping", env: env(resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53")), - wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved nm=no resolv-conf-mode=error ret=systemd-resolved]", + wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -79,16 +78,46 @@ func TestLinuxDNSMode(t *testing.T) { env: env( resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedRunning()), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, + { + name: "resolved_and_nsswitch_resolve", + env: env( + resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"), + resolvedRunning(), + nsswitchDotConf("hosts: files resolve [!UNAVAIL=return] dns"), + ), + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=nss nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + want: "systemd-resolved", + }, + { + name: "resolved_and_nsswitch_dns", + env: env( + resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"), + resolvedRunning(), + nsswitchDotConf("hosts: files dns resolve [!UNAVAIL=return]"), + ), + wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [1.1.1.1]\ndns: [resolved-ping=yes rc=resolved resolved=not-in-use ret=direct]", + want: "direct", + }, + { + name: "resolved_and_nsswitch_none", + env: env( + resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"), + resolvedRunning(), + nsswitchDotConf("hosts:"), + ), + wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [1.1.1.1]\ndns: [resolved-ping=yes rc=resolved resolved=not-in-use ret=direct]", + want: "direct", + }, { name: "resolved_and_networkmanager_not_using_resolved", env: env( resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedRunning(), nmRunning("1.2.3", false)), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -97,7 +126,7 @@ func TestLinuxDNSMode(t *testing.T) { resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedRunning(), nmRunning("1.26.2", true)), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]", want: "network-manager", }, { @@ -106,7 +135,7 @@ func TestLinuxDNSMode(t *testing.T) { resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedRunning(), nmRunning("1.27.0", true)), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -115,7 +144,7 @@ func TestLinuxDNSMode(t *testing.T) { resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedRunning(), nmRunning("1.22.0", true)), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, // Regression tests for extreme corner cases below. @@ -141,7 +170,7 @@ func TestLinuxDNSMode(t *testing.T) { "nameserver 127.0.0.53", "nameserver 127.0.0.53"), resolvedRunning()), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -156,7 +185,7 @@ func TestLinuxDNSMode(t *testing.T) { "# run \"systemd-resolve --status\" to see details about the actual nameservers.", "nameserver 127.0.0.53"), resolvedRunning()), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -171,7 +200,7 @@ func TestLinuxDNSMode(t *testing.T) { "# 127.0.0.53 is the systemd-resolved stub resolver.", "# run \"systemd-resolve --status\" to see details about the actual nameservers.", "nameserver 127.0.0.53")), - wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved nm=no resolv-conf-mode=error ret=systemd-resolved]", + wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -183,7 +212,7 @@ func TestLinuxDNSMode(t *testing.T) { "options edns0 trust-ad"), resolvedRunning(), nmRunning("1.32.12", true)), - wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -194,7 +223,7 @@ func TestLinuxDNSMode(t *testing.T) { "nameserver 127.0.0.53", "options edns0 trust-ad"), nmRunning("1.32.12", true)), - wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=nm nm-resolved=yes nm-safe=no resolv-conf-mode=error ret=systemd-resolved]", + wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=nm resolved=file nm-resolved=yes nm-safe=no resolv-conf-mode=error ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -206,7 +235,7 @@ func TestLinuxDNSMode(t *testing.T) { "options edns0 trust-ad"), resolvedRunning(), nmRunning("1.26.3", true)), - wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm-safe=yes ret=network-manager]", + wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=yes ret=network-manager]", want: "network-manager", }, { @@ -217,7 +246,7 @@ func TestLinuxDNSMode(t *testing.T) { "nameserver 127.0.0.53", "options edns0 trust-ad"), resolvedRunning()), - wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -228,7 +257,7 @@ func TestLinuxDNSMode(t *testing.T) { "search lan", "nameserver 127.0.0.53"), resolvedRunning()), - wantLog: "dns: [resolved-ping=yes rc=nm nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, { @@ -238,14 +267,26 @@ func TestLinuxDNSMode(t *testing.T) { resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"), resolvedDbusProperty(), )), - wantLog: "dns: [resolved-ping=yes rc=resolved nm=no resolv-conf-mode=fortests ret=systemd-resolved]", + wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]", want: "systemd-resolved", }, + { + // regression test for https://github.com/tailscale/tailscale/issues/9687 + name: "networkmanager_endeavouros", + env: env(resolvDotConf( + "# Generated by NetworkManager", + "search example.com localdomain", + "nameserver 10.0.0.1"), + nmRunning("1.44.2", false)), + wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [10.0.0.1]\n" + + "dns: [rc=nm resolved=not-in-use ret=direct]", + want: "direct", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var logBuf tstest.MemLogger - got, err := dnsMode(logBuf.Logf, tt.env) + got, err := dnsMode(logBuf.Logf, nil, tt.env) if err != nil { t.Fatal(err) } @@ -272,8 +313,9 @@ func (m memFS) Stat(name string) (isRegular bool, err error) { return false, nil } -func (m memFS) Rename(_, _ string) error { panic("TODO") } -func (m memFS) Remove(_ string) error { panic("TODO") } +func (m memFS) Chmod(name string, mode os.FileMode) error { panic("TODO") } +func (m memFS) Rename(oldName, newName string) error { panic("TODO") } +func (m memFS) Remove(name string) error { panic("TODO") } func (m memFS) ReadFile(name string) ([]byte, error) { v, ok := m[name] if !ok { @@ -297,7 +339,7 @@ func (m memFS) Truncate(name string) error { return nil } -func (m memFS) WriteFile(name string, contents []byte, _ os.FileMode) error { +func (m memFS) WriteFile(name string, contents []byte, perm os.FileMode) error { m[name] = string(contents) return nil } @@ -381,6 +423,12 @@ func resolvDotConf(ss ...string) envOption { }) } +func nsswitchDotConf(ss ...string) envOption { + return envOpt(func(b *envBuilder) { + b.fs["/etc/nsswitch.conf"] = strings.Join(ss, "\n") + }) +} + // resolvedRunning returns an option that makes resolved reply to a dbusPing // and the ResolvConfMode property. func resolvedRunning() envOption { diff --git a/internal/dns/nm.go b/internal/dns/nm.go index b8bc0c7..24237a1 100644 --- a/internal/dns/nm.go +++ b/internal/dns/nm.go @@ -1,6 +1,5 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause //go:build linux @@ -11,6 +10,7 @@ import ( "fmt" "net" "net/netip" + "sort" "time" "github.com/godbus/dbus/v5" @@ -24,6 +24,13 @@ const ( lowerPriority = int32(200) // lower than all builtin auto priorities ) +// reconfigTimeout is the time interval within which Manager.{Up,Down} should complete. +// +// This is particularly useful because certain conditions can cause indefinite hangs +// (such as improper dbus auth followed by contextless dbus.Object.Call). +// Such operations should be wrapped in a timeout context. +const reconfigTimeout = time.Second + // nmManager uses the NetworkManager DBus API. type nmManager struct { interfaceName string @@ -31,8 +38,6 @@ type nmManager struct { dnsManager dbus.BusObject } -var _ OSConfigurator = (*nmManager)(nil) - func newNMManager(interfaceName string) (*nmManager, error) { conn, err := dbus.SystemBus() if err != nil { @@ -141,18 +146,17 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error { // tell it explicitly to keep it. Read out the current interface // settings and mirror them out to NetworkManager. var addrs6 []map[string]any - if netIface, err := net.InterfaceByName(m.interfaceName); err == nil { - if addrs, err := netIface.Addrs(); err == nil { - for _, a := range addrs { - if ipnet, ok := a.(*net.IPNet); ok { - nip, ok := netip.AddrFromSlice(ipnet.IP) - nip = nip.Unmap() - if ok && nip.Is6() { - addrs6 = append(addrs6, map[string]any{ - "address": nip.String(), - "prefix": uint32(128), - }) - } + if tsIf, err := net.InterfaceByName(m.interfaceName); err == nil { + addrs, _ := tsIf.Addrs() + for _, a := range addrs { + if ipnet, ok := a.(*net.IPNet); ok { + nip, ok := netip.AddrFromSlice(ipnet.IP) + nip = nip.Unmap() + if ok && nip.Is6() { + addrs6 = append(addrs6, map[string]any{ + "address": nip.String(), + "prefix": uint32(128), + }) } } } @@ -260,6 +264,125 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error { return nil } +func (m *nmManager) SupportsSplitDNS() bool { + var mode string + v, err := m.dnsManager.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode") + if err != nil { + return false + } + mode, ok := v.Value().(string) + if !ok { + return false + } + + // Per NM's documentation, it only does split-DNS when it's + // programming dnsmasq or systemd-resolved. All other modes are + // primary-only. + return mode == "dnsmasq" || mode == "systemd-resolved" +} + +func (m *nmManager) GetBaseConfig() (OSConfig, error) { + conn, err := dbus.SystemBus() + if err != nil { + return OSConfig{}, err + } + + nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager")) + v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Configuration") + if err != nil { + return OSConfig{}, err + } + cfgs, ok := v.Value().([]map[string]dbus.Variant) + if !ok { + return OSConfig{}, fmt.Errorf("unexpected NM config type %T", v.Value()) + } + + if len(cfgs) == 0 { + return OSConfig{}, nil + } + + type dnsPrio struct { + resolvers []netip.Addr + domains []string + priority int32 + } + order := make([]dnsPrio, 0, len(cfgs)-1) + + for _, cfg := range cfgs { + if name, ok := cfg["interface"]; ok { + if s, ok := name.Value().(string); ok && s == m.interfaceName { + // Config for the tailscale interface, skip. + continue + } + } + + var p dnsPrio + + if v, ok := cfg["nameservers"]; ok { + if ips, ok := v.Value().([]string); ok { + for _, s := range ips { + ip, err := netip.ParseAddr(s) + if err != nil { + // hmm, what do? Shouldn't really happen. + continue + } + p.resolvers = append(p.resolvers, ip) + } + } + } + if v, ok := cfg["domains"]; ok { + if domains, ok := v.Value().([]string); ok { + p.domains = domains + } + } + if v, ok := cfg["priority"]; ok { + if prio, ok := v.Value().(int32); ok { + p.priority = prio + } + } + + order = append(order, p) + } + + sort.Slice(order, func(i, j int) bool { + return order[i].priority < order[j].priority + }) + + var ( + ret OSConfig + seenResolvers = map[netip.Addr]bool{} + seenSearch = map[string]bool{} + ) + + for _, cfg := range order { + for _, resolver := range cfg.resolvers { + if seenResolvers[resolver] { + continue + } + ret.Nameservers = append(ret.Nameservers, resolver) + seenResolvers[resolver] = true + } + for _, dom := range cfg.domains { + if seenSearch[dom] { + continue + } + fqdn, err := dnsname.ToFQDN(dom) + if err != nil { + continue + } + ret.SearchDomains = append(ret.SearchDomains, fqdn) + seenSearch[dom] = true + } + if cfg.priority < 0 { + // exclusive configurations preempt all other + // configurations, so we're done. + break + } + } + + return ret, nil +} + func (m *nmManager) Close() error { // No need to do anything on close, NetworkManager will delete our // settings when the tailscale interface goes away. diff --git a/internal/dns/openresolv.go b/internal/dns/openresolv.go index 8c53d87..3126d79 100644 --- a/internal/dns/openresolv.go +++ b/internal/dns/openresolv.go @@ -1,6 +1,5 @@ -// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause //go:build linux || freebsd || openbsd @@ -10,22 +9,41 @@ import ( "bytes" "fmt" "os/exec" + "strings" + + "tailscale.com/types/logger" ) // openresolvManager manages DNS configuration using the openresolv // implementation of the `resolvconf` program. -type openresolvManager struct{} +type openresolvManager struct { + logf logger.Logf +} -var _ OSConfigurator = (*openresolvManager)(nil) +func newOpenresolvManager(logf logger.Logf) (openresolvManager, error) { + return openresolvManager{logf}, nil +} -func newOpenresolvManager() (openresolvManager, error) { - return openresolvManager{}, nil +func (m openresolvManager) logCmdErr(cmd *exec.Cmd, err error) { + if err == nil { + return + } + + commandStr := fmt.Sprintf("path=%q args=%q", cmd.Path, cmd.Args) + exerr, ok := err.(*exec.ExitError) + if !ok { + m.logf("error running command %s: %v", commandStr, err) + return + } + + m.logf("error running command %s stderr=%q exitCode=%d: %v", commandStr, exerr.Stderr, exerr.ExitCode(), err) } func (m openresolvManager) deleteTailscaleConfig() error { cmd := exec.Command("resolvconf", "-f", "-d", "ctrld") out, err := cmd.CombinedOutput() if err != nil { + m.logCmdErr(cmd, err) return fmt.Errorf("running %s: %s", cmd, out) } return nil @@ -43,11 +61,55 @@ func (m openresolvManager) SetDNS(config OSConfig) error { cmd.Stdin = &stdin out, err := cmd.CombinedOutput() if err != nil { + m.logCmdErr(cmd, err) return fmt.Errorf("running %s: %s", cmd, out) } return nil } +func (m openresolvManager) SupportsSplitDNS() bool { + return false +} + +func (m openresolvManager) GetBaseConfig() (OSConfig, error) { + // List the names of all config snippets openresolv is aware + // of. Snippets get listed in priority order (most to least), + // which we'll exploit later. + bs, err := exec.Command("resolvconf", "-i").CombinedOutput() + if err != nil { + return OSConfig{}, err + } + + // Remove the "tailscale" snippet from the list. + args := []string{"-l"} + for _, f := range strings.Split(strings.TrimSpace(string(bs)), " ") { + if f == "tailscale" { + continue + } + args = append(args, f) + } + + // List all resolvconf snippets except our own, and parse that as + // a resolv.conf. This effectively generates a blended config of + // "everyone except tailscale", which is what would be in use if + // tailscale hadn't set exclusive mode. + // + // Note that this is not _entirely_ true. To be perfectly correct, + // we should be looking for other interfaces marked exclusive that + // predated tailscale, and stick to only those. However, in + // practice, openresolv uses are generally quite limited, and boil + // down to 1-2 DHCP leases, for which the correct outcome is a + // blended config like the one we produce here. + var buf bytes.Buffer + cmd := exec.Command("resolvconf", args...) + cmd.Stdout = &buf + if err := cmd.Run(); err != nil { + m.logCmdErr(cmd, err) + return OSConfig{}, err + } + return readResolv(&buf) +} + func (m openresolvManager) Close() error { return m.deleteTailscaleConfig() } diff --git a/internal/dns/osconfig.go b/internal/dns/osconfig.go index 36fcaec..9d7fc80 100644 --- a/internal/dns/osconfig.go +++ b/internal/dns/osconfig.go @@ -1,20 +1,20 @@ -// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause package dns import ( "bufio" + "errors" "fmt" "net/netip" + "slices" + "strings" "tailscale.com/types/logger" "tailscale.com/util/dnsname" ) -var _ OSConfigurator = (*directManager)(nil) - // An OSConfigurator applies DNS settings to the operating system. type OSConfigurator interface { // SetDNS updates the OS's DNS configuration to match cfg. @@ -23,8 +23,21 @@ type OSConfigurator interface { // SetDNS must not be called after Close. // SetDNS takes ownership of cfg. SetDNS(cfg OSConfig) error + // SupportsSplitDNS reports whether the configurator is capable of + // installing a resolver only for specific DNS suffixes. If false, + // the configurator can only set a global resolver. + SupportsSplitDNS() bool + // GetBaseConfig returns the OS's "base" configuration, i.e. the + // resolver settings the OS would use without Tailscale + // contributing any configuration. + // GetBaseConfig must return the tailscale-free base config even + // after SetDNS has been called to set a Tailscale configuration. + // Only works when SupportsSplitDNS=false. - // Close removes ctrld-related DNS configuration from the OS. + // Implementations that don't support getting the base config must + // return ErrGetBaseConfigNotSupported. + GetBaseConfig() (OSConfig, error) + // Close removes Tailscale-related DNS configuration from the OS. Close() error Mode() string @@ -50,14 +63,59 @@ type OSConfig struct { SearchDomains []dnsname.FQDN // MatchDomains are the DNS suffixes for which Nameservers should // be used. If empty, Nameservers is installed as the "primary" resolver. + // A non-empty MatchDomains requests a "split DNS" configuration + // from the OS, which will only work with OSConfigurators that + // report SupportsSplitDNS()=true. MatchDomains []dnsname.FQDN } +func (o *OSConfig) WriteToBufioWriter(w *bufio.Writer) { + if o == nil { + w.WriteString("") + return + } + w.WriteString("{") + if len(o.Hosts) > 0 { + fmt.Fprintf(w, "Hosts:%v ", o.Hosts) + } + if len(o.Nameservers) > 0 { + fmt.Fprintf(w, "Nameservers:%v ", o.Nameservers) + } + if len(o.SearchDomains) > 0 { + fmt.Fprintf(w, "SearchDomains:%v ", o.SearchDomains) + } + if len(o.MatchDomains) > 0 { + w.WriteString("MatchDomains:[") + sp := "" + var numARPA int + for _, s := range o.MatchDomains { + if strings.HasSuffix(string(s), ".arpa.") { + numARPA++ + continue + } + w.WriteString(sp) + w.WriteString(string(s)) + sp = " " + } + w.WriteString("]") + if numARPA > 0 { + fmt.Fprintf(w, "+%darpa", numARPA) + } + } + w.WriteString("}") +} + func (o OSConfig) IsZero() bool { - return len(o.Nameservers) == 0 && len(o.SearchDomains) == 0 && len(o.MatchDomains) == 0 + return len(o.Hosts) == 0 && + len(o.Nameservers) == 0 && + len(o.SearchDomains) == 0 && + len(o.MatchDomains) == 0 } func (a OSConfig) Equal(b OSConfig) bool { + if len(a.Hosts) != len(b.Hosts) { + return false + } if len(a.Nameservers) != len(b.Nameservers) { return false } @@ -68,6 +126,15 @@ func (a OSConfig) Equal(b OSConfig) bool { return false } + for i := range a.Hosts { + ha, hb := a.Hosts[i], b.Hosts[i] + if ha.Addr != hb.Addr { + return false + } + if !slices.Equal(ha.Hosts, hb.Hosts) { + return false + } + } for i := range a.Nameservers { if a.Nameservers[i] != b.Nameservers[i] { return false @@ -93,34 +160,39 @@ func (a OSConfig) Equal(b OSConfig) bool { // Fixes https://github.com/tailscale/tailscale/issues/5669 func (a OSConfig) Format(f fmt.State, verb rune) { logger.ArgWriter(func(w *bufio.Writer) { - _, _ = w.WriteString(`{Nameservers:[`) + w.WriteString(`{Nameservers:[`) for i, ns := range a.Nameservers { if i != 0 { - _, _ = w.WriteString(" ") + w.WriteString(" ") } - _, _ = fmt.Fprintf(w, "%+v", ns) + fmt.Fprintf(w, "%+v", ns) } - _, _ = w.WriteString(`] SearchDomains:[`) + w.WriteString(`] SearchDomains:[`) for i, domain := range a.SearchDomains { if i != 0 { - _, _ = w.WriteString(" ") + w.WriteString(" ") } - _, _ = fmt.Fprintf(w, "%+v", domain) + fmt.Fprintf(w, "%+v", domain) } - _, _ = w.WriteString(`] MatchDomains:[`) + w.WriteString(`] MatchDomains:[`) for i, domain := range a.MatchDomains { if i != 0 { - _, _ = w.WriteString(" ") + w.WriteString(" ") } - _, _ = fmt.Fprintf(w, "%+v", domain) + fmt.Fprintf(w, "%+v", domain) } - _, _ = w.WriteString(`] Hosts:[`) + w.WriteString(`] Hosts:[`) for i, host := range a.Hosts { if i != 0 { - _, _ = w.WriteString(" ") + w.WriteString(" ") } - _, _ = fmt.Fprintf(w, "%+v", host) + fmt.Fprintf(w, "%+v", host) } - _, _ = w.WriteString(`]}`) + w.WriteString(`]}`) }).Format(f, verb) } + +// ErrGetBaseConfigNotSupported is the error +// OSConfigurator.GetBaseConfig returns when the OSConfigurator +// doesn't support reading the underlying configuration out of the OS. +var ErrGetBaseConfigNotSupported = errors.New("getting OS base config is not supported") diff --git a/internal/dns/osconfig_test.go b/internal/dns/osconfig_test.go index 24ec35b..2e7c625 100644 --- a/internal/dns/osconfig_test.go +++ b/internal/dns/osconfig_test.go @@ -1,14 +1,15 @@ -// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause package dns import ( "fmt" "net/netip" + "reflect" "testing" + "tailscale.com/tstest" "tailscale.com/util/dnsname" ) @@ -42,3 +43,13 @@ func TestOSConfigPrintable(t *testing.T) { t.Errorf("format mismatch:\n got: %s\n want: %s", s, expected) } } + +func TestIsZero(t *testing.T) { + tstest.CheckIsZero[OSConfig](t, map[reflect.Type]any{ + reflect.TypeFor[dnsname.FQDN](): dnsname.FQDN("foo.bar."), + reflect.TypeFor[*HostEntry](): &HostEntry{ + Addr: netip.AddrFrom4([4]byte{100, 1, 2, 3}), + Hosts: []string{"foo", "bar"}, + }, + }) +} diff --git a/internal/dns/resolvconf-workaround.sh b/internal/dns/resolvconf-workaround.sh index d04c723..9e59ee2 100644 --- a/internal/dns/resolvconf-workaround.sh +++ b/internal/dns/resolvconf-workaround.sh @@ -1,7 +1,6 @@ #!/bin/sh -# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. +# Copyright (c) Ctrld Inc & AUTHORS +# SPDX-License-Identifier: BSD-3-Clause # # This script is a workaround for a vpn-unfriendly behavior of the # original resolvconf by Thomas Hood. Unlike the `openresolv` @@ -29,7 +28,7 @@ if [ -n "$CTRLD_RESOLVCONF_HOOK_LOOP" ]; then exit 0 fi -if [ ! -f ctrld.inet ]; then +if [ ! -f tun-ctrld.inet ]; then # Ctrld isn't trying to manage DNS, do nothing. exit 0 fi @@ -60,4 +59,4 @@ if [ -d /etc/resolvconf/update-libc.d ] ; then # Re-notify libc watchers that we've changed resolv.conf again. export CTRLD_RESOLVCONF_HOOK_LOOP=1 exec run-parts /etc/resolvconf/update-libc.d -fi \ No newline at end of file +fi diff --git a/internal/dns/resolvconf.go b/internal/dns/resolvconf.go index b317b3b..ca584ff 100644 --- a/internal/dns/resolvconf.go +++ b/internal/dns/resolvconf.go @@ -1,12 +1,12 @@ -// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause //go:build linux || freebsd || openbsd package dns import ( + "bytes" "os/exec" ) @@ -14,13 +14,17 @@ func resolvconfStyle() string { if _, err := exec.LookPath("resolvconf"); err != nil { return "" } - if _, err := exec.Command("resolvconf", "--version").CombinedOutput(); err != nil { + output, err := exec.Command("resolvconf", "--version").CombinedOutput() + if err != nil { // Debian resolvconf doesn't understand --version, and // exits with a specific error code. if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 99 { return "debian" } } + if bytes.HasPrefix(output, []byte("Debian resolvconf")) { + return "debian" + } // Treat everything else as openresolv, by far the more popular implementation. return "openresolv" } diff --git a/internal/dns/resolvconfpath_default.go b/internal/dns/resolvconfpath_default.go new file mode 100644 index 0000000..36ef76b --- /dev/null +++ b/internal/dns/resolvconfpath_default.go @@ -0,0 +1,11 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !gokrazy + +package dns + +const ( + resolvConf = "/etc/resolv.conf" + backupConf = "/etc/resolv.pre-ctrld-backup.conf" +) diff --git a/internal/dns/resolvconfpath_gokrazy.go b/internal/dns/resolvconfpath_gokrazy.go new file mode 100644 index 0000000..921dc0b --- /dev/null +++ b/internal/dns/resolvconfpath_gokrazy.go @@ -0,0 +1,11 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build gokrazy + +package dns + +const ( + resolvConf = "/tmp/resolv.conf" + backupConf = "/tmp/resolv.pre-ctrld-backup.conf" +) diff --git a/internal/dns/resolved.go b/internal/dns/resolved.go index a9bf911..d0e9146 100644 --- a/internal/dns/resolved.go +++ b/internal/dns/resolved.go @@ -1,6 +1,5 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause //go:build linux @@ -21,8 +20,6 @@ import ( "tailscale.com/util/dnsname" ) -const reconfigTimeout = time.Second - // DBus entities we talk to. // // DBus is an RPC bus. In particular, the bus we're talking to is the @@ -97,16 +94,14 @@ type resolvedManager struct { ctx context.Context cancel func() // terminate the context, for close - logf logger.Logf - ifidx int + logf logger.Logf + health *health.Tracker + ifidx int configCR chan changeRequest // tracks OSConfigs changes and error responses - revertCh chan struct{} } -var _ OSConfigurator = (*resolvedManager)(nil) - -func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) { +func newResolvedManager(logf logger.Logf, health *health.Tracker, interfaceName string) (*resolvedManager, error) { iface, err := net.InterfaceByName(interfaceName) if err != nil { return nil, err @@ -119,11 +114,11 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage ctx: ctx, cancel: cancel, - logf: logf, - ifidx: iface.Index, + logf: logf, + health: health, + ifidx: iface.Index, configCR: make(chan changeRequest), - revertCh: make(chan struct{}), } go mgr.run(ctx) @@ -132,8 +127,10 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage } func (m *resolvedManager) SetDNS(config OSConfig) error { + // NOTE: don't close this channel, since it's possible that the SetDNS + // call will time out and return before the run loop answers, at which + // point it will send on the now-closed channel. errc := make(chan error, 1) - defer close(errc) select { case <-m.ctx.Done(): @@ -221,14 +218,12 @@ func (m *resolvedManager) run(ctx context.Context) { if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusResolvedObject)); err != nil { m.logf("[v1] Setting DBus signal filter failed: %v", err) } - if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusNetworkdObject)); err != nil { - m.logf("[v1] Setting DBus signal filter failed: %v", err) - } conn.Signal(signals) // Reset backoff and SetNSOSHealth after successful on reconnect. bo.BackOff(ctx, nil) - health.SetDNSOSHealth(nil) + //lint:ignore SA1019 upstream code still use it. + m.health.SetDNSOSHealth(nil) return nil } @@ -243,15 +238,13 @@ func (m *resolvedManager) run(ctx context.Context) { if rManager == nil { return } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // RevertLink resets all per-interface settings on systemd-resolved to defaults. // When ctx goes away systemd-resolved auto reverts. // Keeping for potential use in future refactor. if call := rManager.CallWithContext(ctx, dbusRevertLink, 0, m.ifidx); call.Err != nil { m.logf("[v1] RevertLink: %v", call.Err) + return } - cancel() - close(m.revertCh) return case configCR := <-m.configCR: // Track and update sync with latest config change. @@ -308,7 +301,8 @@ func (m *resolvedManager) run(ctx context.Context) { // Set health while holding the lock, because this will // graciously serialize the resync's health outcome with a // concurrent SetDNS call. - health.SetDNSOSHealth(err) + //lint:ignore SA1019 upstream code still use it. + m.health.SetDNSOSHealth(err) if err != nil { m.logf("failed to configure systemd-resolved: %v", err) } @@ -426,18 +420,22 @@ func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.B m.logf("[v1] failed to disable DoT: %v", call.Err) } - if rManager.Path() == dbusResolvedPath { - if call := rManager.CallWithContext(ctx, dbusFlushCaches, 0); call.Err != nil { - m.logf("failed to flush resolved DNS cache: %v", call.Err) - } + if call := rManager.CallWithContext(ctx, dbusFlushCaches, 0); call.Err != nil { + m.logf("failed to flush resolved DNS cache: %v", call.Err) } - return nil } +func (m *resolvedManager) SupportsSplitDNS() bool { + return true +} + +func (m *resolvedManager) GetBaseConfig() (OSConfig, error) { + return OSConfig{}, ErrGetBaseConfigNotSupported +} + func (m *resolvedManager) Close() error { m.cancel() // stops the 'run' method goroutine - <-m.revertCh return nil } diff --git a/nameservers.go b/nameservers.go index ce99a3b..0aebf9e 100644 --- a/nameservers.go +++ b/nameservers.go @@ -1,9 +1,8 @@ package ctrld -import "net" - type dnsFn func() []string +// nameservers returns DNS nameservers from system settings. func nameservers() []string { var dns []string seen := make(map[string]bool) @@ -21,7 +20,7 @@ func nameservers() []string { continue } seen[ns] = true - dns = append(dns, net.JoinHostPort(ns, "53")) + dns = append(dns, ns) } } diff --git a/nameservers_linux.go b/nameservers_linux.go index 8859ea5..1fad95b 100644 --- a/nameservers_linux.go +++ b/nameservers_linux.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "net" "os" + "strings" "github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile" ) @@ -28,6 +29,7 @@ func dns4() []string { var dns []string seen := make(map[string]bool) + vis := virtualInterfaces() s := bufio.NewScanner(f) first := true for s.Scan() { @@ -39,7 +41,10 @@ func dns4() []string { if len(fields) < 2 { continue } - + // Skip virtual interfaces. + if vis.contains(string(bytes.TrimSpace(fields[0]))) { + continue + } gw := make([]byte, net.IPv4len) // Third fields is gateway. if _, err := hex.Decode(gw, fields[2]); err != nil { @@ -63,12 +68,17 @@ func dns6() []string { defer f.Close() var dns []string + vis := virtualInterfaces() s := bufio.NewScanner(f) for s.Scan() { fields := bytes.Fields(s.Bytes()) if len(fields) < 4 { continue } + // Skip virtual interfaces. + if vis.contains(string(bytes.TrimSpace(fields[len(fields)-1]))) { + continue + } gw := make([]byte, net.IPv6len) // Fifth fields is gateway. @@ -95,3 +105,26 @@ func dnsFromSystemdResolver() []string { } return ns } + +type set map[string]struct{} + +func (s *set) add(e string) { + (*s)[e] = struct{}{} +} + +func (s *set) contains(e string) bool { + _, ok := (*s)[e] + return ok +} + +// virtualInterfaces returns a set of virtual interfaces on current machine. +func virtualInterfaces() set { + s := make(set) + entries, _ := os.ReadDir("/sys/devices/virtual/net") + for _, entry := range entries { + if entry.IsDir() { + s.add(strings.TrimSpace(entry.Name())) + } + } + return s +} diff --git a/nameservers_linux_test.go b/nameservers_linux_test.go new file mode 100644 index 0000000..23f1544 --- /dev/null +++ b/nameservers_linux_test.go @@ -0,0 +1,10 @@ +package ctrld + +import ( + "testing" +) + +func Test_virtualInterfaces(t *testing.T) { + vis := virtualInterfaces() + t.Log(vis) +} diff --git a/resolver.go b/resolver.go index d8b7f8d..f54edfb 100644 --- a/resolver.go +++ b/resolver.go @@ -7,11 +7,14 @@ import ( "net" "net/netip" "slices" + "strings" "sync" + "sync/atomic" "time" "github.com/miekg/dns" - "tailscale.com/net/interfaces" + "tailscale.com/net/netmon" + "tailscale.com/net/tsaddr" ) const ( @@ -29,6 +32,9 @@ const ( ResolverTypeLegacy = "legacy" // ResolverTypePrivate is like ResolverTypeOS, but use for local resolver only. ResolverTypePrivate = "private" + // ResolverTypeSDNS specifies resolver with information encoded using DNS Stamps. + // See: https://dnscrypt.info/stamps-specifications/ + ResolverTypeSDNS = "sdns" ) const ( @@ -39,12 +45,36 @@ const ( var controldPublicDnsWithPort = net.JoinHostPort(controldPublicDns, "53") // or is the Resolver used for ResolverTypeOS. -var or = &osResolver{nameservers: defaultNameservers()} +var or = newResolverWithNameserver(defaultNameservers()) -// defaultNameservers returns OS nameservers plus ControlD public DNS. +// defaultNameservers is like nameservers with each element formed "ip:53". func defaultNameservers() []string { ns := nameservers() - return ns + nss := make([]string, len(ns)) + for i := range ns { + nss[i] = net.JoinHostPort(ns[i], "53") + } + return nss +} + +// availableNameservers returns list of current available DNS servers of the system. +func availableNameservers() []string { + var nss []string + // Ignore local addresses to prevent loop. + regularIPs, loopbackIPs, _ := netmon.LocalAddresses() + machineIPsMap := make(map[string]struct{}, len(regularIPs)) + for _, v := range slices.Concat(regularIPs, loopbackIPs) { + machineIPsMap[v.String()] = struct{}{} + } + for _, ns := range nameservers() { + if _, ok := machineIPsMap[ns]; ok { + continue + } + if testNameserver(ns) { + nss = append(nss, ns) + } + } + return nss } // InitializeOsResolver initializes OS resolver using the current system DNS settings. @@ -53,24 +83,75 @@ func defaultNameservers() []string { // It's the caller's responsibility to ensure the system DNS is in a clean state before // calling this function. func InitializeOsResolver() []string { - or.nameservers = or.nameservers[:0] - for _, ns := range defaultNameservers() { - if testNameserver(ns) { - or.nameservers = append(or.nameservers, ns) + return initializeOsResolver(availableNameservers()) +} +func initializeOsResolver(servers []string) []string { + var ( + nss []string + publicNss []string + ) + var ( + lastLanServer netip.Addr + curLanServer netip.Addr + curLanServerAvailable bool + ) + if p := or.currentLanServer.Load(); p != nil { + curLanServer = *p + or.currentLanServer.Store(nil) + } + if p := or.lastLanServer.Load(); p != nil { + lastLanServer = *p + or.lastLanServer.Store(nil) + } + for _, ns := range servers { + addr, err := netip.ParseAddr(ns) + if err != nil { + continue + } + server := net.JoinHostPort(ns, "53") + // Always use new public nameserver. + if !isLanAddr(addr) { + publicNss = append(publicNss, server) + nss = append(nss, server) + continue + } + // For LAN server, storing only current and last LAN server if any. + if addr.Compare(curLanServer) == 0 { + curLanServerAvailable = true + } else { + if addr.Compare(lastLanServer) == 0 { + or.lastLanServer.Store(&addr) + } else { + if or.currentLanServer.CompareAndSwap(nil, &addr) { + nss = append(nss, server) + } + } } } - or.nameservers = append(or.nameservers, controldPublicDnsWithPort) - return or.nameservers + // Store current LAN server as last one only if it's still available. + if curLanServerAvailable && curLanServer.IsValid() { + or.lastLanServer.Store(&curLanServer) + nss = append(nss, net.JoinHostPort(curLanServer.String(), "53")) + } + if len(publicNss) == 0 { + publicNss = append(publicNss, controldPublicDnsWithPort) + nss = append(nss, controldPublicDnsWithPort) + } + or.publicServer.Store(&publicNss) + return nss } // testPlainDnsNameserver sends a test query to DNS nameserver to check if the server is available. func testNameserver(addr string) bool { msg := new(dns.Msg) - msg.SetQuestion(".", dns.TypeNS) + msg.SetQuestion("controld.com.", dns.TypeNS) client := new(dns.Client) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - _, _, err := client.ExchangeContext(ctx, msg, addr) + _, _, err := client.ExchangeContext(ctx, msg, net.JoinHostPort(addr, "53")) + if err != nil { + ProxyLogger.Load().Debug().Err(err).Msgf("failed to connect to OS nameserver: %s", addr) + } return err == nil } @@ -104,20 +185,31 @@ func NewResolver(uc *UpstreamConfig) (Resolver, error) { } type osResolver struct { - nameservers []string + currentLanServer atomic.Pointer[netip.Addr] + lastLanServer atomic.Pointer[netip.Addr] + publicServer atomic.Pointer[[]string] } type osResolverResult struct { - answer *dns.Msg - err error - isControlDPublicDNS bool + answer *dns.Msg + err error + server string + lan bool } // Resolve resolves DNS queries using pre-configured nameservers. // Query is sent to all nameservers concurrently, and the first // success response will be returned. func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { - numServers := len(o.nameservers) + publicServers := *o.publicServer.Load() + nss := make([]string, 0, 2) + if p := o.currentLanServer.Load(); p != nil { + nss = append(nss, net.JoinHostPort(p.String(), "53")) + } + if p := o.lastLanServer.Load(); p != nil { + nss = append(nss, net.JoinHostPort(p.String(), "53")) + } + numServers := len(nss) + len(publicServers) if numServers == 0 { return nil, errors.New("no nameservers available") } @@ -126,43 +218,70 @@ 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)) + wg := &sync.WaitGroup{} + wg.Add(numServers) go func() { wg.Wait() close(ch) }() - for _, server := range o.nameservers { - go func(server string) { - defer wg.Done() - answer, _, err := dnsClient.ExchangeContext(ctx, msg.Copy(), server) - ch <- &osResolverResult{answer: answer, err: err, isControlDPublicDNS: server == controldPublicDnsWithPort} - }(server) - } + do := func(servers []string, isLan bool) { + for _, server := range servers { + go func(server string) { + defer wg.Done() + answer, _, err := dnsClient.ExchangeContext(ctx, msg.Copy(), server) + ch <- &osResolverResult{answer: answer, err: err, server: server, lan: isLan} + }(server) + } + } + do(nss, true) + do(publicServers, false) + + logAnswer := func(server string) { + if before, _, found := strings.Cut(server, ":"); found { + server = before + } + Log(ctx, ProxyLogger.Load().Debug(), "got answer from nameserver: %s", server) + } var ( nonSuccessAnswer *dns.Msg + nonSuccessServer string controldSuccessAnswer *dns.Msg + publicServerAnswer *dns.Msg + publicServer string ) errs := make([]error, 0, numServers) for res := range ch { switch { case res.answer != nil && res.answer.Rcode == dns.RcodeSuccess: - if res.isControlDPublicDNS { + switch { + case res.server == controldPublicDnsWithPort: controldSuccessAnswer = res.answer // only use ControlD answer as last one. - } else { + case !res.lan && publicServerAnswer == nil: + publicServerAnswer = res.answer // use public DNS answer after LAN server.. + publicServer = res.server + default: cancel() + logAnswer(res.server) return res.answer, nil } case res.answer != nil: nonSuccessAnswer = res.answer + nonSuccessServer = res.server } errs = append(errs, res.err) } - for _, answer := range []*dns.Msg{controldSuccessAnswer, nonSuccessAnswer} { - if answer != nil { - return answer, nil - } + if publicServerAnswer != nil { + logAnswer(publicServer) + return publicServerAnswer, nil + } + if controldSuccessAnswer != nil { + logAnswer(controldPublicDnsWithPort) + return controldSuccessAnswer, nil + } + if nonSuccessAnswer != nil { + logAnswer(nonSuccessServer) + return nonSuccessAnswer, nil } return nil, errors.Join(errs...) } @@ -209,11 +328,12 @@ func LookupIP(domain string) []string { } func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) { - resolver := &osResolver{nameservers: nameservers()} + nss := defaultNameservers() if withBootstrapDNS { - resolver.nameservers = append([]string{net.JoinHostPort(controldBootstrapDns, "53")}, resolver.nameservers...) + nss = append([]string{net.JoinHostPort(controldBootstrapDns, "53")}, nss...) } - ProxyLogger.Load().Debug().Msgf("resolving %q using bootstrap DNS %q", domain, resolver.nameservers) + resolver := newResolverWithNameserver(nss) + ProxyLogger.Load().Debug().Msgf("resolving %q using bootstrap DNS %q", domain, nss) timeoutMs := 2000 if timeout > 0 && timeout < timeoutMs { timeoutMs = timeout @@ -286,12 +406,12 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) // - Gateway IP address (depends on OS). // - Input servers. func NewBootstrapResolver(servers ...string) Resolver { - resolver := &osResolver{nameservers: nameservers()} - resolver.nameservers = append([]string{controldPublicDnsWithPort}, resolver.nameservers...) + nss := defaultNameservers() + nss = append([]string{controldPublicDnsWithPort}, nss...) for _, ns := range servers { - resolver.nameservers = append([]string{net.JoinHostPort(ns, "53")}, resolver.nameservers...) + nss = append([]string{net.JoinHostPort(ns, "53")}, nss...) } - return resolver + return NewResolverWithNameserver(nss) } // NewPrivateResolver returns an OS resolver, which includes only private DNS servers, @@ -302,7 +422,7 @@ func NewBootstrapResolver(servers ...string) Resolver { // // This is useful for doing PTR lookup in LAN network. func NewPrivateResolver() Resolver { - nss := nameservers() + nss := defaultNameservers() resolveConfNss := nameserversFromResolvconf() localRfc1918Addrs := Rfc1918Addresses() n := 0 @@ -328,10 +448,10 @@ func NewPrivateResolver() Resolver { } } nss = nss[:n] - return NewResolverWithNameserver(nss) + return newResolverWithNameserver(nss) } -// NewResolverWithNameserver returns an OS resolver which uses the given nameservers +// NewResolverWithNameserver returns a Resolver which uses the given nameservers // for resolving DNS queries. If nameservers is empty, a dummy resolver will be returned. // // Each nameserver must be form "host:port". It's the caller responsibility to ensure all @@ -340,13 +460,31 @@ func NewResolverWithNameserver(nameservers []string) Resolver { if len(nameservers) == 0 { return &dummyResolver{} } - return &osResolver{nameservers: nameservers} + return newResolverWithNameserver(nameservers) +} + +// newResolverWithNameserver returns an OS resolver from given nameservers list. +// The caller must ensure each server in list is formed "ip:53". +func newResolverWithNameserver(nameservers []string) *osResolver { + r := &osResolver{} + nss := slices.Sorted(slices.Values(nameservers)) + for i, ns := range nss { + ip, _, _ := net.SplitHostPort(ns) + addr, _ := netip.ParseAddr(ip) + if isLanAddr(addr) { + r.currentLanServer.Store(&addr) + nss = slices.Delete(nss, i, i+1) + break + } + } + r.publicServer.Store(&nss) + return r } // Rfc1918Addresses returns the list of local interfaces private IP addresses func Rfc1918Addresses() []string { var res []string - interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { + netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) { addrs, _ := i.Addrs() for _, addr := range addrs { ipNet, ok := addr.(*net.IPNet) @@ -370,3 +508,11 @@ func newDialer(dnsAddress string) *net.Dialer { }, } } + +// isLanAddr reports whether addr is considered a LAN ip address. +func isLanAddr(addr netip.Addr) bool { + return addr.IsPrivate() || + addr.IsLoopback() || + addr.IsLinkLocalUnicast() || + tsaddr.CGNATRange().Contains(addr) +} diff --git a/resolver_test.go b/resolver_test.go index 23c27ae..7b1a49d 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -3,10 +3,13 @@ package ctrld import ( "context" "net" + "slices" "sync" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/miekg/dns" ) @@ -16,7 +19,8 @@ func Test_osResolver_Resolve(t *testing.T) { go func() { defer cancel() - resolver := &osResolver{nameservers: []string{"127.0.0.127:5353"}} + resolver := &osResolver{} + resolver.publicServer.Store(&[]string{"127.0.0.127:5353"}) m := new(dns.Msg) m.SetQuestion("controld.com.", dns.TypeA) m.RecursionDesired = true @@ -69,7 +73,8 @@ func Test_osResolver_ResolveWithNonSuccessAnswer(t *testing.T) { server.Shutdown() } }() - resolver := &osResolver{nameservers: ns} + resolver := &osResolver{} + resolver.publicServer.Store(&ns) msg := new(dns.Msg) msg.SetQuestion(".", dns.TypeNS) answer, err := resolver.Resolve(context.Background(), msg) @@ -81,6 +86,19 @@ func Test_osResolver_ResolveWithNonSuccessAnswer(t *testing.T) { } } +func Test_osResolver_InitializationRace(t *testing.T) { + var wg sync.WaitGroup + n := 10 + wg.Add(n) + for range n { + go func() { + defer wg.Done() + InitializeOsResolver() + }() + } + wg.Wait() +} + func Test_upstreamTypeFromEndpoint(t *testing.T) { tests := []struct { name string @@ -134,3 +152,42 @@ func runLocalPacketConnTestServer(t *testing.T, pc net.PacketConn, handler dns.H waitLock.Lock() return server, addr, nil } + +func Test_initializeOsResolver(t *testing.T) { + lanServer1 := "192.168.1.1" + lanServer2 := "10.0.10.69" + wanServer := "1.1.1.1" + publicServers := []string{net.JoinHostPort(wanServer, "53")} + + // First initialization. + initializeOsResolver([]string{lanServer1, wanServer}) + p := or.currentLanServer.Load() + assert.NotNil(t, p) + assert.Equal(t, lanServer1, p.String()) + assert.True(t, slices.Equal(*or.publicServer.Load(), publicServers)) + + // No new LAN server, current LAN server -> last LAN server. + initializeOsResolver([]string{lanServer1, wanServer}) + p = or.currentLanServer.Load() + assert.Nil(t, p) + p = or.lastLanServer.Load() + assert.NotNil(t, p) + assert.Equal(t, lanServer1, p.String()) + assert.True(t, slices.Equal(*or.publicServer.Load(), publicServers)) + + // New LAN server detected. + initializeOsResolver([]string{lanServer2, lanServer1, wanServer}) + p = or.currentLanServer.Load() + assert.NotNil(t, p) + assert.Equal(t, lanServer2, p.String()) + p = or.lastLanServer.Load() + assert.NotNil(t, p) + assert.Equal(t, lanServer1, p.String()) + assert.True(t, slices.Equal(*or.publicServer.Load(), publicServers)) + + // No LAN server available. + initializeOsResolver([]string{wanServer}) + assert.Nil(t, or.currentLanServer.Load()) + assert.Nil(t, or.lastLanServer.Load()) + assert.True(t, slices.Equal(*or.publicServer.Load(), publicServers)) +}