diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index d536fb8..f3cfe01 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -34,6 +34,9 @@ import ( "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" + "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" ) var ( @@ -165,15 +168,20 @@ func initCLI() { mainLog.Fatal().Msg("network is not up yet") } + p.router = router.NewDummyRouter() + if setupRouter { + p.router = router.New(&cfg) + } + // 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 := router.PreRun(svcConfig); err != nil { + if err := p.router.PreRun(); err != nil { mainLog.Fatal().Err(err).Msg("failed to perform router pre-run check") } oldLogPath := cfg.Service.LogPath - processCDFlags() + processCDFlags(p) if newLogPath := cfg.Service.LogPath; newLogPath != "" && oldLogPath != newLogPath { // After processCDFlags, log config may change, so reset mainLog and re-init logging. mainLog = zerolog.New(io.Discard) @@ -216,7 +224,7 @@ func initCLI() { if setupRouter { switch platform := router.Name(); { - case platform == router.DDWrt: + case platform == ddwrt.Name: rootCertPool = certs.CACertPool() fallthrough case platform != "": @@ -226,13 +234,13 @@ func initCLI() { } p.onStarted = append(p.onStarted, func() { mainLog.Debug().Msg("Router setup") - if err := router.Configure(&cfg); err != nil { + if err := p.router.Setup(); err != nil { mainLog.Error().Err(err).Msg("could not configure router") } }) p.onStopped = append(p.onStopped, func() { mainLog.Debug().Msg("Router cleanup") - if err := router.Cleanup(svcConfig); err != nil { + if err := p.router.Cleanup(); err != nil { mainLog.Error().Err(err).Msg("could not cleanup router") } p.resetDNS() @@ -285,7 +293,13 @@ func initCLI() { } setDependencies(sc) sc.Arguments = append([]string{"run"}, osArgs...) - if err := router.ConfigureService(sc); err != nil { + + p := &prog{router: router.NewDummyRouter()} + if setupRouter { + p.router = router.New(&cfg) + } + + if err := p.router.ConfigureService(sc); err != nil { mainLog.Fatal().Err(err).Msg("failed to configure service on router") } @@ -311,7 +325,7 @@ func initCLI() { initLogging() - processCDFlags() + processCDFlags(p) if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil { mainLog.Fatal().Msgf("invalid config: %v", err) @@ -324,7 +338,6 @@ func initCLI() { sc.Arguments = append(sc.Arguments, "--config="+defaultConfigFile) } - p := &prog{} s, err := newService(p, sc) if err != nil { mainLog.Error().Msg(err.Error()) @@ -332,7 +345,7 @@ func initCLI() { } mainLog.Debug().Msg("cleaning up router before installing") - _ = router.Cleanup(svcConfig) + _ = p.router.Cleanup() tasks := []task{ {s.Stop, false}, @@ -341,7 +354,7 @@ func initCLI() { {s.Start, true}, } if doTasks(tasks) { - if err := router.PostInstall(svcConfig); err != nil { + if err := p.router.Install(sc); err != nil { mainLog.Warn().Err(err).Msg("post installation failed, please check system/service log for details error") return } @@ -720,7 +733,7 @@ func processNoConfigFlags(noConfigStart bool) { v.Set("upstream", upstream) } -func processCDFlags() { +func processCDFlags(p *prog) { if cdUID == "" { return } @@ -778,8 +791,9 @@ func processCDFlags() { switch { case setupRouter: if lc := cfg.Listener["0"]; lc != nil && lc.IP == "" { - lc.IP = router.ListenIP() - lc.Port = router.ListenPort() + if err := p.router.Configure(); err != nil { + mainLog.Fatal().Err(err).Msg("failed to change ctrld config for router") + } } case useSystemdResolved: if lc := cfg.Listener["0"]; lc != nil { @@ -824,11 +838,12 @@ func processCDFlags() { Rules: rules, }, } - if setupRouter { - lc.IP = router.ListenIP() - lc.Port = router.ListenPort() - } cfg.Listener["0"] = lc + if setupRouter { + if err := p.router.Configure(); err != nil { + mainLog.Fatal().Err(err).Msg("failed to change ctrld config for router") + } + } } processLogAndCacheFlags() @@ -940,7 +955,7 @@ func unsupportedPlatformHelp(cmd *cobra.Command) { func userHomeDir() (string, error) { switch router.Name() { - case router.DDWrt, router.Merlin, router.Tomato: + case ddwrt.Name, merlin.Name, tomato.Name: exe, err := os.Executable() if err != nil { return "", err @@ -988,7 +1003,8 @@ func uninstall(p *prog, s service.Service) { } initLogging() if doTasks(tasks) { - if err := router.PostUninstall(svcConfig); err != nil { + r := router.New(&cfg) + if err := r.Uninstall(svcConfig); err != nil { mainLog.Warn().Err(err).Msg("post uninstallation failed, please check system/service log for details error") return } @@ -999,7 +1015,7 @@ func uninstall(p *prog, s service.Service) { mainLog.Debug().Msg("Router cleanup") // Stop already did router.Cleanup and report any error if happens, // ignoring error here to prevent false positive. - _ = router.Cleanup(svcConfig) + _ = r.Cleanup() mainLog.Notice().Msg("Service uninstalled") return } diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index e09289a..cf062b5 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -16,6 +16,10 @@ import ( "github.com/Control-D-Inc/ctrld/internal/clientinfo" "github.com/Control-D-Inc/ctrld/internal/dnscache" "github.com/Control-D-Inc/ctrld/internal/router" + "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" + "github.com/Control-D-Inc/ctrld/internal/router/firewalla" + "github.com/Control-D-Inc/ctrld/internal/router/openwrt" + "github.com/Control-D-Inc/ctrld/internal/router/ubios" ) const defaultSemaphoreCap = 256 @@ -39,10 +43,11 @@ type prog struct { waitCh chan struct{} stopCh chan struct{} - cfg *ctrld.Config - cache dnscache.Cacher - sema semaphore - mt *clientinfo.MacTable + cfg *ctrld.Config + cache dnscache.Cacher + sema semaphore + mt *clientinfo.MacTable + router router.Router started chan struct{} onStarted []func() @@ -207,7 +212,7 @@ func (p *prog) deAllocateIP() error { func (p *prog) setDNS() { switch router.Name() { - case router.DDWrt, router.OpenWrt, router.Ubios: + case ddwrt.Name, openwrt.Name, ubios.Name: // On router, ctrld run as a DNS forwarder, it does not have to change system DNS. // Except for: // + EdgeOS, which /etc/resolv.conf could be managed by vyatta_update_resolv.pl script. @@ -236,7 +241,7 @@ func (p *prog) setDNS() { } logger.Debug().Msg("setting DNS for interface") ns := cfg.Listener["0"].IP - if router.Name() == router.Firewalla && (ns == "127.0.0.1" || ns == "0.0.0.0" || ns == "") { + if router.Name() == firewalla.Name && (ns == "127.0.0.1" || ns == "0.0.0.0" || ns == "") { // 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. if ns == "127.0.0.1" { @@ -264,7 +269,7 @@ func (p *prog) setDNS() { func (p *prog) resetDNS() { switch router.Name() { - case router.DDWrt, router.OpenWrt, router.Ubios: + case ddwrt.Name, openwrt.Name, ubios.Name: // See comment in p.setDNS method. return } diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 38cd1a5..0748b51 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -5,6 +5,7 @@ import ( "github.com/Control-D-Inc/ctrld/internal/dns" "github.com/Control-D-Inc/ctrld/internal/router" + "github.com/Control-D-Inc/ctrld/internal/router/edgeos" ) func init() { @@ -29,7 +30,7 @@ func setDependencies(svc *service.Config) { "After=systemd-networkd-wait-online.service", } // On EdeOS, ctrld needs to start after vyatta-dhcpd, so it can read leases file. - if router.Name() == router.EdgeOS { + if router.Name() == edgeos.Name { svc.Dependencies = append(svc.Dependencies, "Wants=vyatta-dhcpd.service") svc.Dependencies = append(svc.Dependencies, "After=vyatta-dhcpd.service") svc.Dependencies = append(svc.Dependencies, "Wants=dnsmasq.service") diff --git a/internal/controld/config.go b/internal/controld/config.go index eef98f9..6bc5544 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -14,6 +14,7 @@ import ( "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 ( @@ -92,7 +93,7 @@ func FetchResolverConfig(uid, version string, cdDev bool) (*ResolverConfig, erro return d.DialContext(ctx, network, addrs) } - if router.Name() == router.DDWrt { + if router.Name() == ddwrt.Name { transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()} } client := http.Client{ diff --git a/internal/router/ddwrt.go b/internal/router/ddwrt.go deleted file mode 100644 index 92318b1..0000000 --- a/internal/router/ddwrt.go +++ /dev/null @@ -1,72 +0,0 @@ -package router - -import ( - "errors" - "fmt" - "os/exec" -) - -const ( - nvramCtrldKeyPrefix = "ctrld_" - nvramCtrldSetupKey = "ctrld_setup" - nvramCtrldInstallKey = "ctrld_install" - nvramRCStartupKey = "rc_startup" -) - -//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 -`) - -func setupDDWrt() error { - // Already setup. - if val, _ := nvram("get", nvramCtrldSetupKey); val == "1" { - return nil - } - - data, err := dnsMasqConf() - if err != nil { - return err - } - - nvramKvMap := nvramSetupKV() - nvramKvMap["dnsmasq_options"] = data - if err := nvramSetKV(nvramKvMap, nvramCtrldSetupKey); err != nil { - return err - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func cleanupDDWrt() error { - // Restore old configs. - if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func postInstallDDWrt() error { - return nil -} - -func ddwrtRestartDNSMasq() 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("get", "enable_jffs2") - return out == "1" -} diff --git a/internal/router/ddwrt/ddwrt.go b/internal/router/ddwrt/ddwrt.go new file mode 100644 index 0000000..cf45b30 --- /dev/null +++ b/internal/router/ddwrt/ddwrt.go @@ -0,0 +1,115 @@ +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.Wait() +} + +func (d *Ddwrt) Configure() error { + d.cfg.Listener["0"].IP = "127.0.0.1" + d.cfg.Listener["0"].Port = 5354 + return nil +} + +func (d *Ddwrt) Setup() error { + // 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 val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { + 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.go b/internal/router/dnsmasq.go deleted file mode 100644 index 68fc78a..0000000 --- a/internal/router/dnsmasq.go +++ /dev/null @@ -1,104 +0,0 @@ -package router - -import ( - "strings" - "text/template" -) - -const dnsMasqConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY -no-resolv -{{- range .Upstreams}} -server={{ .Ip }}#{{ .Port }} -{{- end}} -{{- if .SendClientInfo}} -add-mac -{{- end}} -` - -const merlinDNSMasqPostConfPath = "/jffs/scripts/dnsmasq.postconf" -const merlinDNSMasqPostConfMarker = `# GENERATED BY ctrld - EOF` - -const merlinDNSMasqPostConfTmpl = `# 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 - pc_append "server=127.0.0.1#5354" "$config_file" # use ctrld as upstream - {{- if .SendClientInfo}} - pc_append "add-mac" "$config_file" # add client mac - {{- end}} - pc_delete "dnssec" "$config_file" # disable DNSSEC - pc_delete "trust-anchor=" "$config_file" # disable DNSSEC - - # 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 dnsmasqUpstream struct { - Ip string - Port int -} - -func dnsMasqConf() (string, error) { - var sb strings.Builder - var tmplText string - switch Name() { - case DDWrt, EdgeOS, Firewalla, OpenWrt, Ubios, Synology, Tomato: - tmplText = dnsMasqConfigContentTmpl - case Merlin: - tmplText = merlinDNSMasqPostConfTmpl - } - tmpl := template.Must(template.New("").Parse(tmplText)) - upstreams := []dnsmasqUpstream{{ListenIP(), ListenPort()}} - if Name() == Firewalla { - if fu := firewallaDnsmasqUpstreams(); len(fu) > 0 { - upstreams = fu - } - } - var to = &struct { - SendClientInfo bool - Upstreams []dnsmasqUpstream - }{ - SendClientInfo: routerPlatform.Load().sendClientInfo, - Upstreams: upstreams, - } - if err := tmpl.Execute(&sb, to); err != nil { - return "", err - } - return sb.String(), nil -} - -func restartDNSMasq() error { - switch Name() { - case DDWrt: - return ddwrtRestartDNSMasq() - case EdgeOS: - return edgeOSRestartDNSMasq() - case Firewalla: - return firewallaRestartDNSMasq() - case Merlin: - return merlinRestartDNSMasq() - case OpenWrt: - return openwrtRestartDNSMasq() - case Ubios: - return ubiosRestartDNSMasq() - case Synology: - return synologyRestartDNSMasq() - case Tomato: - return tomatoRestartService(tomatoDNSMasqSvcName) - } - panic("not supported platform") -} diff --git a/internal/router/dnsmasq/dnsmasq.go b/internal/router/dnsmasq/dnsmasq.go new file mode 100644 index 0000000..6089c43 --- /dev/null +++ b/internal/router/dnsmasq/dnsmasq.go @@ -0,0 +1,115 @@ +package dnsmasq + +import ( + "html/template" + "net" + "path/filepath" + "strings" + + "github.com/Control-D-Inc/ctrld" +) + +const ConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY +no-resolv +{{- range .Upstreams}} +server={{ .Ip }}#{{ .Port }} +{{- end}} +{{- if .SendClientInfo}} +add-mac +{{- end}} +` + +const 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}} + {{- if .SendClientInfo}} + pc_append "add-mac" "$config_file" # add client mac + {{- end}} + pc_delete "dnssec" "$config_file" # disable DNSSEC + pc_delete "trust-anchor=" "$config_file" # disable DNSSEC + + # 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 +} + +func ConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) { + upstreams := make([]Upstream, 0, len(cfg.Listener)) + for _, listener := range cfg.Listener { + upstreams = append(upstreams, Upstream{Ip: listener.IP, Port: listener.Port}) + } + return confTmpl(tmplText, upstreams, cfg.HasUpstreamSendClientInfo()) +} + +func FirewallaConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) { + if lc := cfg.Listener["0"]; lc != nil && lc.IP == "0.0.0.0" { + return confTmpl(tmplText, firewallaUpstreams(lc.Port), cfg.HasUpstreamSendClientInfo()) + } + return ConfTmpl(tmplText, cfg) +} + +func confTmpl(tmplText string, upstreams []Upstream, sendClientInfo bool) (string, error) { + tmpl := template.Must(template.New("").Parse(tmplText)) + var to = &struct { + SendClientInfo bool + Upstreams []Upstream + }{ + SendClientInfo: sendClientInfo, + Upstreams: upstreams, + } + var sb strings.Builder + if err := tmpl.Execute(&sb, to); err != nil { + return "", err + } + return sb.String(), nil +} + +func firewallaUpstreams(port int) []Upstream { + matches, err := filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf") + if err != nil { + return nil + } + upstreams := make([]Upstream, 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 { + 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 +} diff --git a/internal/router/dummy.go b/internal/router/dummy.go new file mode 100644 index 0000000..71d7a82 --- /dev/null +++ b/internal/router/dummy.go @@ -0,0 +1,37 @@ +package router + +import "github.com/kardianos/service" + +type dummy struct{} + +func NewDummyRouter() Router { + return &dummy{} +} + +func (d *dummy) ConfigureService(_ *service.Config) error { + return nil +} + +func (d *dummy) Install(_ *service.Config) error { + return nil +} + +func (d *dummy) Uninstall(_ *service.Config) error { + return nil +} + +func (d *dummy) PreRun() error { + return nil +} + +func (d *dummy) Configure() error { + return nil +} + +func (d *dummy) Setup() error { + return nil +} + +func (d *dummy) Cleanup() error { + return nil +} diff --git a/internal/router/edgeos.go b/internal/router/edgeos/edgeos.go similarity index 52% rename from internal/router/edgeos.go rename to internal/router/edgeos/edgeos.go index f447608..c6b1e3c 100644 --- a/internal/router/edgeos.go +++ b/internal/router/edgeos/edgeos.go @@ -1,4 +1,4 @@ -package router +package edgeos import ( "bufio" @@ -7,52 +7,91 @@ import ( "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 = "edgeos" edgeOSDNSMasqConfigPath = "/etc/dnsmasq.d/dnsmasq-zzz-ctrld.conf" - UsgDNSMasqConfigPath = "/etc/dnsmasq.conf" - UsgDNSMasqBackupConfigPath = "/etc/dnsmasq.conf.bak" + 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" ) -var ( +var ErrContentFilteringEnabled = fmt.Errorf(`the "Content Filtering" feature" is enabled, which is conflicted with ctrld.\n +To disable it, folowing instruction here: %s`, toggleContentFilteringLink) + +type EdgeOS struct { + cfg *ctrld.Config isUSG bool -) - -func setupEdgeOS() error { - if isUSG { - return setupUSG() - } - return setupUDM() } -func setupUDM() error { - // Disable dnsmasq as DNS server. - dnsMasqConfigContent, err := dnsMasqConf() - if err != nil { - return fmt.Errorf("setupUDM: generating dnsmasq config: %w", err) - } - if err := os.WriteFile(edgeOSDNSMasqConfigPath, []byte(dnsMasqConfigContent), 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) +// 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 } return nil } -func setupUSG() error { +func (e *EdgeOS) Uninstall(_ *service.Config) error { + return nil +} + +func (e *EdgeOS) PreRun() error { + return nil +} + +func (e *EdgeOS) Configure() error { + e.cfg.Listener["0"].IP = "127.0.0.1" + e.cfg.Listener["0"].Port = 5354 + return nil +} + +func (e *EdgeOS) Setup() error { + if e.isUSG { + return e.setupUSG() + } + return e.setupUDM() +} + +func (e *EdgeOS) Cleanup() error { + 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) + 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 { + if err := os.WriteFile(usgDNSMasqBackupConfigPath, buf, 0600); err != nil { return fmt.Errorf("setupUSG: backup current config: %w", err) } @@ -70,14 +109,13 @@ func setupUSG() error { sb.WriteString(line) } - // Adding ctrld listener as the only upstream. - dnsMasqConfigContent, err := dnsMasqConf() + data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, e.cfg) if err != nil { - return fmt.Errorf("setupUSG: generating dnsmasq config: %w", err) + return err } sb.WriteString("\n") - sb.WriteString(dnsMasqConfigContent) - if err := os.WriteFile(UsgDNSMasqConfigPath, []byte(sb.String()), 0644); err != nil { + sb.WriteString(data) + if err := os.WriteFile(usgDNSMasqConfigPath, []byte(sb.String()), 0644); err != nil { return fmt.Errorf("setupUSG: writing dnsmasq config: %w", err) } @@ -88,14 +126,33 @@ func setupUSG() error { return nil } -func cleanupEdgeOS() error { - if isUSG { - return cleanupUSG() +func (e *EdgeOS) setupUDM() error { + data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, e.cfg) + if err != nil { + return err } - return cleanupUDM() + 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 cleanupUDM() error { +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) @@ -107,30 +164,17 @@ func cleanupUDM() error { return nil } -func 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 ContentFilteringEnabled() bool { + st, err := os.Stat("/run/dnsfilter/dnsfilter") + return err == nil && !st.IsDir() } -func postInstallEdgeOS() 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 - } - return nil +func checkUSG() bool { + out, _ := exec.Command("mca-cli-op", "info").Output() + return bytes.Contains(out, []byte("UniFi-Gateway-")) } -func edgeOSRestartDNSMasq() error { +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) } diff --git a/internal/router/firewalla.go b/internal/router/firewalla.go deleted file mode 100644 index 7e81b24..0000000 --- a/internal/router/firewalla.go +++ /dev/null @@ -1,106 +0,0 @@ -package router - -import ( - "fmt" - "net" - "os" - "os/exec" - "path/filepath" - "strings" -) - -const ( - 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" -) - -func setupFirewalla() error { - dnsMasqConfigContent, err := dnsMasqConf() - if err != nil { - return fmt.Errorf("setupFirewalla: generating dnsmasq config: %w", err) - } - if err := os.WriteFile(firewallaDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil { - return fmt.Errorf("setupFirewalla: writing ctrld config: %w", err) - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("setupFirewalla: restartDNSMasq: %w", err) - } - - return nil -} - -func cleanupFirewalla() error { - // Removing current config. - if err := os.Remove(firewallaDNSMasqConfigPath); err != nil { - return fmt.Errorf("cleanupFirewalla: removing ctrld config: %w", err) - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("cleanupFirewalla: restartDNSMasq: %w", err) - } - - return nil -} - -func postInstallFirewalla() error { - // Writing startup script. - if err := writeFirewallStartupScript(); err != nil { - return fmt.Errorf("postInstallFirewalla: writing startup script: %w", err) - } - return nil -} - -func postUninstallFirewalla() error { - // Removing startup script. - if err := os.Remove(firewallaCtrldInitScriptPath); err != nil { - return fmt.Errorf("postUninstallFirewalla: removing startup script: %w", err) - } - return nil -} - -func firewallaRestartDNSMasq() error { - return exec.Command("systemctl", "restart", "firerouter_dns").Run() -} - -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 firewallaDnsmasqUpstreams() []dnsmasqUpstream { - matches, err := filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf") - if err != nil { - return nil - } - upstreams := make([]dnsmasqUpstream, 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 { - addrs, _ := netIface.Addrs() - for _, addr := range addrs { - if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { - upstreams = append(upstreams, dnsmasqUpstream{ - Ip: netIP.IP.To4().String(), - Port: ListenPort(), - }) - } - } - } - } - return upstreams -} diff --git a/internal/router/firewalla/firewalla.go b/internal/router/firewalla/firewalla.go new file mode 100644 index 0000000..fd4635f --- /dev/null +++ b/internal/router/firewalla/firewalla.go @@ -0,0 +1,110 @@ +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) Configure() error { + f.cfg.Listener["0"].IP = "0.0.0.0" + f.cfg.Listener["0"].Port = 5354 + return nil +} + +func (f *Firewalla) Setup() error { + 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 { + // 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.go b/internal/router/merlin.go deleted file mode 100644 index 8e20d68..0000000 --- a/internal/router/merlin.go +++ /dev/null @@ -1,89 +0,0 @@ -package router - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "strings" - "unicode" -) - -func setupMerlin() error { - buf, err := os.ReadFile(merlinDNSMasqPostConfPath) - // Already setup. - if bytes.Contains(buf, []byte(merlinDNSMasqPostConfMarker)) { - return nil - } - if err != nil && !os.IsNotExist(err) { - return err - } - - merlinDNSMasqPostConf, err := dnsMasqConf() - if err != nil { - return err - } - data := strings.Join([]string{ - merlinDNSMasqPostConf, - "\n", - merlinDNSMasqPostConfMarker, - "\n", - string(buf), - }, "\n") - // Write dnsmasq post conf file. - if err := os.WriteFile(merlinDNSMasqPostConfPath, []byte(data), 0750); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - - if err := nvramSetKV(nvramSetupKV(), nvramCtrldSetupKey); err != nil { - return err - } - - return nil -} - -func cleanupMerlin() error { - // Restore old configs. - if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil { - return err - } - buf, err := os.ReadFile(merlinDNSMasqPostConfPath) - if err != nil && !os.IsNotExist(err) { - return err - } - // Restore dnsmasq post conf file. - if err := os.WriteFile(merlinDNSMasqPostConfPath, merlinParsePostConf(buf), 0750); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func postInstallMerlin() error { - return nil -} - -func merlinRestartDNSMasq() error { - if out, err := exec.Command("service", "restart_dnsmasq").CombinedOutput(); err != nil { - return fmt.Errorf("restart_dnsmasq: %s, %w", string(out), err) - } - return nil -} - -func merlinParsePostConf(buf []byte) []byte { - if len(buf) == 0 { - return nil - } - parts := bytes.Split(buf, []byte(merlinDNSMasqPostConfMarker)) - if len(parts) != 1 { - return bytes.TrimLeftFunc(parts[1], unicode.IsSpace) - } - return buf -} diff --git a/internal/router/merlin/merlin.go b/internal/router/merlin/merlin.go new file mode 100644 index 0000000..9e84298 --- /dev/null +++ b/internal/router/merlin/merlin.go @@ -0,0 +1,133 @@ +package merlin + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strings" + "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" + +var nvramKvMap = map[string]string{ + "dnspriv_enable": "0", // Ensure Merlin native DoT disabled. +} + +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} +} + +func (m *Merlin) ConfigureService(config *service.Config) error { + return nil +} + +func (m *Merlin) Install(_ *service.Config) error { + return nil +} + +func (m *Merlin) Uninstall(_ *service.Config) error { + return nil +} + +func (m *Merlin) PreRun() error { + _ = m.Cleanup() + return ntp.Wait() +} + +func (m *Merlin) Configure() error { + m.cfg.Listener["0"].IP = "127.0.0.1" + m.cfg.Listener["0"].Port = 5354 + return nil +} + +func (m *Merlin) Setup() 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. + if err := os.WriteFile(dnsmasq.MerlinPostConfPath, []byte(data), 0750); 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 (m *Merlin) Cleanup() error { + if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { + // 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 + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +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 +} + +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 +} diff --git a/internal/router/merlin_test.go b/internal/router/merlin/merlin_test.go similarity index 83% rename from internal/router/merlin_test.go rename to internal/router/merlin/merlin_test.go index e1715af..057628c 100644 --- a/internal/router/merlin_test.go +++ b/internal/router/merlin/merlin_test.go @@ -1,17 +1,19 @@ -package router +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{ - merlinDNSMasqPostConfTmpl, + dnsmasq.MerlinPostConfTmpl, "\n", - merlinDNSMasqPostConfMarker, + dnsmasq.MerlinPostConfMarker, "\n", }, "\n") diff --git a/internal/router/ntp/ntp.go b/internal/router/ntp/ntp.go new file mode 100644 index 0000000..9854fcf --- /dev/null +++ b/internal/router/ntp/ntp.go @@ -0,0 +1,26 @@ +package ntp + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/Control-D-Inc/ctrld/internal/router/nvram" + "tailscale.com/logtail/backoff" +) + +func Wait() error { + // Wait until `ntp_ready=1` set. + b := backoff.NewBackoff("ntp.Wait", func(format string, args ...any) {}, 10*time.Second) + for { + out, err := nvram.Run("get", "ntp_ready") + if err != nil { + return fmt.Errorf("PreStart: nvram: %w", err) + } + if out == "1" { + return nil + } + b.BackOff(context.Background(), errors.New("ntp not ready")) + } +} diff --git a/internal/router/nvram.go b/internal/router/nvram.go deleted file mode 100644 index de3400e..0000000 --- a/internal/router/nvram.go +++ /dev/null @@ -1,110 +0,0 @@ -package router - -import ( - "bytes" - "fmt" - "os/exec" - "strings" -) - -func nvram(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 -*/ -func nvramSetupKV() map[string]string { - switch Name() { - case DDWrt: - return 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. - } - case Merlin: - return map[string]string{ - "dnspriv_enable": "0", // Ensure Merlin native DoT disabled. - } - case Tomato: - return 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 - } - } - return nil -} - -func nvramInstallKV() map[string]string { - switch Name() { - case Tomato: - return map[string]string{ - tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method. - } - } - return nil -} - -func nvramSetKV(m map[string]string, setupKey string) error { - // Backup current value, store ctrld's configs. - for key, value := range m { - old, err := nvram("get", key) - if err != nil { - return fmt.Errorf("%s: %w", old, err) - } - if out, err := nvram("set", nvramCtrldKeyPrefix+key+"="+old); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - if out, err := nvram("set", key+"="+value); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - } - - if out, err := nvram("set", setupKey+"=1"); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - // Commit. - if out, err := nvram("commit"); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - return nil -} - -func nvramRestore(m map[string]string, setupKey string) error { - // Restore old configs. - for key := range m { - ctrldKey := nvramCtrldKeyPrefix + key - old, err := nvram("get", ctrldKey) - if err != nil { - return fmt.Errorf("%s: %w", old, err) - } - _, _ = nvram("unset", ctrldKey) - if out, err := nvram("set", key+"="+old); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - } - - if out, err := nvram("unset", setupKey); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - // Commit. - if out, err := nvram("commit"); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - return nil -} diff --git a/internal/router/nvram/nvram.go b/internal/router/nvram/nvram.go new file mode 100644 index 0000000..e76c017 --- /dev/null +++ b/internal/router/nvram/nvram.go @@ -0,0 +1,89 @@ +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.go b/internal/router/openwrt/openwrt.go similarity index 54% rename from internal/router/openwrt.go rename to internal/router/openwrt/openwrt.go index afc25ae..bd08b12 100644 --- a/internal/router/openwrt.go +++ b/internal/router/openwrt/openwrt.go @@ -1,4 +1,4 @@ -package router +package openwrt import ( "bytes" @@ -7,42 +7,64 @@ import ( "os" "os/exec" "strings" + + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + + "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld" +) + +const ( + Name = "openwrt" + openwrtDNSMasqConfigPath = "/tmp/dnsmasq.d/ctrld.conf" ) var errUCIEntryNotFound = errors.New("uci: Entry not found") -const openwrtDNSMasqConfigPath = "/tmp/dnsmasq.d/ctrld.conf" - -// IsGLiNet reports whether the router is an GL.iNet router. -func IsGLiNet() bool { - if Name() != OpenWrt { - return false - } - buf, _ := os.ReadFile("/proc/version") - // The output of /proc/version contains "(glinet@glinet)". - return bytes.Contains(buf, []byte(" (glinet")) +type Openwrt struct { + cfg *ctrld.Config } -// 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 { - return false - } - cmd, _ := exec.LookPath("service") - return cmd == "" +// New returns a router.Router for configuring/setup/run ctrld on Openwrt routers. +func New(cfg *ctrld.Config) *Openwrt { + return &Openwrt{cfg: cfg} } -func setupOpenWrt() error { +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) Configure() error { + o.cfg.Listener["0"].IP = "127.0.0.1" + o.cfg.Listener["0"].Port = 5354 + return nil +} + +func (o *Openwrt) Setup() error { // Delete dnsmasq port if set. if _, err := uci("delete", "dhcp.@dnsmasq[0].port"); err != nil && !errors.Is(err, errUCIEntryNotFound) { return err } - dnsMasqConfigContent, err := dnsMasqConf() + + data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, o.cfg) if err != nil { return err } - if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil { + if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(data), 0600); err != nil { return err } // Commit. @@ -56,7 +78,7 @@ func setupOpenWrt() error { return nil } -func cleanupOpenWrt() error { +func (o *Openwrt) Cleanup() error { // Remove the custom dnsmasq config if err := os.Remove(openwrtDNSMasqConfigPath); err != nil { return err @@ -68,8 +90,11 @@ func cleanupOpenWrt() error { return nil } -func postInstallOpenWrt() error { - return exec.Command("/etc/init.d/ctrld", "enable").Run() +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 } func uci(args ...string) (string, error) { @@ -85,10 +110,3 @@ func uci(args ...string) (string, error) { } return strings.TrimSpace(stdout.String()), nil } - -func openwrtRestartDNSMasq() error { - if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil { - return fmt.Errorf("%s: %w", string(out), err) - } - return nil -} diff --git a/internal/router/procd.go b/internal/router/openwrt/procd.go similarity index 97% rename from internal/router/procd.go rename to internal/router/openwrt/procd.go index d363f39..8e74461 100644 --- a/internal/router/procd.go +++ b/internal/router/openwrt/procd.go @@ -1,4 +1,4 @@ -package router +package openwrt const openWrtScript = `#!/bin/sh /etc/rc.common USE_PROCD=1 diff --git a/internal/router/pfsense.go b/internal/router/pfsense/pfsense.go similarity index 55% rename from internal/router/pfsense.go rename to internal/router/pfsense/pfsense.go index 3818a58..d724c23 100644 --- a/internal/router/pfsense.go +++ b/internal/router/pfsense/pfsense.go @@ -1,4 +1,4 @@ -package router +package pfsense import ( "fmt" @@ -6,46 +6,18 @@ import ( "os/exec" "path/filepath" + "github.com/Control-D-Inc/ctrld" "github.com/kardianos/service" ) const ( + Name = "pfsens" + rcPath = "/usr/local/etc/rc.d" unboundRcPath = rcPath + "/unbound" dnsmasqRcPath = rcPath + "/dnsmasq" ) -func setupPfsense() error { - // If Pfsense is in DNS Resolver mode, ensure no unbound processes running. - _ = exec.Command("killall", "unbound").Run() - - // If Pfsense is in DNS Forwarder mode, ensure no dnsmasq processes running. - _ = exec.Command("killall", "dnsmasq").Run() - return nil -} - -func cleanupPfsense(svc *service.Config) error { - if err := os.Remove(filepath.Join(rcPath, svc.Name+".sh")); err != nil { - return fmt.Errorf("os.Remove: %w", err) - } - _ = exec.Command(unboundRcPath, "onerestart").Run() - _ = exec.Command(dnsmasqRcPath, "onerestart").Run() - - return nil -} - -func postInstallPfsense(svc *service.Config) error { - // 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, svc.Name) - newname := filepath.Join(rcPath, svc.Name+".sh") - _ = os.Remove(newname) - if err := os.Symlink(oldname, newname); err != nil { - return fmt.Errorf("os.Symlink: %w", err) - } - return nil -} - const pfsenseInitScript = `#!/bin/sh # PROVIDE: {{.Name}} @@ -64,3 +36,56 @@ command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}" run_rc_command "$1" ` + +type Pfsense struct { + cfg *ctrld.Config + svcName string +} + +// New returns a router.Router for configuring/setup/run ctrld on Pfsense routers. +func New(cfg *ctrld.Config) *Pfsense { + return &Pfsense{cfg: cfg} +} + +func (p *Pfsense) ConfigureService(svc *service.Config) error { + svc.Option["SysvScript"] = pfsenseInitScript + p.svcName = svc.Name + return nil +} + +func (p *Pfsense) Install(config *service.Config) error { + return nil +} + +func (p *Pfsense) Uninstall(config *service.Config) error { + return nil +} + +func (p *Pfsense) PreRun() error { + return nil +} + +func (p *Pfsense) Configure() error { + p.cfg.Listener["0"].IP = "127.0.0.1" + p.cfg.Listener["0"].Port = 53 + return nil +} + +func (p *Pfsense) Setup() error { + // If Pfsense is in DNS Resolver mode, ensure no unbound processes running. + _ = exec.Command("killall", "unbound").Run() + + // If Pfsense is in DNS Forwarder mode, ensure no dnsmasq processes running. + _ = exec.Command("killall", "dnsmasq").Run() + return nil +} + +func (p *Pfsense) Cleanup() error { + if err := os.Remove(filepath.Join(rcPath, p.svcName+".sh")); err != nil { + return fmt.Errorf("os.Remove: %w", err) + } + _ = exec.Command(unboundRcPath, "onerestart").Run() + _ = exec.Command(dnsmasqRcPath, "onerestart").Run() + + return nil +} diff --git a/internal/router/router.go b/internal/router/router.go index b4575b0..257e3b4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -2,34 +2,90 @@ package router import ( "bytes" - "context" - "errors" - "fmt" "os" "os/exec" "sync/atomic" - "time" "github.com/kardianos/service" - "tailscale.com/logtail/backoff" "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" + "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" + "github.com/Control-D-Inc/ctrld/internal/router/openwrt" + "github.com/Control-D-Inc/ctrld/internal/router/pfsense" + "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" ) -const ( - DDWrt = "ddwrt" - EdgeOS = "edgeos" - Firewalla = "firewalla" - Merlin = "merlin" - OpenWrt = "openwrt" - Pfsense = "pfsense" - Synology = "synology" - Tomato = "tomato" - Ubios = "ubios" -) +// Service is the interface to manage ctrld service on router. +type Service interface { + ConfigureService(*service.Config) error + Install(*service.Config) error + Uninstall(*service.Config) error +} -// ErrNotSupported reports the current router is not supported error. -var ErrNotSupported = errors.New("unsupported platform") +// Config is the interface to manage ctrld config on router. +type Config interface { + Configure() error +} + +// Router is the interface for managing ctrld running on router. +type Router interface { + Service + Config + + PreRun() error + Setup() error + Cleanup() error +} + +// New returns new Router interface. +func New(cfg *ctrld.Config) 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 pfsense.Name: + return pfsense.New(cfg) + case firewalla.Name: + return firewalla.New(cfg) + } + return NewDummyRouter() +} + +// 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 == "" +} var routerPlatform atomic.Pointer[router] @@ -41,15 +97,15 @@ type router struct { // IsSupported reports whether the given platform is supported by ctrld. func IsSupported(platform string) bool { switch platform { - case DDWrt, - EdgeOS, - Firewalla, - Merlin, - OpenWrt, - Pfsense, - Synology, - Tomato, - Ubios: + case ddwrt.Name, + edgeos.Name, + firewalla.Name, + merlin.Name, + openwrt.Name, + pfsense.Name, + synology.Name, + tomato.Name, + ubios.Name: return true } return false @@ -58,193 +114,18 @@ func IsSupported(platform string) bool { // SupportedPlatforms return all platforms that can be configured to run with ctrld. func SupportedPlatforms() []string { return []string{ - DDWrt, - EdgeOS, - Firewalla, - Merlin, - OpenWrt, - Pfsense, - Synology, - Tomato, - Ubios, + ddwrt.Name, + edgeos.Name, + firewalla.Name, + merlin.Name, + openwrt.Name, + pfsense.Name, + synology.Name, + tomato.Name, + ubios.Name, } } -var configureFunc = map[string]func() error{ - DDWrt: setupDDWrt, - EdgeOS: setupEdgeOS, - Firewalla: setupFirewalla, - Merlin: setupMerlin, - OpenWrt: setupOpenWrt, - Pfsense: setupPfsense, - Synology: setupSynology, - Tomato: setupTomato, - Ubios: setupUbiOS, -} - -// Configure configures things for running ctrld on the router. -func Configure(c *ctrld.Config) error { - name := Name() - switch name { - case DDWrt, - EdgeOS, - Firewalla, - Merlin, - OpenWrt, - Pfsense, - Synology, - Tomato, - Ubios: - if c.HasUpstreamSendClientInfo() { - r := routerPlatform.Load() - r.sendClientInfo = true - } - configure := configureFunc[name] - if err := configure(); err != nil { - return err - } - return nil - default: - return ErrNotSupported - } -} - -// ConfigureService performs necessary setup for running ctrld as a service on router. -func ConfigureService(sc *service.Config) error { - name := Name() - switch name { - case DDWrt: - if !ddwrtJff2Enabled() { - return errDdwrtJffs2NotEnabled - } - case OpenWrt: - sc.Option["SysvScript"] = openWrtScript - case Pfsense: - sc.Option["SysvScript"] = pfsenseInitScript - case EdgeOS, Firewalla, Merlin, Synology, Tomato, Ubios: - } - return nil -} - -// PreRun blocks until the router is ready for running ctrld. -func PreRun(svc *service.Config) (err error) { - // On some routers, NTP may out of sync, so waiting for it to be ready. - switch Name() { - case DDWrt, Merlin, Tomato: - // Cleanup router to ensure valid DNS for NTP synchronization. - _ = Cleanup(svc) - - // Wait until `ntp_ready=1` set. - b := backoff.NewBackoff("PreRun", func(format string, args ...any) {}, 10*time.Second) - for { - out, err := nvram("get", "ntp_ready") - if err != nil { - return fmt.Errorf("PreStart: nvram: %w", err) - } - if out == "1" { - return nil - } - b.BackOff(context.Background(), errors.New("ntp not ready")) - } - default: - return nil - } -} - -// PostInstall performs task after installing ctrld on router. -func PostInstall(svc *service.Config) error { - name := Name() - switch name { - case DDWrt: - return postInstallDDWrt() - case EdgeOS: - return postInstallEdgeOS() - case Firewalla: - return postInstallFirewalla() - case Merlin: - return postInstallMerlin() - case OpenWrt: - return postInstallOpenWrt() - case Pfsense: - return postInstallPfsense(svc) - case Synology: - return postInstallSynology() - case Tomato: - return postInstallTomato() - case Ubios: - return postInstallUbiOS() - } - return nil -} - -// PostUninstall performs task after uninstalling ctrld on router. -func PostUninstall(svc *service.Config) error { - name := Name() - switch name { - case DDWrt: - case EdgeOS: - case Firewalla: - return postUninstallFirewalla() - case Merlin: - case OpenWrt: - case Pfsense: - case Synology: - case Tomato: - case Ubios: - } - return nil -} - -// Cleanup cleans ctrld setup on the router. -func Cleanup(svc *service.Config) error { - name := Name() - switch name { - case DDWrt: - return cleanupDDWrt() - case EdgeOS: - return cleanupEdgeOS() - case Firewalla: - return cleanupFirewalla() - case Merlin: - return cleanupMerlin() - case OpenWrt: - return cleanupOpenWrt() - case Pfsense: - return cleanupPfsense(svc) - case Synology: - return cleanupSynology() - case Tomato: - return cleanupTomato() - case Ubios: - return cleanupUbiOS() - } - return nil -} - -// ListenIP returns the listener IP of ctrld on router. -func ListenIP() string { - name := Name() - switch name { - case Firewalla: - // Firewalla excepts 127.0.0.1 in all interfaces config. So we need to listen on all interfaces, - // making dnsmasq to be able to forward DNS query to specific interface based on VLAN config. - return "0.0.0.0" - } - return "127.0.0.1" -} - -// ListenPort returns the listener port of ctrld on router. -func ListenPort() int { - name := Name() - switch name { - case EdgeOS, DDWrt, Firewalla, Merlin, OpenWrt, Synology, Tomato, Ubios: - return 5354 - case Pfsense: - // On pfsense, we run ctrld as DNS resolver. - } - return 53 -} - // Name returns name of the router platform. func Name() string { if r := routerPlatform.Load(); r != nil { @@ -259,27 +140,25 @@ func Name() string { func distroName() string { switch { case bytes.HasPrefix(unameO(), []byte("DD-WRT")): - return DDWrt + return ddwrt.Name case bytes.HasPrefix(unameO(), []byte("ASUSWRT-Merlin")): - return Merlin + return merlin.Name case haveFile("/etc/openwrt_version"): - return OpenWrt + return openwrt.Name case haveDir("/data/unifi"): - return Ubios + return ubios.Name case bytes.HasPrefix(unameU(), []byte("synology")): - return Synology + return synology.Name case bytes.HasPrefix(unameO(), []byte("Tomato")): - return Tomato + return tomato.Name case haveDir("/config/scripts/post-config.d"): - checkUSG() - return EdgeOS + return edgeos.Name case haveFile("/etc/ubnt/init/vyatta-router"): - checkUSG() - return EdgeOS // For 2.x + return edgeos.Name // For 2.x case isPfsense(): - return Pfsense + return pfsense.Name case haveFile("/etc/firewalla_release"): - return Firewalla + return firewalla.Name } return "" } @@ -308,8 +187,3 @@ func isPfsense() bool { b, err := os.ReadFile("/etc/platform") return err == nil && bytes.HasPrefix(b, []byte("pfSense")) } - -func checkUSG() { - out, _ := exec.Command("mca-cli-op", "info").Output() - isUSG = bytes.Contains(out, []byte("UniFi-Gateway-")) -} diff --git a/internal/router/service.go b/internal/router/service.go index d9476e9..3333964 100644 --- a/internal/router/service.go +++ b/internal/router/service.go @@ -6,13 +6,18 @@ import ( "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 }, + detect: func() bool { return Name() == ddwrt.Name }, interactive: func() bool { is, _ := isInteractive() return is @@ -21,7 +26,7 @@ func init() { }, &linuxSystemService{ name: "merlin", - detect: func() bool { return Name() == Merlin }, + detect: func() bool { return Name() == merlin.Name }, interactive: func() bool { is, _ := isInteractive() return is @@ -31,7 +36,7 @@ func init() { &linuxSystemService{ name: "ubios", detect: func() bool { - if Name() != Ubios { + if Name() != ubios.Name { return false } out, err := exec.Command("ubnt-device-info", "firmware").CombinedOutput() @@ -50,7 +55,7 @@ func init() { }, &linuxSystemService{ name: "tomato", - detect: func() bool { return Name() == Tomato }, + detect: func() bool { return Name() == tomato.Name }, interactive: func() bool { is, _ := isInteractive() return is diff --git a/internal/router/service_ddwrt.go b/internal/router/service_ddwrt.go index ac177f9..3e8b9bf 100644 --- a/internal/router/service_ddwrt.go +++ b/internal/router/service_ddwrt.go @@ -12,6 +12,8 @@ import ( "text/template" "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld/internal/router/nvram" ) type ddwrtSvc struct { @@ -94,19 +96,19 @@ func (s *ddwrtSvc) Install() error { return err } s.rcStartup = sb.String() - curVal, err := nvram("get", nvramRCStartupKey) + curVal, err := nvram.Run("get", nvram.RCStartupKey) if err != nil { return err } - if _, err := nvram("set", nvramCtrldKeyPrefix+nvramRCStartupKey+"="+curVal); err != nil { + 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("set", nvramRCStartupKey+"="+val); err != nil { + if _, err := nvram.Run("set", nvram.RCStartupKey+"="+val); err != nil { return err } - if out, err := nvram("commit"); err != nil { + if out, err := nvram.Run("commit"); err != nil { return fmt.Errorf("%s: %w", out, err) } @@ -118,16 +120,16 @@ func (s *ddwrtSvc) Uninstall() error { return err } - ctrldStartupKey := nvramCtrldKeyPrefix + nvramRCStartupKey - rcStartup, err := nvram("get", ctrldStartupKey) + ctrldStartupKey := nvram.CtrldKeyPrefix + nvram.RCStartupKey + rcStartup, err := nvram.Run("get", ctrldStartupKey) if err != nil { return err } - _, _ = nvram("unset", ctrldStartupKey) - if _, err := nvram("set", nvramRCStartupKey+"="+rcStartup); err != nil { + _, _ = nvram.Run("unset", ctrldStartupKey) + if _, err := nvram.Run("set", nvram.RCStartupKey+"="+rcStartup); err != nil { return err } - if out, err := nvram("commit"); err != nil { + if out, err := nvram.Run("commit"); err != nil { return fmt.Errorf("%s: %w", out, err) } diff --git a/internal/router/service_merlin.go b/internal/router/service_merlin.go index 3878c71..9273eca 100644 --- a/internal/router/service_merlin.go +++ b/internal/router/service_merlin.go @@ -13,6 +13,8 @@ import ( "text/template" "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld/internal/router/nvram" ) const ( @@ -67,10 +69,10 @@ func (s *merlinSvc) Install() error { if !strings.HasPrefix(exePath, "/jffs/") { return errors.New("could not install service outside /jffs") } - if _, err := nvram("set", "jffs2_scripts=1"); err != nil { + if _, err := nvram.Run("set", "jffs2_scripts=1"); err != nil { return err } - if _, err := nvram("commit"); err != nil { + if _, err := nvram.Run("commit"); err != nil { return err } diff --git a/internal/router/service_tomato.go b/internal/router/service_tomato.go index 8b7590c..aa96d4b 100644 --- a/internal/router/service_tomato.go +++ b/internal/router/service_tomato.go @@ -12,6 +12,8 @@ import ( "text/template" "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld/internal/router/nvram" ) const tomatoNvramScriptWanupKey = "script_wanup" @@ -63,10 +65,10 @@ func (s *tomatoSvc) Install() error { if !strings.HasPrefix(exePath, "/jffs/") { return errors.New("could not install service outside /jffs") } - if _, err := nvram("set", "jffs2_on=1"); err != nil { + if _, err := nvram.Run("set", "jffs2_on=1"); err != nil { return err } - if _, err := nvram("commit"); err != nil { + if _, err := nvram.Run("commit"); err != nil { return err } @@ -97,13 +99,15 @@ func (s *tomatoSvc) Install() error { return fmt.Errorf("os.Chmod: startup script: %w", err) } - nvramKvMap := nvramInstallKV() - old, err := nvram("get", tomatoNvramScriptWanupKey) + 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 := nvramSetKV(nvramKvMap, nvramCtrldInstallKey); err != nil { + if err := nvram.SetKV(nvramKvMap, nvram.CtrldInstallKey); err != nil { return err } return nil @@ -113,8 +117,11 @@ 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 := nvramRestore(nvramInstallKV(), nvramCtrldInstallKey); err != nil { + if err := nvram.Restore(nvramKvMap, nvram.CtrldInstallKey); err != nil { return err } return nil diff --git a/internal/router/service_ubios.go b/internal/router/service_ubios.go index 5c4d99d..0b49cd2 100644 --- a/internal/router/service_ubios.go +++ b/internal/router/service_ubios.go @@ -18,6 +18,9 @@ import ( // 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. +// Keep in sync with ubios.ubiosDNSMasqConfigPath +const ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf" + type ubiosSvc struct { i service.Interface platform string diff --git a/internal/router/synology.go b/internal/router/synology.go deleted file mode 100644 index 8c1d1d6..0000000 --- a/internal/router/synology.go +++ /dev/null @@ -1,55 +0,0 @@ -package router - -import ( - "fmt" - "os" - "os/exec" -) - -const ( - synologyDNSMasqConfigPath = "/etc/dhcpd/dhcpd-zzz-ctrld.conf" - synologyDhcpdInfoPath = "/etc/dhcpd/dhcpd-zzz-ctrld.info" -) - -func setupSynology() error { - dnsMasqConfigContent, err := dnsMasqConf() - if err != nil { - return err - } - if err := os.WriteFile(synologyDNSMasqConfigPath, []byte(dnsMasqConfigContent), 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 cleanupSynology() error { - // 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 postInstallSynology() error { - return nil -} - -func synologyRestartDNSMasq() 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 -} diff --git a/internal/router/synology/synology.go b/internal/router/synology/synology.go new file mode 100644 index 0000000..e1d51bd --- /dev/null +++ b/internal/router/synology/synology.go @@ -0,0 +1,88 @@ +package synology + +import ( + "fmt" + "os" + "os/exec" + + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + + "github.com/Control-D-Inc/ctrld" + "github.com/kardianos/service" +) + +const ( + Name = "synology" + + synologyDNSMasqConfigPath = "/etc/dhcpd/dhcpd-zzz-ctrld.conf" + synologyDhcpdInfoPath = "/etc/dhcpd/dhcpd-zzz-ctrld.info" +) + +type Synology struct { + cfg *ctrld.Config +} + +// New returns a router.Router for configuring/setup/run ctrld on Ubios routers. +func New(cfg *ctrld.Config) *Synology { + return &Synology{cfg: cfg} +} + +func (s *Synology) ConfigureService(config *service.Config) error { + 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 { + return nil +} + +func (s *Synology) Configure() error { + s.cfg.Listener["0"].IP = "127.0.0.1" + s.cfg.Listener["0"].Port = 5354 + return nil +} + +func (s *Synology) Setup() error { + 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 { + // 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 +} diff --git a/internal/router/tomato.go b/internal/router/tomato.go deleted file mode 100644 index 945e992..0000000 --- a/internal/router/tomato.go +++ /dev/null @@ -1,82 +0,0 @@ -package router - -import ( - "fmt" - "os/exec" -) - -const ( - tomatoDnsCryptProxySvcName = "dnscrypt-proxy" - tomatoStubbySvcName = "stubby" - tomatoDNSMasqSvcName = "dnsmasq" -) - -func setupTomato() error { - // Already setup. - if val, _ := nvram("get", nvramCtrldSetupKey); val == "1" { - return nil - } - - data, err := dnsMasqConf() - if err != nil { - return err - } - - nvramKvMap := nvramSetupKV() - nvramKvMap["dnsmasq_custom"] = data - if err := nvramSetKV(nvramKvMap, nvramCtrldSetupKey); 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 postInstallTomato() error { - return nil -} - -func cleanupTomato() error { - // Restore old configs. - if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); 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 -} diff --git a/internal/router/tomato/tomato.go b/internal/router/tomato/tomato.go new file mode 100644 index 0000000..937f8ba --- /dev/null +++ b/internal/router/tomato/tomato.go @@ -0,0 +1,131 @@ +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.Wait() +} + +func (f *FreshTomato) Configure() error { + f.cfg.Listener["0"].IP = "127.0.0.1" + f.cfg.Listener["0"].Port = 5354 + return nil +} + +func (f *FreshTomato) Setup() error { + // 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 val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { + 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.go b/internal/router/ubios.go deleted file mode 100644 index 48e5d41..0000000 --- a/internal/router/ubios.go +++ /dev/null @@ -1,73 +0,0 @@ -package router - -import ( - "bytes" - "fmt" - "os" - "strconv" -) - -var errContentFilteringEnabled = fmt.Errorf(`the "Content Filtering" feature" is enabled, which is conflicted with ctrld.\n -To disable it, folowing instruction here: %s`, toggleContentFilteringLink) - -const ( - ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf" - toggleContentFilteringLink = "https://community.ui.com/questions/UDM-Pro-disable-enable-DNS-filtering/e2cc4060-e56a-4139-b200-62d7f773ff8f" -) - -func setupUbiOS() error { - // Disable dnsmasq as DNS server. - dnsMasqConfigContent, err := dnsMasqConf() - if err != nil { - return err - } - if err := os.WriteFile(ubiosDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func cleanupUbiOS() error { - // Remove the custom dnsmasq config - if err := os.Remove(ubiosDNSMasqConfigPath); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func postInstallUbiOS() error { - // See comment in postInstallEdgeOS. - if contentFilteringEnabled() { - return errContentFilteringEnabled - } - return nil -} - -func ubiosRestartDNSMasq() error { - buf, err := os.ReadFile("/run/dnsmasq.pid") - 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() -} - -func contentFilteringEnabled() bool { - st, err := os.Stat("/run/dnsfilter/dnsfilter") - return err == nil && !st.IsDir() -} diff --git a/internal/router/ubios/ubios.go b/internal/router/ubios/ubios.go new file mode 100644 index 0000000..61ab8e0 --- /dev/null +++ b/internal/router/ubios/ubios.go @@ -0,0 +1,96 @@ +package ubios + +import ( + "bytes" + "os" + "strconv" + + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + + "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/router/edgeos" + "github.com/kardianos/service" +) + +const ( + Name = "ubios" + ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf" +) + +type Ubios struct { + cfg *ctrld.Config +} + +// New returns a router.Router for configuring/setup/run ctrld on Ubios routers. +func New(cfg *ctrld.Config) *Ubios { + return &Ubios{cfg: cfg} +} + +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 + } + return nil +} + +func (u *Ubios) Uninstall(_ *service.Config) error { + return nil +} + +func (u *Ubios) PreRun() error { + return nil +} + +func (u *Ubios) Configure() error { + u.cfg.Listener["0"].IP = "127.0.0.1" + u.cfg.Listener["0"].Port = 5354 + return nil +} + +func (u *Ubios) Setup() error { + data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, u.cfg) + if err != nil { + return err + } + if err := os.WriteFile(ubiosDNSMasqConfigPath, []byte(data), 0600); err != nil { + return err + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +func (u *Ubios) Cleanup() error { + // Remove the custom dnsmasq config + if err := os.Remove(ubiosDNSMasqConfigPath); err != nil { + return err + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +func restartDNSMasq() error { + buf, err := os.ReadFile("/run/dnsmasq.pid") + 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() +}