diff --git a/README.md b/README.md index 5b048ca..680ea36 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ A highly configurable DNS forwarding proxy with support for: - Multiple upstreams with fallbacks - Multiple network policy driven DNS query steering (via network cidr, MAC address or FQDN) - Policy driven domain based "split horizon" DNS with wildcard support -- Integrations with common router vendors and firmware - LAN client discovery via DHCP, mDNS, ARP, NDP, hosts file parsing - Prometheus metrics exporter @@ -26,11 +25,10 @@ All DNS protocols are supported, including: - `DNS-over-QUIC` # Use Cases -1. Use secure DNS protocols on networks and devices that don't natively support them (legacy routers, legacy OSes, TVs, smart toasters). +1. Use secure DNS protocols on networks and devices that don't natively support them (legacy OSes, TVs, smart toasters). 2. Create source IP based DNS routing policies with variable secure DNS upstreams. Subnet 1 (admin) uses upstream resolver A, while Subnet 2 (employee) uses upstream resolver B. 3. Create destination IP based DNS routing policies with variable secure DNS upstreams. Listener 1 uses upstream resolver C, while Listener 2 uses upstream resolver D. 4. Create domain level "split horizon" DNS routing policies to send internal domains (*.company.int) to a local DNS server, while everything else goes to another upstream. -5. Deploy on a router and create LAN client specific DNS routing policies from a web GUI (When using ControlD.com). ## OS Support @@ -39,22 +37,6 @@ All DNS protocols are supported, including: - MacOS (amd64, arm64) - Linux (386, amd64, arm, mips) - FreeBSD (386, amd64, arm) -- Common routers (See below) - - -### Supported Routers -You can run `ctrld` on any supported router. The list of supported routers and firmware includes: -- Asus Merlin -- DD-WRT -- Firewalla -- FreshTomato -- GL.iNet -- OpenWRT -- pfSense / OPNsense -- Synology -- Ubiquiti (UniFi, EdgeOS) - -`ctrld` will attempt to interface with dnsmasq (or Windows Server) whenever possible and set itself as the upstream, while running on port 5354. On FreeBSD based OSes, `ctrld` will terminate dnsmasq and unbound in order to be able to listen on port 53 directly. # Install There are several ways to download and install `ctrld`. @@ -161,9 +143,7 @@ You can then run a test query using a DNS client, for example, `dig`: If `verify.controld.com` resolves, you're successfully using the default Control D upstream. From here, you can start editing the config file that was generated. To enforce a new config, restart the server. ## Service Mode -This mode will run the application as a background system service on any Windows, MacOS, Linux, FreeBSD distribution or supported router. This will create a generic `ctrld.toml` file in the **C:\ControlD** directory (on Windows) or `/etc/controld/` (almost everywhere else), start the system service, and **configure the listener on all physical network interface**. Service will start on OS boot. - -When Control D upstreams are used on a router type device, `ctrld` will [relay your network topology](https://docs.controld.com/docs/device-clients) to Control D (LAN IPs, MAC addresses, and hostnames), and you will be able to see your LAN devices in the web panel, view analytics and apply unique profiles to them. +This mode will run the application as a background system service on any Windows, MacOS, Linux or FreeBSD distribution. This will create a generic `ctrld.toml` file in the **C:\ControlD** directory (on Windows) or `/etc/controld/` (almost everywhere else), start the system service, and **configure the listener on all physical network interface**. Service will start on OS boot. ### Command diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index cc5d1fe..c884f18 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -40,7 +40,6 @@ import ( "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/controld" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" - "github.com/Control-D-Inc/ctrld/internal/router" ) // selfCheckInternalTestDomain is used for testing ctrld self response to clients. @@ -290,21 +289,12 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { p.Fatal().Msg("network is not up yet") } - p.router = router.New(&cfg, cdUID != "") cs, err := newControlServer(filepath.Join(sockDir, ControlSocketName())) if err != nil { p.Warn().Err(err).Msg("could not create control server") } p.cs = cs - // Processing --cd flag require connecting to ControlD API, which needs valid - // 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() - p.Fatal().Err(err).Msg("failed to perform router pre-run check") - } - oldLogPath := cfg.Service.LogPath if uid := cdUIDFromProvToken(); uid != "" { cdUID = uid @@ -413,25 +403,6 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { } } }) - if platform := router.Name(); platform != "" { - if cp := router.CertPool(); cp != nil { - rootCertPool = cp - } - if iface != "" { - p.onStarted = append(p.onStarted, func() { - p.Debug().Msg("router setup on start") - if err := p.router.Setup(); err != nil { - p.Error().Err(err).Msg("could not configure router") - } - }) - p.onStopped = append(p.onStopped, func() { - p.Debug().Msg("router cleanup on stop") - if err := p.router.Cleanup(); err != nil { - p.Error().Err(err).Msg("could not cleanup router") - } - }) - } - } p.onStopped = append(p.onStopped, func() { // restore static DNS settings or DHCP p.resetDNS(false, true) @@ -809,9 +780,6 @@ func netInterface(ifaceName string) (*net.Interface, error) { } func defaultIfaceName() string { - if ifaceName := router.DefaultInterfaceName(); ifaceName != "" { - return ifaceName - } dri, err := netmon.DefaultRouteInterface() if err != nil { // On WSL 1, the route table does not have any default route. But the fact that @@ -962,13 +930,6 @@ func selfCheckResolveDomain(ctx context.Context, addr, scope string, domain stri } func userHomeDir() (string, error) { - dir, err := router.HomeDir() - if err != nil { - return "", err - } - if dir != "" { - return dir, nil - } // Mobile platform should provide a rw dir path for this. if isMobile() { return homedir, nil @@ -1051,13 +1012,6 @@ func uninstall(p *prog, s service.Service) { } initInteractiveLogging() if doTasks(tasks) { - if err := p.router.ConfigureService(svcConfig); err != nil { - mainLog.Load().Fatal().Err(err).Msg("could not configure service") - } - if err := p.router.Uninstall(svcConfig); err != nil { - mainLog.Load().Warn().Err(err).Msg("post uninstallation failed, please check system/service log for details error") - return - } // restore static DNS settings or DHCP p.resetDNS(false, true) @@ -1078,12 +1032,6 @@ func uninstall(p *prog, s service.Service) { return nil }) - if router.Name() != "" { - mainLog.Load().Debug().Msg("Router cleanup") - } - // Stop already did router.Cleanup and report any error if happens, - // ignoring error here to prevent false positive. - _ = p.router.Cleanup() mainLog.Load().Notice().Msg("Service uninstalled") return } @@ -1201,7 +1149,6 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) ( nextdnsMode := nextdns != "" // For Windows server with local Dns server running, we can only try on random local IP. hasLocalDnsServer := hasLocalDnsServerRunning() - notRouter := router.Name() == "" isDesktop := ctrld.IsDesktopPlatform() for n, listener := range cfg.Listener { lcc[n] = &listenerConfigCheck{} @@ -1309,21 +1256,19 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) ( // On firewalla, we don't need to check localhost, because the lo interface is excluded in dnsmasq // config, so we can always listen on localhost port 53, but no traffic could be routed there. - tryLocalhost := !isLoopback(listener.IP) && router.CanListenLocalhost() + tryLocalhost := !isLoopback(listener.IP) tryAllPort53 := true - tryOldIPPort5354 := true - tryPort5354 := true + // We should not try to listen on any port other than 53, + // if we do, this will break the dns resolution for the system. + // TODO: cleanup these codes when refactoring this function. + tryOldIPPort5354 := false + tryPort5354 := false if hasLocalDnsServer { tryAllPort53 = false tryOldIPPort5354 = false tryPort5354 = false } - // if not running on a router, we should not try to listen on any port other than 53 - // if we do, this will break the dns resolution for the system. - if notRouter { - tryOldIPPort5354 = false - tryPort5354 = false - } + attempts := 0 maxAttempts := 10 for { @@ -1400,9 +1345,7 @@ func tryUpdateListenerConfig(cfg *ctrld.Config, notifyFunc func(), fatal bool) ( } else { listener.IP = oldIP } - // if we are not running on a router, we should not try to listen on any port other than 53 - // if we do, this will break the dns resolution for the system. - if check.Port && !notRouter { + if check.Port { listener.Port = randomPort() } else { listener.Port = oldPort @@ -1738,11 +1681,6 @@ func exchangeContextWithTimeout(c *dns.Client, timeout time.Duration, msg *dns.M return c.ExchangeContext(ctx, msg, addr) } -// runInCdMode reports whether ctrld service is running in cd mode. -func runInCdMode() bool { - return curCdUID() != "" -} - // curCdUID returns the current ControlD UID used by running ctrld process. func curCdUID() string { if s, _ := newService(&prog{}, svcConfig); s != nil { diff --git a/cmd/cli/commands.go b/cmd/cli/commands.go index d610463..31ca495 100644 --- a/cmd/cli/commands.go +++ b/cmd/cli/commands.go @@ -23,11 +23,9 @@ import ( "github.com/minio/selfupdate" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" - "github.com/spf13/pflag" "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/clientinfo" - "github.com/Control-D-Inc/ctrld/internal/router" ) // dialSocketControlServerTimeout is the default timeout to wait when ping control server. @@ -47,7 +45,7 @@ func initLogCmd() *cobra.Command { }, Run: func(cmd *cobra.Command, args []string) { - p := &prog{router: router.New(&cfg, false)} + p := &prog{} s, _ := newService(p, svcConfig) status, err := s.Status() @@ -100,7 +98,7 @@ func initLogCmd() *cobra.Command { }, Run: func(cmd *cobra.Command, args []string) { - p := &prog{router: router.New(&cfg, false)} + p := &prog{} s, _ := newService(p, svcConfig) status, err := s.Status() @@ -225,10 +223,7 @@ NOTE: running "ctrld start" without any arguments will start already installed c setDependencies(sc) sc.Arguments = append([]string{"run"}, osArgs...) - p := &prog{ - router: router.New(&cfg, cdUID != ""), - cfg: &cfg, - } + p := &prog{cfg: &cfg} s, err := newService(p, sc) if err != nil { mainLog.Load().Error().Msg(err.Error()) @@ -400,10 +395,6 @@ NOTE: running "ctrld start" without any arguments will start already installed c validateCdUpstreamProtocol() } - if err := p.router.ConfigureService(sc); err != nil { - mainLog.Load().Fatal().Err(err).Msg("failed to configure service on router") - } - if configPath != "" { v.SetConfigFile(configPath) } @@ -427,11 +418,6 @@ NOTE: running "ctrld start" without any arguments will start already installed c sc.Arguments = append(sc.Arguments, "--config="+defaultConfigFile) } - if router.Name() != "" && iface != "" { - mainLog.Load().Debug().Msg("cleaning up router before installing") - _ = p.router.Cleanup() - } - tasks := []task{ {s.Stop, false, "Stop"}, {func() error { return doGenerateNextDNSConfig(nextdns) }, true, "Checking config"}, @@ -458,11 +444,6 @@ NOTE: running "ctrld start" without any arguments will start already installed c } mainLog.Load().Notice().Msg("Starting service") if doTasks(tasks) { - if err := p.router.Install(sc); err != nil { - mainLog.Load().Warn().Err(err).Msg("post installation failed, please check system/service log for details error") - return - } - // add a small delay to ensure the service is started and did not crash time.Sleep(1 * time.Second) @@ -529,33 +510,6 @@ NOTE: running "ctrld start" without any arguments will start already installed c startCmd.Flags().BoolVarP(&startOnly, "start_only", "", false, "Do not install new service") _ = startCmd.Flags().MarkHidden("start_only") - routerCmd := &cobra.Command{ - Use: "setup", - Run: func(cmd *cobra.Command, _ []string) { - exe, err := os.Executable() - if err != nil { - mainLog.Load().Fatal().Msgf("could not find executable path: %v", err) - os.Exit(1) - } - flags := make([]string, 0) - cmd.Flags().Visit(func(flag *pflag.Flag) { - flags = append(flags, fmt.Sprintf("--%s=%s", flag.Name, flag.Value)) - }) - cmdArgs := []string{"start"} - cmdArgs = append(cmdArgs, flags...) - command := exec.Command(exe, cmdArgs...) - command.Stdout = os.Stdout - command.Stderr = os.Stderr - command.Stdin = os.Stdin - if err := command.Run(); err != nil { - mainLog.Load().Fatal().Msg(err.Error()) - } - }, - } - routerCmd.Flags().AddFlagSet(startCmd.Flags()) - routerCmd.Hidden = true - rootCmd.AddCommand(routerCmd) - startCmdAlias := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { checkHasElevatedPrivilege() @@ -601,7 +555,7 @@ func initStopCmd() *cobra.Command { Run: func(cmd *cobra.Command, args []string) { readConfig(false) v.Unmarshal(&cfg) - p := &prog{router: router.New(&cfg, runInCdMode())} + p := &prog{} s, err := newService(p, svcConfig) if err != nil { mainLog.Load().Error().Msg(err.Error()) @@ -629,23 +583,6 @@ func initStopCmd() *cobra.Command { os.Exit(deactivationPinInvalidExitCode) } if doTasks([]task{{s.Stop, true, "Stop"}}) { - if router.WaitProcessExited() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - for { - select { - case <-ctx.Done(): - mainLog.Load().Error().Msg("timeout while waiting for service to stop") - return - default: - } - time.Sleep(time.Second) - if status, _ := s.Status(); status == service.StatusStopped { - break - } - } - } mainLog.Load().Notice().Msg("Service stopped") } }, @@ -689,7 +626,7 @@ func initRestartCmd() *cobra.Command { cdUID = curCdUID() cdMode := cdUID != "" - p := &prog{router: router.New(&cfg, cdMode)} + p := &prog{} s, err := newService(p, svcConfig) if err != nil { mainLog.Load().Error().Msg(err.Error()) @@ -723,7 +660,6 @@ func initRestartCmd() *cobra.Command { tasks := []task{ {s.Stop, true, "Stop"}, {func() error { - p.router.Cleanup() // restore static DNS settings or DHCP p.resetDNS(false, true) return nil @@ -733,27 +669,7 @@ func initRestartCmd() *cobra.Command { return nil }, false, "Waiting for service to stop"}, } - if doTasks(tasks) { - - if router.WaitProcessExited() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - loop: - for { - select { - case <-ctx.Done(): - mainLog.Load().Error().Msg("timeout while waiting for service to stop") - break loop - default: - } - time.Sleep(time.Second) - if status, _ := s.Status(); status == service.StatusStopped { - break - } - } - } - } else { + if !doTasks(tasks) { return false } @@ -814,7 +730,7 @@ func initReloadCmd(restartCmd *cobra.Command) *cobra.Command { Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - p := &prog{router: router.New(&cfg, false)} + p := &prog{} s, _ := newService(p, svcConfig) status, err := s.Status() @@ -939,7 +855,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, Run: func(cmd *cobra.Command, args []string) { readConfig(false) v.Unmarshal(&cfg) - p := &prog{router: router.New(&cfg, runInCdMode())} + p := &prog{} s, err := newService(p, svcConfig) if err != nil { mainLog.Load().Error().Msg(err.Error()) @@ -1115,7 +1031,7 @@ func initClientsCmd() *cobra.Command { }, Run: func(cmd *cobra.Command, args []string) { - p := &prog{router: router.New(&cfg, false)} + p := &prog{} s, _ := newService(p, svcConfig) status, err := s.Status() @@ -1228,7 +1144,7 @@ func initUpgradeCmd() *cobra.Command { sc.Executable = bin readConfig(false) v.Unmarshal(&cfg) - p := &prog{router: router.New(&cfg, runInCdMode())} + p := &prog{} s, err := newService(p, sc) if err != nil { mainLog.Load().Error().Msg(err.Error()) @@ -1285,7 +1201,6 @@ func initUpgradeCmd() *cobra.Command { tasks := []task{ {s.Stop, true, "Stop"}, {func() error { - p.router.Cleanup() // restore static DNS settings or DHCP p.resetDNS(false, true) return nil @@ -1295,27 +1210,7 @@ func initUpgradeCmd() *cobra.Command { return nil }, false, "Waiting for service to stop"}, } - if doTasks(tasks) { - - if router.WaitProcessExited() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - loop: - for { - select { - case <-ctx.Done(): - mainLog.Load().Error().Msg("timeout while waiting for service to stop") - break loop - default: - } - time.Sleep(time.Second) - if status, _ := s.Status(); status == service.StatusStopped { - break - } - } - } - } + doTasks(tasks) tasks = []task{ {s.Start, true, "Start"}, diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index a5bbd0b..1c6d39d 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -25,7 +25,6 @@ import ( "github.com/Control-D-Inc/ctrld/internal/controld" "github.com/Control-D-Inc/ctrld/internal/dnscache" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" - "github.com/Control-D-Inc/ctrld/internal/router" ) const ( @@ -1405,10 +1404,7 @@ func (p *prog) monitorNetworkChanges(ctx context.Context) error { } p.Debug().Msgf("Set default local IPv4: %s, IPv6: %s", selfIP, ipv6) - // we only trigger recovery flow for network changes on non router devices - if router.Name() == "" { - p.handleRecovery(RecoveryReasonNetworkChange) - } + p.handleRecovery(RecoveryReasonNetworkChange) }) mon.Start() diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index ff5530c..4b0fe97 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -34,8 +34,6 @@ import ( "github.com/Control-D-Inc/ctrld/internal/clientinfo" "github.com/Control-D-Inc/ctrld/internal/controld" "github.com/Control-D-Inc/ctrld/internal/dnscache" - "github.com/Control-D-Inc/ctrld/internal/router" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" ) const ( @@ -120,7 +118,6 @@ type prog struct { sema semaphore ciTable *clientinfo.Table um *upstreamMonitor - router router.Router ptrLoopGuard *loopGuard lanLoopGuard *loopGuard metricsQueryStats atomic.Bool @@ -612,12 +609,6 @@ func (p *prog) setupClientInfoDiscover() { format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat) p.ciTable.AddLeaseFile(leaseFile, format) } - if leaseFiles := dnsmasq.AdditionalLeaseFiles(); len(leaseFiles) > 0 { - mainLog.Load().Debug().Msgf("watching additional lease files: %v", leaseFiles) - for _, leaseFile := range leaseFiles { - p.ciTable.AddLeaseFile(leaseFile, ctrld.Dnsmasq) - } - } } // runClientInfoDiscover runs the client info discover. @@ -724,9 +715,6 @@ func (p *prog) setDNS() { ns = "127.0.0.1" case lc.Port != 53: ns = "127.0.0.1" - if resolver := router.LocalResolverIP(); resolver != "" { - ns = resolver - } default: // If we ever reach here, it means ctrld is running on lc.IP port 53, // so we could just use lc.IP as nameserver. @@ -1493,10 +1481,7 @@ 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 - } + // if we are running on ADDC, we should not leak on upstream failure if p.runningOnDomainController { return false diff --git a/cmd/cli/prog_linux.go b/cmd/cli/prog_linux.go index 2e5c7c7..a964501 100644 --- a/cmd/cli/prog_linux.go +++ b/cmd/cli/prog_linux.go @@ -9,8 +9,6 @@ import ( "strings" "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld/internal/router" ) func init() { @@ -37,9 +35,6 @@ func setDependencies(svc *service.Config) { svc.Dependencies = append(svc.Dependencies, "Wants=systemd-networkd-wait-online.service") } } - if routerDeps := router.ServiceDependencies(); len(routerDeps) > 0 { - svc.Dependencies = append(svc.Dependencies, routerDeps...) - } } func setWorkingDirectory(svc *service.Config, dir string) { diff --git a/cmd/cli/service.go b/cmd/cli/service.go index f75ee55..35e82f5 100644 --- a/cmd/cli/service.go +++ b/cmd/cli/service.go @@ -11,9 +11,6 @@ import ( "github.com/coreos/go-systemd/v22/unit" "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld/internal/router" - "github.com/Control-D-Inc/ctrld/internal/router/openwrt" ) // newService wraps service.New call to return service.Service @@ -24,10 +21,6 @@ func newService(i service.Interface, c *service.Config) (service.Service, error) return nil, err } switch { - case router.IsOldOpenwrt(), router.IsNetGearOrbi(): - return &procd{sysV: &sysV{s}, svcConfig: c}, nil - case router.IsGLiNet(): - return &sysV{s}, nil case s.Platform() == "unix-systemv": return &sysV{s}, nil case s.Platform() == "linux-systemd": @@ -42,7 +35,7 @@ func newService(i service.Interface, c *service.Config) (service.Service, error) // sysV wraps a service.Service, and provide start/stop/status command // base on "/etc/init.d/". // -// Use this on system where "service" command is not available, like GL.iNET router. +// Use this on system where "service" command is not available. type sysV struct { service.Service } @@ -89,37 +82,6 @@ func (s *sysV) Status() (service.Status, error) { return unixSystemVServiceStatus() } -// procd wraps a service.Service, and provide start/stop command -// base on "/etc/init.d/", status command base on parsing "ps" command output. -// -// Use this on system where "/etc/init.d/ status" command is not available, -// like old GL.iNET Opal router. -type procd struct { - *sysV - svcConfig *service.Config -} - -func (s *procd) Status() (service.Status, error) { - if !s.installed() { - return service.StatusUnknown, service.ErrNotInstalled - } - bin := s.svcConfig.Executable - if bin == "" { - exe, err := os.Executable() - if err != nil { - return service.StatusUnknown, nil - } - bin = exe - } - - // Looking for something like "/sbin/ctrld run ". - shellCmd := fmt.Sprintf("ps | grep -q %q", bin+" [r]un ") - if err := exec.Command("sh", "-c", shellCmd).Run(); err != nil { - return service.StatusStopped, nil - } - return service.StatusRunning, nil -} - // systemd wraps a service.Service, and provide status command to // report the status correctly. type systemd struct { @@ -249,13 +211,6 @@ func checkHasElevatedPrivilege() { func unixSystemVServiceStatus() (service.Status, error) { out, err := exec.Command("/etc/init.d/ctrld", "status").CombinedOutput() if err != nil { - // Specific case for openwrt >= 24.10, it returns non-success code - // for above status command, which may not right. - if router.Name() == openwrt.Name { - if string(bytes.ToLower(bytes.TrimSpace(out))) == "inactive" { - return service.StatusStopped, nil - } - } return service.StatusUnknown, nil } diff --git a/docs/config.md b/docs/config.md index 99e98c9..69ba010 100644 --- a/docs/config.md +++ b/docs/config.md @@ -18,10 +18,6 @@ The config file allows for advanced configuration of the `ctrld` utility to cove - `/etc/controld` on *nix. - User's home directory on Windows. - - Same directory with `ctrld` binary on these routers: - - `ddwrt` - - `merlin` - - `freshtomato` - Current directory. The user can choose to override default value using command line `--config` or `-c`: @@ -293,7 +289,7 @@ If a remote upstream fails to resolve a query or is unreachable, `ctrld` will fo - Type: boolean - Required: no -- Default: true on Windows, MacOS and non-router Linux. +- Default: true on Windows, MacOS and Linux. ## Upstream The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to. diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index 719e205..a66830b 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -82,8 +82,6 @@ type Table struct { logger *ctrld.Logger dhcp *dhcp - merlin *merlinDiscover - ubios *ubiosDiscover arp *arpDiscover ndp *ndpDiscover ptr *ptrDiscover @@ -206,30 +204,6 @@ func (t *Table) init() { return } - // Otherwise, process all possible sources in order, that means - // the first result of IP/MAC/Hostname lookup will be used. - // - // Routers custom clients: - // - Merlin - // - Ubios - if t.discoverDHCP() || t.discoverARP() { - t.merlin = &merlinDiscover{logger: t.logger} - t.ubios = &ubiosDiscover{} - discovers := map[string]interface { - refresher - HostnameResolver - }{ - "Merlin": t.merlin, - "Ubios": t.ubios, - } - for platform, discover := range discovers { - if err := discover.refresh(); err != nil { - t.logger.Warn().Err(err).Msgf("failed to init %s discover", platform) - } - t.hostnameResolvers = append(t.hostnameResolvers, discover) - t.refreshers = append(t.refreshers, discover) - } - } // Hosts file mapping. if t.discoverHosts() { t.hf = &hostsFile{logger: t.logger} diff --git a/internal/clientinfo/dhcp.go b/internal/clientinfo/dhcp.go index fbd7b08..b387806 100644 --- a/internal/clientinfo/dhcp.go +++ b/internal/clientinfo/dhcp.go @@ -18,7 +18,6 @@ import ( "tailscale.com/util/lineread" "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/router" ) type dhcp struct { @@ -39,10 +38,6 @@ func (d *dhcp) init() error { } d.addSelf() d.watcher = watcher - for file, format := range clientInfoFiles { - // Ignore errors for default lease files. - _ = d.addLeaseFile(file, format) - } return nil } @@ -50,11 +45,7 @@ func (d *dhcp) watchChanges() { if d.watcher == nil { return } - if dir := router.LeaseFilesDir(); dir != "" { - if err := d.watcher.Add(dir); err != nil { - d.logger.Err(err).Str("dir", dir).Msg("could not watch lease dir") - } - } + for { select { case event, ok := <-d.watcher.Events: @@ -390,22 +381,4 @@ func (d *dhcp) addSelf() { } } }) - for _, netIface := range router.SelfInterfaces() { - mac := netIface.HardwareAddr.String() - if mac == "" { - return - } - d.mac2name.Store(mac, hostname) - addrs, _ := netIface.Addrs() - for _, addr := range addrs { - ipNet, ok := addr.(*net.IPNet) - if !ok { - continue - } - ip := ipNet.IP - d.mac.LoadOrStore(ip.String(), mac) - d.ip.LoadOrStore(mac, ip.String()) - d.ip2name.Store(ip.String(), hostname) - } - } } diff --git a/internal/clientinfo/dhcp_lease_files.go b/internal/clientinfo/dhcp_lease_files.go index 1b5d829..3f1c5ac 100644 --- a/internal/clientinfo/dhcp_lease_files.go +++ b/internal/clientinfo/dhcp_lease_files.go @@ -3,17 +3,5 @@ package clientinfo import "github.com/Control-D-Inc/ctrld" // clientInfoFiles specifies client info files and how to read them on supported platforms. -var clientInfoFiles = map[string]ctrld.LeaseFileFormat{ - "/tmp/dnsmasq.leases": ctrld.Dnsmasq, // ddwrt - "/tmp/dhcp.leases": ctrld.Dnsmasq, // openwrt - "/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // merlin - "/mnt/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDM Pro - "/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDR - "/etc/dhcpd/dhcpd-leases.log": ctrld.Dnsmasq, // Synology - "/tmp/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // Tomato - "/run/dnsmasq-dhcp.leases": ctrld.Dnsmasq, // EdgeOS - "/run/dhcpd.leases": ctrld.IscDhcpd, // EdgeOS - "/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense - "/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla - "/var/lib/kea/dhcp4.leases": ctrld.KeaDHCP4, // Pfsense -} +// TODO: cleanup this after server support removal. +var clientInfoFiles = map[string]ctrld.LeaseFileFormat{} diff --git a/internal/clientinfo/merlin.go b/internal/clientinfo/merlin.go deleted file mode 100644 index 8ba6c5c..0000000 --- a/internal/clientinfo/merlin.go +++ /dev/null @@ -1,72 +0,0 @@ -package clientinfo - -import ( - "strings" - "sync" - - "github.com/Control-D-Inc/ctrld/internal/router" - "github.com/Control-D-Inc/ctrld/internal/router/merlin" - - "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/router/nvram" -) - -const merlinNvramCustomClientListKey = "custom_clientlist" - -type merlinDiscover struct { - hostname sync.Map // mac => hostname - logger *ctrld.Logger -} - -func (m *merlinDiscover) refresh() error { - if router.Name() != merlin.Name { - return nil - } - out, err := nvram.Run("get", merlinNvramCustomClientListKey) - if err != nil { - return err - } - m.logger.Debug().Msg("reading Merlin custom client list") - m.parseMerlinCustomClientList(out) - return nil -} - -func (m *merlinDiscover) LookupHostnameByIP(ip string) string { - return "" -} - -func (m *merlinDiscover) LookupHostnameByMac(mac string) string { - val, ok := m.hostname.Load(mac) - if !ok { - return "" - } - return val.(string) -} - -// "nvram get custom_clientlist" output: -// -// 00:00:00:00:00:01>0>4>>00:00:00:00:00:02>0>24>>... -// -// So to parse it, do the following steps: -// -// - Split by "<" => entries -// - For each entry, split by ">" => parts -// - Empty parts => skip -// - Empty parts[0] => skip empty hostname -// - Empty parts[1] => skip empty MAC -func (m *merlinDiscover) parseMerlinCustomClientList(data string) { - entries := strings.Split(data, "<") - for _, entry := range entries { - parts := strings.SplitN(string(entry), ">", 3) - if len(parts) < 2 || len(parts[0]) == 0 || len(parts[1]) == 0 { - continue - } - hostname := normalizeHostname(parts[0]) - mac := strings.ToLower(parts[1]) - m.hostname.Store(mac, hostname) - } -} - -func (m *merlinDiscover) String() string { - return "merlin" -} diff --git a/internal/clientinfo/merlin_test.go b/internal/clientinfo/merlin_test.go deleted file mode 100644 index 0437035..0000000 --- a/internal/clientinfo/merlin_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package clientinfo - -import ( - "testing" -) - -func TestParseMerlinCustomClientList(t *testing.T) { - tests := []struct { - name string - clientList string - macList []string - hostnameList []string - macNotPresentList []string - }{ - { - "normal", - "00:00:00:00:00:01>0>4>>", - []string{"00:00:00:00:00:01"}, - []string{"client1"}, - nil, - }, - { - "multiple clients", - "00:00:00:00:00:01>0>4>>00:00:00:00:00:02>0>24>>", - []string{"00:00:00:00:00:01", "00:00:00:00:00:02"}, - []string{"client1", "client2"}, - nil, - }, - { - "empty hostname", - "00:00:00:00:00:01>0>4>><>00:00:00:00:00:02>0>24>>", - []string{"00:00:00:00:00:01"}, - []string{"client1"}, - []string{"00:00:00:00:00:02"}, - }, - { - "empty dhcp", - "00:00:00:00:00:01>0>4>>>>", - []string{"00:00:00:00:00:01"}, - []string{"client1"}, - []string{""}, - }, - { - "invalid", - "qwerty", - nil, - nil, - nil, - }, - { - "empty", - "", - - nil, - nil, - nil, - }, - } - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - m := &merlinDiscover{} - m.parseMerlinCustomClientList(tc.clientList) - for i, mac := range tc.macList { - val, ok := m.hostname.Load(mac) - if !ok { - t.Errorf("missing hostname: %s", mac) - } - hostname := val.(string) - if hostname != tc.hostnameList[i] { - t.Errorf("hostname mismatch, want: %q, got: %q", tc.hostnameList[i], hostname) - } - } - for _, mac := range tc.macNotPresentList { - if _, ok := m.hostname.Load(mac); ok { - t.Errorf("mac2name address %q should not be present", mac) - } - } - }) - } -} diff --git a/internal/clientinfo/ubios.go b/internal/clientinfo/ubios.go deleted file mode 100644 index 0ffd6e5..0000000 --- a/internal/clientinfo/ubios.go +++ /dev/null @@ -1,79 +0,0 @@ -package clientinfo - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "os/exec" - "strings" - "sync" - - "github.com/Control-D-Inc/ctrld/internal/router" - "github.com/Control-D-Inc/ctrld/internal/router/ubios" -) - -// ubiosDiscover provides client discovery functionality on Ubios routers. -type ubiosDiscover struct { - hostname sync.Map // mac => hostname -} - -// refresh reloads unifi devices from database. -func (u *ubiosDiscover) refresh() error { - if router.Name() != ubios.Name { - return nil - } - return u.refreshDevices() -} - -// LookupHostnameByIP returns hostname for given IP. -func (u *ubiosDiscover) LookupHostnameByIP(ip string) string { - return "" -} - -// LookupHostnameByMac returns unifi device custom hostname for the given MAC address. -func (u *ubiosDiscover) LookupHostnameByMac(mac string) string { - val, ok := u.hostname.Load(mac) - if !ok { - return "" - } - return val.(string) -} - -// refreshDevices updates unifi devices name from local mongodb. -func (u *ubiosDiscover) refreshDevices() error { - cmd := exec.Command("/usr/bin/mongo", "localhost:27117/ace", "--quiet", "--eval", ` - DBQuery.shellBatchSize = 256; - db.user.find({name: {$exists: true, $ne: ""}}, {_id:0, mac:1, name:1});`) - b, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("out: %s, err: %w", string(b), err) - } - return u.storeDevices(bytes.NewReader(b)) -} - -// storeDevices saves unifi devices name for caching. -func (u *ubiosDiscover) storeDevices(r io.Reader) error { - decoder := json.NewDecoder(r) - device := struct { - MAC string - Name string - }{} - for { - err := decoder.Decode(&device) - if err == io.EOF { - break - } - if err != nil { - return err - } - mac := strings.ToLower(device.MAC) - u.hostname.Store(mac, normalizeHostname(device.Name)) - } - return nil -} - -// String returns human-readable format of ubiosDiscover. -func (u *ubiosDiscover) String() string { - return "ubios" -} diff --git a/internal/clientinfo/ubios_test.go b/internal/clientinfo/ubios_test.go deleted file mode 100644 index 657cf18..0000000 --- a/internal/clientinfo/ubios_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package clientinfo - -import ( - "strings" - "testing" -) - -func Test_ubiosDiscover_storeDevices(t *testing.T) { - ud := &ubiosDiscover{} - r := strings.NewReader(`{ "mac": "00:00:00:00:00:01", "name": "device 1" } -{ "mac": "00:00:00:00:00:02", "name": "device 2" } -`) - if err := ud.storeDevices(r); err != nil { - t.Fatal(err) - } - - tests := []struct { - name string - mac string - hostname string - }{ - {"device 1", "00:00:00:00:00:01", "device 1"}, - {"device 2", "00:00:00:00:00:02", "device 2"}, - {"non-existed", "00:00:00:00:00:03", ""}, - } - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - if got := ud.LookupHostnameByMac(tc.mac); got != tc.hostname { - t.Errorf("hostname mismatched, want: %q, got: %q", tc.hostname, got) - } - }) - } - - // Test for invalid input. - r = strings.NewReader(`{ "mac": "00:00:00:00:00:01", "name": "device 1"`) - if err := ud.storeDevices(r); err == nil { - t.Fatal("expected error, got nil") - } else { - t.Log(err) - } -} diff --git a/internal/controld/config.go b/internal/controld/config.go index 97ec8e2..813fcd5 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -18,8 +18,6 @@ import ( "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/certs" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" - "github.com/Control-D-Inc/ctrld/internal/router" - "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" ) const ( @@ -271,7 +269,7 @@ func apiTransport(loggerCtx context.Context, cdDev bool) *http.Transport { // Fallback to direct IPv6 return dial(ctx, "tcp6", addrsFromPort(apiIpsV6, port)) } - if router.Name() == ddwrt.Name || runtime.GOOS == "android" { + if runtime.GOOS == "android" { transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()} } return transport diff --git a/internal/router/ddwrt/ddwrt.go b/internal/router/ddwrt/ddwrt.go deleted file mode 100644 index edd7e6b..0000000 --- a/internal/router/ddwrt/ddwrt.go +++ /dev/null @@ -1,117 +0,0 @@ -package ddwrt - -import ( - "errors" - "fmt" - "os/exec" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" - "github.com/Control-D-Inc/ctrld/internal/router/ntp" - "github.com/Control-D-Inc/ctrld/internal/router/nvram" -) - -const Name = "ddwrt" - -//lint:ignore ST1005 This error is for human. -var errDdwrtJffs2NotEnabled = errors.New(`could not install service without jffs, follow this guide to enable: - -https://wiki.dd-wrt.com/wiki/index.php/Journalling_Flash_File_System -`) - -var nvramKvMap = map[string]string{ - "dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it. - "dnsmasq_options": "", // Configuration of dnsmasq set by ctrld, filled by setupDDWrt. - "dns_crypt": "0", // Disable DNSCrypt. - "dnssec": "0", // Disable DNSSEC. -} - -type Ddwrt struct { - cfg *ctrld.Config -} - -// New returns a router.Router for configuring/setup/run ctrld on ddwrt routers. -func New(cfg *ctrld.Config) *Ddwrt { - return &Ddwrt{cfg: cfg} -} - -func (d *Ddwrt) ConfigureService(config *service.Config) error { - if !ddwrtJff2Enabled() { - return errDdwrtJffs2NotEnabled - } - return nil -} - -func (d *Ddwrt) Install(_ *service.Config) error { - return nil -} - -func (d *Ddwrt) Uninstall(_ *service.Config) error { - return nil -} - -func (d *Ddwrt) PreRun() error { - _ = d.Cleanup() - return ntp.WaitNvram() -} - -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 - } - - data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, d.cfg) - if err != nil { - return err - } - - nvramKvMap["dnsmasq_options"] = data - if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil { - return err - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func (d *Ddwrt) Cleanup() error { - if d.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" { - return nil // was restored, nothing to do. - } - - nvramKvMap["dnsmasq_options"] = "" - // Restore old configs. - if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { - return err - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func restartDNSMasq() error { - if out, err := exec.Command("restart_dns").CombinedOutput(); err != nil { - return fmt.Errorf("restart_dns: %s, %w", string(out), err) - } - return nil -} - -func ddwrtJff2Enabled() bool { - out, _ := nvram.Run("get", "enable_jffs2") - return out == "1" -} diff --git a/internal/router/dnsmasq/conf.go b/internal/router/dnsmasq/conf.go deleted file mode 100644 index bb81d60..0000000 --- a/internal/router/dnsmasq/conf.go +++ /dev/null @@ -1,90 +0,0 @@ -package dnsmasq - -import ( - "bufio" - "bytes" - "errors" - "io" - "os" - "path/filepath" - "strings" -) - -func InterfaceNameFromConfig(filename string) (string, error) { - buf, err := os.ReadFile(filename) - if err != nil { - return "", err - } - return interfaceNameFromReader(bytes.NewReader(buf)) -} - -func interfaceNameFromReader(r io.Reader) (string, error) { - scanner := bufio.NewScanner(r) - for scanner.Scan() { - line := scanner.Text() - after, found := strings.CutPrefix(line, "interface=") - if found { - return after, nil - } - } - return "", errors.New("not found") -} - -// AdditionalConfigFiles returns a list of Dnsmasq configuration files found in the "/tmp/etc" directory. -func AdditionalConfigFiles() []string { - if paths, err := filepath.Glob("/tmp/etc/dnsmasq-*.conf"); err == nil { - return paths - } - return nil -} - -// AdditionalLeaseFiles returns a list of lease file paths corresponding to the Dnsmasq configuration files. -func AdditionalLeaseFiles() []string { - cfgFiles := AdditionalConfigFiles() - if len(cfgFiles) == 0 { - return nil - } - leaseFiles := make([]string, 0, len(cfgFiles)) - for _, cfgFile := range cfgFiles { - if leaseFile := leaseFileFromConfigFileName(cfgFile); leaseFile != "" { - leaseFiles = append(leaseFiles, leaseFile) - - } else { - leaseFiles = append(leaseFiles, defaultLeaseFileFromConfigPath(cfgFile)) - } - } - return leaseFiles -} - -// leaseFileFromConfigFileName retrieves the DHCP lease file path by reading and parsing the provided configuration file. -func leaseFileFromConfigFileName(cfgFile string) string { - if f, err := os.Open(cfgFile); err == nil { - return leaseFileFromReader(f) - } - return "" -} - -// leaseFileFromReader parses the given io.Reader for the "dhcp-leasefile" configuration and returns its value as a string. -func leaseFileFromReader(r io.Reader) string { - scanner := bufio.NewScanner(r) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "#") { - continue - } - before, after, found := strings.Cut(line, "=") - if !found { - continue - } - if before == "dhcp-leasefile" { - return after - } - } - return "" -} - -// defaultLeaseFileFromConfigPath generates the default lease file path based on the provided configuration file path. -func defaultLeaseFileFromConfigPath(path string) string { - name := filepath.Base(path) - return filepath.Join("/var/lib/misc", strings.TrimSuffix(name, ".conf")+".leases") -} diff --git a/internal/router/dnsmasq/conf_test.go b/internal/router/dnsmasq/conf_test.go deleted file mode 100644 index 9ca672b..0000000 --- a/internal/router/dnsmasq/conf_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package dnsmasq - -import ( - "io" - "strings" - "testing" -) - -func Test_interfaceNameFromReader(t *testing.T) { - tests := []struct { - name string - in string - wantIface string - }{ - { - "good", - `interface=lo`, - "lo", - }, - { - "multiple", - `interface=lo -interface=eth0 -`, - "lo", - }, - { - "no iface", - `cache-size=100`, - "", - }, - } - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - ifaceName, err := interfaceNameFromReader(strings.NewReader(tc.in)) - if tc.wantIface != "" && err != nil { - t.Errorf("unexpected error: %v", err) - return - } - if tc.wantIface != ifaceName { - t.Errorf("mismatched, want: %q, got: %q", tc.wantIface, ifaceName) - } - }) - } -} - -func Test_leaseFileFromReader(t *testing.T) { - tests := []struct { - name string - in io.Reader - expected string - }{ - { - "default", - strings.NewReader(` -dhcp-script=/sbin/dhcpc_lease -dhcp-leasefile=/var/lib/misc/dnsmasq-1.leases -script-arp -`), - "/var/lib/misc/dnsmasq-1.leases", - }, - { - "non-default", - strings.NewReader(` -dhcp-script=/sbin/dhcpc_lease -dhcp-leasefile=/tmp/var/lib/misc/dnsmasq-1.leases -script-arp -`), - "/tmp/var/lib/misc/dnsmasq-1.leases", - }, - { - "missing", - strings.NewReader(` -dhcp-script=/sbin/dhcpc_lease -script-arp -`), - "", - }, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - if got := leaseFileFromReader(tc.in); got != tc.expected { - t.Errorf("leaseFileFromReader() = %v, want %v", got, tc.expected) - } - }) - } - -} diff --git a/internal/router/dnsmasq/dnsmasq.go b/internal/router/dnsmasq/dnsmasq.go deleted file mode 100644 index 058b0b5..0000000 --- a/internal/router/dnsmasq/dnsmasq.go +++ /dev/null @@ -1,190 +0,0 @@ -package dnsmasq - -import ( - "errors" - "html/template" - "net" - "os" - "path/filepath" - "strings" - - "github.com/Control-D-Inc/ctrld" -) - -const CtrldMarker = `# GENERATED BY ctrld - DO NOT MODIFY` - -const ConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY -no-resolv -{{- range .Upstreams}} -server={{ .IP }}#{{ .Port }} -{{- end}} -add-mac -add-subnet=32,128 -{{- if .CacheDisabled}} -cache-size=0 -{{- else}} -max-cache-ttl=0 -{{- end}} -` - -const ( - MerlinConfPath = "/tmp/etc/dnsmasq.conf" - MerlinJffsConfDir = "/jffs/configs" - MerlinJffsConfPath = "/jffs/configs/dnsmasq.conf" - MerlinPostConfPath = "/jffs/scripts/dnsmasq.postconf" -) - -const MerlinPostConfMarker = `# GENERATED BY ctrld - EOF` -const MerlinPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY - -#!/bin/sh - -config_file="$1" -. /usr/sbin/helper.sh - -pid=$(cat /tmp/ctrld.pid 2>/dev/null) -if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then - pc_delete "servers-file" "$config_file" # no WAN DNS settings - pc_append "no-resolv" "$config_file" # do not read /etc/resolv.conf - # use ctrld as upstream - pc_delete "server=" "$config_file" - {{- range .Upstreams}} - pc_append "server={{ .IP }}#{{ .Port }}" "$config_file" - {{- end}} - pc_delete "add-mac" "$config_file" - pc_delete "add-subnet" "$config_file" - pc_append "add-mac" "$config_file" # add client mac - pc_append "add-subnet=32,128" "$config_file" # add client ip - pc_delete "dnssec" "$config_file" # disable DNSSEC - pc_delete "trust-anchor=" "$config_file" # disable DNSSEC - pc_delete "cache-size=" "$config_file" - pc_append "cache-size=0" "$config_file" # disable cache - - # For John fork - pc_delete "resolv-file" "$config_file" # no WAN DNS settings - - # Change /etc/resolv.conf, which may be changed by WAN DNS setup - pc_delete "nameserver" /etc/resolv.conf - pc_append "nameserver 127.0.0.1" /etc/resolv.conf - - exit 0 -fi -` - -type Upstream struct { - IP string - Port int -} - -// ConfTmpl generates dnsmasq configuration from ctrld config. -func ConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) { - return ConfTmplWithCacheDisabled(tmplText, cfg, true) -} - -// ConfTmplWithCacheDisabled is like ConfTmpl, but the caller can control whether -// dnsmasq cache is disabled using cacheDisabled parameter. -// -// Generally, the caller should use ConfTmpl, but on some routers which dnsmasq config may be changed -// after ctrld started (like EdgeOS/Ubios, Firewalla ...), dnsmasq cache should not be disabled because -// the cache-size=0 generated by ctrld will conflict with router's generated config. -func ConfTmplWithCacheDisabled(tmplText string, cfg *ctrld.Config, cacheDisabled bool) (string, error) { - listener := cfg.FirstListener() - if listener == nil { - return "", errors.New("missing listener") - } - ip := listener.IP - if ip == "0.0.0.0" || ip == "::" || ip == "" { - ip = "127.0.0.1" - } - upstreams := []Upstream{{IP: ip, Port: listener.Port}} - return confTmpl(tmplText, upstreams, cacheDisabled) -} - -// FirewallaConfTmpl generates dnsmasq config for Firewalla routers. -func FirewallaConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) { - // If ctrld listen on all interfaces, generating config for all of them. - if lc := cfg.FirstListener(); lc != nil && (lc.IP == "0.0.0.0" || lc.IP == "") { - return confTmpl(tmplText, firewallaUpstreams(lc.Port), false) - } - // Otherwise, generating config for the specific listener from ctrld's config. - return ConfTmplWithCacheDisabled(tmplText, cfg, false) -} - -func confTmpl(tmplText string, upstreams []Upstream, cacheDisabled bool) (string, error) { - tmpl := template.Must(template.New("").Parse(tmplText)) - var to = &struct { - Upstreams []Upstream - CacheDisabled bool - }{ - Upstreams: upstreams, - CacheDisabled: cacheDisabled, - } - var sb strings.Builder - if err := tmpl.Execute(&sb, to); err != nil { - return "", err - } - return sb.String(), nil -} - -func firewallaUpstreams(port int) []Upstream { - ifaces := FirewallaSelfInterfaces() - upstreams := make([]Upstream, 0, len(ifaces)) - for _, netIface := range ifaces { - addrs, _ := netIface.Addrs() - for _, addr := range addrs { - if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { - upstreams = append(upstreams, Upstream{ - IP: netIP.IP.To4().String(), - Port: port, - }) - } - } - } - return upstreams -} - -// firewallaDnsmasqConfFiles returns dnsmasq config files of all firewalla interfaces. -func firewallaDnsmasqConfFiles() ([]string, error) { - return filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf") -} - -// FirewallaSelfInterfaces returns list of interfaces that will be configured with default dnsmasq setup on Firewalla. -func FirewallaSelfInterfaces() []*net.Interface { - matches, err := firewallaDnsmasqConfFiles() - if err != nil { - return nil - } - ifaces := make([]*net.Interface, 0, len(matches)) - for _, match := range matches { - // Trim prefix and suffix to get the iface name only. - ifaceName := strings.TrimSuffix(strings.TrimPrefix(match, "/home/pi/firerouter/etc/dnsmasq.dns."), ".conf") - if netIface, _ := net.InterfaceByName(ifaceName); netIface != nil { - ifaces = append(ifaces, netIface) - } - } - return ifaces -} - -const ( - ubios43ConfPath = "/run/dnsmasq.dhcp.conf.d" - ubios42ConfPath = "/run/dnsmasq.conf.d" - ubios43PidFile = "/run/dnsmasq-main.pid" - ubios42PidFile = "/run/dnsmasq.pid" - UbiosConfName = "zzzctrld.conf" -) - -// UbiosConfPath returns the appropriate configuration path based on the system's directory structure. -func UbiosConfPath() string { - if st, _ := os.Stat(ubios43ConfPath); st != nil && st.IsDir() { - return ubios43ConfPath - } - return ubios42ConfPath -} - -// UbiosPidFile returns the appropriate dnsmasq pid file based on the system's directory structure. -func UbiosPidFile() string { - if st, _ := os.Stat(ubios43PidFile); st != nil && !st.IsDir() { - return ubios43PidFile - } - return ubios42PidFile -} diff --git a/internal/router/edgeos/edgeos.go b/internal/router/edgeos/edgeos.go deleted file mode 100644 index 7364ac1..0000000 --- a/internal/router/edgeos/edgeos.go +++ /dev/null @@ -1,209 +0,0 @@ -package edgeos - -import ( - "bufio" - "bytes" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" -) - -const ( - Name = "edgeos" - edgeOSDNSMasqConfigPath = "/etc/dnsmasq.d/dnsmasq-zzz-ctrld.conf" - usgDNSMasqConfigPath = "/etc/dnsmasq.conf" - usgDNSMasqBackupConfigPath = "/etc/dnsmasq.conf.bak" - toggleContentFilteringLink = "https://community.ui.com/questions/UDM-Pro-disable-enable-DNS-filtering/e2cc4060-e56a-4139-b200-62d7f773ff8f" - toggleDnsShieldLink = "https://community.ui.com/questions/UniFi-OS-3-2-7-DNS-Shield-Missing/d3a85905-4ce0-4fe4-8bf0-6cb04f21371d" -) - -var ErrContentFilteringEnabled = fmt.Errorf(`the "Content Filtering" feature" is enabled, which is conflicted with ctrld.\n -To disable it, folowing instruction here: %s`, toggleContentFilteringLink) - -var ErrDnsShieldEnabled = fmt.Errorf(`the "DNS Shield" feature" is enabled, which is conflicted with ctrld.\n -To disable it, folowing screenshot here: %s`, toggleDnsShieldLink) - -type EdgeOS struct { - cfg *ctrld.Config - isUSG bool -} - -// New returns a router.Router for configuring/setup/run ctrld on EdgeOS routers. -func New(cfg *ctrld.Config) *EdgeOS { - e := &EdgeOS{cfg: cfg} - e.isUSG = checkUSG() - return e -} - -func (e *EdgeOS) ConfigureService(config *service.Config) error { - return nil -} - -func (e *EdgeOS) Install(_ *service.Config) error { - // If "Content Filtering" is enabled, UniFi OS will create firewall rules to intercept all DNS queries - // from outside, and route those queries to separated interfaces (e.g: dnsfilter-2@if79) created by UniFi OS. - // Thus, those queries will never reach ctrld listener. UniFi OS does not provide any mechanism to toggle this - // feature via command line, so there's nothing ctrld can do to disable this feature. For now, reporting an - // error and guiding users to disable the feature using UniFi OS web UI. - if ContentFilteringEnabled() { - return ErrContentFilteringEnabled - } - // If "DNS Shield" is enabled, UniFi OS will spawn dnscrypt-proxy process, and route all DNS queries to it. So - // reporting an error and guiding users to disable the feature using UniFi OS web UI. - if DnsShieldEnabled() { - return ErrDnsShieldEnabled - } - return nil -} - -func (e *EdgeOS) Uninstall(_ *service.Config) error { - return nil -} - -func (e *EdgeOS) PreRun() error { - return nil -} - -func (e *EdgeOS) Setup() error { - if e.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - if e.isUSG { - return e.setupUSG() - } - return e.setupUDM() -} - -func (e *EdgeOS) Cleanup() error { - if e.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - if e.isUSG { - return e.cleanupUSG() - } - return e.cleanupUDM() -} - -func (e *EdgeOS) setupUSG() error { - // On USG, dnsmasq is configured to forward queries to external provider by default. - // So instead of generating config in /etc/dnsmasq.d, we need to create a backup of - // the config, then modify it to forward queries to ctrld listener. - - // Creating a backup. - buf, err := os.ReadFile(usgDNSMasqConfigPath) - if err != nil { - return fmt.Errorf("setupUSG: reading current config: %w", err) - } - if err := os.WriteFile(usgDNSMasqBackupConfigPath, buf, 0600); err != nil { - return fmt.Errorf("setupUSG: backup current config: %w", err) - } - - // Removing all configured upstreams and cache config. - var sb strings.Builder - scanner := bufio.NewScanner(bytes.NewReader(buf)) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "server=") { - continue - } - if strings.HasPrefix(line, "all-servers") { - continue - } - sb.WriteString(line) - } - - data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, e.cfg, false) - if err != nil { - return err - } - sb.WriteString("\n") - sb.WriteString(data) - if err := os.WriteFile(usgDNSMasqConfigPath, []byte(sb.String()), 0644); err != nil { - return fmt.Errorf("setupUSG: writing dnsmasq config: %w", err) - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("setupUSG: restartDNSMasq: %w", err) - } - return nil -} - -func (e *EdgeOS) setupUDM() error { - data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, e.cfg, false) - if err != nil { - return err - } - if err := os.WriteFile(edgeOSDNSMasqConfigPath, []byte(data), 0600); err != nil { - return fmt.Errorf("setupUDM: generating dnsmasq config: %w", err) - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("setupUDM: restartDNSMasq: %w", err) - } - return nil -} - -func (e *EdgeOS) cleanupUSG() error { - if err := os.Rename(usgDNSMasqBackupConfigPath, usgDNSMasqConfigPath); err != nil { - return fmt.Errorf("cleanupUSG: os.Rename: %w", err) - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("cleanupUSG: restartDNSMasq: %w", err) - } - return nil -} - -func (e *EdgeOS) cleanupUDM() error { - // Remove the custom dnsmasq config - if err := os.Remove(edgeOSDNSMasqConfigPath); err != nil { - return fmt.Errorf("cleanupUDM: os.Remove: %w", err) - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("cleanupUDM: restartDNSMasq: %w", err) - } - return nil -} - -func ContentFilteringEnabled() bool { - st, err := os.Stat("/run/dnsfilter/dnsfilter") - return err == nil && !st.IsDir() -} - -// DnsShieldEnabled reports whether DNS Shield is enabled. -// See: https://community.ui.com/releases/UniFi-OS-Dream-Machines-3-2-7/251dfc1e-f4dd-4264-a080-3be9d8b9e02b -func DnsShieldEnabled() bool { - buf, err := os.ReadFile(filepath.Join(dnsmasq.UbiosConfPath(), "dns.conf")) - if err != nil { - return false - } - return bytes.Contains(buf, []byte("server=127.0.0.1#5053")) -} - -func LeaseFileDir() string { - if checkUSG() { - return "" - } - return "/run" -} - -func checkUSG() bool { - out, _ := os.ReadFile("/etc/version") - return bytes.HasPrefix(out, []byte("UniFiSecurityGateway.")) -} - -func restartDNSMasq() error { - if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil { - return fmt.Errorf("edgeosRestartDNSMasq: %s, %w", string(out), err) - } - return nil -} diff --git a/internal/router/firewalla/firewalla.go b/internal/router/firewalla/firewalla.go deleted file mode 100644 index cdf6586..0000000 --- a/internal/router/firewalla/firewalla.go +++ /dev/null @@ -1,110 +0,0 @@ -package firewalla - -import ( - "fmt" - "os" - "os/exec" - "strings" - - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" - - "github.com/Control-D-Inc/ctrld" - "github.com/kardianos/service" -) - -const ( - Name = "firewalla" - - firewallaDNSMasqConfigPath = "/home/pi/.firewalla/config/dnsmasq_local/ctrld" - firewallaConfigPostMainDir = "/home/pi/.firewalla/config/post_main.d" - firewallaCtrldInitScriptPath = "/home/pi/.firewalla/config/post_main.d/start_ctrld.sh" -) - -type Firewalla struct { - cfg *ctrld.Config -} - -// New returns a router.Router for configuring/setup/run ctrld on Firewalla routers. -func New(cfg *ctrld.Config) *Firewalla { - return &Firewalla{cfg: cfg} -} - -func (f *Firewalla) ConfigureService(_ *service.Config) error { - return nil -} - -func (f *Firewalla) Install(_ *service.Config) error { - // Writing startup script. - if err := writeFirewallStartupScript(); err != nil { - return fmt.Errorf("writing startup script: %w", err) - } - return nil -} - -func (f *Firewalla) Uninstall(_ *service.Config) error { - // Removing startup script. - if err := os.Remove(firewallaCtrldInitScriptPath); err != nil { - return fmt.Errorf("removing startup script: %w", err) - } - return nil -} - -func (f *Firewalla) PreRun() error { - return nil -} - -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) - } - if err := os.WriteFile(firewallaDNSMasqConfigPath, []byte(data), 0600); err != nil { - return fmt.Errorf("writing ctrld config: %w", err) - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("restartDNSMasq: %w", err) - } - - return nil -} - -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) - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("restartDNSMasq: %w", err) - } - - return nil -} - -func writeFirewallStartupScript() error { - if err := os.MkdirAll(firewallaConfigPostMainDir, 0775); err != nil { - return err - } - exe, err := os.Executable() - if err != nil { - return err - } - // This is called when "ctrld start ..." runs, so recording - // the same command line arguments to use in startup script. - argStr := strings.Join(os.Args[1:], " ") - script := fmt.Sprintf("#!/bin/bash\n\nsudo %q %s\n", exe, argStr) - return os.WriteFile(firewallaCtrldInitScriptPath, []byte(script), 0755) -} - -func restartDNSMasq() error { - return exec.Command("systemctl", "restart", "firerouter_dns").Run() -} diff --git a/internal/router/merlin/merlin.go b/internal/router/merlin/merlin.go deleted file mode 100644 index c1c6821..0000000 --- a/internal/router/merlin/merlin.go +++ /dev/null @@ -1,266 +0,0 @@ -package merlin - -import ( - "bytes" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - "unicode" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" - "github.com/Control-D-Inc/ctrld/internal/router/ntp" - "github.com/Control-D-Inc/ctrld/internal/router/nvram" -) - -const Name = "merlin" - -// nvramKvMap is a map of NVRAM key-value pairs used to configure and manage Merlin-specific settings. -var nvramKvMap = map[string]string{ - "dnspriv_enable": "0", // Ensure Merlin native DoT disabled. -} - -// dnsmasqConfig represents configuration paths for dnsmasq operations in Merlin firmware. -type dnsmasqConfig struct { - confPath string - jffsConfPath string -} - -// Merlin represents a configuration handler for setting up and managing ctrld on Merlin routers. -type Merlin struct { - cfg *ctrld.Config -} - -// New returns a router.Router for configuring/setup/run ctrld on Merlin routers. -func New(cfg *ctrld.Config) *Merlin { - return &Merlin{cfg: cfg} -} - -// ConfigureService configures the service based on the provided configuration. It returns an error if the configuration fails. -func (m *Merlin) ConfigureService(config *service.Config) error { - return nil -} - -// Install sets up the necessary configurations and services required for the Merlin instance to function properly. -func (m *Merlin) Install(_ *service.Config) error { - return nil -} - -// Uninstall removes the ctrld-related configurations and services from the Merlin router and reverts to the original state. -func (m *Merlin) Uninstall(_ *service.Config) error { - return nil -} - -// PreRun prepares the Merlin instance for operation by waiting for essential services and directories to become available. -func (m *Merlin) PreRun() error { - // Wait NTP ready. - _ = m.Cleanup() - if err := ntp.WaitNvram(); err != nil { - return err - } - // Wait until directories mounted. - for _, dir := range []string{"/tmp", "/proc"} { - waitDirExists(dir) - } - // Wait dnsmasq started. - for { - out, _ := exec.Command("pidof", "dnsmasq").CombinedOutput() - if len(bytes.TrimSpace(out)) > 0 { - break - } - time.Sleep(time.Second) - } - return nil -} - -// Setup initializes and configures the Merlin instance for use, including setting up dnsmasq and necessary nvram settings. -func (m *Merlin) Setup() error { - if m.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - // Already setup. - if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { - return nil - } - - if err := m.writeDnsmasqPostconf(); err != nil { - return err - } - - for _, cfg := range getDnsmasqConfigs() { - if err := m.setupDnsmasq(cfg); err != nil { - return fmt.Errorf("failed to setup dnsmasq: config: %s, error: %w", cfg.confPath, err) - } - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - - if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil { - return err - } - - return nil -} - -// Cleanup restores the original dnsmasq and nvram configurations and restarts dnsmasq if necessary. -func (m *Merlin) Cleanup() error { - if m.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" { - return nil // was restored, nothing to do. - } - - // Restore old configs. - if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { - return err - } - - buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath) - if err != nil && !os.IsNotExist(err) { - return err - } - // Restore dnsmasq post conf file. - if err := os.WriteFile(dnsmasq.MerlinPostConfPath, merlinParsePostConf(buf), 0750); err != nil { - return err - } - - for _, cfg := range getDnsmasqConfigs() { - if err := m.cleanupDnsmasqJffs(cfg); err != nil { - return fmt.Errorf("failed to cleanup jffs dnsmasq: config: %s, error: %w", cfg.confPath, err) - } - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -// setupDnsmasq sets up dnsmasq configuration by writing postconf, copying configuration, and running a postconf script. -func (m *Merlin) setupDnsmasq(cfg *dnsmasqConfig) error { - src, err := os.Open(cfg.confPath) - if os.IsNotExist(err) { - return nil // nothing to do if conf file does not exist. - } - if err != nil { - return fmt.Errorf("failed to open dnsmasq config: %w", err) - } - defer src.Close() - - // Copy current dnsmasq config to cfg.jffsConfPath, - // Then we will run postconf script on this file. - // - // Normally, adding postconf script is enough. However, we see - // reports on some Merlin devices that postconf scripts does not - // work, but manipulating the config directly via /jffs/configs does. - dst, err := os.Create(cfg.jffsConfPath) - if err != nil { - return fmt.Errorf("failed to create %s: %w", cfg.jffsConfPath, err) - } - defer dst.Close() - - if _, err := io.Copy(dst, src); err != nil { - return fmt.Errorf("failed to copy current dnsmasq config: %w", err) - } - if err := dst.Close(); err != nil { - return fmt.Errorf("failed to save %s: %w", cfg.jffsConfPath, err) - } - - // Run postconf script on cfg.jffsConfPath directly. - cmd := exec.Command("/bin/sh", dnsmasq.MerlinPostConfPath, cfg.jffsConfPath) - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to run post conf: %s: %w", string(out), err) - } - return nil -} - -// cleanupDnsmasqJffs removes the JFFS configuration file specified in the given dnsmasqConfig, if it exists. -func (m *Merlin) cleanupDnsmasqJffs(cfg *dnsmasqConfig) error { - // Remove cfg.jffsConfPath file. - if err := os.Remove(cfg.jffsConfPath); err != nil && !os.IsNotExist(err) { - return err - } - return nil -} - -// writeDnsmasqPostconf writes the requireddnsmasqConfigs post-configuration for dnsmasq to enable custom DNS settings with ctrld. -func (m *Merlin) writeDnsmasqPostconf() error { - buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath) - // Already setup. - if bytes.Contains(buf, []byte(dnsmasq.MerlinPostConfMarker)) { - return nil - } - if err != nil && !os.IsNotExist(err) { - return err - } - - data, err := dnsmasq.ConfTmpl(dnsmasq.MerlinPostConfTmpl, m.cfg) - if err != nil { - return err - } - data = strings.Join([]string{ - data, - "\n", - dnsmasq.MerlinPostConfMarker, - "\n", - string(buf), - }, "\n") - // Write dnsmasq post conf file. - return os.WriteFile(dnsmasq.MerlinPostConfPath, []byte(data), 0750) -} - -// restartDNSMasq restarts the dnsmasq service by executing the appropriate system command using "service". -// Returns an error if the command fails or if there is an issue processing the command output. -func restartDNSMasq() error { - if out, err := exec.Command("service", "restart_dnsmasq").CombinedOutput(); err != nil { - return fmt.Errorf("restart_dnsmasq: %s, %w", string(out), err) - } - return nil -} - -// getDnsmasqConfigs retrieves a list of dnsmasqConfig containing configuration and JFFS paths for dnsmasq operations. -func getDnsmasqConfigs() []*dnsmasqConfig { - cfgs := []*dnsmasqConfig{ - {dnsmasq.MerlinConfPath, dnsmasq.MerlinJffsConfPath}, - } - for _, path := range dnsmasq.AdditionalConfigFiles() { - jffsConfPath := filepath.Join(dnsmasq.MerlinJffsConfDir, filepath.Base(path)) - cfgs = append(cfgs, &dnsmasqConfig{path, jffsConfPath}) - } - - return cfgs -} - -// merlinParsePostConf parses the dnsmasq post configuration by removing content after the MerlinPostConfMarker, if present. -// If no marker is found, the original buffer is returned unmodified. -// Returns nil if the input buffer is empty. -func merlinParsePostConf(buf []byte) []byte { - if len(buf) == 0 { - return nil - } - parts := bytes.Split(buf, []byte(dnsmasq.MerlinPostConfMarker)) - if len(parts) != 1 { - return bytes.TrimLeftFunc(parts[1], unicode.IsSpace) - } - return buf -} - -// waitDirExists waits until the specified directory exists, polling its existence every second. -func waitDirExists(dir string) { - for { - if _, err := os.Stat(dir); !os.IsNotExist(err) { - return - } - time.Sleep(time.Second) - } -} diff --git a/internal/router/merlin/merlin_test.go b/internal/router/merlin/merlin_test.go deleted file mode 100644 index 057628c..0000000 --- a/internal/router/merlin/merlin_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package merlin - -import ( - "bytes" - "strings" - "testing" - - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" -) - -func Test_merlinParsePostConf(t *testing.T) { - origContent := "# foo" - data := strings.Join([]string{ - dnsmasq.MerlinPostConfTmpl, - "\n", - dnsmasq.MerlinPostConfMarker, - "\n", - }, "\n") - - tests := []struct { - name string - data string - expected string - }{ - {"empty", "", ""}, - {"no ctrld", origContent, origContent}, - {"ctrld with data", data + origContent, origContent}, - {"ctrld without data", data, ""}, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - //t.Parallel() - if got := merlinParsePostConf([]byte(tc.data)); !bytes.Equal(got, []byte(tc.expected)) { - t.Errorf("unexpected result, want: %q, got: %q", tc.expected, string(got)) - } - }) - } -} diff --git a/internal/router/netgear_orbi_voxel/procd.go b/internal/router/netgear_orbi_voxel/procd.go deleted file mode 100644 index 750a17d..0000000 --- a/internal/router/netgear_orbi_voxel/procd.go +++ /dev/null @@ -1,22 +0,0 @@ -package netgear - -const openWrtScript = `#!/bin/sh /etc/rc.common -USE_PROCD=1 -# After dnsmasq starts -START=61 -# Before network stops -STOP=89 -cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}" -name="{{.Name}}" -pid_file="/var/run/${name}.pid" - -start_service() { - echo "Starting ${name}" - procd_open_instance - procd_set_param command ${cmd} - procd_set_param respawn # respawn automatically if something died - procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop - procd_close_instance - echo "${name} has been started" -} -` diff --git a/internal/router/netgear_orbi_voxel/voxel.go b/internal/router/netgear_orbi_voxel/voxel.go deleted file mode 100644 index 4338f9c..0000000 --- a/internal/router/netgear_orbi_voxel/voxel.go +++ /dev/null @@ -1,220 +0,0 @@ -package netgear - -import ( - "bufio" - "bytes" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" - "github.com/Control-D-Inc/ctrld/internal/router/nvram" -) - -const ( - Name = "netgear_orbi_voxel" - netgearOrbiVoxelDNSMasqConfigPath = "/etc/dnsmasq.conf" - netgearOrbiVoxelHomedir = "/mnt/bitdefender" - netgearOrbiVoxelStartupScript = "/mnt/bitdefender/rc.user" - netgearOrbiVoxelStartupScriptBackup = "/mnt/bitdefender/rc.user.bak" - netgearOrbiVoxelStartupScriptMarker = "\n# GENERATED BY ctrld" -) - -var nvramKvMap = map[string]string{ - "dns_hijack": "0", // Disable dns hijacking -} - -type NetgearOrbiVoxel struct { - cfg *ctrld.Config -} - -// New returns a router.Router for configuring/setup/run ctrld on ddwrt routers. -func New(cfg *ctrld.Config) *NetgearOrbiVoxel { - return &NetgearOrbiVoxel{cfg: cfg} -} - -func (d *NetgearOrbiVoxel) ConfigureService(svc *service.Config) error { - if err := d.checkInstalledDir(); err != nil { - return err - } - svc.Option["SysvScript"] = openWrtScript - return nil -} - -func (d *NetgearOrbiVoxel) Install(_ *service.Config) error { - // Ignoring error here at this moment is ok, since everything will be wiped out on reboot. - _ = exec.Command("/etc/init.d/ctrld", "enable").Run() - if err := d.checkInstalledDir(); err != nil { - return err - } - if err := backupVoxelStartupScript(); err != nil { - return fmt.Errorf("backup startup script: %w", err) - } - if err := writeVoxelStartupScript(); err != nil { - return fmt.Errorf("writing startup script: %w", err) - } - return nil -} - -func (d *NetgearOrbiVoxel) Uninstall(_ *service.Config) error { - if err := os.Remove(netgearOrbiVoxelStartupScript); err != nil && !os.IsNotExist(err) { - return err - } - err := os.Rename(netgearOrbiVoxelStartupScriptBackup, netgearOrbiVoxelStartupScript) - if err != nil && !os.IsNotExist(err) { - return err - } - return nil -} - -func (d *NetgearOrbiVoxel) PreRun() error { - return nil -} - -func (d *NetgearOrbiVoxel) Setup() error { - if d.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - // Already setup. - if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { - return nil - } - - data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, d.cfg, false) - if err != nil { - return err - } - currentConfig, _ := os.ReadFile(netgearOrbiVoxelDNSMasqConfigPath) - configContent := append(currentConfig, data...) - if err := os.WriteFile(netgearOrbiVoxelDNSMasqConfigPath, configContent, 0600); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - - if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil { - return err - } - - return nil -} - -func (d *NetgearOrbiVoxel) Cleanup() error { - if d.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" { - return nil // was restored, nothing to do. - } - - // Restore old configs. - if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { - return err - } - - // Restore dnsmasq config. - if err := restoreDnsmasqConf(); err != nil { - return err - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -// checkInstalledDir checks that ctrld binary was installed in the correct directory. -func (d *NetgearOrbiVoxel) checkInstalledDir() error { - exePath, err := os.Executable() - if err != nil { - return fmt.Errorf("checkHomeDir: failed to get binary path %w", err) - } - if !strings.HasSuffix(filepath.Dir(exePath), netgearOrbiVoxelHomedir) { - return fmt.Errorf("checkHomeDir: could not install service outside %s", netgearOrbiVoxelHomedir) - } - return nil -} - -// backupVoxelStartupScript creates a backup of original startup script if existed. -func backupVoxelStartupScript() error { - // Do nothing if the startup script was modified by ctrld. - script, _ := os.ReadFile(netgearOrbiVoxelStartupScript) - if bytes.Contains(script, []byte(netgearOrbiVoxelStartupScriptMarker)) { - return nil - } - err := os.Rename(netgearOrbiVoxelStartupScript, netgearOrbiVoxelStartupScriptBackup) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("backupVoxelStartupScript: %w", err) - } - return nil -} - -// writeVoxelStartupScript writes startup script to re-install ctrld upon reboot. -// See: https://github.com/SVoxel/ORBI-RBK50/pull/7 -func writeVoxelStartupScript() error { - exe, err := os.Executable() - if err != nil { - return fmt.Errorf("configure service: failed to get binary path %w", err) - } - // This is called when "ctrld start ..." runs, so recording - // the same command line arguments to use in startup script. - argStr := strings.Join(os.Args[1:], " ") - script, _ := os.ReadFile(netgearOrbiVoxelStartupScriptBackup) - script = append(script, fmt.Sprintf("%s\n%q %s\n", netgearOrbiVoxelStartupScriptMarker, exe, argStr)...) - f, err := os.Create(netgearOrbiVoxelStartupScript) - if err != nil { - return fmt.Errorf("failed to create startup script: %w", err) - } - defer f.Close() - - if _, err := f.Write(script); err != nil { - return fmt.Errorf("failed to write startup script: %w", err) - } - if err := f.Close(); err != nil { - return fmt.Errorf("failed to save startup script: %w", err) - } - return nil -} - -// restoreDnsmasqConf restores original dnsmasq configuration. -func restoreDnsmasqConf() error { - f, err := os.Open(netgearOrbiVoxelDNSMasqConfigPath) - if err != nil { - return err - } - defer f.Close() - - var bs []byte - buf := bytes.NewBuffer(bs) - - removed := false - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := scanner.Text() - if line == dnsmasq.CtrldMarker { - removed = true - } - if !removed { - _, err := buf.WriteString(line + "\n") - if err != nil { - return err - } - } - } - return os.WriteFile(netgearOrbiVoxelDNSMasqConfigPath, buf.Bytes(), 0644) -} - -func restartDNSMasq() error { - if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil { - return fmt.Errorf("restartDNSMasq: %s, %w", string(out), err) - } - return nil -} diff --git a/internal/router/ntp/ntp.go b/internal/router/ntp/ntp.go deleted file mode 100644 index 5c04a36..0000000 --- a/internal/router/ntp/ntp.go +++ /dev/null @@ -1,49 +0,0 @@ -package ntp - -import ( - "bytes" - "context" - "errors" - "fmt" - "os/exec" - "time" - - "tailscale.com/logtail/backoff" - - "github.com/Control-D-Inc/ctrld/internal/router/nvram" -) - -// WaitNvram waits NTP synced by checking "ntp_ready" value using nvram. -func WaitNvram() error { - // Wait until `ntp_ready=1` set. - b := backoff.NewBackoff("ntp.Wait", func(format string, args ...any) {}, 10*time.Second) - for { - // ddwrt use "ntp_done": https://github.com/mirror/dd-wrt/blob/a08c693527ab3204bf7bebd408a7c9a83b6ede47/src/router/rc/ntp.c#L100 - for _, key := range []string{"ntp_ready", "ntp_done"} { - out, err := nvram.Run("get", key) - if err != nil { - return fmt.Errorf("PreStart: nvram: %w", err) - } - if out == "1" { - return nil - } - } - b.BackOff(context.Background(), errors.New("ntp not ready")) - } -} - -// WaitUpstart waits NTP synced by checking upstart task "ntpsync" is in "stop/waiting" state. -func WaitUpstart() error { - // Wait until `initctl status ntpsync` returns stop state. - b := backoff.NewBackoff("ntp.WaitUpstart", func(format string, args ...any) {}, 10*time.Second) - for { - out, err := exec.Command("initctl", "status", "ntpsync").CombinedOutput() - if err != nil { - return fmt.Errorf("exec.Command: %w", err) - } - if bytes.Contains(out, []byte("stop/waiting")) { - return nil - } - b.BackOff(context.Background(), errors.New("ntp not ready")) - } -} diff --git a/internal/router/nvram/nvram.go b/internal/router/nvram/nvram.go deleted file mode 100644 index e76c017..0000000 --- a/internal/router/nvram/nvram.go +++ /dev/null @@ -1,89 +0,0 @@ -package nvram - -import ( - "bytes" - "fmt" - "os/exec" - "strings" -) - -const ( - CtrldKeyPrefix = "ctrld_" - CtrldSetupKey = "ctrld_setup" - CtrldInstallKey = "ctrld_install" - RCStartupKey = "rc_startup" -) - -// Run runs the given nvram command. -func Run(args ...string) (string, error) { - cmd := exec.Command("nvram", args...) - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("%s:%w", stderr.String(), err) - } - return strings.TrimSpace(stdout.String()), nil -} - -/* -NOTE: - - For Openwrt, DNSSEC is not included in default dnsmasq (require dnsmasq-full). - - For Merlin, DNSSEC is configured during postconf script (see merlinDNSMasqPostConfTmpl). - - For Ubios UDM Pro/Dream Machine, DNSSEC is not included in their dnsmasq package: - +https://community.ui.com/questions/Implement-DNSSEC-into-UniFi/951c72b0-4d88-4c86-9174-45417bd2f9ca - +https://community.ui.com/questions/Enable-DNSSEC-for-Unifi-Dream-Machine-FW-updates/e68e367c-d09b-4459-9444-18908f7c1ea1 -*/ - -// SetKV writes the given key/value from map to nvram. -// The given setupKey is set to 1 to indicates key/value set. -func SetKV(m map[string]string, setupKey string) error { - // Backup current value, store ctrld's configs. - for key, value := range m { - old, err := Run("get", key) - if err != nil { - return fmt.Errorf("%s: %w", old, err) - } - if out, err := Run("set", CtrldKeyPrefix+key+"="+old); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - if out, err := Run("set", key+"="+value); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - } - - if out, err := Run("set", setupKey+"=1"); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - // Commit. - if out, err := Run("commit"); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - return nil -} - -// Restore restores the old value of given key from map m. -// The given setupKey is set to 0 to indicates key/value restored. -func Restore(m map[string]string, setupKey string) error { - // Restore old configs. - for key := range m { - ctrldKey := CtrldKeyPrefix + key - old, err := Run("get", ctrldKey) - if err != nil { - return fmt.Errorf("%s: %w", old, err) - } - _, _ = Run("unset", ctrldKey) - if out, err := Run("set", key+"="+old); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - } - - if out, err := Run("unset", setupKey); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - // Commit. - if out, err := Run("commit"); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - return nil -} diff --git a/internal/router/openwrt/openwrt.go b/internal/router/openwrt/openwrt.go deleted file mode 100644 index 73f5a06..0000000 --- a/internal/router/openwrt/openwrt.go +++ /dev/null @@ -1,191 +0,0 @@ -package openwrt - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" -) - -const ( - Name = "openwrt" - openwrtDNSMasqConfigName = "ctrld.conf" - openwrtDNSMasqDefaultConfigDir = "/tmp/dnsmasq.d" -) - -var openwrtDnsmasqDefaultConfigPath = filepath.Join(openwrtDNSMasqDefaultConfigDir, openwrtDNSMasqConfigName) - -type Openwrt struct { - cfg *ctrld.Config - dnsmasqCacheSize string -} - -// New returns a router.Router for configuring/setup/run ctrld on Openwrt routers. -func New(cfg *ctrld.Config) *Openwrt { - return &Openwrt{cfg: cfg} -} - -func (o *Openwrt) ConfigureService(svc *service.Config) error { - svc.Option["SysvScript"] = openWrtScript - return nil -} - -func (o *Openwrt) Install(config *service.Config) error { - return exec.Command("/etc/init.d/ctrld", "enable").Run() -} - -func (o *Openwrt) Uninstall(config *service.Config) error { - return nil -} - -func (o *Openwrt) PreRun() error { - return nil -} - -func (o *Openwrt) Setup() error { - if o.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - - // Save current dnsmasq config cache size if present. - if cs, err := uci("get", "dhcp.@dnsmasq[0].cachesize"); err == nil { - o.dnsmasqCacheSize = cs - if _, err := uci("delete", "dhcp.@dnsmasq[0].cachesize"); err != nil { - return err - } - // Commit. - if _, err := uci("commit", "dhcp"); err != nil { - return err - } - } - - data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, o.cfg) - if err != nil { - return err - } - if err := os.WriteFile(dnsmasqConfPathFromUbus(), []byte(data), 0600); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func (o *Openwrt) Cleanup() error { - if o.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - // Remove the custom dnsmasq config - if err := os.Remove(dnsmasqConfPathFromUbus()); err != nil { - return err - } - - // Restore original value if present. - if o.dnsmasqCacheSize != "" { - if _, err := uci("set", fmt.Sprintf("dhcp.@dnsmasq[0].cachesize=%s", o.dnsmasqCacheSize)); err != nil { - return err - } - // Commit. - if _, err := uci("commit", "dhcp"); err != nil { - return err - } - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func restartDNSMasq() error { - if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil { - return fmt.Errorf("%s: %w", string(out), err) - } - return nil -} - -var errUCIEntryNotFound = errors.New("uci: Entry not found") - -func uci(args ...string) (string, error) { - cmd := exec.Command("uci", args...) - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - if strings.HasPrefix(stderr.String(), errUCIEntryNotFound.Error()) { - return "", errUCIEntryNotFound - } - return "", fmt.Errorf("%s:%w", stderr.String(), err) - } - return strings.TrimSpace(stdout.String()), nil -} - -// openwrtServiceList represents openwrt services config. -type openwrtServiceList struct { - Dnsmasq dnsmasqConf `json:"dnsmasq"` -} - -// dnsmasqConf represents dnsmasq config. -type dnsmasqConf struct { - Instances map[string]confInstances `json:"instances"` -} - -// confInstances represents an instance config of a service. -type confInstances struct { - Mount map[string]string `json:"mount"` -} - -// dnsmasqConfPath returns the dnsmasq config path. -// -// Since version 24.10, openwrt makes some changes to dnsmasq to support -// multiple instances of dnsmasq. This change causes breaking changes to -// software which depends on the default dnsmasq path. -// -// There are some discussion/PRs in openwrt repo to address this: -// -// - https://github.com/openwrt/openwrt/pull/16806 -// - https://github.com/openwrt/openwrt/pull/16890 -// -// In the meantime, workaround this problem by querying the actual config path -// by querying ubus service list. -func dnsmasqConfPath(r io.Reader) string { - var svc openwrtServiceList - if err := json.NewDecoder(r).Decode(&svc); err != nil { - return openwrtDnsmasqDefaultConfigPath - } - for _, inst := range svc.Dnsmasq.Instances { - for mount := range inst.Mount { - dirName := filepath.Base(mount) - parts := strings.Split(dirName, ".") - if len(parts) < 2 { - continue - } - if parts[0] == "dnsmasq" && parts[len(parts)-1] == "d" { - return filepath.Join(mount, openwrtDNSMasqConfigName) - } - } - } - return openwrtDnsmasqDefaultConfigPath -} - -// dnsmasqConfPathFromUbus get dnsmasq config path from ubus service list. -func dnsmasqConfPathFromUbus() string { - output, err := exec.Command("ubus", "call", "service", "list").Output() - if err != nil { - return openwrtDnsmasqDefaultConfigPath - } - return dnsmasqConfPath(bytes.NewReader(output)) -} diff --git a/internal/router/openwrt/openwrt_test.go b/internal/router/openwrt/openwrt_test.go deleted file mode 100644 index 8b260e8..0000000 --- a/internal/router/openwrt/openwrt_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package openwrt - -import ( - "io" - "path/filepath" - "strings" - "testing" -) - -// Sample output from https://github.com/openwrt/openwrt/pull/16806#issuecomment-2448255734 -const ubusDnsmasqBefore2410 = `{ - "dnsmasq": { - "instances": { - "guest_dns": { - "mount": { - "/tmp/dnsmasq.d": "0", - "/var/run/dnsmasq/": "1" - } - } - } - } -}` - -const ubusDnsmasq2410 = `{ - "dnsmasq": { - "instances": { - "guest_dns": { - "mount": { - "/tmp/dnsmasq.guest_dns.d": "0", - "/var/run/dnsmasq/": "1" - } - } - } - } -}` - -func Test_dnsmasqConfPath(t *testing.T) { - var dnsmasq2410expected = filepath.Join("/tmp/dnsmasq.guest_dns.d", openwrtDNSMasqConfigName) - tests := []struct { - name string - in io.Reader - expected string - }{ - {"empty", strings.NewReader(""), openwrtDnsmasqDefaultConfigPath}, - {"invalid", strings.NewReader("}}"), openwrtDnsmasqDefaultConfigPath}, - {"before 24.10", strings.NewReader(ubusDnsmasqBefore2410), openwrtDnsmasqDefaultConfigPath}, - {"24.10", strings.NewReader(ubusDnsmasq2410), dnsmasq2410expected}, - } - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - if got := dnsmasqConfPath(tc.in); got != tc.expected { - t.Errorf("dnsmasqConfPath() = %v, want %v", got, tc.expected) - } - }) - } -} diff --git a/internal/router/openwrt/procd.go b/internal/router/openwrt/procd.go deleted file mode 100644 index bf7253e..0000000 --- a/internal/router/openwrt/procd.go +++ /dev/null @@ -1,25 +0,0 @@ -package openwrt - -const openWrtScript = `#!/bin/sh /etc/rc.common -USE_PROCD=1 -# After network starts -START=21 -# Before network stops -STOP=89 -cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}" -name="{{.Name}}" -pid_file="/var/run/${name}.pid" - -start_service() { - echo "Starting ${name}" - procd_open_instance - procd_set_param command ${cmd} - procd_set_param respawn # respawn automatically if something died - procd_set_param stdout 1 # forward stdout of the command to logd - procd_set_param stderr 1 # same for stderr - procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop - procd_set_param term_timeout 10 - procd_close_instance - echo "${name} has been started" -} -` diff --git a/internal/router/os_config_freebsd.go b/internal/router/os_config_freebsd.go deleted file mode 100644 index 9066191..0000000 --- a/internal/router/os_config_freebsd.go +++ /dev/null @@ -1,40 +0,0 @@ -package router - -import ( - "encoding/xml" - "os" -) - -// Config represents /conf/config.xml file found on pfsense/opnsense. -type Config struct { - PfsenseUnbound *string `xml:"unbound>enable,omitempty"` - OPNsenseUnbound *string `xml:"OPNsense>unboundplus>general>enabled,omitempty"` - Dnsmasq *string `xml:"dnsmasq>enable,omitempty"` -} - -// DnsmasqEnabled reports whether dnsmasq is enabled. -func (c *Config) DnsmasqEnabled() bool { - if isPfsense() { // pfsense only set the attribute if dnsmasq is enabled. - return c.Dnsmasq != nil - } - return c.Dnsmasq != nil && *c.Dnsmasq == "1" -} - -// UnboundEnabled reports whether unbound is enabled. -func (c *Config) UnboundEnabled() bool { - if isPfsense() { // pfsense only set the attribute if unbound is enabled. - return c.PfsenseUnbound != nil - } - return c.OPNsenseUnbound != nil && *c.OPNsenseUnbound == "1" -} - -// currentConfig does unmarshalling /conf/config.xml file, -// return the corresponding *Config represent it. -func currentConfig() (*Config, error) { - buf, _ := os.ReadFile("/conf/config.xml") - c := Config{} - if err := xml.Unmarshal(buf, &c); err != nil { - return nil, err - } - return &c, nil -} diff --git a/internal/router/os_freebsd.go b/internal/router/os_freebsd.go deleted file mode 100644 index 9a79188..0000000 --- a/internal/router/os_freebsd.go +++ /dev/null @@ -1,157 +0,0 @@ -package router - -import ( - "bytes" - "fmt" - "net" - "os" - "os/exec" - "path/filepath" - "text/template" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld" -) - -const ( - osName = "freebsd" - rcPath = "/usr/local/etc/rc.d" - rcConfPath = "/etc/rc.conf.d/" - unboundRcPath = rcPath + "/unbound" - dnsmasqRcPath = rcPath + "/dnsmasq" -) - -func newOsRouter(cfg *ctrld.Config, cdMode bool) Router { - return &osRouter{cfg: cfg, cdMode: cdMode} -} - -type osRouter struct { - cfg *ctrld.Config - svcName string - // cdMode indicates whether the router will configure ctrld in cd mode (aka --cd=). - // When ctrld is running on freebsd-like routers, and there's process running on port 53 - // in cd mode, ctrld will attempt to kill the process and become direct listener. - // See details implemenation in osRouter.PreRun method. - cdMode bool -} - -func (or *osRouter) ConfigureService(svc *service.Config) error { - svc.Option["SysvScript"] = bsdInitScript - or.svcName = svc.Name - rcFile := filepath.Join(rcConfPath, or.svcName) - var to = &struct { - Name string - }{ - or.svcName, - } - - f, err := os.Create(rcFile) - if err != nil { - return fmt.Errorf("os.Create: %w", err) - } - defer f.Close() - if err := template.Must(template.New("").Parse(rcConfTmpl)).Execute(f, to); err != nil { - return err - } - return f.Close() -} - -func (or *osRouter) Install(_ *service.Config) error { - if isPfsense() { - // pfsense need ".sh" extension for script to be run at boot. - // See: https://docs.netgate.com/pfsense/en/latest/development/boot-commands.html#shell-script-option - oldname := filepath.Join(rcPath, or.svcName) - newname := filepath.Join(rcPath, or.svcName+".sh") - _ = os.Remove(newname) - if err := os.Symlink(oldname, newname); err != nil { - return fmt.Errorf("os.Symlink: %w", err) - } - } - return nil -} - -func (or *osRouter) Uninstall(_ *service.Config) error { - rcFiles := []string{filepath.Join(rcConfPath, or.svcName)} - if isPfsense() { - rcFiles = append(rcFiles, filepath.Join(rcPath, or.svcName+".sh")) - } - for _, filename := range rcFiles { - if err := os.Remove(filename); err != nil { - return fmt.Errorf("os.Remove: %w", err) - } - } - - return nil -} - -func (or *osRouter) PreRun() error { - if or.cdMode { - addr := "0.0.0.0:53" - udpLn, udpErr := net.ListenPacket("udp", addr) - if udpLn != nil { - udpLn.Close() - } - tcpLn, tcpErr := net.Listen("tcp", addr) - if tcpLn != nil { - tcpLn.Close() - } - // If we could not listen on :53 for any reason, try killing unbound/dnsmasq, become direct listener - if udpErr != nil || tcpErr != nil { - _ = exec.Command("killall", "unbound").Run() - _ = exec.Command("killall", "dnsmasq").Run() - } - } - return nil -} - -func (or *osRouter) Setup() error { - return nil -} - -func (or *osRouter) Cleanup() error { - if or.cdMode { - c, err := currentConfig() - if err != nil { - return err - } - if c.UnboundEnabled() { - _ = exec.Command(unboundRcPath, "onerestart").Run() - } - if c.DnsmasqEnabled() { - _ = exec.Command(dnsmasqRcPath, "onerestart").Run() - } - } - return nil -} - -func isPfsense() bool { - b, err := os.ReadFile("/etc/platform") - return err == nil && bytes.HasPrefix(b, []byte("pfSense")) -} - -const bsdInitScript = `#!/bin/sh - -# PROVIDE: {{.Name}} -# REQUIRE: SERVERS -# REQUIRE: unbound dnsmasq securelevel -# KEYWORD: shutdown - -. /etc/rc.subr - -name="{{.Name}}" -rcvar="${name}_enable" -{{.Name}}_env="IS_DAEMON=1" -pidfile="/var/run/${name}.pid" -child_pidfile="/var/run/${name}_child.pid" -command="/usr/sbin/daemon" -daemon_args="-r -P ${pidfile} -p ${child_pidfile} -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}" -command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}" - -load_rc_config "${name}" -run_rc_command "$1" -` - -var rcConfTmpl = `# {{.Name}} -{{.Name}}_enable="YES" -` diff --git a/internal/router/os_others.go b/internal/router/os_others.go deleted file mode 100644 index 52b41e4..0000000 --- a/internal/router/os_others.go +++ /dev/null @@ -1,41 +0,0 @@ -//go:build !freebsd - -package router - -import ( - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld" -) - -const osName = "" - -func newOsRouter(cfg *ctrld.Config, cdMode bool) Router { - return &osRouter{} -} - -type osRouter struct{} - -func (d *osRouter) ConfigureService(_ *service.Config) error { - return nil -} - -func (d *osRouter) Install(_ *service.Config) error { - return nil -} - -func (d *osRouter) Uninstall(_ *service.Config) error { - return nil -} - -func (d *osRouter) PreRun() error { - return nil -} - -func (d *osRouter) Setup() error { - return nil -} - -func (d *osRouter) Cleanup() error { - return nil -} diff --git a/internal/router/router.go b/internal/router/router.go deleted file mode 100644 index 2d8c462..0000000 --- a/internal/router/router.go +++ /dev/null @@ -1,288 +0,0 @@ -package router - -import ( - "bytes" - "crypto/x509" - "net" - "os" - "os/exec" - "path/filepath" - "strings" - "sync/atomic" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/certs" - "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" - "github.com/Control-D-Inc/ctrld/internal/router/edgeos" - "github.com/Control-D-Inc/ctrld/internal/router/firewalla" - "github.com/Control-D-Inc/ctrld/internal/router/merlin" - netgear "github.com/Control-D-Inc/ctrld/internal/router/netgear_orbi_voxel" - "github.com/Control-D-Inc/ctrld/internal/router/openwrt" - "github.com/Control-D-Inc/ctrld/internal/router/synology" - "github.com/Control-D-Inc/ctrld/internal/router/tomato" - "github.com/Control-D-Inc/ctrld/internal/router/ubios" -) - -// Service is the interface to manage ctrld service on router. -type Service interface { - // ConfigureService performs works for installing ctrla as a service on router. - ConfigureService(*service.Config) error - // Install performs necessary works after service.Install done. - Install(*service.Config) error - // Uninstall performs necessary works after service.Uninstallation done. - Uninstall(*service.Config) error -} - -// Router is the interface for managing ctrld running on router. -type Router interface { - Service - - // PreRun performs works need to be done before ctrld being run on router. - // Implementation should only return if the pre-condition was met (e.g: ntp synced). - PreRun() error - // Setup configures ctrld to be run on the router. - Setup() error - // Cleanup cleans up works setup on router by ctrld. - Cleanup() error -} - -// New returns new Router interface. -func New(cfg *ctrld.Config, cdMode bool) Router { - switch Name() { - case ddwrt.Name: - return ddwrt.New(cfg) - case merlin.Name: - return merlin.New(cfg) - case openwrt.Name: - return openwrt.New(cfg) - case edgeos.Name: - return edgeos.New(cfg) - case ubios.Name: - return ubios.New(cfg) - case synology.Name: - return synology.New(cfg) - case tomato.Name: - return tomato.New(cfg) - case firewalla.Name: - return firewalla.New(cfg) - case netgear.Name: - return netgear.New(cfg) - } - return newOsRouter(cfg, cdMode) -} - -// IsNetGearOrbi reports whether the router is a Netgear Orbi router. -func IsNetGearOrbi() bool { - return Name() == netgear.Name -} - -// IsGLiNet reports whether the router is an GL.iNet router. -func IsGLiNet() bool { - if Name() != openwrt.Name { - return false - } - buf, _ := os.ReadFile("/proc/version") - // The output of /proc/version contains "(glinet@glinet)". - return bytes.Contains(buf, []byte(" (glinet")) -} - -// IsOldOpenwrt reports whether the router is an "old" version of Openwrt, -// aka versions which don't have "service" command. -func IsOldOpenwrt() bool { - if Name() != openwrt.Name { - return false - } - cmd, _ := exec.LookPath("service") - return cmd == "" -} - -// WaitProcessExited reports whether the "ctrld stop" command have to wait until ctrld process exited. -func WaitProcessExited() bool { - return Name() == openwrt.Name -} - -var routerPlatform atomic.Pointer[router] - -type router struct { - name string -} - -// Name returns name of the router platform. -func Name() string { - if r := routerPlatform.Load(); r != nil { - return r.name - } - r := &router{} - r.name = distroName() - routerPlatform.Store(r) - return r.name -} - -// DefaultInterfaceName returns the default interface name of the current router. -func DefaultInterfaceName() string { - switch Name() { - case ubios.Name: - return "lo" - } - return "" -} - -// LocalResolverIP returns the IP that could be used as nameserver in /etc/resolv.conf file. -func LocalResolverIP() string { - var iface string - switch Name() { - case edgeos.Name: - // On EdgeOS, dnsmasq is run with "--local-service", so we need to get - // the proper interface from dnsmasq config. - if name, _ := dnsmasq.InterfaceNameFromConfig("/etc/dnsmasq.conf"); name != "" { - iface = name - } - case firewalla.Name: - // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. - // Thus, we use "br0" as the nameserver in /etc/resolv.conf file. - iface = "br0" - } - if netIface, _ := net.InterfaceByName(iface); netIface != nil { - addrs, _ := netIface.Addrs() - for _, addr := range addrs { - if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { - return netIP.IP.To4().String() - } - } - } - return "" -} - -// HomeDir returns the home directory of ctrld on current router. -func HomeDir() (string, error) { - switch Name() { - case ddwrt.Name, firewalla.Name, merlin.Name, netgear.Name, tomato.Name: - exe, err := os.Executable() - if err != nil { - return "", err - } - return filepath.Dir(exe), nil - case edgeos.Name: - exe, err := os.Executable() - if err != nil { - return "", err - } - // Using binary directory as home dir if it is located in /config. - // Otherwise, fallback to old behavior for compatibility. - if strings.HasPrefix(exe, "/config/") { - return filepath.Dir(exe), nil - } - } - return "", nil -} - -// CertPool returns the system certificate pool of the current router. -func CertPool() *x509.CertPool { - if Name() == ddwrt.Name { - return certs.CACertPool() - } - return nil -} - -// CanListenLocalhost reports whether the ctrld can listen on localhost with current host. -func CanListenLocalhost() bool { - switch { - case Name() == firewalla.Name: - return false - default: - return true - } -} - -// SelfInterfaces return list of *net.Interface that will be source of requests from router itself. -func SelfInterfaces() []*net.Interface { - switch Name() { - case firewalla.Name: - return dnsmasq.FirewallaSelfInterfaces() - default: - return nil - } -} - -// LeaseFilesDir is the directory which contains lease files. -func LeaseFilesDir() string { - if Name() == edgeos.Name { - edgeos.LeaseFileDir() - } - return "" -} - -// ServiceDependencies returns list of dependencies that ctrld services needs on this router. -// See https://pkg.go.dev/github.com/kardianos/service#Config for list format. -func ServiceDependencies() []string { - if Name() == ubios.Name { - // On Ubios, ctrld needs to start after unifi-mongodb, - // so it can query custom client info mapping. - return []string{ - "Wants=unifi-mongodb.service", - "After=unifi-mongodb.service", - } - } - return nil -} - -func distroName() string { - switch { - case bytes.HasPrefix(unameO(), []byte("DD-WRT")): - return ddwrt.Name - case bytes.HasPrefix(unameO(), []byte("ASUSWRT-Merlin")): - return merlin.Name - case haveFile("/etc/openwrt_version"): - if haveFile("/bin/config") { // TODO: is there any more reliable way? - return netgear.Name - } - return openwrt.Name - case isUbios(): - return ubios.Name - case bytes.HasPrefix(unameU(), []byte("synology")): - return synology.Name - case bytes.HasPrefix(unameO(), []byte("Tomato")): - return tomato.Name - case haveDir("/config/scripts/post-config.d"): - return edgeos.Name - case haveFile("/etc/ubnt/init/vyatta-router"): - return edgeos.Name // For 2.x - case haveFile("/etc/firewalla_release"): - return firewalla.Name - } - return osName -} - -func haveFile(file string) bool { - _, err := os.Stat(file) - return err == nil -} - -func haveDir(dir string) bool { - fi, _ := os.Stat(dir) - return fi != nil && fi.IsDir() -} - -func unameO() []byte { - out, _ := exec.Command("uname", "-o").Output() - return out -} - -func unameU() []byte { - out, _ := exec.Command("uname", "-u").Output() - return out -} - -// isUbios reports whether the current machine is running on Ubios. -func isUbios() bool { - if haveDir("/data/unifi") { - return true - } - if err := exec.Command("ubnt-device-info", "firmware").Run(); err == nil { - return true - } - return false -} diff --git a/internal/router/service.go b/internal/router/service.go deleted file mode 100644 index 3333964..0000000 --- a/internal/router/service.go +++ /dev/null @@ -1,96 +0,0 @@ -package router - -import ( - "bytes" - "os" - "os/exec" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" - "github.com/Control-D-Inc/ctrld/internal/router/merlin" - "github.com/Control-D-Inc/ctrld/internal/router/tomato" - "github.com/Control-D-Inc/ctrld/internal/router/ubios" -) - -func init() { - systems := []service.System{ - &linuxSystemService{ - name: "ddwrt", - detect: func() bool { return Name() == ddwrt.Name }, - interactive: func() bool { - is, _ := isInteractive() - return is - }, - new: newddwrtService, - }, - &linuxSystemService{ - name: "merlin", - detect: func() bool { return Name() == merlin.Name }, - interactive: func() bool { - is, _ := isInteractive() - return is - }, - new: newMerlinService, - }, - &linuxSystemService{ - name: "ubios", - detect: func() bool { - if Name() != ubios.Name { - return false - } - out, err := exec.Command("ubnt-device-info", "firmware").CombinedOutput() - if err == nil { - // For v2/v3, UbiOS use a Debian base with systemd, so it is not - // necessary to use custom implementation for supporting init system. - return bytes.HasPrefix(out, []byte("1.")) - } - return true - }, - interactive: func() bool { - is, _ := isInteractive() - return is - }, - new: newUbiosService, - }, - &linuxSystemService{ - name: "tomato", - detect: func() bool { return Name() == tomato.Name }, - interactive: func() bool { - is, _ := isInteractive() - return is - }, - new: newTomatoService, - }, - } - systems = append(systems, service.AvailableSystems()...) - service.ChooseSystem(systems...) -} - -type linuxSystemService struct { - name string - detect func() bool - interactive func() bool - new func(i service.Interface, platform string, c *service.Config) (service.Service, error) -} - -func (sc linuxSystemService) String() string { - return sc.name -} -func (sc linuxSystemService) Detect() bool { - return sc.detect() -} -func (sc linuxSystemService) Interactive() bool { - return sc.interactive() -} -func (sc linuxSystemService) New(i service.Interface, c *service.Config) (service.Service, error) { - return sc.new(i, sc.String(), c) -} - -func isInteractive() (bool, error) { - ppid := os.Getppid() - if ppid == 1 { - return false, nil - } - return true, nil -} diff --git a/internal/router/service_ddwrt.go b/internal/router/service_ddwrt.go deleted file mode 100644 index 3217f8a..0000000 --- a/internal/router/service_ddwrt.go +++ /dev/null @@ -1,294 +0,0 @@ -package router - -import ( - "bytes" - "errors" - "fmt" - "os" - "os/exec" - "os/signal" - "strings" - "syscall" - "text/template" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld/internal/router/nvram" -) - -type ddwrtSvc struct { - i service.Interface - platform string - *service.Config - rcStartup string -} - -func newddwrtService(i service.Interface, platform string, c *service.Config) (service.Service, error) { - s := &ddwrtSvc{ - i: i, - platform: platform, - Config: c, - } - if err := os.MkdirAll("/jffs/etc/config", 0644); err != nil { - return nil, err - } - return s, nil -} - -func (s *ddwrtSvc) String() string { - if len(s.DisplayName) > 0 { - return s.DisplayName - } - return s.Name -} - -func (s *ddwrtSvc) Platform() string { - return s.platform -} - -func (s *ddwrtSvc) configPath() string { - return fmt.Sprintf("/jffs/etc/config/%s.startup", s.Config.Name) -} - -func (s *ddwrtSvc) template() *template.Template { - return template.Must(template.New("").Parse(ddwrtSvcScript)) -} - -func (s *ddwrtSvc) Install() error { - confPath := s.configPath() - if _, err := os.Stat(confPath); err == nil { - return fmt.Errorf("already installed: %s", confPath) - } - - path, err := os.Executable() - if err != nil { - return err - } - - if !strings.HasPrefix(path, "/jffs/") { - return errors.New("could not install service outside /jffs") - } - - var to = &struct { - *service.Config - Path string - }{ - s.Config, - path, - } - - f, err := os.Create(confPath) - if err != nil { - return err - } - defer f.Close() - - if err := s.template().Execute(f, to); err != nil { - return err - } - - if err = os.Chmod(confPath, 0755); err != nil { - return err - } - - var sb strings.Builder - if err := template.Must(template.New("").Parse(ddwrtStartupCmd)).Execute(&sb, to); err != nil { - return err - } - s.rcStartup = sb.String() - curVal, err := nvram.Run("get", nvram.RCStartupKey) - if err != nil { - return err - } - if _, err := nvram.Run("set", nvram.CtrldKeyPrefix+nvram.RCStartupKey+"="+curVal); err != nil { - return err - } - val := strings.Join([]string{curVal, s.rcStartup + " &", fmt.Sprintf(`echo $! > "/tmp/%s.pid"`, s.Config.Name)}, "\n") - - if _, err := nvram.Run("set", nvram.RCStartupKey+"="+val); err != nil { - return err - } - if out, err := nvram.Run("commit"); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - - return nil -} - -func (s *ddwrtSvc) Uninstall() error { - if err := os.Remove(s.configPath()); err != nil { - return err - } - - ctrldStartupKey := nvram.CtrldKeyPrefix + nvram.RCStartupKey - rcStartup, err := nvram.Run("get", ctrldStartupKey) - if err != nil { - return err - } - _, _ = nvram.Run("unset", ctrldStartupKey) - if _, err := nvram.Run("set", nvram.RCStartupKey+"="+rcStartup); err != nil { - return err - } - if out, err := nvram.Run("commit"); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - - return nil -} - -func (s *ddwrtSvc) Logger(errs chan<- error) (service.Logger, error) { - if service.Interactive() { - return service.ConsoleLogger, nil - } - return s.SystemLogger(errs) -} - -func (s *ddwrtSvc) SystemLogger(errs chan<- error) (service.Logger, error) { - // TODO(cuonglm): detect syslog enable and return proper logger? - // this at least works with default configuration. - if service.Interactive() { - return service.ConsoleLogger, nil - - } - return &noopLogger{}, nil -} - -func (s *ddwrtSvc) Run() (err error) { - err = s.i.Start(s) - if err != nil { - return err - } - - if interactice, _ := isInteractive(); !interactice { - signal.Ignore(syscall.SIGHUP) - } - var sigChan = make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt) - <-sigChan - - return s.i.Stop(s) -} - -func (s *ddwrtSvc) Status() (service.Status, error) { - if _, err := os.Stat(s.configPath()); os.IsNotExist(err) { - return service.StatusUnknown, service.ErrNotInstalled - } - out, err := exec.Command(s.configPath(), "status").CombinedOutput() - if err != nil { - return service.StatusUnknown, err - } - switch string(bytes.TrimSpace(out)) { - case "running": - return service.StatusRunning, nil - default: - return service.StatusStopped, nil - } -} - -func (s *ddwrtSvc) Start() error { - return exec.Command(s.configPath(), "start").Run() -} - -func (s *ddwrtSvc) Stop() error { - return exec.Command(s.configPath(), "stop").Run() -} - -func (s *ddwrtSvc) Restart() error { - err := s.Stop() - if err != nil { - return err - } - return s.Start() -} - -type noopLogger struct { -} - -func (c noopLogger) Error(v ...interface{}) error { - return nil -} -func (c noopLogger) Warning(v ...interface{}) error { - return nil -} -func (c noopLogger) Info(v ...interface{}) error { - return nil -} -func (c noopLogger) Errorf(format string, a ...interface{}) error { - return nil -} -func (c noopLogger) Warningf(format string, a ...interface{}) error { - return nil -} -func (c noopLogger) Infof(format string, a ...interface{}) error { - return nil -} - -const ddwrtStartupCmd = `{{.Path}}{{range .Arguments}} {{.}}{{end}}` -const ddwrtSvcScript = `#!/bin/sh - -name="{{.Name}}" -cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}" -pid_file="/tmp/$name.pid" - -get_pid() { - cat "$pid_file" -} - -is_running() { - [ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) " -} - -case "$1" in - start) - if is_running; then - echo "Already started" - else - echo "Starting $name" - $cmd & - echo $! > "$pid_file" - chmod 600 "$pid_file" - if ! is_running; then - echo "Failed to start $name" - exit 1 - fi - fi - ;; - stop) - if is_running; then - echo -n "Stopping $name..." - kill "$(get_pid)" - for _ in 1 2 3 4 5; do - if ! is_running; then - echo "stopped" - if [ -f "$pid_file" ]; then - rm "$pid_file" - fi - exit 0 - fi - printf "." - sleep 2 - done - echo "failed to stop $name" - exit 1 - fi - exit 0 - ;; - restart) - $0 stop - $0 start - ;; - status) - if is_running; then - echo "running" - else - echo "stopped" - exit 1 - fi - ;; - *) - echo "Usage: $0 {start|stop|restart|status}" - exit 1 - ;; -esac -exit 0 -` diff --git a/internal/router/service_merlin.go b/internal/router/service_merlin.go deleted file mode 100644 index 8ab6d6a..0000000 --- a/internal/router/service_merlin.go +++ /dev/null @@ -1,360 +0,0 @@ -package router - -import ( - "bytes" - "errors" - "fmt" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strings" - "syscall" - "text/template" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld/internal/router/nvram" -) - -const ( - merlinJFFSScriptPath = "/jffs/scripts/services-start" - merlinJFFSServiceEventScriptPath = "/jffs/scripts/service-event" -) - -type merlinSvc struct { - i service.Interface - platform string - *service.Config -} - -func newMerlinService(i service.Interface, platform string, c *service.Config) (service.Service, error) { - s := &merlinSvc{ - i: i, - platform: platform, - Config: c, - } - return s, nil -} - -func (s *merlinSvc) String() string { - if len(s.DisplayName) > 0 { - return s.DisplayName - } - return s.Name -} - -func (s *merlinSvc) Platform() string { - return s.platform -} - -func (s *merlinSvc) configPath() string { - bin := s.Config.Executable - if bin == "" { - path, err := os.Executable() - if err != nil { - return "" - } - bin = path - } - return bin + ".startup" -} - -func (s *merlinSvc) template() *template.Template { - return template.Must(template.New("").Parse(merlinSvcScript)) -} - -func (s *merlinSvc) Install() error { - exePath, err := os.Executable() - if err != nil { - return err - } - - if !strings.HasPrefix(exePath, "/jffs/") { - return errors.New("could not install service outside /jffs") - } - if _, err := nvram.Run("set", "jffs2_scripts=1"); err != nil { - return err - } - if _, err := nvram.Run("commit"); err != nil { - return err - } - - confPath := s.configPath() - if _, err := os.Stat(confPath); err == nil { - return fmt.Errorf("already installed: %s", confPath) - } - - var to = &struct { - *service.Config - Path string - }{ - s.Config, - exePath, - } - - f, err := os.Create(confPath) - if err != nil { - return fmt.Errorf("os.Create: %w", err) - } - defer f.Close() - - if err := s.template().Execute(f, to); err != nil { - return fmt.Errorf("s.template.Execute: %w", err) - } - - if err = os.Chmod(confPath, 0755); err != nil { - return fmt.Errorf("os.Chmod: startup script: %w", err) - } - - if err := os.MkdirAll(filepath.Dir(merlinJFFSScriptPath), 0755); err != nil { - return fmt.Errorf("os.MkdirAll: %w", err) - } - - tmpScript, err := os.CreateTemp("", "ctrld_install") - if err != nil { - return fmt.Errorf("os.CreateTemp: %w", err) - } - defer os.Remove(tmpScript.Name()) - defer tmpScript.Close() - - if _, err := tmpScript.WriteString(merlinAddLineToScript); err != nil { - return fmt.Errorf("tmpScript.WriteString: %w", err) - } - if err := tmpScript.Close(); err != nil { - return fmt.Errorf("tmpScript.Close: %w", err) - } - addLineToScript := func(line, script string) error { - if _, err := os.Stat(script); os.IsNotExist(err) { - if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil { - return err - } - } - if err := os.Chmod(script, 0755); err != nil { - return fmt.Errorf("os.Chmod: jffs script: %w", err) - } - - if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil { - return fmt.Errorf("exec.Command: add startup script: %w", err) - } - return nil - } - - for script, line := range map[string]string{ - merlinJFFSScriptPath: s.configPath() + " start", - merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`, - } { - if err := addLineToScript(line, script); err != nil { - return err - } - } - - return nil -} - -func (s *merlinSvc) Uninstall() error { - if err := os.Remove(s.configPath()); err != nil { - return fmt.Errorf("os.Remove: %w", err) - } - tmpScript, err := os.CreateTemp("", "ctrld_uninstall") - if err != nil { - return fmt.Errorf("os.CreateTemp: %w", err) - } - defer os.Remove(tmpScript.Name()) - defer tmpScript.Close() - - if _, err := tmpScript.WriteString(merlinRemoveLineFromScript); err != nil { - return fmt.Errorf("tmpScript.WriteString: %w", err) - } - if err := tmpScript.Close(); err != nil { - return fmt.Errorf("tmpScript.Close: %w", err) - } - removeLineFromScript := func(line, script string) error { - if _, err := os.Stat(script); os.IsNotExist(err) { - if err := os.WriteFile(script, []byte("#!/bin/sh\n"), 0755); err != nil { - return err - } - } - if err := os.Chmod(script, 0755); err != nil { - return fmt.Errorf("os.Chmod: jffs script: %w", err) - } - - if err := exec.Command("sh", tmpScript.Name(), line, script).Run(); err != nil { - return fmt.Errorf("exec.Command: add startup script: %w", err) - } - return nil - } - - for script, line := range map[string]string{ - merlinJFFSScriptPath: s.configPath() + " start", - merlinJFFSServiceEventScriptPath: s.configPath() + ` service_event "$1" "$2"`, - } { - if err := removeLineFromScript(line, script); err != nil { - return err - } - } - - return nil -} - -func (s *merlinSvc) Logger(errs chan<- error) (service.Logger, error) { - if service.Interactive() { - return service.ConsoleLogger, nil - } - return s.SystemLogger(errs) -} - -func (s *merlinSvc) SystemLogger(errs chan<- error) (service.Logger, error) { - return newSysLogger(s.Name, errs) -} - -func (s *merlinSvc) Run() (err error) { - err = s.i.Start(s) - if err != nil { - return err - } - - if interactice, _ := isInteractive(); !interactice { - signal.Ignore(syscall.SIGHUP) - } - - var sigChan = make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt) - <-sigChan - - return s.i.Stop(s) -} - -func (s *merlinSvc) Status() (service.Status, error) { - if _, err := os.Stat(s.configPath()); os.IsNotExist(err) { - return service.StatusUnknown, service.ErrNotInstalled - } - out, err := exec.Command(s.configPath(), "status").CombinedOutput() - if err != nil { - return service.StatusUnknown, err - } - switch string(bytes.TrimSpace(out)) { - case "running": - return service.StatusRunning, nil - default: - return service.StatusStopped, nil - } -} - -func (s *merlinSvc) Start() error { - return exec.Command(s.configPath(), "start").Run() -} - -func (s *merlinSvc) Stop() error { - return exec.Command(s.configPath(), "stop").Run() -} - -func (s *merlinSvc) Restart() error { - err := s.Stop() - if err != nil { - return err - } - return s.Start() -} - -const merlinSvcScript = `#!/bin/sh - -name="{{.Name}}" -cmd="{{.Path}}{{range .Arguments}} {{.}}{{end}}" -pid_file="/tmp/$name.pid" - -get_pid() { - cat "$pid_file" -} - -is_running() { - [ -f "$pid_file" ] && ps | grep -q "^ *$(get_pid) " -} - -case "$1" in - start) - if is_running; then - logger -c "Already started" - else - logger -c "Starting $name" - if [ -f /rom/ca-bundle.crt ]; then - # For John’s fork - export SSL_CERT_FILE=/rom/ca-bundle.crt - fi - $cmd & - echo $! > "$pid_file" - chmod 600 "$pid_file" - if ! is_running; then - logger -c "Failed to start $name" - exit 1 - fi - fi - ;; - stop) - if is_running; then - logger -c "Stopping $name..." - kill "$(get_pid)" - for _ in 1 2 3 4 5; do - if ! is_running; then - logger -c "stopped" - if [ -f "$pid_file" ]; then - rm "$pid_file" - fi - exit 0 - fi - printf "." - sleep 2 - done - logger -c "failed to stop $name" - exit 1 - fi - exit 0 - ;; - restart) - $0 stop - $0 start - ;; - status) - if is_running; then - echo "running" - else - echo "stopped" - exit 1 - fi - ;; - service_event) - event=$2 - svc=$3 - dnsmasq_pid_file=$(sed -n '/pid-file=/s///p' /etc/dnsmasq.conf) - - if [ "$event" = "restart" ] && [ "$svc" = "diskmon" ]; then - kill "$(cat "$dnsmasq_pid_file")" >/dev/null 2>&1 - fi - ;; - *) - echo "Usage: $0 {start|stop|restart|status}" - exit 1 - ;; -esac -exit 0 -` - -const merlinAddLineToScript = `#!/bin/sh - -line=$1 -file=$2 - -. /usr/sbin/helper.sh - -pc_append "$line" "$file" -` - -const merlinRemoveLineFromScript = `#!/bin/sh - -line=$1 -file=$2 - -. /usr/sbin/helper.sh - -pc_delete "$line" "$file" -` diff --git a/internal/router/service_tomato.go b/internal/router/service_tomato.go deleted file mode 100644 index 2cf5939..0000000 --- a/internal/router/service_tomato.go +++ /dev/null @@ -1,289 +0,0 @@ -package router - -import ( - "bytes" - "errors" - "fmt" - "os" - "os/exec" - "os/signal" - "strings" - "syscall" - "text/template" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld/internal/router/nvram" -) - -const tomatoNvramScriptWanupKey = "script_wanup" - -type tomatoSvc struct { - i service.Interface - platform string - *service.Config -} - -func newTomatoService(i service.Interface, platform string, c *service.Config) (service.Service, error) { - s := &tomatoSvc{ - i: i, - platform: platform, - Config: c, - } - return s, nil -} - -func (s *tomatoSvc) String() string { - if len(s.DisplayName) > 0 { - return s.DisplayName - } - return s.Name -} - -func (s *tomatoSvc) Platform() string { - return s.platform -} - -func (s *tomatoSvc) configPath() string { - bin := s.Config.Executable - if bin == "" { - path, err := os.Executable() - if err != nil { - return "" - } - bin = path - } - return bin + ".startup" -} - -func (s *tomatoSvc) template() *template.Template { - return template.Must(template.New("").Parse(tomatoSvcScript)) -} - -func (s *tomatoSvc) Install() error { - exePath, err := os.Executable() - if err != nil { - return err - } - - if !strings.HasPrefix(exePath, "/jffs/") { - return errors.New("could not install service outside /jffs") - } - if _, err := nvram.Run("set", "jffs2_on=1"); err != nil { - return err - } - if _, err := nvram.Run("commit"); err != nil { - return err - } - - confPath := s.configPath() - if _, err := os.Stat(confPath); err == nil { - return fmt.Errorf("already installed: %s", confPath) - } - - var to = &struct { - *service.Config - Path string - }{ - s.Config, - exePath, - } - - f, err := os.Create(confPath) - if err != nil { - return fmt.Errorf("os.Create: %w", err) - } - defer f.Close() - - if err := s.template().Execute(f, to); err != nil { - return fmt.Errorf("s.template.Execute: %w", err) - } - - if err = os.Chmod(confPath, 0755); err != nil { - return fmt.Errorf("os.Chmod: startup script: %w", err) - } - - nvramKvMap := map[string]string{ - tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method. - } - old, err := nvram.Run("get", tomatoNvramScriptWanupKey) - if err != nil { - return fmt.Errorf("nvram: %w", err) - } - nvramKvMap[tomatoNvramScriptWanupKey] = strings.Join([]string{old, s.configPath() + " start"}, "\n") - if err := nvram.SetKV(nvramKvMap, nvram.CtrldInstallKey); err != nil { - return err - } - return nil -} - -func (s *tomatoSvc) Uninstall() error { - if err := os.Remove(s.configPath()); err != nil { - return fmt.Errorf("os.Remove: %w", err) - } - nvramKvMap := map[string]string{ - tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method. - } - // Restore old configs. - if err := nvram.Restore(nvramKvMap, nvram.CtrldInstallKey); err != nil { - return err - } - return nil -} - -func (s *tomatoSvc) Logger(errs chan<- error) (service.Logger, error) { - if service.Interactive() { - return service.ConsoleLogger, nil - } - return s.SystemLogger(errs) -} - -func (s *tomatoSvc) SystemLogger(errs chan<- error) (service.Logger, error) { - return newSysLogger(s.Name, errs) -} - -func (s *tomatoSvc) Run() (err error) { - err = s.i.Start(s) - if err != nil { - return err - } - - if interactice, _ := isInteractive(); !interactice { - signal.Ignore(syscall.SIGHUP) - } - - var sigChan = make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt) - <-sigChan - - return s.i.Stop(s) -} - -func (s *tomatoSvc) Status() (service.Status, error) { - if _, err := os.Stat(s.configPath()); os.IsNotExist(err) { - return service.StatusUnknown, service.ErrNotInstalled - } - out, err := exec.Command(s.configPath(), "status").CombinedOutput() - if err != nil { - return service.StatusUnknown, err - } - switch string(bytes.TrimSpace(out)) { - case "running": - return service.StatusRunning, nil - default: - return service.StatusStopped, nil - } -} - -func (s *tomatoSvc) Start() error { - return exec.Command(s.configPath(), "start").Run() -} - -func (s *tomatoSvc) Stop() error { - return exec.Command(s.configPath(), "stop").Run() -} - -func (s *tomatoSvc) Restart() error { - return exec.Command(s.configPath(), "restart").Run() -} - -// https://wiki.freshtomato.org/doku.php/freshtomato_zerotier?s[]=%2Aservice%2A -const tomatoSvcScript = `#!/bin/sh - - -NAME="{{.Name}}" -CMD="{{.Path}}{{range .Arguments}} {{.}}{{end}}" -LOG_FILE="/var/log/${NAME}.log" -PID_FILE="/tmp/$NAME.pid" - - -alias elog="logger -t $NAME -s" - - -COND=$1 -[ $# -eq 0 ] && COND="start" - -get_pid() { - cat "$PID_FILE" -} - -is_running() { - [ -f "$PID_FILE" ] && ps | grep -q "^ *$(get_pid) " -} - -start() { - if is_running; then - elog "$NAME is already running." - exit 1 - fi - elog "Starting $NAME Services: " - $CMD & - echo $! > "$PID_FILE" - chmod 600 "$PID_FILE" - if is_running; then - elog "succeeded." - else - elog "failed." - fi -} - - -stop() { - if ! is_running; then - elog "$NAME is not running." - exit 0 - fi - elog "Shutting down $NAME Services: " - kill -SIGTERM "$(get_pid)" - for _ in 1 2 3 4 5; do - if ! is_running; then - if [ -f "$pid_file" ]; then - rm "$pid_file" - fi - return 0 - fi - printf "." - sleep 2 - done - if ! is_running; then - elog "succeeded." - else - elog "failed." - fi -} - - -do_restart() { - stop - start -} - - -do_status() { - if ! is_running; then - echo "stopped" - else - echo "running" - fi -} - - -case "$COND" in -start) - start - ;; -stop) - stop - ;; -restart) - do_restart - ;; -status) - do_status - ;; -*) - elog "Usage: $0 (start|stop|restart|status)" - ;; -esac -exit 0 -` diff --git a/internal/router/service_ubios.go b/internal/router/service_ubios.go deleted file mode 100644 index 9ad971d..0000000 --- a/internal/router/service_ubios.go +++ /dev/null @@ -1,340 +0,0 @@ -package router - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strings" - "syscall" - "text/template" - "time" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" -) - -// This is a copy of https://github.com/kardianos/service/blob/v1.2.1/service_sysv_linux.go, -// with modification for supporting ubios v1 init system. - -type ubiosSvc struct { - i service.Interface - platform string - *service.Config -} - -func newUbiosService(i service.Interface, platform string, c *service.Config) (service.Service, error) { - s := &ubiosSvc{ - i: i, - platform: platform, - Config: c, - } - return s, nil -} - -func (s *ubiosSvc) String() string { - if len(s.DisplayName) > 0 { - return s.DisplayName - } - return s.Name -} - -func (s *ubiosSvc) Platform() string { - return s.platform -} - -func (s *ubiosSvc) configPath() string { - return "/etc/init.d/" + s.Config.Name -} - -func (s *ubiosSvc) execPath() (string, error) { - if len(s.Executable) != 0 { - return filepath.Abs(s.Executable) - } - return os.Executable() -} - -func (s *ubiosSvc) template() *template.Template { - return template.Must(template.New("").Funcs(tf).Parse(ubiosSvcScript)) -} - -func (s *ubiosSvc) Install() error { - confPath := s.configPath() - if _, err := os.Stat(confPath); err == nil { - return fmt.Errorf("init already exists: %s", confPath) - } - - f, err := os.Create(confPath) - if err != nil { - return fmt.Errorf("failed to create config path: %w", err) - } - defer f.Close() - - path, err := s.execPath() - if err != nil { - return fmt.Errorf("failed to get exec path: %w", err) - } - - var to = &struct { - *service.Config - Path string - DnsMasqConfPath string - }{ - s.Config, - path, - filepath.Join(dnsmasq.UbiosConfPath(), dnsmasq.UbiosConfName), - } - - if err := s.template().Execute(f, to); err != nil { - return fmt.Errorf("failed to create init script: %w", err) - } - - if err := f.Close(); err != nil { - return fmt.Errorf("failed to save init script: %w", err) - } - - if err = os.Chmod(confPath, 0755); err != nil { - return fmt.Errorf("failed to set init script executable: %w", err) - } - - // Enable on boot - script, err := os.CreateTemp("", "ctrld_boot.service") - if err != nil { - return fmt.Errorf("failed to create boot service tmp file: %w", err) - } - defer script.Close() - - svcConfig := *to.Config - svcConfig.Arguments = os.Args[1:] - to.Config = &svcConfig - if err := template.Must(template.New("").Funcs(tf).Parse(ubiosBootSystemdService)).Execute(script, &to); err != nil { - return fmt.Errorf("failed to create boot service file: %w", err) - } - if err := script.Close(); err != nil { - return fmt.Errorf("failed to save boot service file: %w", err) - } - - // Copy the boot script to container and start. - cmd := exec.Command("podman", "cp", "--pause=false", script.Name(), "unifi-os:/lib/systemd/system/ctrld-boot.service") - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to copy boot script, out: %s, err: %v", string(out), err) - } - cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "enable", "--now", "ctrld-boot.service") - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to start ctrld boot script, out: %s, err: %v", string(out), err) - } - return nil -} - -func (s *ubiosSvc) Uninstall() error { - if err := os.Remove(s.configPath()); err != nil { - return err - } - // Remove ctrld-boot service inside unifi-os container. - cmd := exec.Command("podman", "exec", "unifi-os", "systemctl", "disable", "ctrld-boot.service") - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to disable ctrld-boot service, out: %s, err: %v", string(out), err) - } - cmd = exec.Command("podman", "exec", "unifi-os", "rm", "/lib/systemd/system/ctrld-boot.service") - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to remove ctrld-boot service file, out: %s, err: %v", string(out), err) - } - cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "daemon-reload") - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to reload systemd service, out: %s, err: %v", string(out), err) - } - cmd = exec.Command("podman", "exec", "unifi-os", "systemctl", "reset-failed") - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to reset-failed systemd service, out: %s, err: %v", string(out), err) - } - return nil -} - -func (s *ubiosSvc) Logger(errs chan<- error) (service.Logger, error) { - if service.Interactive() { - return service.ConsoleLogger, nil - } - return s.SystemLogger(errs) -} - -func (s *ubiosSvc) SystemLogger(errs chan<- error) (service.Logger, error) { - return newSysLogger(s.Name, errs) -} - -func (s *ubiosSvc) Run() (err error) { - err = s.i.Start(s) - if err != nil { - return err - } - - if interactice, _ := isInteractive(); !interactice { - signal.Ignore(syscall.SIGHUP) - } - - var sigChan = make(chan os.Signal, 3) - signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt) - <-sigChan - - return s.i.Stop(s) -} - -func (s *ubiosSvc) Status() (service.Status, error) { - if _, err := os.Stat(s.configPath()); os.IsNotExist(err) { - return service.StatusUnknown, service.ErrNotInstalled - } - out, err := exec.Command(s.configPath(), "status").CombinedOutput() - if err != nil { - return service.StatusUnknown, err - } - switch string(bytes.TrimSpace(out)) { - case "Running": - return service.StatusRunning, nil - default: - return service.StatusStopped, nil - } -} - -func (s *ubiosSvc) Start() error { - return exec.Command(s.configPath(), "start").Run() -} - -func (s *ubiosSvc) Stop() error { - return exec.Command(s.configPath(), "stop").Run() -} - -func (s *ubiosSvc) Restart() error { - err := s.Stop() - if err != nil { - return err - } - time.Sleep(50 * time.Millisecond) - return s.Start() -} - -const ubiosBootSystemdService = `[Unit] -Description=Run ctrld On Startup UDM -Wants=network-online.target -After=network-online.target -Wants=unifi-mongodb -After=unifi-mongodb -StartLimitIntervalSec=500 -StartLimitBurst=5 - -[Service] -Restart=on-failure -RestartSec=5s -ExecStart=/sbin/ssh-proxy '[ -f "{{.DnsMasqConfPath}}" ] || {{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}' -RemainAfterExit=true -[Install] -WantedBy=multi-user.target -` - -const ubiosSvcScript = `#!/bin/sh -# For RedHat and cousins: -# chkconfig: - 99 01 -# description: {{.Description}} -# processname: {{.Path}} - -### BEGIN INIT INFO -# Provides: {{.Path}} -# Required-Start: -# Required-Stop: -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: {{.DisplayName}} -# Description: {{.Description}} -### END INIT INFO - -cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}" - -name=$(basename $(readlink -f $0)) -pid_file="/var/run/$name.pid" -stdout_log="/var/log/$name.log" -stderr_log="/var/log/$name.err" - -[ -e /etc/sysconfig/$name ] && . /etc/sysconfig/$name - -get_pid() { - cat "$pid_file" -} - -is_running() { - [ -f "$pid_file" ] && cat /proc/$(get_pid)/stat > /dev/null 2>&1 -} - -case "$1" in - start) - if is_running; then - echo "Already started" - else - echo "Starting $name" - {{if .WorkingDirectory}}cd '{{.WorkingDirectory}}'{{end}} - $cmd >> "$stdout_log" 2>> "$stderr_log" & - echo $! > "$pid_file" - if ! is_running; then - echo "Unable to start, see $stdout_log and $stderr_log" - exit 1 - fi - fi - ;; - stop) - if is_running; then - echo -n "Stopping $name.." - kill $(get_pid) - for i in $(seq 1 10) - do - if ! is_running; then - break - fi - echo -n "." - sleep 1 - done - echo - if is_running; then - echo "Not stopped; may still be shutting down or shutdown may have failed" - exit 1 - else - echo "Stopped" - if [ -f "$pid_file" ]; then - rm "$pid_file" - fi - fi - else - echo "Not running" - fi - ;; - restart) - $0 stop - if is_running; then - echo "Unable to stop, will not attempt to start" - exit 1 - fi - $0 start - ;; - status) - if is_running; then - echo "Running" - else - echo "Stopped" - exit 1 - fi - ;; - *) - echo "Usage: $0 {start|stop|restart|status}" - exit 1 - ;; -esac -exit 0 -` - -var tf = map[string]interface{}{ - "cmd": func(s string) string { - return `"` + strings.Replace(s, `"`, `\"`, -1) + `"` - }, - "cmdEscape": func(s string) string { - return strings.Replace(s, " ", `\x20`, -1) - }, -} diff --git a/internal/router/synology/synology.go b/internal/router/synology/synology.go deleted file mode 100644 index 7933943..0000000 --- a/internal/router/synology/synology.go +++ /dev/null @@ -1,125 +0,0 @@ -package synology - -import ( - "bytes" - "context" - "errors" - "fmt" - "os" - "os/exec" - "strings" - "time" - - "github.com/kardianos/service" - "tailscale.com/logtail/backoff" - - "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" - "github.com/Control-D-Inc/ctrld/internal/router/ntp" -) - -const ( - Name = "synology" - - synologyDNSMasqConfigPath = "/etc/dhcpd/dhcpd-zzz-ctrld.conf" - synologyDhcpdInfoPath = "/etc/dhcpd/dhcpd-zzz-ctrld.info" -) - -type Synology struct { - cfg *ctrld.Config - useUpstart bool -} - -// New returns a router.Router for configuring/setup/run ctrld on Ubios routers. -func New(cfg *ctrld.Config) *Synology { - return &Synology{ - cfg: cfg, - useUpstart: service.Platform() == "linux-upstart", - } -} - -func (s *Synology) ConfigureService(svc *service.Config) error { - svc.Option["LogOutput"] = true - return nil -} - -func (s *Synology) Install(_ *service.Config) error { - return nil -} - -func (s *Synology) Uninstall(_ *service.Config) error { - return nil -} - -func (s *Synology) PreRun() error { - if s.useUpstart { - if err := ntp.WaitUpstart(); err != nil { - return err - } - return waitDhcpServer() - } - return nil -} - -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 - } - if err := os.WriteFile(synologyDNSMasqConfigPath, []byte(data), 0600); err != nil { - return err - } - if err := os.WriteFile(synologyDhcpdInfoPath, []byte(`enable="yes"`), 0600); err != nil { - return err - } - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -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 { - return err - } - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func restartDNSMasq() error { - if out, err := exec.Command("/etc/rc.network", "nat-restart-dhcp").CombinedOutput(); err != nil { - return fmt.Errorf("synologyRestartDNSMasq: %s - %w", string(out), err) - } - return nil -} - -func waitDhcpServer() error { - // Wait until `initctl status dhcpserver` returns running state. - b := backoff.NewBackoff("waitDhcpServer", func(format string, args ...any) {}, 10*time.Second) - for { - out, err := exec.Command("initctl", "status", "dhcpserver").CombinedOutput() - if err != nil { - if strings.Contains(err.Error(), "Unknown job") { - // dhcpserver service does not exist. - return nil - } - return fmt.Errorf("exec.Command: %w", err) - } - if bytes.Contains(out, []byte("start/running")) { - return nil - } - b.BackOff(context.Background(), errors.New("ntp not ready")) - } -} diff --git a/internal/router/syslog.go b/internal/router/syslog.go deleted file mode 100644 index 008bbeb..0000000 --- a/internal/router/syslog.go +++ /dev/null @@ -1,49 +0,0 @@ -//go:build linux || darwin || freebsd - -package router - -import ( - "fmt" - "log/syslog" - - "github.com/kardianos/service" -) - -func newSysLogger(name string, errs chan<- error) (service.Logger, error) { - w, err := syslog.New(syslog.LOG_INFO, name) - if err != nil { - return nil, err - } - return sysLogger{w, errs}, nil -} - -type sysLogger struct { - *syslog.Writer - errs chan<- error -} - -func (s sysLogger) send(err error) error { - if err != nil && s.errs != nil { - s.errs <- err - } - return err -} - -func (s sysLogger) Error(v ...interface{}) error { - return s.send(s.Writer.Err(fmt.Sprint(v...))) -} -func (s sysLogger) Warning(v ...interface{}) error { - return s.send(s.Writer.Warning(fmt.Sprint(v...))) -} -func (s sysLogger) Info(v ...interface{}) error { - return s.send(s.Writer.Info(fmt.Sprint(v...))) -} -func (s sysLogger) Errorf(format string, a ...interface{}) error { - return s.send(s.Writer.Err(fmt.Sprintf(format, a...))) -} -func (s sysLogger) Warningf(format string, a ...interface{}) error { - return s.send(s.Writer.Warning(fmt.Sprintf(format, a...))) -} -func (s sysLogger) Infof(format string, a ...interface{}) error { - return s.send(s.Writer.Info(fmt.Sprintf(format, a...))) -} diff --git a/internal/router/syslog_windows.go b/internal/router/syslog_windows.go deleted file mode 100644 index ecac969..0000000 --- a/internal/router/syslog_windows.go +++ /dev/null @@ -1,7 +0,0 @@ -package router - -import "github.com/kardianos/service" - -func newSysLogger(name string, errs chan<- error) (service.Logger, error) { - return service.ConsoleLogger, nil -} diff --git a/internal/router/tomato/tomato.go b/internal/router/tomato/tomato.go deleted file mode 100644 index ee5f09b..0000000 --- a/internal/router/tomato/tomato.go +++ /dev/null @@ -1,133 +0,0 @@ -package tomato - -import ( - "fmt" - "os/exec" - - "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" - "github.com/Control-D-Inc/ctrld/internal/router/ntp" - "github.com/Control-D-Inc/ctrld/internal/router/nvram" - "github.com/kardianos/service" -) - -const ( - Name = "freshtomato" - - tomatoDnsCryptProxySvcName = "dnscrypt-proxy" - tomatoStubbySvcName = "stubby" - tomatoDNSMasqSvcName = "dnsmasq" -) - -var nvramKvMap = map[string]string{ - "dnsmasq_custom": "", // Configuration of dnsmasq set by ctrld, filled by setupTomato. - "dnscrypt_proxy": "0", // Disable DNSCrypt. - "dnssec_enable": "0", // Disable DNSSEC. - "stubby_proxy": "0", // Disable Stubby -} - -type FreshTomato struct { - cfg *ctrld.Config -} - -// New returns a router.Router for configuring/setup/run ctrld on Ubios routers. -func New(cfg *ctrld.Config) *FreshTomato { - return &FreshTomato{cfg: cfg} -} - -func (f *FreshTomato) ConfigureService(config *service.Config) error { - return nil -} - -func (f *FreshTomato) Install(_ *service.Config) error { - return nil -} - -func (f *FreshTomato) Uninstall(_ *service.Config) error { - return nil -} - -func (f *FreshTomato) PreRun() error { - _ = f.Cleanup() - return ntp.WaitNvram() -} - -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 - } - - data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, f.cfg) - if err != nil { - return err - } - nvramKvMap["dnsmasq_custom"] = data - if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil { - return err - } - - // Restart dnscrypt-proxy service. - if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil { - return err - } - // Restart stubby service. - if err := tomatoRestartService(tomatoStubbySvcName); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func (f *FreshTomato) Cleanup() error { - if f.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" { - return nil // was restored, nothing to do. - } - - nvramKvMap["dnsmasq_custom"] = "" - // Restore old configs. - if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { - return err - } - - // Restart dnscrypt-proxy service. - if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil { - return err - } - // Restart stubby service. - if err := tomatoRestartService(tomatoStubbySvcName); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func tomatoRestartService(name string) error { - return tomatoRestartServiceWithKill(name, false) -} - -func tomatoRestartServiceWithKill(name string, killBeforeRestart bool) error { - if killBeforeRestart { - _, _ = exec.Command("killall", name).CombinedOutput() - } - if out, err := exec.Command("service", name, "restart").CombinedOutput(); err != nil { - return fmt.Errorf("service restart %s: %s, %w", name, string(out), err) - } - return nil -} - -func restartDNSMasq() error { - return tomatoRestartService(tomatoDNSMasqSvcName) -} diff --git a/internal/router/ubios/ubios.go b/internal/router/ubios/ubios.go deleted file mode 100644 index cba6842..0000000 --- a/internal/router/ubios/ubios.go +++ /dev/null @@ -1,102 +0,0 @@ -package ubios - -import ( - "bytes" - "os" - "path/filepath" - "strconv" - - "github.com/kardianos/service" - - "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" - "github.com/Control-D-Inc/ctrld/internal/router/edgeos" -) - -const Name = "ubios" - -type Ubios struct { - cfg *ctrld.Config - dnsmasqConfPath string -} - -// New returns a router.Router for configuring/setup/run ctrld on Ubios routers. -func New(cfg *ctrld.Config) *Ubios { - return &Ubios{ - cfg: cfg, - dnsmasqConfPath: filepath.Join(dnsmasq.UbiosConfPath(), dnsmasq.UbiosConfName), - } -} - -func (u *Ubios) ConfigureService(config *service.Config) error { - return nil -} - -func (u *Ubios) Install(config *service.Config) error { - // See comment in (*edgeos.EdgeOS).Install method. - if edgeos.ContentFilteringEnabled() { - return edgeos.ErrContentFilteringEnabled - } - // See comment in (*edgeos.EdgeOS).Install method. - if edgeos.DnsShieldEnabled() { - return edgeos.ErrDnsShieldEnabled - } - return nil -} - -func (u *Ubios) Uninstall(_ *service.Config) error { - return nil -} - -func (u *Ubios) PreRun() error { - return nil -} - -func (u *Ubios) Setup() error { - if u.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, u.cfg, false) - if err != nil { - return err - } - if err := os.WriteFile(u.dnsmasqConfPath, []byte(data), 0600); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func (u *Ubios) Cleanup() error { - if u.cfg.FirstListener().IsDirectDnsListener() { - return nil - } - // Remove the custom dnsmasq config - if err := os.Remove(u.dnsmasqConfPath); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func restartDNSMasq() error { - buf, err := os.ReadFile(dnsmasq.UbiosPidFile()) - if err != nil { - return err - } - pid, err := strconv.ParseUint(string(bytes.TrimSpace(buf)), 10, 64) - if err != nil { - return err - } - proc, err := os.FindProcess(int(pid)) - if err != nil { - return err - } - return proc.Kill() -} diff --git a/scripts/build.sh b/scripts/build.sh index 2faeddc..fa36598 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -44,11 +44,11 @@ compress() { return 0 ;; *-linux-armv*) - echo >&2 "upx does not work on arm routers" + echo >&2 "upx does not work on arm platforms" return 0 ;; *-linux-mips*) - echo >&2 "upx does not work on mips routers" + echo >&2 "upx does not work on mips platforms" return 0 ;; esac