diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 6370488..2384de8 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -275,22 +275,19 @@ func initCLI() { if cp := router.CertPool(); cp != nil { rootCertPool = cp } - // Perform router setup/cleanup if ctrld could not be direct listener. - if !couldBeDirectListener(cfg.FirstListener()) { - p.onStarted = append(p.onStarted, func() { - mainLog.Debug().Msg("router setup") - if err := p.router.Setup(); err != nil { - mainLog.Error().Err(err).Msg("could not configure router") - } - }) - p.onStopped = append(p.onStopped, func() { - mainLog.Debug().Msg("router cleanup") - if err := p.router.Cleanup(); err != nil { - mainLog.Error().Err(err).Msg("could not cleanup router") - } - p.resetDNS() - }) - } + p.onStarted = append(p.onStarted, func() { + mainLog.Debug().Msg("router setup") + if err := p.router.Setup(); err != nil { + mainLog.Error().Err(err).Msg("could not configure router") + } + }) + p.onStopped = append(p.onStopped, func() { + mainLog.Debug().Msg("router cleanup") + if err := p.router.Cleanup(); err != nil { + mainLog.Error().Err(err).Msg("could not cleanup router") + } + p.resetDNS() + }) } close(waitCh) @@ -404,7 +401,7 @@ func initCLI() { return } - if router.Name() != "" && !couldBeDirectListener(cfg.FirstListener()) { + if router.Name() != "" { mainLog.Debug().Msg("cleaning up router before installing") _ = p.router.Cleanup() } @@ -504,15 +501,18 @@ func initCLI() { Short: "Stop the ctrld service", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - prog := &prog{} - s, err := newService(prog, svcConfig) + tryReadingConfig(false) + v.Unmarshal(&cfg) + p := &prog{router: router.New(&cfg)} + s, err := newService(p, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) return } initLogging() if doTasks([]task{{s.Stop, true}}) { - prog.resetDNS() + p.router.Cleanup() + p.resetDNS() mainLog.Notice().Msg("Service stopped") } }, @@ -591,7 +591,9 @@ func initCLI() { NOTE: Uninstalling will set DNS to values provided by DHCP.`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - p := &prog{} + tryReadingConfig(false) + v.Unmarshal(&cfg) + p := &prog{router: router.New(&cfg)} s, err := newService(p, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) @@ -1157,19 +1159,20 @@ func uninstall(p *prog, s service.Service) { } initLogging() if doTasks(tasks) { - r := router.New(&cfg) - if err := r.Uninstall(svcConfig); err != nil { + if err := p.router.ConfigureService(svcConfig); err != nil { + mainLog.Fatal().Err(err).Msg("could not configure service") + } + if err := p.router.Uninstall(svcConfig); err != nil { mainLog.Warn().Err(err).Msg("post uninstallation failed, please check system/service log for details error") return } - // Stop already reset DNS on router. - if router.Name() == "" { - p.resetDNS() + p.resetDNS() + if router.Name() != "" { + mainLog.Debug().Msg("Router cleanup") } - mainLog.Debug().Msg("Router cleanup") // Stop already did router.Cleanup and report any error if happens, // ignoring error here to prevent false positive. - _ = r.Cleanup() + _ = p.router.Cleanup() mainLog.Notice().Msg("Service uninstalled") return } diff --git a/cmd/ctrld/os_linux.go b/cmd/ctrld/os_linux.go index 94ab8d1..570cabc 100644 --- a/cmd/ctrld/os_linux.go +++ b/cmd/ctrld/os_linux.go @@ -147,7 +147,7 @@ func resetDNS(iface *net.Interface) (err error) { if ctrldnet.IPv6Available(ctx) { c := client6.NewClient() conversation, err := c.Exchange(iface.Name) - if err != nil { + if err != nil && !errAddrInUse(err) { mainLog.Debug().Err(err).Msg("could not exchange DHCPv6") } for _, packet := range conversation { diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index f75ae16..d22b7e2 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -1,12 +1,14 @@ package main import ( + "errors" "fmt" "math/rand" "net" "os" "strconv" "sync" + "syscall" "github.com/kardianos/service" @@ -216,7 +218,7 @@ func (p *prog) setDNS() { logger.Debug().Msg("setting DNS for interface") ns := lc.IP switch { - case couldBeDirectListener(lc): + case lc.IsDirectDnsListener(): // If ctrld is direct listener, use 127.0.0.1 as nameserver. ns = "127.0.0.1" case lc.Port != 53: @@ -294,3 +296,11 @@ func runLogServer(sockPath string) net.Conn { } return server } + +func errAddrInUse(err error) bool { + opErr, ok := err.(*net.OpError) + if !ok { + return false + } + return errors.Is(opErr.Err, syscall.EADDRINUSE) +} diff --git a/config.go b/config.go index 9e8f518..ff803c5 100644 --- a/config.go +++ b/config.go @@ -207,6 +207,24 @@ type ListenerConfig struct { Policy *ListenerPolicyConfig `mapstructure:"policy" toml:"policy,omitempty"` } +// IsDirectDnsListener reports whether ctrld can be a direct listener on port 53. +// It returns true only if ctrld can listen on port 53 for all interfaces. That means +// there's no other software listening on port 53. +// +// If someone listening on port 53, or ctrld could only listen on port 53 for a specific +// interface, ctrld could only be configured as a DNS forwarder. +func (lc *ListenerConfig) IsDirectDnsListener() bool { + if lc == nil || lc.Port != 53 { + return false + } + switch lc.IP { + case "", "::", "0.0.0.0": + return true + default: + return false + } +} + // ListenerPolicyConfig specifies the policy rules for ctrld to filter incoming requests. type ListenerPolicyConfig struct { Name string `mapstructure:"name" toml:"name,omitempty"` diff --git a/internal/router/ddwrt/ddwrt.go b/internal/router/ddwrt/ddwrt.go index f2cffdc..dc220ce 100644 --- a/internal/router/ddwrt/ddwrt.go +++ b/internal/router/ddwrt/ddwrt.go @@ -58,6 +58,9 @@ func (d *Ddwrt) PreRun() error { } func (d *Ddwrt) Setup() error { + if d.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Already setup. if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { return nil @@ -81,6 +84,9 @@ func (d *Ddwrt) Setup() error { } func (d *Ddwrt) Cleanup() error { + if d.cfg.FirstListener().IsDirectDnsListener() { + return nil + } if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { nvramKvMap["dnsmasq_options"] = "" // Restore old configs. diff --git a/internal/router/edgeos/edgeos.go b/internal/router/edgeos/edgeos.go index f84e3b1..014a594 100644 --- a/internal/router/edgeos/edgeos.go +++ b/internal/router/edgeos/edgeos.go @@ -62,6 +62,9 @@ func (e *EdgeOS) PreRun() error { } func (e *EdgeOS) Setup() error { + if e.cfg.FirstListener().IsDirectDnsListener() { + return nil + } if e.isUSG { return e.setupUSG() } @@ -69,6 +72,9 @@ func (e *EdgeOS) Setup() error { } func (e *EdgeOS) Cleanup() error { + if e.cfg.FirstListener().IsDirectDnsListener() { + return nil + } if e.isUSG { return e.cleanupUSG() } diff --git a/internal/router/firewalla/firewalla.go b/internal/router/firewalla/firewalla.go index 4e177ed..cdf6586 100644 --- a/internal/router/firewalla/firewalla.go +++ b/internal/router/firewalla/firewalla.go @@ -54,6 +54,9 @@ func (f *Firewalla) PreRun() error { } func (f *Firewalla) Setup() error { + if f.cfg.FirstListener().IsDirectDnsListener() { + return nil + } data, err := dnsmasq.FirewallaConfTmpl(dnsmasq.ConfigContentTmpl, f.cfg) if err != nil { return fmt.Errorf("generating dnsmasq config: %w", err) @@ -71,6 +74,9 @@ func (f *Firewalla) Setup() error { } func (f *Firewalla) Cleanup() error { + if f.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Removing current config. if err := os.Remove(firewallaDNSMasqConfigPath); err != nil { return fmt.Errorf("removing ctrld config: %w", err) diff --git a/internal/router/merlin/merlin.go b/internal/router/merlin/merlin.go index 18b07c5..19d14b3 100644 --- a/internal/router/merlin/merlin.go +++ b/internal/router/merlin/merlin.go @@ -49,6 +49,9 @@ func (m *Merlin) PreRun() error { } func (m *Merlin) Setup() error { + if m.cfg.FirstListener().IsDirectDnsListener() { + return nil + } buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath) // Already setup. if bytes.Contains(buf, []byte(dnsmasq.MerlinPostConfMarker)) { @@ -86,6 +89,9 @@ func (m *Merlin) Setup() error { } func (m *Merlin) Cleanup() error { + if m.cfg.FirstListener().IsDirectDnsListener() { + return nil + } if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { // Restore old configs. if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { diff --git a/internal/router/openwrt/openwrt.go b/internal/router/openwrt/openwrt.go index 1d8de34..83ea884 100644 --- a/internal/router/openwrt/openwrt.go +++ b/internal/router/openwrt/openwrt.go @@ -49,6 +49,9 @@ func (o *Openwrt) PreRun() error { } func (o *Openwrt) Setup() error { + if o.cfg.FirstListener().IsDirectDnsListener() { + return nil + } data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, o.cfg) if err != nil { return err @@ -68,6 +71,9 @@ func (o *Openwrt) Setup() error { } func (o *Openwrt) Cleanup() error { + if o.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Remove the custom dnsmasq config if err := os.Remove(openwrtDNSMasqConfigPath); err != nil { return err diff --git a/internal/router/pfsense/pfsense.go b/internal/router/pfsense/pfsense.go index ec2b890..1806ec7 100644 --- a/internal/router/pfsense/pfsense.go +++ b/internal/router/pfsense/pfsense.go @@ -66,6 +66,9 @@ func (p *Pfsense) Install(config *service.Config) error { } func (p *Pfsense) Uninstall(config *service.Config) error { + if err := os.Remove(filepath.Join(rcPath, p.svcName+".sh")); err != nil { + return fmt.Errorf("os.Remove: %w", err) + } return nil } @@ -84,11 +87,10 @@ func (p *Pfsense) Setup() error { } func (p *Pfsense) Cleanup() error { - if err := os.Remove(filepath.Join(rcPath, p.svcName+".sh")); err != nil { - return fmt.Errorf("os.Remove: %w", err) + if p.cfg.FirstListener().IsDirectDnsListener() { + _ = exec.Command(unboundRcPath, "onerestart").Run() + _ = exec.Command(dnsmasqRcPath, "onerestart").Run() } - _ = exec.Command(unboundRcPath, "onerestart").Run() - _ = exec.Command(dnsmasqRcPath, "onerestart").Run() return nil } diff --git a/internal/router/synology/synology.go b/internal/router/synology/synology.go index 78551e4..3ad0388 100644 --- a/internal/router/synology/synology.go +++ b/internal/router/synology/synology.go @@ -44,6 +44,9 @@ func (s *Synology) PreRun() error { } func (s *Synology) Setup() error { + if s.cfg.FirstListener().IsDirectDnsListener() { + return nil + } data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, s.cfg) if err != nil { return err @@ -61,6 +64,9 @@ func (s *Synology) Setup() error { } func (s *Synology) Cleanup() error { + if s.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Remove the custom config files. for _, f := range []string{synologyDNSMasqConfigPath, synologyDhcpdInfoPath} { if err := os.Remove(f); err != nil { diff --git a/internal/router/tomato/tomato.go b/internal/router/tomato/tomato.go index 4c1824d..40a70e5 100644 --- a/internal/router/tomato/tomato.go +++ b/internal/router/tomato/tomato.go @@ -53,6 +53,9 @@ func (f *FreshTomato) PreRun() error { } func (f *FreshTomato) Setup() error { + if f.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Already setup. if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { return nil @@ -83,6 +86,9 @@ func (f *FreshTomato) Setup() error { } func (f *FreshTomato) Cleanup() error { + if f.cfg.FirstListener().IsDirectDnsListener() { + return nil + } if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { nvramKvMap["dnsmasq_custom"] = "" // Restore old configs. diff --git a/internal/router/ubios/ubios.go b/internal/router/ubios/ubios.go index 06194d9..b0762db 100644 --- a/internal/router/ubios/ubios.go +++ b/internal/router/ubios/ubios.go @@ -47,6 +47,9 @@ func (u *Ubios) PreRun() error { } func (u *Ubios) Setup() error { + if u.cfg.FirstListener().IsDirectDnsListener() { + return nil + } data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, u.cfg) if err != nil { return err @@ -62,6 +65,9 @@ func (u *Ubios) Setup() error { } func (u *Ubios) Cleanup() error { + if u.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Remove the custom dnsmasq config if err := os.Remove(ubiosDNSMasqConfigPath); err != nil { return err