diff --git a/cmd/cli/os_linux.go b/cmd/cli/os_linux.go index fcff741..3d9bffd 100644 --- a/cmd/cli/os_linux.go +++ b/cmd/cli/os_linux.go @@ -9,12 +9,10 @@ import ( "net" "net/netip" "os/exec" - "path/filepath" "strings" "syscall" "time" - "github.com/fsnotify/fsnotify" "github.com/insomniacslk/dhcp/dhcpv4/nclient4" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/client6" @@ -25,11 +23,6 @@ import ( "github.com/Control-D-Inc/ctrld/internal/resolvconffile" ) -const ( - resolvConfPath = "/etc/resolv.conf" - resolvConfBackupFailedMsg = "open /etc/resolv.pre-ctrld-backup.conf: read-only file system" -) - // allocate loopback ip // sudo ip a add 127.0.0.2/24 dev lo func allocateIP(ip string) error { @@ -69,12 +62,6 @@ func setDNS(iface *net.Interface, nameservers []string) error { Nameservers: ns, SearchDomains: []dnsname.FQDN{}, } - defer func() { - if r.Mode() == "direct" { - go watchResolveConf(osConfig) - } - }() - trySystemdResolve := false for i := 0; i < maxSetDNSAttempts; i++ { if err := r.SetDNS(osConfig); err != nil { @@ -314,59 +301,3 @@ func sliceIndex[S ~[]E, E comparable](s S, v E) int { } return -1 } - -// watchResolveConf watches any changes to /etc/resolv.conf file, -// and reverting to the original config set by ctrld. -func watchResolveConf(oc dns.OSConfig) { - mainLog.Load().Debug().Msg("start watching /etc/resolv.conf file") - watcher, err := fsnotify.NewWatcher() - if err != nil { - mainLog.Load().Warn().Err(err).Msg("could not create watcher for /etc/resolv.conf") - return - } - - // We watch /etc instead of /etc/resolv.conf directly, - // see: https://github.com/fsnotify/fsnotify#watching-a-file-doesnt-work-well - watchDir := filepath.Dir(resolvConfPath) - if err := watcher.Add(watchDir); err != nil { - mainLog.Load().Warn().Err(err).Msg("could not add /etc/resolv.conf to watcher list") - return - } - - r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter. - if err != nil { - mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator") - return - } - - for { - select { - case event, ok := <-watcher.Events: - if !ok { - return - } - if event.Name != resolvConfPath { // skip if not /etc/resolv.conf changes. - continue - } - if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) { - mainLog.Load().Debug().Msg("/etc/resolv.conf changes detected, reverting to ctrld setting") - if err := watcher.Remove(watchDir); err != nil { - mainLog.Load().Error().Err(err).Msg("failed to pause watcher") - continue - } - if err := r.SetDNS(oc); err != nil { - mainLog.Load().Error().Err(err).Msg("failed to revert /etc/resolv.conf changes") - } - if err := watcher.Add(watchDir); err != nil { - mainLog.Load().Error().Err(err).Msg("failed to continue running watcher") - return - } - } - case err, ok := <-watcher.Errors: - if !ok { - return - } - mainLog.Load().Err(err).Msg("could not get event for /etc/resolv.conf") - } - } -} diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 2b5accc..0716e9d 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -468,6 +468,13 @@ func (p *prog) setDNS() { return } logger.Debug().Msg("setting DNS successfully") + if shouldWatchResolvconf() { + servers := make([]netip.Addr, len(nameservers)) + for i := range nameservers { + servers[i] = netip.MustParseAddr(nameservers[i]) + } + go watchResolvConf(netIface, servers, setResolvConf) + } if allIfaces { withEachPhysicalInterfaces(netIface.Name, "set DNS", func(i *net.Interface) error { return setDNS(i, nameservers) diff --git a/cmd/cli/resolvconf.go b/cmd/cli/resolvconf.go new file mode 100644 index 0000000..f09d864 --- /dev/null +++ b/cmd/cli/resolvconf.go @@ -0,0 +1,65 @@ +package cli + +import ( + "net" + "net/netip" + "path/filepath" + + "github.com/fsnotify/fsnotify" +) + +const ( + resolvConfPath = "/etc/resolv.conf" + resolvConfBackupFailedMsg = "open /etc/resolv.pre-ctrld-backup.conf: read-only file system" +) + +// watchResolvConf watches any changes to /etc/resolv.conf file, +// and reverting to the original config set by ctrld. +func watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn func(iface *net.Interface, ns []netip.Addr) error) { + mainLog.Load().Debug().Msg("start watching /etc/resolv.conf file") + watcher, err := fsnotify.NewWatcher() + if err != nil { + mainLog.Load().Warn().Err(err).Msg("could not create watcher for /etc/resolv.conf") + return + } + defer watcher.Close() + + // We watch /etc instead of /etc/resolv.conf directly, + // see: https://github.com/fsnotify/fsnotify#watching-a-file-doesnt-work-well + watchDir := filepath.Dir(resolvConfPath) + if err := watcher.Add(watchDir); err != nil { + mainLog.Load().Warn().Err(err).Msg("could not add /etc/resolv.conf to watcher list") + return + } + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Name != resolvConfPath { // skip if not /etc/resolv.conf changes. + continue + } + if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) { + mainLog.Load().Debug().Msg("/etc/resolv.conf changes detected, reverting to ctrld setting") + if err := watcher.Remove(watchDir); err != nil { + mainLog.Load().Error().Err(err).Msg("failed to pause watcher") + continue + } + if err := setDnsFn(iface, ns); err != nil { + mainLog.Load().Error().Err(err).Msg("failed to revert /etc/resolv.conf changes") + } + if err := watcher.Add(watchDir); err != nil { + mainLog.Load().Error().Err(err).Msg("failed to continue running watcher") + return + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + mainLog.Load().Err(err).Msg("could not get event for /etc/resolv.conf") + } + } +} diff --git a/cmd/cli/resolvconf_darwin.go b/cmd/cli/resolvconf_darwin.go new file mode 100644 index 0000000..7e26f41 --- /dev/null +++ b/cmd/cli/resolvconf_darwin.go @@ -0,0 +1,20 @@ +package cli + +import ( + "net" + "net/netip" +) + +// setResolvConf sets the content of resolv.conf file using the given nameservers list. +func setResolvConf(iface *net.Interface, ns []netip.Addr) error { + servers := make([]string, len(ns)) + for i := range ns { + servers[i] = ns[i].String() + } + return setDNS(iface, servers) +} + +// shouldWatchResolvconf reports whether ctrld should watch changes to resolv.conf file with given OS configurator. +func shouldWatchResolvconf() bool { + return true +} diff --git a/cmd/cli/resolvconf_not_darwin_unix.go b/cmd/cli/resolvconf_not_darwin_unix.go new file mode 100644 index 0000000..b98496e --- /dev/null +++ b/cmd/cli/resolvconf_not_darwin_unix.go @@ -0,0 +1,40 @@ +//go:build unix && !darwin + +package cli + +import ( + "net" + "net/netip" + + "tailscale.com/util/dnsname" + + "github.com/Control-D-Inc/ctrld/internal/dns" +) + +// setResolvConf sets the content of resolv.conf file using the given nameservers list. +func setResolvConf(iface *net.Interface, ns []netip.Addr) error { + r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter. + if err != nil { + return err + } + + oc := dns.OSConfig{ + Nameservers: ns, + SearchDomains: []dnsname.FQDN{}, + } + return r.SetDNS(oc) +} + +// shouldWatchResolvconf reports whether ctrld should watch changes to resolv.conf file with given OS configurator. +func shouldWatchResolvconf() bool { + r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter. + if err != nil { + return false + } + switch r.Mode() { + case "direct", "resolvconf": + return true + default: + return false + } +} diff --git a/cmd/cli/resolvconf_windows.go b/cmd/cli/resolvconf_windows.go new file mode 100644 index 0000000..3e4ba1c --- /dev/null +++ b/cmd/cli/resolvconf_windows.go @@ -0,0 +1,16 @@ +package cli + +import ( + "net" + "net/netip" +) + +// setResolvConf sets the content of resolv.conf file using the given nameservers list. +func setResolvConf(_ *net.Interface, _ []netip.Addr) error { + return nil +} + +// shouldWatchResolvconf reports whether ctrld should watch changes to resolv.conf file with given OS configurator. +func shouldWatchResolvconf() bool { + return false +}