From 617674ce4368312ccb8b2ce2289d18369ab65dd8 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 13 Sep 2024 20:10:26 +0700 Subject: [PATCH 01/26] all: update tailscale.com to v1.74.0 --- cmd/cli/cli.go | 8 +- cmd/cli/dns_proxy.go | 5 +- cmd/cli/os_freebsd.go | 8 +- cmd/cli/os_linux.go | 8 +- cmd/cli/prog.go | 9 +- cmd/cli/prog_linux.go | 4 +- cmd/cli/resolvconf_not_darwin_unix.go | 7 +- go.mod | 104 +++++++---- go.sum | 227 ++++++++++++++++--------- internal/clientinfo/dhcp.go | 5 +- internal/dns/README.md | 2 +- internal/dns/debian_resolvconf.go | 47 ++++- internal/dns/direct.go | 170 +++++++++--------- internal/dns/direct_linux.go | 68 ++++++-- internal/dns/direct_notlinux.go | 5 +- internal/dns/direct_test.go | 17 +- internal/dns/manager_freebsd.go | 22 ++- internal/dns/manager_linux.go | 79 +++++++-- internal/dns/manager_linux_test.go | 92 +++++++--- internal/dns/nm.go | 157 +++++++++++++++-- internal/dns/openresolv.go | 76 ++++++++- internal/dns/osconfig.go | 112 +++++++++--- internal/dns/osconfig_test.go | 17 +- internal/dns/resolvconf-workaround.sh | 9 +- internal/dns/resolvconf.go | 12 +- internal/dns/resolvconfpath_default.go | 11 ++ internal/dns/resolvconfpath_gokrazy.go | 11 ++ internal/dns/resolved.go | 56 +++--- resolver.go | 5 +- 29 files changed, 974 insertions(+), 379 deletions(-) create mode 100644 internal/dns/resolvconfpath_default.go create mode 100644 internal/dns/resolvconfpath_gokrazy.go diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 4b5fbdf..861472a 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" @@ -730,7 +730,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() @@ -1662,7 +1662,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 +1680,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. diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index a7c62af..266c880 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -14,9 +14,10 @@ import ( "sync" "time" + "tailscale.com/net/netmon" + "github.com/miekg/dns" "golang.org/x/sync/errgroup" - "tailscale.com/net/interfaces" "tailscale.com/net/netaddr" "tailscale.com/net/tsaddr" @@ -890,7 +891,7 @@ func (p *prog) selfUninstallCoolOfPeriod() { // queryFromSelf reports whether the input IP is from device running ctrld. func queryFromSelf(ip string) bool { netIP := netip.MustParseAddr(ip) - ifaces, err := interfaces.GetList() + ifaces, err := netmon.GetInterfaceList() if err != nil { mainLog.Load().Warn().Err(err).Msg("could not get interfaces list") return false 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/prog.go b/cmd/cli/prog.go index 6018efb..d1ff0b8 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -21,10 +21,11 @@ import ( "syscall" "time" + "tailscale.com/net/netmon" + "github.com/kardianos/service" "github.com/rs/zerolog" "github.com/spf13/viper" - "tailscale.com/net/interfaces" "tailscale.com/net/tsaddr" "github.com/Control-D-Inc/ctrld" @@ -834,7 +835,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 +855,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 +895,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 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_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/go.mod b/go.mod index bfe6060b..525e70a 100644 --- a/go.mod +++ b/go.mod @@ -1,95 +1,125 @@ 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/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..fb2b650 100644 --- a/go.sum +++ b/go.sum @@ -38,52 +38,77 @@ 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/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 +120,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 +149,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 +164,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 +182,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 +195,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 +240,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 +274,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 +304,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 +322,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 +332,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 +372,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 +384,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 +396,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 +421,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 +455,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 +476,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 +524,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 +542,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 +596,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 +694,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 +708,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 +717,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/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/resolver.go b/resolver.go index d8b7f8d..8df4a29 100644 --- a/resolver.go +++ b/resolver.go @@ -10,8 +10,9 @@ import ( "sync" "time" + "tailscale.com/net/netmon" + "github.com/miekg/dns" - "tailscale.com/net/interfaces" ) const ( @@ -346,7 +347,7 @@ func NewResolverWithNameserver(nameservers []string) Resolver { // 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) From 082d14a9ba5b8ce8dc880667b8684327c90cad37 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 13 Sep 2024 21:28:54 +0700 Subject: [PATCH 02/26] cmd/cli: implement auto captive portal detection ControlD have global list of known captive portals that user can augment with proper setup. However, this requires manual actions, and involving restart ctrld for taking effects. By allowing ctrld "leaks" DNS queries to OS resolver, this process becomes automatically, the captive portal could intercept these queries, and as long as it was passed, ctrld will resume normal operation. --- cmd/cli/dns_proxy.go | 44 +++++++++++++++++++++++++++++++++++++++++--- cmd/cli/prog.go | 8 ++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 266c880..0bf85f2 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "errors" "fmt" + "io" "net" "net/netip" "runtime" @@ -14,11 +15,11 @@ import ( "sync" "time" - "tailscale.com/net/netmon" - "github.com/miekg/dns" "golang.org/x/sync/errgroup" + "tailscale.com/net/captivedetection" "tailscale.com/net/netaddr" + "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" "github.com/Control-D-Inc/ctrld" @@ -494,12 +495,21 @@ 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) } } + if cdUID != "" && (isNetworkErr || err == io.EOF) { + p.captivePortalMu.Lock() + if !p.captivePortalCheckWasRun { + p.captivePortalCheckWasRun = true + go p.performCaptivePortalDetection() + } + p.captivePortalMu.Unlock() + } // For timeout error (i.e: context deadline exceed), force re-bootstrapping. var e net.Error if errors.As(err, &e) && e.Timeout() { @@ -585,6 +595,9 @@ func (p *prog) upstreamsAndUpstreamConfigForLanAndPtr(upstreams []string, upstre } func (p *prog) upstreamConfigsFromUpstreamNumbers(upstreams []string) []*ctrld.UpstreamConfig { + if p.captivePortalDetected.Load() { + return nil // always use OS resolver if behind captive portal. + } upstreamConfigs := make([]*ctrld.UpstreamConfig, 0, len(upstreams)) for _, upstream := range upstreams { upstreamNum := strings.TrimPrefix(upstream, upstreamPrefix) @@ -888,6 +901,31 @@ func (p *prog) selfUninstallCoolOfPeriod() { p.selfUninstallMu.Unlock() } +// performCaptivePortalDetection check if ctrld is running behind a captive portal. +func (p *prog) performCaptivePortalDetection() { + mainLog.Load().Warn().Msg("Performing captive portal detection") + d := captivedetection.NewDetector(logf) + found := true + var resetDnsOnce sync.Once + for found { + time.Sleep(2 * time.Second) + found = d.Detect(context.Background(), netmon.NewStatic(), nil, 0) + if found { + resetDnsOnce.Do(func() { + mainLog.Load().Warn().Msg("found captive portal, leaking query to OS resolver") + p.resetDNS() + }) + } + p.captivePortalDetected.Store(found) + } + + p.captivePortalMu.Lock() + p.captivePortalCheckWasRun = false + p.captivePortalMu.Unlock() + p.setDNS() + mainLog.Load().Warn().Msg("captive portal login finished, stop leaking query") +} + // queryFromSelf reports whether the input IP is from device running ctrld. func queryFromSelf(ip string) bool { netIP := netip.MustParseAddr(ip) diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index d1ff0b8..711e966 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -105,6 +105,10 @@ type prog struct { loopMu sync.Mutex loop map[string]bool + captivePortalMu sync.Mutex + captivePortalCheckWasRun bool + captivePortalDetected atomic.Bool + started chan struct{} onStartedDone chan struct{} onStarted []func() @@ -240,6 +244,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) } } @@ -534,8 +540,6 @@ func (p *prog) setDNS() { setDnsOK := false defer func() { p.csSetDnsOk = setDnsOK - p.csSetDnsDone <- struct{}{} - close(p.csSetDnsDone) }() if cfg.Listener == nil { From 08fe04f1eeda111d3042fc45858455a8d03cf94a Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 17 Sep 2024 16:52:50 +0700 Subject: [PATCH 03/26] all: support h3:// protocol prefix --- cmd/cli/cli.go | 6 +++++- config.go | 21 ++++++++++++++++----- config_internal_test.go | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 861472a..aabd3cc 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -1473,7 +1473,11 @@ 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{ diff --git a/config.go b/config.go index e09fdad..ab22045 100644 --- a/config.go +++ b/config.go @@ -59,6 +59,10 @@ const ( controlDComDomain = "controld.com" controlDNetDomain = "controld.net" controlDDevDomain = "controld.dev" + + endpointPrefixHTTPS = "https://" + endpointPrefixQUIC = "quic://" + endpointPrefixH3 = "h3://" ) var ( @@ -677,12 +681,16 @@ func (uc *UpstreamConfig) netForDNSType(dnsType uint16) (string, string) { // initDoHScheme initializes the endpoint scheme for DoH/DoH3 upstream if not present. func (uc *UpstreamConfig) initDoHScheme() { 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 } } @@ -767,13 +775,16 @@ 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 // - 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 } host := endpoint if strings.Contains(endpoint, ":") { diff --git a/config_internal_test.go b/config_internal_test.go index 96beddc..2dc05c3 100644 --- a/config_internal_test.go +++ b/config_internal_test.go @@ -178,6 +178,27 @@ 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, + }, + }, } for _, tc := range tests { From 282a8ce78eab2c51de14b4a181ba4e0f78e6c15f Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 17 Sep 2024 20:42:27 +0700 Subject: [PATCH 04/26] all: add DNS Stamps support See: https://dnscrypt.info/stamps-specifications --- cmd/cli/prog.go | 4 ++ config.go | 61 ++++++++++++++++++++++++++++- config_internal_test.go | 86 +++++++++++++++++++++++++++++++++++++++++ config_test.go | 15 +++++++ go.mod | 1 + go.sum | 2 + resolver.go | 3 ++ 7 files changed, 170 insertions(+), 2 deletions(-) diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 711e966..bfe32e0 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -308,7 +308,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() diff --git a/config.go b/config.go index ab22045..86ca4b7 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" @@ -229,7 +231,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:"-"` @@ -303,10 +305,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 @@ -694,6 +699,47 @@ func (uc *UpstreamConfig) initDoHScheme() { } } +// initDnsStamps initializes upstream config based on encoded DNS Stamps Endpoint. +func (uc *UpstreamConfig) initDnsStamps() error { + 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 { @@ -746,6 +792,17 @@ func upstreamConfigStructLevelValidation(sl validator.StructLevel) { 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 { diff --git a/config_internal_test.go b/config_internal_test.go index 2dc05c3..41edd32 100644 --- a/config_internal_test.go +++ b/config_internal_test.go @@ -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 @@ -199,6 +200,91 @@ func TestUpstreamConfig_Init(t *testing.T) { 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, + }, + }, } for _, tc := range tests { diff --git a/config_test.go b/config_test.go index 03a1a3f..c1ffeb4 100644 --- a/config_test.go +++ b/config_test.go @@ -127,6 +127,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") diff --git a/go.mod b/go.mod index 525e70a..1dc51e0 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ 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.6 diff --git a/go.sum b/go.sum index fb2b650..5c560e9 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ 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= diff --git a/resolver.go b/resolver.go index 8df4a29..6f25ba3 100644 --- a/resolver.go +++ b/resolver.go @@ -30,6 +30,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 ( From ede354166b9111e36391e658f9dafb24db86b33b Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 17 Sep 2024 14:50:31 +0700 Subject: [PATCH 05/26] cmd/cli: add split route AD domain on Windows --- cmd/cli/ad_others.go | 10 ++++++++++ cmd/cli/ad_windows.go | 43 +++++++++++++++++++++++++++++++++++++++++++ cmd/cli/prog.go | 1 + 3 files changed, 54 insertions(+) create mode 100644 cmd/cli/ad_others.go create mode 100644 cmd/cli/ad_windows.go diff --git a/cmd/cli/ad_others.go b/cmd/cli/ad_others.go new file mode 100644 index 0000000..1249033 --- /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.ListenerConfig) {} diff --git a/cmd/cli/ad_windows.go b/cmd/cli/ad_windows.go new file mode 100644 index 0000000..ef9e2fc --- /dev/null +++ b/cmd/cli/ad_windows.go @@ -0,0 +1,43 @@ +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(lc *ctrld.ListenerConfig) { + if lc.Policy == nil { + lc.Policy = &ctrld.ListenerPolicyConfig{} + } + 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 + } + domainRule := "*." + strings.TrimPrefix(domain, ".") + for _, rule := range lc.Policy.Rules { + if _, ok := rule[domainRule]; ok { + mainLog.Load().Debug().Msg("domain rule already exist") + return + } + } + mainLog.Load().Debug().Msg("adding active directory domain") + 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 = GetWmiObject 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/prog.go b/cmd/cli/prog.go index bfe32e0..34f050d 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -435,6 +435,7 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) { for listenerNum := range p.cfg.Listener { p.cfg.Listener[listenerNum].Init() + addExtraSplitDnsRule(p.cfg.Listener[listenerNum]) if !reload { go func(listenerNum string) { listenerConfig := p.cfg.Listener[listenerNum] From e6f256d640074c6f1248916cb843c85bafb83f79 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 19 Sep 2024 21:43:09 +0700 Subject: [PATCH 06/26] all: add pull API config based on special DNS query For query domain that matches "uid.verify.controld.com" in cd mode, and the uid has the same value with "--cd" flag, ctrld will fetch uid config from ControlD API, using this config if valid. This is useful for force syncing API without waiting until the API reload ticker fire. --- cmd/cli/cli.go | 1 + cmd/cli/dns_proxy.go | 37 +++++++++++++++++ cmd/cli/prog.go | 99 +++++++++++++++++++++++--------------------- config.go | 1 + 4 files changed, 90 insertions(+), 48 deletions(-) diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index aabd3cc..007f45e 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -1141,6 +1141,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, } diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 0bf85f2..b9eb8f5 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -151,6 +151,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) @@ -168,6 +169,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") @@ -926,6 +928,41 @@ func (p *prog) performCaptivePortalDetection() { mainLog.Load().Warn().Msg("captive portal login finished, stop leaking query") } +// 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 { netIP := netip.MustParseAddr(ip) diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 34f050d..781edd1 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -21,11 +21,11 @@ import ( "syscall" "time" - "tailscale.com/net/netmon" - "github.com/kardianos/service" "github.com/rs/zerolog" "github.com/spf13/viper" + "golang.org/x/sync/singleflight" + "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" "github.com/Control-D-Inc/ctrld" @@ -69,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 + dnsWatchDogOnce sync.Once + dnsWg sync.WaitGroup + dnsWatcherStopCh chan struct{} cfg *ctrld.Config localUpstreams []string @@ -255,47 +257,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 } diff --git a/config.go b/config.go index 86ca4b7..d20c695 100644 --- a/config.go +++ b/config.go @@ -217,6 +217,7 @@ 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"` Daemon bool `mapstructure:"-" toml:"-"` AllocateIP bool `mapstructure:"-" toml:"-"` } From 8c661c4401899a2fb27629366c4d984fc4740978 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Sat, 21 Sep 2024 01:42:03 +0700 Subject: [PATCH 07/26] cmd/cli: fix typo in powershell command to get domain --- cmd/cli/ad_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/cli/ad_windows.go b/cmd/cli/ad_windows.go index ef9e2fc..2697475 100644 --- a/cmd/cli/ad_windows.go +++ b/cmd/cli/ad_windows.go @@ -34,7 +34,7 @@ func addExtraSplitDnsRule(lc *ctrld.ListenerConfig) { // getActiveDirectoryDomain returns AD domain name of this computer. func getActiveDirectoryDomain() (string, error) { - cmd := "$obj = GetWmiObject Win32_ComputerSystem; if ($obj.PartOfDomain) { $obj.Domain }" + 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)) From 5a88a7c22c4259e77e51e4b95c35821b4a58d259 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 20 Sep 2024 21:33:44 +0700 Subject: [PATCH 08/26] cmd/cli: decouple reset DNS task from ctrld status So it can be run regardless of ctrld current status. This prevents a racy behavior when reset DNS task restores DNS settings of the system, but current running ctrld process may revert it immediately. --- cmd/cli/cli.go | 50 +++++++++++------ cmd/cli/dns_proxy.go | 2 + cmd/cli/os_windows.go | 1 + cmd/cli/prog.go | 124 +++++++++++++++++++++++------------------- cmd/cli/resolvconf.go | 3 + 5 files changed, 106 insertions(+), 74 deletions(-) diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 007f45e..d07c145 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -194,11 +194,15 @@ 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 { @@ -213,12 +217,15 @@ NOTE: running "ctrld start" without any arguments will start already installed c initLogging() tasks := []task{ - resetDnsTask(p, s), {s.Stop, 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}, @@ -334,14 +341,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}, @@ -1340,9 +1350,7 @@ 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() } @@ -2642,17 +2650,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 @@ -2662,11 +2673,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 b9eb8f5..5652f07 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -915,6 +915,8 @@ func (p *prog) performCaptivePortalDetection() { if found { resetDnsOnce.Do(func() { mainLog.Load().Warn().Msg("found captive portal, leaking query to OS resolver") + // Store the result once here, so changes made below won't be reverted by DNS watchers. + p.captivePortalDetected.Store(found) p.resetDNS() }) } 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 781edd1..a87d7e8 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -69,21 +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 - apiForceReloadCh chan struct{} - apiForceReloadGroup singleflight.Group - 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 @@ -512,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 { @@ -521,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() @@ -611,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 { @@ -622,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() { @@ -661,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.captivePortalDetected.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() { @@ -965,11 +977,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 } @@ -1005,9 +1019,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/resolvconf.go b/cmd/cli/resolvconf.go index 5be34fc..21e435d 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.captivePortalDetected.Load() { + return + } if !ok { return } From cfe1209d61526f9132d476afbff654c04bf07710 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 20 Sep 2024 21:57:12 +0700 Subject: [PATCH 09/26] cmd/cli: use powershell to get physical interfaces --- cmd/cli/net.go | 34 ---------------------------------- cmd/cli/net_darwin.go | 1 + cmd/cli/net_windows.go | 27 +++++++++++++++++++-------- 3 files changed, 20 insertions(+), 42 deletions(-) delete mode 100644 cmd/cli/net.go 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 +} From 3e388c2857c969c0e07ba4b32ff8c502899e967a Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 23 Sep 2024 18:27:14 +0700 Subject: [PATCH 10/26] all: leaking queries to OS resolver instead of SRVFAIL So it would work in more general case than just captive portal network, which ctrld have supported recently. Uses who may want no leaking behavior can use a config to turn off this feature. --- cmd/cli/dns_proxy.go | 66 ++++++++++++++++--------------------- cmd/cli/prog.go | 20 ++++++++--- cmd/cli/resolvconf.go | 2 +- cmd/cli/upstream_monitor.go | 26 +++++++++------ config.go | 1 + docs/config.md | 7 ++++ 6 files changed, 69 insertions(+), 53 deletions(-) diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 5652f07..81be1d0 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "errors" "fmt" - "io" "net" "net/netip" "runtime" @@ -17,7 +16,6 @@ import ( "github.com/miekg/dns" "golang.org/x/sync/errgroup" - "tailscale.com/net/captivedetection" "tailscale.com/net/netaddr" "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" @@ -412,6 +410,16 @@ 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) + + // 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 + } + if len(upstreamConfigs) == 0 { upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig} upstreams = []string{upstreamOS} @@ -501,17 +509,9 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse { 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) } } - if cdUID != "" && (isNetworkErr || err == io.EOF) { - p.captivePortalMu.Lock() - if !p.captivePortalCheckWasRun { - p.captivePortalCheckWasRun = true - go p.performCaptivePortalDetection() - } - p.captivePortalMu.Unlock() - } // For timeout error (i.e: context deadline exceed), force re-bootstrapping. var e net.Error if errors.As(err, &e) && e.Timeout() { @@ -580,6 +580,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 @@ -597,9 +605,6 @@ func (p *prog) upstreamsAndUpstreamConfigForLanAndPtr(upstreams []string, upstre } func (p *prog) upstreamConfigsFromUpstreamNumbers(upstreams []string) []*ctrld.UpstreamConfig { - if p.captivePortalDetected.Load() { - return nil // always use OS resolver if behind captive portal. - } upstreamConfigs := make([]*ctrld.UpstreamConfig, 0, len(upstreams)) for _, upstream := range upstreams { upstreamNum := strings.TrimPrefix(upstream, upstreamPrefix) @@ -903,31 +908,16 @@ func (p *prog) selfUninstallCoolOfPeriod() { p.selfUninstallMu.Unlock() } -// performCaptivePortalDetection check if ctrld is running behind a captive portal. -func (p *prog) performCaptivePortalDetection() { - mainLog.Load().Warn().Msg("Performing captive portal detection") - d := captivedetection.NewDetector(logf) - found := true - var resetDnsOnce sync.Once - for found { - time.Sleep(2 * time.Second) - found = d.Detect(context.Background(), netmon.NewStatic(), nil, 0) - if found { - resetDnsOnce.Do(func() { - mainLog.Load().Warn().Msg("found captive portal, leaking query to OS resolver") - // Store the result once here, so changes made below won't be reverted by DNS watchers. - p.captivePortalDetected.Store(found) - p.resetDNS() - }) - } - p.captivePortalDetected.Store(found) - } - - p.captivePortalMu.Lock() - p.captivePortalCheckWasRun = false - p.captivePortalMu.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() - mainLog.Load().Warn().Msg("captive portal login finished, stop leaking query") } // forceFetchingAPI sends signal to force syncing API config if run in cd mode, diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index a87d7e8..54ea194 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -107,9 +107,9 @@ type prog struct { loopMu sync.Mutex loop map[string]bool - captivePortalMu sync.Mutex - captivePortalCheckWasRun bool - captivePortalDetected atomic.Bool + leakingQueryMu sync.Mutex + leakingQueryWasRun bool + leakingQuery atomic.Bool started chan struct{} onStartedDone chan struct{} @@ -685,7 +685,7 @@ func (p *prog) dnsWatchdog(iface *net.Interface, nameservers []string, allIfaces mainLog.Load().Debug().Msg("stop dns watchdog") return case <-ticker.C: - if p.captivePortalDetected.Load() { + if p.leakingQuery.Load() { return } if dnsChanged(iface, ns) { @@ -742,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) diff --git a/cmd/cli/resolvconf.go b/cmd/cli/resolvconf.go index 21e435d..6df7be6 100644 --- a/cmd/cli/resolvconf.go +++ b/cmd/cli/resolvconf.go @@ -40,7 +40,7 @@ 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.captivePortalDetected.Load() { + if p.leakingQuery.Load() { return } if !ok { 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 d20c695..6c66f62 100644 --- a/config.go +++ b/config.go @@ -218,6 +218,7 @@ type ServiceConfig struct { 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:"-"` } 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. From 14c88f4a6dcf54079ef7c6a44a848434c8518478 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 23 Sep 2024 15:06:11 +0700 Subject: [PATCH 11/26] all: allow empty type for h3 and sdns --- cmd/cli/cli.go | 18 ++++++++++-------- config.go | 18 +++++++++++++++++- config_internal_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ config_test.go | 24 ++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 9 deletions(-) diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index d07c145..3f606b7 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -1489,22 +1489,24 @@ func processNoConfigFlags(noConfigStart bool) { 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"}}) diff --git a/config.go b/config.go index 6c66f62..3f9b2f8 100644 --- a/config.go +++ b/config.go @@ -65,6 +65,7 @@ const ( endpointPrefixHTTPS = "https://" endpointPrefixQUIC = "quic://" endpointPrefixH3 = "h3://" + endpointPrefixSdns = "sdns://" ) var ( @@ -233,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 sdns"` + 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:"-"` @@ -687,6 +688,9 @@ 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: case ResolverTypeDOH3: @@ -703,6 +707,9 @@ func (uc *UpstreamConfig) initDoHScheme() { // 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 } @@ -794,6 +801,12 @@ 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) { @@ -835,6 +848,7 @@ func defaultPortFor(typ string) string { // - 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 { @@ -844,6 +858,8 @@ func ResolverTypeFromEndpoint(endpoint string) string { 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 41edd32..7b09da3 100644 --- a/config_internal_test.go +++ b/config_internal_test.go @@ -200,6 +200,26 @@ func TestUpstreamConfig_Init(t *testing.T) { 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{ @@ -285,6 +305,26 @@ func TestUpstreamConfig_Init(t *testing.T) { 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 c1ffeb4..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}, } @@ -194,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 From f507bc8f9ee4e63cfe5c5bc6dfc95642df736600 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 23 Sep 2024 21:51:10 +0700 Subject: [PATCH 12/26] cmd/cli: cache query from self result So we don't waste time to compute a result which is not likely to be changed. --- cmd/cli/dns_proxy.go | 9 +++++++-- cmd/cli/prog.go | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 81be1d0..370d8de 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -838,7 +838,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 { @@ -956,7 +956,10 @@ func timeDurationOrDefault(n *int, defaultN int) time.Duration { } // 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 := netmon.GetInterfaceList() if err != nil { @@ -973,11 +976,13 @@ func queryFromSelf(ip string) bool { switch v := a.(type) { case *net.IPNet: if pfx, ok := netaddr.FromStdIPNet(v); ok && pfx.Addr().Compare(netIP) == 0 { + p.queryFromSelfMap.Store(ip, true) return true } } } } + p.queryFromSelfMap.Store(ip, false) return false } diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 54ea194..bca8bfb 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -98,6 +98,7 @@ type prog struct { ptrLoopGuard *loopGuard lanLoopGuard *loopGuard metricsQueryStats atomic.Bool + queryFromSelfMap sync.Map selfUninstallMu sync.Mutex refusedQueryCount int From 4b36e3ac44bb173b5c45bde24d19a4cd4b18be56 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 24 Sep 2024 00:15:08 +0700 Subject: [PATCH 13/26] Change test query to use controld.com Since some Active Directory could blocks clients to query for "." --- resolver.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/resolver.go b/resolver.go index 6f25ba3..e9cfd99 100644 --- a/resolver.go +++ b/resolver.go @@ -70,11 +70,14 @@ func InitializeOsResolver() []string { // 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) + if err != nil { + ProxyLogger.Load().Debug().Err(err).Msgf("failed to connect to OS nameserver: %s", addr) + } return err == nil } From 4befd33866395b30080605713df4b3fd206dfd07 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 24 Sep 2024 19:50:16 +0700 Subject: [PATCH 14/26] cmd/cli: notify log server before ctrld process exit So if ctrld process terminated for any reason, other processes will get the signal immediately instead of waiting for timeout to report error. --- cmd/cli/cli.go | 165 ++++++++++++++++++++++++++++--------------------- 1 file changed, 96 insertions(+), 69 deletions(-) diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 3f606b7..e50219e 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -52,6 +52,7 @@ const ( windowsForwardersFilename = ".forwarders.txt" oldBinSuffix = "_previous" oldLogSuffix = ".1" + msgExit = "$$EXIT$$" ) var ( @@ -205,6 +206,71 @@ NOTE: running "ctrld start" without any arguments will start already installed c currentIface = runningIface(s) } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + 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") + } + } + } + + // No config path, generating config in HOME directory. + noConfigStart := isNoConfigStart(cmd) + writeDefaultConfig := !noConfigStart && configBase64 == "" + + 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 { + mainLog.Load().Warn().Msg("log server did not start") + close(logServerStarted) + } else { + setWorkingDirectory(sc, ud) + if configPath == "" && writeDefaultConfig { + defaultConfigFile = filepath.Join(ud, defaultConfigFile) + } + sc.Arguments = append(sc.Arguments, "--homedir="+ud) + if d, err := socketDir(); err == nil { + sockDir = d + } + sockPath := filepath.Join(sockDir, ctrldLogUnixSock) + _ = os.Remove(sockPath) + go func() { + defer func() { + 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. + buf := make([]byte, 1024) + for { + n, err := conn.Read(buf) + if err != nil { + return + } + msg := string(buf[:n]) + if _, _, found := strings.Cut(msg, msgExit); found { + cancel() + } + runCmdLogCh <- msg + } + } + }() + } + <-logServerStarted + if !startOnly { startOnly = len(osArgs) == 0 } @@ -240,15 +306,7 @@ NOTE: running "ctrld start" without any arguments will start already installed c 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(sockDir) } else { mainLog.Load().Error().Err(err).Msg("Failed to start existing ctrld service") os.Exit(1) @@ -273,49 +331,10 @@ NOTE: running "ctrld start" without any arguments will start already installed c 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) } - // 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 { - setWorkingDirectory(sc, ud) - if configPath == "" && writeDefaultConfig { - defaultConfigFile = filepath.Join(ud, defaultConfigFile) - } - sc.Arguments = append(sc.Arguments, "--homedir="+ud) - if d, err := socketDir(); err == nil { - sockDir = d - } - sockPath := filepath.Join(sockDir, ctrldLogUnixSock) - _ = os.Remove(sockPath) - go func() { - defer func() { - close(runCmdLogCh) - _ = os.Remove(sockPath) - }() - if conn := runLogServer(sockPath); conn != nil { - // Enough buffer for log message, we don't produce - // such long log message, but just in case. - buf := make([]byte, 1024) - for { - n, err := conn.Read(buf) - if err != nil { - return - } - runCmdLogCh <- string(buf[:n]) - } - } - }() - } - tryReadingConfigWithNotice(writeDefaultConfig, true) if err := v.Unmarshal(&cfg); err != nil { @@ -368,19 +387,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. @@ -406,15 +425,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) } }, } @@ -553,7 +564,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) @@ -1045,7 +1056,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 } @@ -1172,6 +1183,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.") @@ -1197,8 +1211,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() @@ -1214,6 +1232,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") } @@ -1228,6 +1247,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") } @@ -1249,6 +1269,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") } } @@ -1261,6 +1282,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) @@ -1282,6 +1304,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { } if err := validateConfig(&cfg); err != nil { + notifyExitToLogServer() os.Exit(1) } initCache() @@ -1290,11 +1313,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. @@ -1302,6 +1327,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") @@ -1717,7 +1743,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") @@ -1733,7 +1759,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") } @@ -2362,14 +2388,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) @@ -2389,6 +2414,8 @@ func newSocketControlClient(s service.Service, dir string) *controlClient { select { case <-timeout.C: return nil + case <-ctx.Done(): + return nil default: } } @@ -2506,7 +2533,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. @@ -2590,7 +2617,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 { @@ -2636,7 +2663,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 "" From ce353cd4d92ba8cf1998917f2ec1f479030e1408 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 24 Sep 2024 15:09:05 +0700 Subject: [PATCH 15/26] cmd/cli: write auto split rule for AD to config file --- cmd/cli/ad_others.go | 2 +- cmd/cli/ad_windows.go | 24 +++++++++++++----------- cmd/cli/cli.go | 1 + cmd/cli/prog.go | 1 - 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/cmd/cli/ad_others.go b/cmd/cli/ad_others.go index 1249033..eb1b506 100644 --- a/cmd/cli/ad_others.go +++ b/cmd/cli/ad_others.go @@ -7,4 +7,4 @@ import ( ) // addExtraSplitDnsRule adds split DNS rule if present. -func addExtraSplitDnsRule(_ *ctrld.ListenerConfig) {} +func addExtraSplitDnsRule(_ *ctrld.Config) {} diff --git a/cmd/cli/ad_windows.go b/cmd/cli/ad_windows.go index 2697475..a3e7917 100644 --- a/cmd/cli/ad_windows.go +++ b/cmd/cli/ad_windows.go @@ -8,10 +8,7 @@ import ( ) // addExtraSplitDnsRule adds split DNS rule for domain if it's part of active directory. -func addExtraSplitDnsRule(lc *ctrld.ListenerConfig) { - if lc.Policy == nil { - lc.Policy = &ctrld.ListenerPolicyConfig{} - } +func addExtraSplitDnsRule(cfg *ctrld.Config) { domain, err := getActiveDirectoryDomain() if err != nil { mainLog.Load().Debug().Msgf("unable to get active directory domain: %v", err) @@ -21,15 +18,20 @@ func addExtraSplitDnsRule(lc *ctrld.ListenerConfig) { mainLog.Load().Debug().Msg("no active directory domain found") return } - domainRule := "*." + strings.TrimPrefix(domain, ".") - for _, rule := range lc.Policy.Rules { - if _, ok := rule[domainRule]; ok { - mainLog.Load().Debug().Msg("domain rule already exist") - 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{}}) } - mainLog.Load().Debug().Msg("adding active directory domain") - lc.Policy.Rules = append(lc.Policy.Rules, ctrld.Rule{domainRule: []string{}}) } // getActiveDirectoryDomain returns AD domain name of this computer. diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index e50219e..e9b3dc7 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -1383,6 +1383,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { } func writeConfigFile(cfg *ctrld.Config) error { + addExtraSplitDnsRule(cfg) if cfu := v.ConfigFileUsed(); cfu != "" { defaultConfigFile = cfu } else if configPath != "" { diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index bca8bfb..2a2c59c 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -439,7 +439,6 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) { for listenerNum := range p.cfg.Listener { p.cfg.Listener[listenerNum].Init() - addExtraSplitDnsRule(p.cfg.Listener[listenerNum]) if !reload { go func(listenerNum string) { listenerConfig := p.cfg.Listener[listenerNum] From b320662d67ab58e523c283f7bb62dcbc33a8b131 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 26 Sep 2024 15:12:00 +0700 Subject: [PATCH 16/26] cmd/cli: emit warning for MacOS 15.0 in case of timeout error --- cmd/cli/cli.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index e9b3dc7..1a93c9d 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -1814,6 +1814,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") @@ -1830,6 +1831,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)) From e88372fc8c7d906a151a9cff096b6f4cf4f879bc Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 26 Sep 2024 15:38:26 +0700 Subject: [PATCH 17/26] cmd/cli: log request id when leaking --- cmd/cli/dns_proxy.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 370d8de..242864f 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -411,6 +411,7 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse { 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() { @@ -418,6 +419,8 @@ func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse { 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 { @@ -435,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): From cb14992ddccc72dda2bceb9b0372e1916110a34e Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 4 Oct 2024 14:37:27 +0700 Subject: [PATCH 18/26] Ignore local addresses for OS resolver Otherwise, DNS loop may be triggered if requests are forwarded from ctrld to OS resolver. --- resolver.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/resolver.go b/resolver.go index e9cfd99..f058e75 100644 --- a/resolver.go +++ b/resolver.go @@ -58,7 +58,16 @@ func defaultNameservers() []string { // calling this function. func InitializeOsResolver() []string { or.nameservers = or.nameservers[:0] + // 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[net.JoinHostPort(v.String(), "53")] = struct{}{} + } for _, ns := range defaultNameservers() { + if _, ok := machineIPsMap[ns]; ok { + continue + } if testNameserver(ns) { or.nameservers = append(or.nameservers, ns) } From 5ac9d17bdf2881179960ff9b3e03a80ab0f31422 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 4 Oct 2024 14:40:57 +0700 Subject: [PATCH 19/26] cmd/cli: simplify queryFromSelf By using netmon.LocalAddresses instead of looping through interfaces list manually. --- cmd/cli/dns_proxy.go | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 242864f..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,7 +17,6 @@ import ( "github.com/miekg/dns" "golang.org/x/sync/errgroup" - "tailscale.com/net/netaddr" "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" @@ -968,25 +968,15 @@ func (p *prog) queryFromSelf(ip string) bool { return val.(bool) } netIP := netip.MustParseAddr(ip) - ifaces, err := netmon.GetInterfaceList() + 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 { - p.queryFromSelfMap.Store(ip, true) - 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) From 9501e35c608b00b4f804d5c0a66f1df7fe9c2d48 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 11 Oct 2024 17:15:47 +0700 Subject: [PATCH 20/26] Skip virtual interfaces when parsing route table Since routing through virtual interfaces may trigger DNS loop in VPN like observing in UnifiOS Site Magic VPN. --- nameservers_linux.go | 35 ++++++++++++++++++++++++++++++++++- nameservers_linux_test.go | 10 ++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 nameservers_linux_test.go 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) +} From 30ea0c649986007add445560c5e18dce903b4d5f Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 16 Oct 2024 14:13:21 +0700 Subject: [PATCH 21/26] Log nameserver in OS resolver response --- resolver.go | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/resolver.go b/resolver.go index f058e75..226ad1f 100644 --- a/resolver.go +++ b/resolver.go @@ -7,6 +7,7 @@ import ( "net" "net/netip" "slices" + "strings" "sync" "time" @@ -124,9 +125,9 @@ type osResolver struct { } type osResolverResult struct { - answer *dns.Msg - err error - isControlDPublicDNS bool + answer *dns.Msg + err error + server string } // Resolve resolves DNS queries using pre-configured nameservers. @@ -152,33 +153,45 @@ func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error go func(server string) { defer wg.Done() answer, _, err := dnsClient.ExchangeContext(ctx, msg.Copy(), server) - ch <- &osResolverResult{answer: answer, err: err, isControlDPublicDNS: server == controldPublicDnsWithPort} + ch <- &osResolverResult{answer: answer, err: err, server: server} }(server) } + 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 ) errs := make([]error, 0, numServers) for res := range ch { switch { case res.answer != nil && res.answer.Rcode == dns.RcodeSuccess: - if res.isControlDPublicDNS { + if res.server == controldPublicDnsWithPort { controldSuccessAnswer = res.answer // only use ControlD answer as last one. } else { 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 controldSuccessAnswer != nil { + logAnswer(controldPublicDnsWithPort) + return controldSuccessAnswer, nil + } + if nonSuccessAnswer != nil { + logAnswer(nonSuccessServer) + return nonSuccessAnswer, nil } return nil, errors.Join(errs...) } From f87220a908fc7946b0549896b9d0b342f2635328 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 17 Oct 2024 18:09:16 +0700 Subject: [PATCH 22/26] Avoid data race when initializing OS resolver With new leaking queries features, the initialization of OS resolver can now lead to data race if queries are resolving while re-initialization happens. To fix it, using an atomic pointer to store list of nameservers which were initialized, making read/write to the list concurrently safe. --- resolver.go | 48 +++++++++++++++++++++++++++++------------------- resolver_test.go | 19 +++++++++++++++++-- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/resolver.go b/resolver.go index 226ad1f..1e896c1 100644 --- a/resolver.go +++ b/resolver.go @@ -9,6 +9,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "time" "tailscale.com/net/netmon" @@ -44,7 +45,7 @@ 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. func defaultNameservers() []string { @@ -58,7 +59,7 @@ 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] + var nss []string // Ignore local addresses to prevent loop. regularIPs, loopbackIPs, _ := netmon.LocalAddresses() machineIPsMap := make(map[string]struct{}, len(regularIPs)) @@ -70,11 +71,12 @@ func InitializeOsResolver() []string { continue } if testNameserver(ns) { - or.nameservers = append(or.nameservers, ns) + nss = append(nss, ns) } } - or.nameservers = append(or.nameservers, controldPublicDnsWithPort) - return or.nameservers + nss = append(nss, controldPublicDnsWithPort) + or.nameservers.Store(&nss) + return nss } // testPlainDnsNameserver sends a test query to DNS nameserver to check if the server is available. @@ -121,7 +123,7 @@ func NewResolver(uc *UpstreamConfig) (Resolver, error) { } type osResolver struct { - nameservers []string + nameservers atomic.Pointer[[]string] } type osResolverResult struct { @@ -134,7 +136,8 @@ type osResolverResult struct { // 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) + nss := *o.nameservers.Load() + numServers := len(nss) if numServers == 0 { return nil, errors.New("no nameservers available") } @@ -144,12 +147,12 @@ 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.Add(len(nss)) go func() { wg.Wait() close(ch) }() - for _, server := range o.nameservers { + for _, server := range nss { go func(server string) { defer wg.Done() answer, _, err := dnsClient.ExchangeContext(ctx, msg.Copy(), server) @@ -238,11 +241,12 @@ func LookupIP(domain string) []string { } func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) { - resolver := &osResolver{nameservers: nameservers()} + nss := nameservers() 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 @@ -315,12 +319,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 := nameservers() + 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, @@ -357,10 +361,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 @@ -369,7 +373,13 @@ func NewResolverWithNameserver(nameservers []string) Resolver { if len(nameservers) == 0 { return &dummyResolver{} } - return &osResolver{nameservers: nameservers} + return newResolverWithNameserver(nameservers) +} + +func newResolverWithNameserver(nameservers []string) *osResolver { + r := &osResolver{} + r.nameservers.Store(&nameservers) + return r } // Rfc1918Addresses returns the list of local interfaces private IP addresses diff --git a/resolver_test.go b/resolver_test.go index 23c27ae..9d1cb34 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -16,7 +16,8 @@ func Test_osResolver_Resolve(t *testing.T) { go func() { defer cancel() - resolver := &osResolver{nameservers: []string{"127.0.0.127:5353"}} + resolver := &osResolver{} + resolver.nameservers.Store(&[]string{"127.0.0.127:5353"}) m := new(dns.Msg) m.SetQuestion("controld.com.", dns.TypeA) m.RecursionDesired = true @@ -69,7 +70,8 @@ func Test_osResolver_ResolveWithNonSuccessAnswer(t *testing.T) { server.Shutdown() } }() - resolver := &osResolver{nameservers: ns} + resolver := &osResolver{} + resolver.nameservers.Store(&ns) msg := new(dns.Msg) msg.SetQuestion(".", dns.TypeNS) answer, err := resolver.Resolve(context.Background(), msg) @@ -81,6 +83,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 From 0cdff0d368b005e415f64da246bf0edf18365e99 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 18 Oct 2024 01:31:40 +0700 Subject: [PATCH 23/26] Prefer LAN server answer over public one While at it, also implementing new OS resolver chosing logic, keeping only 2 LAN servers at any time, 1 for current one, and 1 for last used one. --- config_internal_test.go | 2 +- nameservers.go | 5 +- resolver.go | 153 +++++++++++++++++++++++++++++++--------- resolver_test.go | 4 +- 4 files changed, 124 insertions(+), 40 deletions(-) diff --git a/config_internal_test.go b/config_internal_test.go index 7b09da3..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) 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/resolver.go b/resolver.go index 1e896c1..b38504c 100644 --- a/resolver.go +++ b/resolver.go @@ -12,9 +12,9 @@ import ( "sync/atomic" "time" - "tailscale.com/net/netmon" - "github.com/miekg/dns" + "tailscale.com/net/netmon" + "tailscale.com/net/tsaddr" ) const ( @@ -47,10 +47,34 @@ var controldPublicDnsWithPort = net.JoinHostPort(controldPublicDns, "53") // or is the Resolver used for ResolverTypeOS. 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. @@ -59,23 +83,39 @@ 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 { - 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[net.JoinHostPort(v.String(), "53")] = struct{}{} + var ( + nss []string + publicNss []string + ) + var curLanServer netip.Addr + if p := or.currentLanServer.Load(); p != nil { + curLanServer = *p + or.currentLanServer.Store(nil) } - for _, ns := range defaultNameservers() { - if _, ok := machineIPsMap[ns]; ok { + for _, ns := range availableNameservers() { + addr, err := netip.ParseAddr(ns) + if err != nil { continue } - if testNameserver(ns) { - nss = append(nss, ns) + server := net.JoinHostPort(ns, "53") + if isLanAddr(addr) { + if addr.Compare(curLanServer) != 0 && or.currentLanServer.CompareAndSwap(nil, &addr) { + nss = append(nss, server) + } + } else { + publicNss = append(publicNss, server) + nss = append(nss, server) } } - nss = append(nss, controldPublicDnsWithPort) - or.nameservers.Store(&nss) + if 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 } @@ -86,7 +126,7 @@ func testNameserver(addr string) bool { 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) } @@ -123,21 +163,31 @@ func NewResolver(uc *UpstreamConfig) (Resolver, error) { } type osResolver struct { - nameservers atomic.Pointer[[]string] + currentLanServer atomic.Pointer[netip.Addr] + lastLanServer atomic.Pointer[netip.Addr] + publicServer atomic.Pointer[[]string] } type osResolverResult struct { 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) { - nss := *o.nameservers.Load() - numServers := len(nss) + 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") } @@ -146,19 +196,24 @@ 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(nss)) + wg := &sync.WaitGroup{} + wg.Add(numServers) go func() { wg.Wait() close(ch) }() - for _, server := range nss { - go func(server string) { - defer wg.Done() - answer, _, err := dnsClient.ExchangeContext(ctx, msg.Copy(), server) - ch <- &osResolverResult{answer: answer, err: err, server: server} - }(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 { @@ -170,14 +225,20 @@ func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error 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.server == controldPublicDnsWithPort { + 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 @@ -188,6 +249,10 @@ func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error } errs = append(errs, res.err) } + if publicServerAnswer != nil { + logAnswer(publicServer) + return publicServerAnswer, nil + } if controldSuccessAnswer != nil { logAnswer(controldPublicDnsWithPort) return controldSuccessAnswer, nil @@ -241,7 +306,7 @@ func LookupIP(domain string) []string { } func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) { - nss := nameservers() + nss := defaultNameservers() if withBootstrapDNS { nss = append([]string{net.JoinHostPort(controldBootstrapDns, "53")}, nss...) } @@ -319,7 +384,7 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) // - Gateway IP address (depends on OS). // - Input servers. func NewBootstrapResolver(servers ...string) Resolver { - nss := nameservers() + nss := defaultNameservers() nss = append([]string{controldPublicDnsWithPort}, nss...) for _, ns := range servers { nss = append([]string{net.JoinHostPort(ns, "53")}, nss...) @@ -335,7 +400,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 @@ -376,9 +441,21 @@ func NewResolverWithNameserver(nameservers []string) Resolver { 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{} - r.nameservers.Store(&nameservers) + 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 } @@ -409,3 +486,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 9d1cb34..44b170a 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -17,7 +17,7 @@ func Test_osResolver_Resolve(t *testing.T) { go func() { defer cancel() resolver := &osResolver{} - resolver.nameservers.Store(&[]string{"127.0.0.127:5353"}) + resolver.publicServer.Store(&[]string{"127.0.0.127:5353"}) m := new(dns.Msg) m.SetQuestion("controld.com.", dns.TypeA) m.RecursionDesired = true @@ -71,7 +71,7 @@ func Test_osResolver_ResolveWithNonSuccessAnswer(t *testing.T) { } }() resolver := &osResolver{} - resolver.nameservers.Store(&ns) + resolver.publicServer.Store(&ns) msg := new(dns.Msg) msg.SetQuestion(".", dns.TypeNS) answer, err := resolver.Resolve(context.Background(), msg) From 65de7edcdee8c500c1d8a56b316759e2770a24a8 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 22 Oct 2024 00:47:03 +0700 Subject: [PATCH 24/26] Only store last LAN server if available Otherwise, queries may still be forwarded to this un-available LAN server, causing slow query time. --- resolver.go | 38 ++++++++++++++++++++++++++++++-------- resolver_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/resolver.go b/resolver.go index b38504c..f54edfb 100644 --- a/resolver.go +++ b/resolver.go @@ -83,31 +83,53 @@ func availableNameservers() []string { // It's the caller's responsibility to ensure the system DNS is in a clean state before // calling this function. func InitializeOsResolver() []string { + return initializeOsResolver(availableNameservers()) +} +func initializeOsResolver(servers []string) []string { var ( nss []string publicNss []string ) - var curLanServer netip.Addr + var ( + lastLanServer netip.Addr + curLanServer netip.Addr + curLanServerAvailable bool + ) if p := or.currentLanServer.Load(); p != nil { curLanServer = *p or.currentLanServer.Store(nil) } - for _, ns := range availableNameservers() { + 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") - if isLanAddr(addr) { - if addr.Compare(curLanServer) != 0 && or.currentLanServer.CompareAndSwap(nil, &addr) { - nss = append(nss, server) - } - } else { + // 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) + } + } } } - if curLanServer.IsValid() { + // 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")) } diff --git a/resolver_test.go b/resolver_test.go index 44b170a..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" ) @@ -149,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)) +} From 9d666be5d41844394c86667e29559e1aecc09e21 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 23 Oct 2024 16:00:09 +0700 Subject: [PATCH 25/26] all: add custom hostname support for provisoning --- cmd/cli/cli.go | 34 +++++++++++++++++++++++++--------- cmd/cli/hostname.go | 14 ++++++++++++++ cmd/cli/hostname_test.go | 35 +++++++++++++++++++++++++++++++++++ cmd/cli/main.go | 8 +++++--- internal/controld/config.go | 16 ++++++++++++---- 5 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 cmd/cli/hostname.go create mode 100644 cmd/cli/hostname_test.go diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 1a93c9d..1e9c541 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -147,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", "", "", "") @@ -319,7 +320,7 @@ NOTE: running "ctrld start" without any arguments will start already installed c } else if uid := cdUIDFromProvToken(); uid != "" { cdUID = uid mainLog.Load().Debug().Msg("using uid from provision token") - removeProvTokenFromArgs(sc) + removeOrgFlagsFromArgs(sc) // Pass --cd flag to "ctrld run" command, so the provision token takes no effect. sc.Arguments = append(sc.Arguments, "--cd="+cdUID) } @@ -440,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`) @@ -2363,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 { @@ -2381,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) 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/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)) } From 6ca1a7ccc72b9d9d95cde470ecd41d53931cea81 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 30 Sep 2024 18:39:53 +0700 Subject: [PATCH 26/26] .github/workflows: use go1.23.x And also upgrade staticcheck version to 2024.1.1 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 }}