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.
This commit is contained in:
Cuong Manh Le
2024-03-19 18:11:33 +07:00
committed by Cuong Manh Le
parent b50cccac85
commit 20f8f22bae
6 changed files with 261 additions and 3 deletions

View File

@@ -1783,6 +1783,10 @@ func readConfigWithNotice(writeDefaultConfig, notice bool) {
} }
func uninstall(p *prog, s service.Service) { 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{ tasks := []task{
{s.Stop, false}, {s.Stop, false},
{s.Uninstall, true}, {s.Uninstall, true},
@@ -2233,7 +2237,6 @@ func newSocketControlClient(s service.Service, dir string) *controlClient {
for { for {
curStatus, err := s.Status() curStatus, err := s.Status()
if err != nil { if err != nil {
mainLog.Load().Warn().Err(err).Msg("could not get service status while doing self-check")
return nil return nil
} }
if curStatus != service.StatusRunning { if curStatus != service.StatusRunning {

View File

@@ -20,7 +20,7 @@ func newService(i service.Interface, c *service.Config) (service.Service, error)
return nil, err return nil, err
} }
switch { switch {
case router.IsOldOpenwrt(): case router.IsOldOpenwrt(), router.IsNetGearOrbi():
return &procd{&sysV{s}}, nil return &procd{&sysV{s}}, nil
case router.IsGLiNet(): case router.IsGLiNet():
return &sysV{s}, nil return &sysV{s}, nil

View File

@@ -10,6 +10,8 @@ import (
"github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld"
) )
const CtrldMarker = `# GENERATED BY ctrld - DO NOT MODIFY`
const ConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY const ConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
no-resolv no-resolv
{{- range .Upstreams}} {{- range .Upstreams}}

View File

@@ -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"
}
`

View File

@@ -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
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/Control-D-Inc/ctrld/internal/router/edgeos" "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/firewalla"
"github.com/Control-D-Inc/ctrld/internal/router/merlin" "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/openwrt"
"github.com/Control-D-Inc/ctrld/internal/router/synology" "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/tomato"
@@ -66,10 +67,17 @@ func New(cfg *ctrld.Config, cdMode bool) Router {
return tomato.New(cfg) return tomato.New(cfg)
case firewalla.Name: case firewalla.Name:
return firewalla.New(cfg) return firewalla.New(cfg)
case netgear.Name:
return netgear.New(cfg)
} }
return newOsRouter(cfg, cdMode) 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. // IsGLiNet reports whether the router is an GL.iNet router.
func IsGLiNet() bool { func IsGLiNet() bool {
if Name() != openwrt.Name { if Name() != openwrt.Name {
@@ -145,7 +153,7 @@ func LocalResolverIP() string {
// HomeDir returns the home directory of ctrld on current router. // HomeDir returns the home directory of ctrld on current router.
func HomeDir() (string, error) { func HomeDir() (string, error) {
switch Name() { 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() exe, err := os.Executable()
if err != nil { if err != nil {
return "", err return "", err
@@ -198,6 +206,9 @@ func distroName() string {
case bytes.HasPrefix(unameO(), []byte("ASUSWRT-Merlin")): case bytes.HasPrefix(unameO(), []byte("ASUSWRT-Merlin")):
return merlin.Name return merlin.Name
case haveFile("/etc/openwrt_version"): case haveFile("/etc/openwrt_version"):
if haveFile("/bin/config") { // TODO: is there any more reliable way?
return netgear.Name
}
return openwrt.Name return openwrt.Name
case isUbios(): case isUbios():
return ubios.Name return ubios.Name