diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74f72a0..074d713 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.20.x"] + go: ["1.21.x"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index 4816731..8e70cc6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,12 @@ gon.hcl /Build .DS_Store + +# Release folder +dist/ + +# Binaries +ctrld-* + +# generated file +cmd/cli/rsrc_*.syso diff --git a/README.md b/README.md index 4c7db37..eddcb41 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ $ docker pull controldns/ctrld Alternatively, if you know what you're doing you can download pre-compiled binaries from the [Releases](https://github.com/Control-D-Inc/ctrld/releases) section for the appropriate platform. ## Build -Lastly, you can build `ctrld` from source which requires `go1.20+`: +Lastly, you can build `ctrld` from source which requires `go1.21+`: ```shell $ go build ./cmd/ctrld diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 57761f2..6d6360f 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -50,9 +50,10 @@ var ( ) var ( - v = viper.NewWithOptions(viper.KeyDelimiter("::")) - defaultConfigFile = "ctrld.toml" - rootCertPool *x509.CertPool + v = viper.NewWithOptions(viper.KeyDelimiter("::")) + defaultConfigFile = "ctrld.toml" + rootCertPool *x509.CertPool + errSelfCheckNoAnswer = errors.New("no answer from ctrld listener") ) var basicModeFlags = []string{"listen", "primary_upstream", "secondary_upstream", "domains"} @@ -258,6 +259,16 @@ func initCLI() { return } + status, err := s.Status() + isCtrldInstalled := !errors.Is(err, service.ErrNotInstalled) + + // If pin code was set, do not allow running start command. + if status == service.StatusRunning { + if err := checkDeactivationPin(s); isCheckDeactivationPinErr(err) { + os.Exit(deactivationPinInvalidExitCode) + } + } + if router.Name() != "" && iface != "" { mainLog.Load().Debug().Msg("cleaning up router before installing") _ = p.router.Cleanup() @@ -266,7 +277,22 @@ func initCLI() { tasks := []task{ {s.Stop, false}, {func() error { return doGenerateNextDNSConfig(nextdns) }, true}, - {s.Uninstall, false}, + {func() error { return ensureUninstall(s) }, false}, + {func() error { + // If ctrld is installed, we should not save current DNS settings, because: + // + // - The DNS settings was being set by ctrld already. + // - We could not determine the state of DNS settings before installing ctrld. + if isCtrldInstalled { + return nil + } + + // Save current DNS so we can restore later. + withEachPhysicalInterfaces("", "save DNS settings", func(i *net.Interface) error { + return saveCurrentStaticDNS(i) + }) + return nil + }, false}, {s.Install, false}, {s.Start, true}, // Note that startCmd do not actually write ControlD config, but the config file was @@ -280,17 +306,40 @@ func initCLI() { return } - status := selfCheckStatus(s) - switch status { - case service.StatusRunning: + ok, status, err := selfCheckStatus(s) + switch { + case ok && status == service.StatusRunning: mainLog.Load().Notice().Msg("Service started") default: marker := bytes.Repeat([]byte("="), 32) - mainLog.Load().Error().Msg("ctrld service may not have started due to an error or misconfiguration, service log:") - _, _ = mainLog.Load().Write(marker) - for msg := range runCmdLogCh { - _, _ = mainLog.Load().Write([]byte(msg)) + // If ctrld service is not running, emitting log obtained from ctrld process. + if status != service.StatusRunning { + 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)) + haveLog = true + } + // If we're unable to get log from "ctrld run", notice users about it. + if !haveLog { + mainLog.Load().Write([]byte(`"`)) + } } + // Report any error if occurred. + if err != nil { + _, _ = mainLog.Load().Write(marker) + msg := fmt.Sprintf("An error occurred while performing test query: %s", err) + mainLog.Load().Write([]byte(msg)) + } + // If ctrld service is running but selfCheckStatus failed, it could be related + // to user's system firewall configuration, notice users about it. + if status == service.StatusRunning { + _, _ = mainLog.Load().Write(marker) + mainLog.Load().Write([]byte(`ctrld service was running, but a DNS query could not be sent to its listener`)) + mainLog.Load().Write([]byte(`Please check your system firewall if it is configured to block/intercept/redirect DNS queries`)) + } + _, _ = mainLog.Load().Write(marker) uninstall(p, s) os.Exit(1) @@ -364,6 +413,9 @@ func initCLI() { return } initLogging() + if err := checkDeactivationPin(s); isCheckDeactivationPinErr(err) { + os.Exit(deactivationPinInvalidExitCode) + } if doTasks([]task{{s.Stop, true}}) { p.router.Cleanup() p.resetDNS() @@ -372,6 +424,8 @@ func initCLI() { }, } stopCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, "auto" means the default interface gateway`) + stopCmd.Flags().Int64VarP(&deactivationPin, "pin", "", defaultDeactivationPin, `Pin code for stopping ctrld`) + _ = stopCmd.Flags().MarkHidden("pin") restartCmd := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { @@ -518,10 +572,15 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, if iface == "" { iface = "auto" } + if err := checkDeactivationPin(s); isCheckDeactivationPinErr(err) { + os.Exit(deactivationPinInvalidExitCode) + } uninstall(p, s) }, } uninstallCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, use "auto" for the default gateway interface`) + uninstallCmd.Flags().Int64VarP(&deactivationPin, "pin", "", defaultDeactivationPin, `Pin code for uninstalling ctrld`) + _ = uninstallCmd.Flags().MarkHidden("pin") listIfacesCmd := &cobra.Command{ Use: "list", @@ -792,6 +851,15 @@ func RunMobile(appConfig *AppConfig, appCallback *AppCallback, stopCh chan struc run(appCallback, stopCh) } +// CheckDeactivationPin checks if deactivation pin is valid +func CheckDeactivationPin(pin int64) int { + deactivationPin = pin + if err := checkDeactivationPin(nil); isCheckDeactivationPinErr(err) { + return deactivationPinInvalidExitCode + } + return 0 +} + // run runs ctrld cli with given app callback and stop channel. func run(appCallback *AppCallback, stopCh chan struct{}) { if stopCh == nil { @@ -1171,6 +1239,18 @@ func processNoConfigFlags(noConfigStart bool) { v.Set("upstream", upstream) } +// defaultDeactivationPin is the default value for cdDeactivationPin. +// If cdDeactivationPin equals to this default, it means the pin code is not set from Control D API. +const defaultDeactivationPin = -1 + +// cdDeactivationPin is used in cd mode to decide whether stop and uninstall commands can be run. +var cdDeactivationPin int64 = defaultDeactivationPin + +// deactivationPinNotSet reports whether cdDeactivationPin was not set by processCDFlags. +func deactivationPinNotSet() bool { + return cdDeactivationPin == defaultDeactivationPin +} + func processCDFlags(cfg *ctrld.Config) error { logger := mainLog.Load().With().Str("mode", "cd").Logger() logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID) @@ -1195,6 +1275,11 @@ func processCDFlags(cfg *ctrld.Config) error { return err } + if resolverConfig.DeactivationPin != nil { + logger.Debug().Msg("saving deactivation pin") + cdDeactivationPin = *resolverConfig.DeactivationPin + } + logger.Info().Msg("generating ctrld config from Control-D configuration") *cfg = ctrld.Config{} @@ -1310,41 +1395,44 @@ func defaultIfaceName() string { return dri } -func selfCheckStatus(s service.Service) service.Status { +// selfCheckStatus performs the end-to-end DNS test by sending query to ctrld listener. +// It returns a boolean to indicate whether the check is succeeded, the actual status +// of ctrld service, and an additional error if any. +func selfCheckStatus(s service.Service) (bool, service.Status, error) { status, err := s.Status() if err != nil { mainLog.Load().Warn().Err(err).Msg("could not get service status") - return status + return false, service.StatusUnknown, err } // If ctrld is not running, do nothing, just return the status as-is. if status != service.StatusRunning { - return status + return false, status, nil } dir, err := socketDir() if err != nil { mainLog.Load().Error().Err(err).Msg("failed to check ctrld listener status: could not get home directory") - return service.StatusUnknown + return false, status, err } mainLog.Load().Debug().Msg("waiting for ctrld listener to be ready") cc := newSocketControlClient(s, dir) if cc == nil { - return service.StatusUnknown + return false, status, errors.New("could not connect to control server") } resp, err := cc.post(startedPath, nil) if err != nil { mainLog.Load().Error().Err(err).Msg("failed to connect to control server") - return service.StatusUnknown + return false, status, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { mainLog.Load().Error().Msg("ctrld listener is not ready") - return service.StatusUnknown + return false, status, errors.New("ctrld listener is not ready") } // Not a ctrld upstream, return status as-is. if cfg.FirstUpstream().VerifyDomain() == "" { - return status + return true, status, nil } mainLog.Load().Debug().Msg("ctrld listener is ready") @@ -1369,12 +1457,12 @@ func selfCheckStatus(s service.Service) service.Status { domain := cfg.FirstUpstream().VerifyDomain() if domain == "" { // Nothing to do, return the status as-is. - return status + return true, status, nil } watcher, err := fsnotify.NewWatcher() if err != nil { mainLog.Load().Error().Err(err).Msg("could not watch config change") - return service.StatusUnknown + return false, status, err } defer watcher.Close() @@ -1413,14 +1501,18 @@ func selfCheckStatus(s service.Service) service.Status { m := new(dns.Msg) m.SetQuestion(domain+".", dns.TypeA) m.RecursionDesired = true - r, _, err := c.ExchangeContext(ctx, m, net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))) + r, _, exErr := exchangeContextWithTimeout(c, time.Second, m, net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))) if r != nil && r.Rcode == dns.RcodeSuccess && len(r.Answer) > 0 { mainLog.Load().Debug().Msgf("self-check against %q succeeded", domain) - return status + return true, status, nil + } + // Return early if this is a connection refused. + if errConnectionRefused(exErr) { + return false, status, exErr } lastAnswer = r - lastErr = err - bo.BackOff(ctx, fmt.Errorf("ExchangeContext: %w", err)) + lastErr = exErr + bo.BackOff(ctx, fmt.Errorf("ExchangeContext: %w", exErr)) } mainLog.Load().Debug().Msgf("self-check against %q failed", domain) lc := cfg.FirstListener() @@ -1435,9 +1527,9 @@ func selfCheckStatus(s service.Service) service.Status { for _, s := range strings.Split(lastAnswer.String(), "\n") { mainLog.Load().Debug().Msgf("%s", s) } - mainLog.Load().Debug().Msg(marker) + return false, status, errSelfCheckNoAnswer } - return service.StatusUnknown + return false, status, lastErr } func userHomeDir() (string, error) { @@ -1664,10 +1756,17 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, infoLogger *zerolog.Logger, fata lcc := make(map[string]*listenerConfigCheck) cdMode := cdUID != "" nextdnsMode := nextdns != "" + // For Windows server with local Dns server running, we can only try on random local IP. + hasLocalDnsServer := windowsHasLocalDnsServerRunning() for n, listener := range cfg.Listener { lcc[n] = &listenerConfigCheck{} if listener.IP == "" { listener.IP = "0.0.0.0" + if hasLocalDnsServer { + // Windows Server lies to us that we could listen on 0.0.0.0:53 + // even there's a process already done that, stick to local IP only. + listener.IP = "127.0.0.1" + } lcc[n].IP = true } if listener.Port == 0 { @@ -1676,9 +1775,15 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, infoLogger *zerolog.Logger, fata } // In cd mode, we always try to pick an ip:port pair to work. // Same if nextdns resolver is used. + // + // Except on Windows Server with local Dns running, + // we could only listen on random local IP port 53. if cdMode || nextdnsMode { lcc[n].IP = true lcc[n].Port = true + if hasLocalDnsServer { + lcc[n].Port = false + } } updated = updated || lcc[n].IP || lcc[n].Port } @@ -1764,6 +1869,11 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, infoLogger *zerolog.Logger, fata tryAllPort53 := true tryOldIPPort5354 := true tryPort5354 := true + if hasLocalDnsServer { + tryAllPort53 = false + tryOldIPPort5354 = false + tryPort5354 = false + } attempts := 0 maxAttempts := 10 for { @@ -2049,3 +2159,103 @@ func noticeWritingControlDConfig() error { } return nil } + +// deactivationPinInvalidExitCode indicates exit code due to invalid pin code. +const deactivationPinInvalidExitCode = 126 + +// errInvalidDeactivationPin indicates that the deactivation pin is invalid. +var errInvalidDeactivationPin = errors.New("deactivation pin is invalid") + +// errRequiredDeactivationPin indicates that the deactivation pin is required but not provided by users. +var errRequiredDeactivationPin = errors.New("deactivation pin is required to stop or uninstall the service") + +// checkDeactivationPin validates if the deactivation pin matches one in ControlD config. +func checkDeactivationPin(s service.Service) error { + dir, err := socketDir() + if err != nil { + mainLog.Load().Err(err).Msg("could not check deactivation pin") + return err + } + var cc *controlClient + if s == nil { + cc = newControlClient(filepath.Join(dir, ctrldControlUnixSock)) + } else { + cc = newSocketControlClient(s, dir) + } + if cc == nil { + return nil // ctrld is not running. + } + data, _ := json.Marshal(&deactivationRequest{Pin: deactivationPin}) + resp, _ := cc.post(deactivationPath, bytes.NewReader(data)) + if resp != nil { + switch resp.StatusCode { + case http.StatusBadRequest: + mainLog.Load().Error().Msg(errRequiredDeactivationPin.Error()) + return errRequiredDeactivationPin // pin is required + case http.StatusOK: + return nil // valid pin + case http.StatusNotFound: + return nil // the server is running older version of ctrld + } + } + mainLog.Load().Error().Msg(errInvalidDeactivationPin.Error()) + return errInvalidDeactivationPin +} + +// isCheckDeactivationPinErr reports whether there is an error during check deactivation pin process. +func isCheckDeactivationPinErr(err error) bool { + return errors.Is(err, errInvalidDeactivationPin) || errors.Is(err, errRequiredDeactivationPin) +} + +// ensureUninstall ensures that s.Uninstall will remove ctrld service from system completely. +func ensureUninstall(s service.Service) error { + maxAttempts := 10 + var err error + for i := 0; i < maxAttempts; i++ { + err = s.Uninstall() + if _, err := s.Status(); errors.Is(err, service.ErrNotInstalled) { + return nil + } + time.Sleep(time.Second) + } + return errors.Join(err, errors.New("uninstall failed")) +} + +// exchangeContextWithTimeout wraps c.ExchangeContext with the given timeout. +func exchangeContextWithTimeout(c *dns.Client, timeout time.Duration, msg *dns.Msg, addr string) (*dns.Msg, time.Duration, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return c.ExchangeContext(ctx, msg, addr) +} + +// powershell runs the given powershell command. +func powershell(cmd string) ([]byte, error) { + out, err := exec.Command("powershell", "-Command", cmd).CombinedOutput() + return bytes.TrimSpace(out), err +} + +// windowsHasLocalDnsServerRunning reports whether we are on Windows and having Dns server running. +func windowsHasLocalDnsServerRunning() bool { + if runtime.GOOS == "windows" { + out, _ := powershell("Get-WindowsFeature -Name DNS") + if !bytes.Contains(bytes.ToLower(out), []byte("installed")) { + return false + } + + _, err := powershell("Get-Process -Name DNS") + return err == nil + } + return false +} + +// absHomeDir returns the absolute path to given filename using home directory as root dir. +func absHomeDir(filename string) string { + if homedir != "" { + return filepath.Join(homedir, filename) + } + dir, err := userHomeDir() + if err != nil { + return filename + } + return filepath.Join(dir, filename) +} diff --git a/cmd/cli/control_client.go b/cmd/cli/control_client.go index c626602..73002e8 100644 --- a/cmd/cli/control_client.go +++ b/cmd/cli/control_client.go @@ -27,3 +27,8 @@ func newControlClient(addr string) *controlClient { func (c *controlClient) post(path string, data io.Reader) (*http.Response, error) { return c.c.Post("http://unix"+path, contentTypeJson, data) } + +// deactivationRequest represents request for validating deactivation pin. +type deactivationRequest struct { + Pin int64 `json:"pin"` +} diff --git a/cmd/cli/control_server.go b/cmd/cli/control_server.go index 36749c1..28c20a6 100644 --- a/cmd/cli/control_server.go +++ b/cmd/cli/control_server.go @@ -16,10 +16,11 @@ import ( ) const ( - contentTypeJson = "application/json" - listClientsPath = "/clients" - startedPath = "/started" - reloadPath = "/reload" + contentTypeJson = "application/json" + listClientsPath = "/clients" + startedPath = "/started" + reloadPath = "/reload" + deactivationPath = "/deactivation" ) type controlServer struct { @@ -146,6 +147,30 @@ func (p *prog) registerControlServerHandler() { // Otherwise, reload is done. w.WriteHeader(http.StatusOK) })) + p.cs.register(deactivationPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + // Non-cd mode or pin code not set, always allowing deactivation. + if cdUID == "" || deactivationPinNotSet() { + w.WriteHeader(http.StatusOK) + return + } + + var req deactivationRequest + if err := json.NewDecoder(request.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusPreconditionFailed) + mainLog.Load().Err(err).Msg("invalid deactivation request") + return + } + + code := http.StatusForbidden + switch req.Pin { + case cdDeactivationPin: + code = http.StatusOK + case defaultDeactivationPin: + // If the pin code was set, but users do not provide --pin, return proper code to client. + code = http.StatusBadRequest + } + w.WriteHeader(code) + })) } func jsonResponse(next http.Handler) http.Handler { diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 3f1ef8b..9c64aa0 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -34,6 +34,7 @@ var ( ifaceStartStop string nextdns string cdUpstreamProto string + deactivationPin int64 mainLog atomic.Pointer[zerolog.Logger] consoleWriter zerolog.ConsoleWriter diff --git a/cmd/cli/net.go b/cmd/cli/net.go new file mode 100644 index 0000000..80da827 --- /dev/null +++ b/cmd/cli/net.go @@ -0,0 +1,34 @@ +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 f456327..37f8d7b 100644 --- a/cmd/cli/net_darwin.go +++ b/cmd/cli/net_darwin.go @@ -42,3 +42,21 @@ func networkServiceName(ifaceName string, r io.Reader) string { } return "" } + +// validInterface reports whether the *net.Interface is a valid one, which includes: +// +// - en0: physical wireless +// - en1: Thunderbolt 1 +// - en2: Thunderbolt 2 +// - en3: Thunderbolt 3 +// - en4: Thunderbolt 4 +// +// For full list, see: https://unix.stackexchange.com/questions/603506/what-are-these-ifconfig-interfaces-on-macos +func validInterface(iface *net.Interface) bool { + switch iface.Name { + case "en0", "en1", "en2", "en3", "en4": + return true + default: + return false + } +} diff --git a/cmd/cli/net_others.go b/cmd/cli/net_others.go index 2f7aec8..ebe7ba0 100644 --- a/cmd/cli/net_others.go +++ b/cmd/cli/net_others.go @@ -1,7 +1,9 @@ -//go:build !darwin +//go:build !darwin && !windows package cli import "net" func patchNetIfaceName(iface *net.Interface) error { return nil } + +func validInterface(iface *net.Interface) bool { return true } diff --git a/cmd/cli/net_windows.go b/cmd/cli/net_windows.go new file mode 100644 index 0000000..c75ee32 --- /dev/null +++ b/cmd/cli/net_windows.go @@ -0,0 +1,21 @@ +package cli + +import ( + "net" +) + +func patchNetIfaceName(iface *net.Interface) error { + return nil +} + +// validInterface reports whether the *net.Interface is a valid one. +// On Windows, only physical interfaces are considered valid. +func validInterface(iface *net.Interface) bool { + if iface == nil { + return false + } + if isPhysicalInterface(iface.HardwareAddr.String()) { + return true + } + return false +} diff --git a/cmd/cli/os_darwin.go b/cmd/cli/os_darwin.go index 5931819..7ce4aa1 100644 --- a/cmd/cli/os_darwin.go +++ b/cmd/cli/os_darwin.go @@ -1,6 +1,9 @@ package cli import ( + "bufio" + "bytes" + "fmt" "net" "os/exec" @@ -34,22 +37,23 @@ func setDNS(iface *net.Interface, nameservers []string) error { cmd := "networksetup" args := []string{"-setdnsservers", iface.Name} args = append(args, nameservers...) - - if err := exec.Command(cmd, args...).Run(); err != nil { - mainLog.Load().Error().Err(err).Msgf("setDNS failed, ips = %q", nameservers) - return err + if out, err := exec.Command(cmd, args...).CombinedOutput(); err != nil { + return fmt.Errorf("%v: %w", string(out), err) } return nil } // TODO(cuonglm): use system API func resetDNS(iface *net.Interface) error { + if ns := savedStaticNameservers(iface); len(ns) > 0 { + if err := setDNS(iface, ns); err == nil { + return nil + } + } cmd := "networksetup" args := []string{"-setdnsservers", iface.Name, "empty"} - - if err := exec.Command(cmd, args...).Run(); err != nil { - mainLog.Load().Error().Err(err).Msgf("resetDNS failed") - return err + if out, err := exec.Command(cmd, args...).CombinedOutput(); err != nil { + return fmt.Errorf("%v: %w", string(out), err) } return nil } @@ -57,3 +61,22 @@ func resetDNS(iface *net.Interface) error { func currentDNS(_ *net.Interface) []string { return resolvconffile.NameServers("") } + +// currentStaticDNS returns the current static DNS settings of given interface. +func currentStaticDNS(iface *net.Interface) ([]string, error) { + cmd := "networksetup" + args := []string{"-getdnsservers", iface.Name} + out, err := exec.Command(cmd, args...).Output() + if err != nil { + return nil, err + } + scanner := bufio.NewScanner(bytes.NewReader(out)) + var ns []string + for scanner.Scan() { + line := scanner.Text() + if ip := net.ParseIP(line); ip != nil { + ns = append(ns, ip.String()) + } + } + return ns, nil +} diff --git a/cmd/cli/os_freebsd.go b/cmd/cli/os_freebsd.go index a6d6dde..216b36f 100644 --- a/cmd/cli/os_freebsd.go +++ b/cmd/cli/os_freebsd.go @@ -66,3 +66,8 @@ func resetDNS(iface *net.Interface) error { func currentDNS(_ *net.Interface) []string { return resolvconffile.NameServers("") } + +// currentStaticDNS returns the current static DNS settings of given interface. +func currentStaticDNS(iface *net.Interface) ([]string, error) { + return currentDNS(iface), nil +} diff --git a/cmd/cli/os_linux.go b/cmd/cli/os_linux.go index 3036d03..3d9bffd 100644 --- a/cmd/cli/os_linux.go +++ b/cmd/cli/os_linux.go @@ -9,12 +9,10 @@ import ( "net" "net/netip" "os/exec" - "path/filepath" "strings" "syscall" "time" - "github.com/fsnotify/fsnotify" "github.com/insomniacslk/dhcp/dhcpv4/nclient4" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/client6" @@ -25,11 +23,6 @@ import ( "github.com/Control-D-Inc/ctrld/internal/resolvconffile" ) -const ( - resolvConfPath = "/etc/resolv.conf" - resolvConfBackupFailedMsg = "open /etc/resolv.pre-ctrld-backup.conf: read-only file system" -) - // allocate loopback ip // sudo ip a add 127.0.0.2/24 dev lo func allocateIP(ip string) error { @@ -69,12 +62,6 @@ func setDNS(iface *net.Interface, nameservers []string) error { Nameservers: ns, SearchDomains: []dnsname.FQDN{}, } - defer func() { - if r.Mode() == "direct" { - go watchResolveConf(osConfig) - } - }() - trySystemdResolve := false for i := 0; i < maxSetDNSAttempts; i++ { if err := r.SetDNS(osConfig); err != nil { @@ -203,6 +190,11 @@ func currentDNS(iface *net.Interface) []string { return nil } +// currentStaticDNS returns the current static DNS settings of given interface. +func currentStaticDNS(iface *net.Interface) ([]string, error) { + return currentDNS(iface), nil +} + func getDNSByResolvectl(iface string) []string { b, err := exec.Command("resolvectl", "dns", "-i", iface).Output() if err != nil { @@ -309,59 +301,3 @@ func sliceIndex[S ~[]E, E comparable](s S, v E) int { } return -1 } - -// watchResolveConf watches any changes to /etc/resolv.conf file, -// and reverting to the original config set by ctrld. -func watchResolveConf(oc dns.OSConfig) { - mainLog.Load().Debug().Msg("start watching /etc/resolv.conf file") - watcher, err := fsnotify.NewWatcher() - if err != nil { - mainLog.Load().Warn().Err(err).Msg("could not create watcher for /etc/resolv.conf") - return - } - - // We watch /etc instead of /etc/resolv.conf directly, - // see: https://github.com/fsnotify/fsnotify#watching-a-file-doesnt-work-well - watchDir := filepath.Dir(resolvConfPath) - if err := watcher.Add(watchDir); err != nil { - mainLog.Load().Warn().Err(err).Msg("could not add /etc/resolv.conf to watcher list") - return - } - - r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter. - if err != nil { - mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator") - return - } - - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return - } - if event.Name != resolvConfPath { // skip if not /etc/resolv.conf changes. - continue - } - if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) { - mainLog.Load().Debug().Msg("/etc/resolv.conf changes detected, reverting to ctrld setting") - if err := watcher.Remove(watchDir); err != nil { - mainLog.Load().Error().Err(err).Msg("failed to pause watcher") - continue - } - if err := r.SetDNS(oc); err != nil { - mainLog.Load().Error().Err(err).Msg("failed to revert /etc/resolv.conf changes") - } - if err := watcher.Add(watchDir); err != nil { - mainLog.Load().Error().Err(err).Msg("failed to continue running watcher") - return - } - } - case err, ok := <-watcher.Errors: - if !ok { - return - } - mainLog.Load().Err(err).Msg("could not get event for /etc/resolv.conf") - } - } -} diff --git a/cmd/cli/os_windows.go b/cmd/cli/os_windows.go index a58411e..56097f8 100644 --- a/cmd/cli/os_windows.go +++ b/cmd/cli/os_windows.go @@ -2,21 +2,56 @@ package cli import ( "errors" + "fmt" "net" + "os" "os/exec" "strconv" + "strings" + "sync" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" ) +const ( + forwardersFilename = ".forwarders.txt" + v4InterfaceKeyPathFormat = `HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\` + v6InterfaceKeyPathFormat = `HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\` +) + +var ( + setDNSOnce sync.Once + resetDNSOnce sync.Once +) + func setDNS(iface *net.Interface, nameservers []string) error { if len(nameservers) == 0 { return errors.New("empty DNS nameservers") } + setDNSOnce.Do(func() { + // If there's a Dns server running, that means we are on AD with Dns feature enabled. + // Configuring the Dns server to forward queries to ctrld instead. + if windowsHasLocalDnsServerRunning() { + file := absHomeDir(forwardersFilename) + if data, _ := os.ReadFile(file); len(data) > 0 { + if err := removeDnsServerForwarders(strings.Split(string(data), ",")); err != nil { + mainLog.Load().Error().Err(err).Msg("could not remove current forwarders settings") + } else { + mainLog.Load().Debug().Msg("removed current forwarders settings.") + } + } + if err := os.WriteFile(file, []byte(strings.Join(nameservers, ",")), 0600); err != nil { + mainLog.Load().Warn().Err(err).Msg("could not save forwarders settings") + } + if err := addDnsServerForwarders(nameservers); err != nil { + mainLog.Load().Warn().Err(err).Msg("could not set forwarders settings") + } + } + }) primaryDNS := nameservers[0] - if err := setPrimaryDNS(iface, primaryDNS); err != nil { + if err := setPrimaryDNS(iface, primaryDNS, true); err != nil { return err } if len(nameservers) > 1 { @@ -28,20 +63,64 @@ func setDNS(iface *net.Interface, nameservers []string) error { // TODO(cuonglm): should we use system API? func resetDNS(iface *net.Interface) error { + resetDNSOnce.Do(func() { + // See corresponding comment in setDNS. + if windowsHasLocalDnsServerRunning() { + file := absHomeDir(forwardersFilename) + content, err := os.ReadFile(file) + if err != nil { + mainLog.Load().Error().Err(err).Msg("could not read forwarders settings") + return + } + nameservers := strings.Split(string(content), ",") + if err := removeDnsServerForwarders(nameservers); err != nil { + mainLog.Load().Error().Err(err).Msg("could not remove forwarders settings") + return + } + } + }) + + // Restoring ipv6 first. if ctrldnet.SupportsIPv6ListenLocal() { if output, err := netsh("interface", "ipv6", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp"); err != nil { mainLog.Load().Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output)) } } + // Restoring ipv4 DHCP. output, err := netsh("interface", "ipv4", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp") if err != nil { - mainLog.Load().Error().Err(err).Msgf("failed to reset ipv4 DNS: %s", string(output)) - return err + return fmt.Errorf("%s: %w", string(output), err) + } + // If there's static DNS saved, restoring it. + if nss := savedStaticNameservers(iface); len(nss) > 0 { + v4ns := make([]string, 0, 2) + v6ns := make([]string, 0, 2) + for _, ns := range nss { + if ctrldnet.IsIPv6(ns) { + v6ns = append(v6ns, ns) + } else { + v4ns = append(v4ns, ns) + } + } + + for _, ns := range [][]string{v4ns, v6ns} { + if len(ns) == 0 { + continue + } + primaryDNS := ns[0] + if err := setPrimaryDNS(iface, primaryDNS, false); err != nil { + return err + } + if len(ns) > 1 { + secondaryDNS := ns[1] + _ = addSecondaryDNS(iface, secondaryDNS) + } + } } return nil } -func setPrimaryDNS(iface *net.Interface, dns string) error { +func setPrimaryDNS(iface *net.Interface, dns string, disablev6 bool) error { ipVer := "ipv4" if ctrldnet.IsIPv6(dns) { ipVer = "ipv6" @@ -52,7 +131,7 @@ func setPrimaryDNS(iface *net.Interface, dns string) error { mainLog.Load().Error().Err(err).Msgf("failed to set primary DNS: %s", string(output)) return err } - if ipVer == "ipv4" && ctrldnet.SupportsIPv6ListenLocal() { + if disablev6 && ipVer == "ipv4" && ctrldnet.SupportsIPv6ListenLocal() { // Disable IPv6 DNS, so the query will be fallback to IPv4. _, _ = netsh("interface", "ipv6", "set", "dnsserver", idx, "static", "::1", "primary") } @@ -93,3 +172,54 @@ func currentDNS(iface *net.Interface) []string { } return ns } + +// currentStaticDNS returns the current static DNS settings of given interface. +func currentStaticDNS(iface *net.Interface) ([]string, error) { + luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index)) + if err != nil { + return nil, err + } + guid, err := luid.GUID() + if err != nil { + return nil, err + } + var ns []string + for _, path := range []string{v4InterfaceKeyPathFormat, v6InterfaceKeyPathFormat} { + interfaceKeyPath := path + guid.String() + found := false + for _, key := range []string{"NameServer", "ProfileNameServer"} { + if found { + continue + } + cmd := fmt.Sprintf(`Get-ItemPropertyValue -Path "%s" -Name "%s"`, interfaceKeyPath, key) + out, err := powershell(cmd) + if err == nil && len(out) > 0 { + found = true + ns = append(ns, strings.Split(string(out), ",")...) + } + } + } + return ns, nil +} + +// addDnsServerForwarders adds given nameservers to DNS server forwarders list. +func addDnsServerForwarders(nameservers []string) error { + for _, ns := range nameservers { + cmd := fmt.Sprintf("Add-DnsServerForwarder -IPAddress %s", ns) + if out, err := powershell(cmd); err != nil { + return fmt.Errorf("%w: %s", err, string(out)) + } + } + return nil +} + +// removeDnsServerForwarders removes given nameservers from DNS server forwarders list. +func removeDnsServerForwarders(nameservers []string) error { + for _, ns := range nameservers { + cmd := fmt.Sprintf("Remove-DnsServerForwarder -IPAddress %s -Force", ns) + if out, err := powershell(cmd); err != nil { + return fmt.Errorf("%w: %s", err, string(out)) + } + } + return nil +} diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 4b3968f..6febff8 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "io/fs" "math/rand" "net" "net/netip" @@ -13,6 +14,7 @@ import ( "runtime" "sort" "strconv" + "strings" "sync" "syscall" @@ -417,8 +419,14 @@ func (p *prog) setDNS() { if iface == "" { return } + // allIfaces tracks whether we should set DNS for all physical interfaces. + allIfaces := false if iface == "auto" { iface = defaultIfaceName() + // If iface is "auto", it means user does not specify "--iface" flag. + // In this case, ctrld has to set DNS for all physical interfaces, so + // thing will still work when user switch from one to the other. + allIfaces = requiredMultiNICsConfig() } lc := cfg.FirstListener() if lc == nil { @@ -460,14 +468,29 @@ func (p *prog) setDNS() { return } logger.Debug().Msg("setting DNS successfully") + if shouldWatchResolvconf() { + servers := make([]netip.Addr, len(nameservers)) + for i := range nameservers { + servers[i] = netip.MustParseAddr(nameservers[i]) + } + go watchResolvConf(netIface, servers, setResolvConf) + } + if allIfaces { + withEachPhysicalInterfaces(netIface.Name, "set DNS", func(i *net.Interface) error { + return setDNS(i, nameservers) + }) + } } func (p *prog) resetDNS() { if iface == "" { return } + allIfaces := false if iface == "auto" { iface = defaultIfaceName() + // See corresponding comments in (*prog).setDNS function. + allIfaces = requiredMultiNICsConfig() } logger := mainLog.Load().With().Str("iface", iface).Logger() netIface, err := netInterface(iface) @@ -485,6 +508,9 @@ func (p *prog) resetDNS() { return } logger.Debug().Msg("Restoring DNS successfully") + if allIfaces { + withEachPhysicalInterfaces(netIface.Name, "reset DNS", resetDNS) + } } func randomLocalIP() string { @@ -568,6 +594,15 @@ func errNetworkError(err error) bool { return false } +// errConnectionRefused reports whether err is connection refused. +func errConnectionRefused(err error) bool { + var opErr *net.OpError + if !errors.As(err, &opErr) { + return false + } + return errors.Is(opErr.Err, syscall.ECONNREFUSED) || errors.Is(opErr.Err, windowsECONNREFUSED) +} + func ifaceFirstPrivateIP(iface *net.Interface) string { if iface == nil { return "" @@ -649,3 +684,87 @@ func canBeLocalUpstream(addr string) bool { } return false } + +// withEachPhysicalInterfaces runs the function f with each physical interfaces, excluding +// the interface that matches excludeIfaceName. The context is used to clarify the +// log message when error happens. +func withEachPhysicalInterfaces(excludeIfaceName, context string, f func(i *net.Interface) error) { + interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { + // Skip loopback/virtual interface. + if i.IsLoopback() || len(i.HardwareAddr) == 0 { + return + } + // Skip invalid interface. + if !validInterface(i.Interface) { + return + } + netIface := i.Interface + if err := patchNetIfaceName(netIface); err != nil { + mainLog.Load().Debug().Err(err).Msg("failed to patch net interface name") + return + } + // Skip excluded interface. + if netIface.Name == excludeIfaceName { + return + } + // TODO: investigate whether we should report this error? + if err := f(netIface); err == nil { + mainLog.Load().Debug().Msgf("%s for interface %q successfully", context, i.Name) + } else if !errors.Is(err, errSaveCurrentStaticDNSNotSupported) { + mainLog.Load().Err(err).Msgf("%s for interface %q failed", context, i.Name) + } + }) +} + +// requiredMultiNicConfig reports whether ctrld needs to set/reset DNS for multiple NICs. +func requiredMultiNICsConfig() bool { + switch runtime.GOOS { + case "windows", "darwin": + return true + default: + return false + } +} + +var errSaveCurrentStaticDNSNotSupported = errors.New("saving current DNS is not supported on this platform") + +// saveCurrentStaticDNS saves the current static DNS settings for restoring later. +// Only works on Windows and Mac. +func saveCurrentStaticDNS(iface *net.Interface) error { + switch runtime.GOOS { + case "windows", "darwin": + default: + return errSaveCurrentStaticDNSNotSupported + } + file := savedStaticDnsSettingsFilePath(iface) + ns, _ := currentStaticDNS(iface) + if len(ns) == 0 { + _ = os.Remove(file) // removing old static DNS settings + return nil + } + 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 { + mainLog.Load().Err(err).Msgf("could not save DNS settings for iface: %s", iface.Name) + return err + } + return nil +} + +// savedStaticDnsSettingsFilePath returns the path to saved DNS settings of the given interface. +func savedStaticDnsSettingsFilePath(iface *net.Interface) string { + return absHomeDir(".dns_" + iface.Name) +} + +// savedStaticNameservers returns the static DNS nameservers of the given interface. +// +//lint:ignore U1000 use in os_windows.go and os_darwin.go +func savedStaticNameservers(iface *net.Interface) []string { + file := savedStaticDnsSettingsFilePath(iface) + if data, _ := os.ReadFile(file); len(data) > 0 { + return strings.Split(string(data), ",") + } + return nil +} diff --git a/cmd/cli/resolvconf.go b/cmd/cli/resolvconf.go new file mode 100644 index 0000000..f09d864 --- /dev/null +++ b/cmd/cli/resolvconf.go @@ -0,0 +1,65 @@ +package cli + +import ( + "net" + "net/netip" + "path/filepath" + + "github.com/fsnotify/fsnotify" +) + +const ( + resolvConfPath = "/etc/resolv.conf" + resolvConfBackupFailedMsg = "open /etc/resolv.pre-ctrld-backup.conf: read-only file system" +) + +// watchResolvConf watches any changes to /etc/resolv.conf file, +// and reverting to the original config set by ctrld. +func watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn func(iface *net.Interface, ns []netip.Addr) error) { + mainLog.Load().Debug().Msg("start watching /etc/resolv.conf file") + watcher, err := fsnotify.NewWatcher() + if err != nil { + mainLog.Load().Warn().Err(err).Msg("could not create watcher for /etc/resolv.conf") + return + } + defer watcher.Close() + + // We watch /etc instead of /etc/resolv.conf directly, + // see: https://github.com/fsnotify/fsnotify#watching-a-file-doesnt-work-well + watchDir := filepath.Dir(resolvConfPath) + if err := watcher.Add(watchDir); err != nil { + mainLog.Load().Warn().Err(err).Msg("could not add /etc/resolv.conf to watcher list") + return + } + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Name != resolvConfPath { // skip if not /etc/resolv.conf changes. + continue + } + if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) { + mainLog.Load().Debug().Msg("/etc/resolv.conf changes detected, reverting to ctrld setting") + if err := watcher.Remove(watchDir); err != nil { + mainLog.Load().Error().Err(err).Msg("failed to pause watcher") + continue + } + if err := setDnsFn(iface, ns); err != nil { + mainLog.Load().Error().Err(err).Msg("failed to revert /etc/resolv.conf changes") + } + if err := watcher.Add(watchDir); err != nil { + mainLog.Load().Error().Err(err).Msg("failed to continue running watcher") + return + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + mainLog.Load().Err(err).Msg("could not get event for /etc/resolv.conf") + } + } +} diff --git a/cmd/cli/resolvconf_darwin.go b/cmd/cli/resolvconf_darwin.go new file mode 100644 index 0000000..7e26f41 --- /dev/null +++ b/cmd/cli/resolvconf_darwin.go @@ -0,0 +1,20 @@ +package cli + +import ( + "net" + "net/netip" +) + +// setResolvConf sets the content of resolv.conf file using the given nameservers list. +func setResolvConf(iface *net.Interface, ns []netip.Addr) error { + servers := make([]string, len(ns)) + for i := range ns { + servers[i] = ns[i].String() + } + return setDNS(iface, servers) +} + +// shouldWatchResolvconf reports whether ctrld should watch changes to resolv.conf file with given OS configurator. +func shouldWatchResolvconf() bool { + return true +} diff --git a/cmd/cli/resolvconf_not_darwin_unix.go b/cmd/cli/resolvconf_not_darwin_unix.go new file mode 100644 index 0000000..b98496e --- /dev/null +++ b/cmd/cli/resolvconf_not_darwin_unix.go @@ -0,0 +1,40 @@ +//go:build unix && !darwin + +package cli + +import ( + "net" + "net/netip" + + "tailscale.com/util/dnsname" + + "github.com/Control-D-Inc/ctrld/internal/dns" +) + +// 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. + if err != nil { + return err + } + + oc := dns.OSConfig{ + Nameservers: ns, + SearchDomains: []dnsname.FQDN{}, + } + return r.SetDNS(oc) +} + +// 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. + if err != nil { + return false + } + switch r.Mode() { + case "direct", "resolvconf": + return true + default: + return false + } +} diff --git a/cmd/cli/resolvconf_windows.go b/cmd/cli/resolvconf_windows.go new file mode 100644 index 0000000..3e4ba1c --- /dev/null +++ b/cmd/cli/resolvconf_windows.go @@ -0,0 +1,16 @@ +package cli + +import ( + "net" + "net/netip" +) + +// setResolvConf sets the content of resolv.conf file using the given nameservers list. +func setResolvConf(_ *net.Interface, _ []netip.Addr) error { + return nil +} + +// shouldWatchResolvconf reports whether ctrld should watch changes to resolv.conf file with given OS configurator. +func shouldWatchResolvconf() bool { + return false +} diff --git a/cmd/cli/winres/winres.json b/cmd/cli/winres/winres.json new file mode 100644 index 0000000..fd12759 --- /dev/null +++ b/cmd/cli/winres/winres.json @@ -0,0 +1,20 @@ +{ + "RT_VERSION": { + "#1": { + "0000": { + "fixed": { + "file_version": "0.0.0.1" + }, + "info": { + "0409": { + "CompanyName": "ControlD Inc", + "FileDescription": "Control D DNS daemon", + "ProductName": "ctrld", + "InternalName": "ctrld", + "LegalCopyright": "ControlD Inc 2024" + } + } + } + } + } +} diff --git a/cmd/cli/winres_windows.go b/cmd/cli/winres_windows.go new file mode 100644 index 0000000..30ebd95 --- /dev/null +++ b/cmd/cli/winres_windows.go @@ -0,0 +1,4 @@ +//go:generate go-winres make --product-version=git-tag --file-version=git-tag +package cli + +// Placeholder file for windows builds. diff --git a/cmd/ctrld_library/main.go b/cmd/ctrld_library/main.go index 9bcc151..ec42b9c 100644 --- a/cmd/ctrld_library/main.go +++ b/cmd/ctrld_library/main.go @@ -61,13 +61,13 @@ func mapCallback(callback AppCallback) cli.AppCallback { } } -func (c *Controller) Stop() bool { - if c.stopCh != nil { +func (c *Controller) Stop(Pin int64) int { + errorCode := cli.CheckDeactivationPin(Pin) + if errorCode == 0 && c.stopCh != nil { close(c.stopCh) c.stopCh = nil - return true } - return false + return errorCode } func (c *Controller) IsRunning() bool { diff --git a/config_quic.go b/config_quic.go index 5103231..a6dd8b7 100644 --- a/config_quic.go +++ b/config_quic.go @@ -1,5 +1,3 @@ -//go:build !qf - package ctrld import ( diff --git a/config_quic_free.go b/config_quic_free.go deleted file mode 100644 index a674a1b..0000000 --- a/config_quic_free.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build qf - -package ctrld - -import "net/http" - -func (uc *UpstreamConfig) setupDOH3Transport() {} - -func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper { return nil } diff --git a/docs/config.md b/docs/config.md index 8691ff6..5d099ea 100644 --- a/docs/config.md +++ b/docs/config.md @@ -224,11 +224,8 @@ DHCP leases file format. - Default: "" ### client_id_preference -Decide how the client ID is generated +Decide how the client ID is generated. By default client ID will use both MAC address and Hostname i.e. `hash(mac + host)`. To override this behavior, select one of the 2 allowed values to scope client ID to just MAC address OR Hostname. -If `host` -> client id will only use the hostname i.e.`hash(hostname)`. -If `mac` -> client id will only use the MAC address `hash(mac)`. -Else -> client ID will use both Mac and Hostname i.e. `hash(mac + host) - Type: string - Required: no - Valid values: `mac`, `host` @@ -242,7 +239,7 @@ If set to `true`, collect and export the query counters, and show them in `clien - Default: false ### metrics_listener -Specifying the `ip` and `port` of the metrics server. +Specifying the `ip` and `port` of the Prometheus metrics server. The Prometheus metrics will be available on: `http://ip:port/metrics`. You can also append `/metrics/json` to get the same data in json format. - Type: string - Required: no diff --git a/go.mod b/go.mod index ebb62d2..0476717 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Control-D-Inc/ctrld -go 1.20 +go 1.21 require ( github.com/coreos/go-systemd/v22 v22.5.0 @@ -20,8 +20,9 @@ require ( 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/prom2json v1.3.3 - github.com/quic-go/quic-go v0.38.0 + github.com/quic-go/quic-go v0.41.0 github.com/rs/zerolog v1.28.0 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 @@ -43,7 +44,6 @@ require ( 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/mock v1.6.0 // 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 @@ -66,11 +66,9 @@ require ( 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/client_model v0.4.0 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-20 v0.3.2 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/spf13/afero v1.9.5 // indirect @@ -79,10 +77,11 @@ require ( github.com/subosito/gotenv v1.4.2 // indirect github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect github.com/vishvananda/netns v0.0.4 // indirect + go.uber.org/mock v0.3.0 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect - golang.org/x/mod v0.10.0 // indirect + golang.org/x/mod v0.11.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.9.1 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/go.sum b/go.sum index bbfe6c7..6ab5340 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR 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/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= @@ -78,6 +79,7 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 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-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= @@ -102,8 +104,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -162,6 +162,7 @@ github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf 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/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= @@ -228,6 +229,7 @@ github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6 github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= 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= @@ -251,10 +253,8 @@ github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcET github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-20 v0.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI= -github.com/quic-go/qtls-go1-20 v0.3.2/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/quic-go/quic-go v0.38.0 h1:T45lASr5q/TrVwt+jrVccmqHhPL2XuSyoCLVCpfOSLc= -github.com/quic-go/quic-go v0.38.0/go.mod h1:MPCuRq7KBK2hNcfKj/1iD1BGuN3eAYMeNxp3T42LRUg= +github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k= +github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -304,13 +304,14 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= +go.uber.org/mock v0.3.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= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -358,9 +359,8 @@ 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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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/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= @@ -393,7 +393,6 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= @@ -416,7 +415,6 @@ 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.0.0-20210220032951-036812b2e83c/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -456,10 +454,8 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -532,7 +528,6 @@ 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.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index 7f41fca..1a775ea 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -384,6 +384,7 @@ func (t *Table) ListClients() []*Client { } } } + clientsByMAC := make(map[string]*Client) for ip := range ipMap { c := ipMap[ip] for _, e := range t.lookupMacAll(ip) { @@ -397,6 +398,7 @@ func (t *Table) ListClients() []*Client { for _, e := range t.lookupHostnameAll(ip, c.Mac) { if c.Hostname == "" && e.name != "" { c.Hostname = e.name + clientsByMAC[c.Mac] = c } if e.name != "" { c.Source[e.src] = struct{}{} @@ -405,6 +407,11 @@ func (t *Table) ListClients() []*Client { } clients := make([]*Client, 0, len(ipMap)) for _, c := range ipMap { + // If we found a client with empty hostname, use hostname from + // an existed client which has the same MAC address. + if cFromMac := clientsByMAC[c.Mac]; cFromMac != nil && c.Hostname == "" { + c.Hostname = cFromMac.Hostname + } clients = append(clients, c) } return clients diff --git a/internal/clientinfo/client_info_test.go b/internal/clientinfo/client_info_test.go index e6575f2..b5bdfa5 100644 --- a/internal/clientinfo/client_info_test.go +++ b/internal/clientinfo/client_info_test.go @@ -44,3 +44,31 @@ func TestTable_LookupRFC1918IPv4(t *testing.T) { t.Fatalf("unexpected result, want: %s, got: %s", rfc1918IPv4, got) } } + +func TestTable_ListClients(t *testing.T) { + mac := "74:56:3c:44:eb:5e" + ipv6_1 := "2405:4803:a04b:4190:fbe9:cd14:d522:bbae" + ipv6_2 := "2405:4803:a04b:4190:fbe9:cd14:d522:bbab" + table := &Table{} + + // NDP init. + table.ndp = &ndpDiscover{} + table.ndp.mac.Store(ipv6_1, mac) + table.ndp.mac.Store(ipv6_2, mac) + table.ndp.ip.Store(mac, ipv6_1) + table.ndp.ip.Store(mac, ipv6_2) + table.ipResolvers = append(table.ipResolvers, table.ndp) + table.macResolvers = append(table.macResolvers, table.ndp) + + hostname := "foo" + // mdns init. + table.mdns = &mdns{} + table.mdns.name.Store(ipv6_2, hostname) + table.hostnameResolvers = append(table.hostnameResolvers, table.mdns) + + for _, c := range table.ListClients() { + if c.Hostname != hostname { + t.Fatalf("missing hostname for client: %v", c) + } + } +} diff --git a/internal/controld/config.go b/internal/controld/config.go index 4cc6770..c095c0c 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -35,8 +35,9 @@ type ResolverConfig struct { Ctrld struct { CustomConfig string `json:"custom_config"` } `json:"ctrld"` - Exclude []string `json:"exclude"` - UID string `json:"uid"` + Exclude []string `json:"exclude"` + UID string `json:"uid"` + DeactivationPin *int64 `json:"deactivation_pin,omitempty"` } type utilityResponse struct { diff --git a/internal/router/router.go b/internal/router/router.go index b8a414b..2990cd7 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -199,7 +199,7 @@ func distroName() string { return merlin.Name case haveFile("/etc/openwrt_version"): return openwrt.Name - case haveDir("/data/unifi"): + case isUbios(): return ubios.Name case bytes.HasPrefix(unameU(), []byte("synology")): return synology.Name @@ -234,3 +234,14 @@ func unameU() []byte { out, _ := exec.Command("uname", "-u").Output() return out } + +// isUbios reports whether the current machine is running on Ubios. +func isUbios() bool { + if haveDir("/data/unifi") { + return true + } + if err := exec.Command("ubnt-device-info", "firmware").Run(); err == nil { + return true + } + return false +} diff --git a/scripts/build.sh b/scripts/build.sh index 6c96f0f..2faeddc 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -72,17 +72,9 @@ build() { if [ "$CGO_ENABLED" = "0" ]; then binary=${binary}-nocgo fi + GOOS=${goos} GOARCH=${goarch} GOARM=${3} "$go" generate ./... GOOS=${goos} GOARCH=${goarch} GOARM=${3} "$go" build -ldflags="$ldflags" -o "$binary" ./cmd/ctrld compress "$binary" - - if [ -z "${CTRLD_NO_QF}" ]; then - binary_qf=${executable_name}-qf-${goos}-${goarch}v${3} - if [ "$CGO_ENABLED" = "0" ]; then - binary_qf=${binary_qf}-nocgo - fi - GOOS=${goos} GOARCH=${goarch} GOARM=${3} "$go" build -ldflags="$ldflags" -tags=qf -o "$binary_qf" ./cmd/ctrld - compress "$binary_qf" - fi ;; *) # GOMIPS is required for linux/mips: https://nileshgr.com/2020/02/16/golang-on-openwrt-mips/ @@ -90,17 +82,9 @@ build() { if [ "$CGO_ENABLED" = "0" ]; then binary=${binary}-nocgo fi + GOOS=${goos} GOARCH=${goarch} GOMIPS=softfloat "$go" generate ./... GOOS=${goos} GOARCH=${goarch} GOMIPS=softfloat "$go" build -ldflags="$ldflags" -o "$binary" ./cmd/ctrld compress "$binary" - - if [ -z "${CTRLD_NO_QF}" ]; then - binary_qf=${executable_name}-qf-${goos}-${goarch} - if [ "$CGO_ENABLED" = "0" ]; then - binary_qf=${binary_qf}-nocgo - fi - GOOS=${goos} GOARCH=${goarch} GOMIPS=softfloat "$go" build -ldflags="$ldflags" -tags=qf -o "$binary_qf" ./cmd/ctrld - compress "$binary_qf" - fi ;; esac } diff --git a/scripts/build_lib.sh b/scripts/build_lib.sh new file mode 100755 index 0000000..332a5ef --- /dev/null +++ b/scripts/build_lib.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# This script is used to locally build Android .aar library and iOS .xcframework from ctrld source using go mobile tool. + +# Requirements: +# - Android NDK (version 23+) +# - Android SDK (version 33+) +# - Xcode 15 + Build tools +# - Go 1.21 +# - Git +# usage: $ ./build_lib.sh v1.3.4 + +TAG="$1" +# Hacky way to replace version info. +update_versionInfo() { + local file="$1/ctrld/cmd/cli/cli.go" + local tag="$2" + local commit="$3" + awk -v tag="$tag" -v commit="$commit" ' + BEGIN { version_updated = 0; commit_updated = 0 } + /^\tversion/ { + sub(/= ".+"/, "= \"" tag "\""); + version_updated = 1; + } + /^\tcommit/ { + sub(/= ".+"/, "= \"" commit "\""); + commit_updated = 1; + } + { print } + END { + if (version_updated == 0) { + print "\tversion = \"" tag "\""; + } + if (commit_updated == 0) { + print "\tcommit = \"" commit "\""; + } + } + ' "$file" > "$file.tmp" && mv "$file.tmp" "$file" +} +export ANDROID_NDK_HOME=~/Library/Android/sdk/ndk/23.0.7599858 +mkdir bin +cd bin || exit +root=$(pwd) +# Get source from github and switch to tag +git clone --depth 1 --branch "$TAG" https://github.com/Control-D-Inc/ctrld.git +# Prepare gomobile tool +sourcePath=./ctrld/cmd/ctrld_library +cd $sourcePath || exit +go mod tidy +go install golang.org/x/mobile/cmd/gomobile@latest +go get golang.org/x/mobile/bind +gomobile init +# Prepare build info +buildDir=$root/../build +mkdir -p "$buildDir" +COMMIT=$(git rev-parse HEAD) +update_versionInfo "$root" "$TAG" "$COMMIT" +ldflags="-s -w" +# Build +gomobile bind -target ios/arm64 -ldflags="$ldflags" -o "$buildDir"/ctrld-"$TAG".xcframework || exit +gomobile bind -ldflags="$ldflags" -o "$buildDir"/ctrld-"$TAG".aar || exit +# Clean up +rm -r "$root" +echo "Successfully built Ctrld library $TAG($COMMIT)." \ No newline at end of file