all: refactor router code to use interface

So the code is more modular, easier to read/maintain.
This commit is contained in:
Cuong Manh Le
2023-06-28 20:08:52 +07:00
committed by Cuong Manh Le
parent 78a7c87ecc
commit aec2596262
33 changed files with 1347 additions and 1093 deletions

View File

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

View File

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

View File

@@ -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")

View File

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

View File

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

View File

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

View File

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

View File

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

37
internal/router/dummy.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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")

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package router
package openwrt
const openWrtScript = `#!/bin/sh /etc/rc.common
USE_PROCD=1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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