From 20f8f22baedebdc5c4e1e96bfbb9c37e4ddf0dd3 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 19 Mar 2024 18:11:33 +0700 Subject: [PATCH] all: add support to Netgear Orbi Voxel While at it, also ensure checking the service is installed or not before executing uninstall function, so we won't emit un-necessary errors. --- cmd/cli/cli.go | 5 +- cmd/cli/service.go | 2 +- internal/router/dnsmasq/dnsmasq.go | 2 + internal/router/netgear_orbi_voxel/procd.go | 22 ++ internal/router/netgear_orbi_voxel/voxel.go | 220 ++++++++++++++++++++ internal/router/router.go | 13 +- 6 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 internal/router/netgear_orbi_voxel/procd.go create mode 100644 internal/router/netgear_orbi_voxel/voxel.go diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index f9e8c68..daf4358 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -1783,6 +1783,10 @@ func readConfigWithNotice(writeDefaultConfig, notice bool) { } func uninstall(p *prog, s service.Service) { + if _, err := s.Status(); err != nil && errors.Is(err, service.ErrNotInstalled) { + mainLog.Load().Error().Msg(err.Error()) + return + } tasks := []task{ {s.Stop, false}, {s.Uninstall, true}, @@ -2233,7 +2237,6 @@ func newSocketControlClient(s service.Service, dir string) *controlClient { for { curStatus, err := s.Status() if err != nil { - mainLog.Load().Warn().Err(err).Msg("could not get service status while doing self-check") return nil } if curStatus != service.StatusRunning { diff --git a/cmd/cli/service.go b/cmd/cli/service.go index c6ed68c..ef37796 100644 --- a/cmd/cli/service.go +++ b/cmd/cli/service.go @@ -20,7 +20,7 @@ func newService(i service.Interface, c *service.Config) (service.Service, error) return nil, err } switch { - case router.IsOldOpenwrt(): + case router.IsOldOpenwrt(), router.IsNetGearOrbi(): return &procd{&sysV{s}}, nil case router.IsGLiNet(): return &sysV{s}, nil diff --git a/internal/router/dnsmasq/dnsmasq.go b/internal/router/dnsmasq/dnsmasq.go index c2f8845..55c62e8 100644 --- a/internal/router/dnsmasq/dnsmasq.go +++ b/internal/router/dnsmasq/dnsmasq.go @@ -10,6 +10,8 @@ import ( "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}} diff --git a/internal/router/netgear_orbi_voxel/procd.go b/internal/router/netgear_orbi_voxel/procd.go new file mode 100644 index 0000000..750a17d --- /dev/null +++ b/internal/router/netgear_orbi_voxel/procd.go @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..4338f9c --- /dev/null +++ b/internal/router/netgear_orbi_voxel/voxel.go @@ -0,0 +1,220 @@ +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/router.go b/internal/router/router.go index a4383ac..18b7a90 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -18,6 +18,7 @@ import ( "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" @@ -66,10 +67,17 @@ func New(cfg *ctrld.Config, cdMode bool) Router { 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 { @@ -145,7 +153,7 @@ func LocalResolverIP() string { // HomeDir returns the home directory of ctrld on current router. func HomeDir() (string, error) { switch Name() { - case ddwrt.Name, firewalla.Name, merlin.Name, tomato.Name: + case ddwrt.Name, firewalla.Name, merlin.Name, netgear.Name, tomato.Name: exe, err := os.Executable() if err != nil { return "", err @@ -198,6 +206,9 @@ func distroName() string { 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