Removing router platforms support

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