diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 00d964d..90c5484 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -186,7 +186,7 @@ func initCLI() { mainLog.Fatal().Msg("network is not up yet") } - p.router = router.New(&cfg) + p.router = router.New(&cfg, cdUID != "") cs, err := newControlServer(filepath.Join(homedir, ctrldControlUnixSock)) if err != nil { mainLog.Warn().Err(err).Msg("could not create control server") @@ -337,7 +337,7 @@ func initCLI() { sc.Arguments = append([]string{"run"}, osArgs...) p := &prog{ - router: router.New(&cfg), + router: router.New(&cfg, cdUID != ""), cfg: &cfg, } if err := p.router.ConfigureService(sc); err != nil { @@ -503,9 +503,9 @@ func initCLI() { Short: "Stop the ctrld service", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - tryReadingConfig(false) + readConfig(false) v.Unmarshal(&cfg) - p := &prog{router: router.New(&cfg)} + p := &prog{router: router.New(&cfg, cdUID != "")} s, err := newService(p, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) @@ -593,9 +593,9 @@ func initCLI() { NOTE: Uninstalling will set DNS to values provided by DHCP.`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - tryReadingConfig(false) + readConfig(false) v.Unmarshal(&cfg) - p := &prog{router: router.New(&cfg)} + p := &prog{router: router.New(&cfg, cdUID != "")} s, err := newService(p, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) @@ -1212,6 +1212,10 @@ func tryReadingConfig(writeDefaultConfig bool) { if !writeDefaultConfig { return } + readConfig(writeDefaultConfig) +} + +func readConfig(writeDefaultConfig bool) { configs := []struct { name string written bool diff --git a/internal/router/dummy.go b/internal/router/dummy.go deleted file mode 100644 index dea54e0..0000000 --- a/internal/router/dummy.go +++ /dev/null @@ -1,33 +0,0 @@ -package router - -import "github.com/kardianos/service" - -type dummy struct{} - -func (d *dummy) ConfigureService(_ *service.Config) error { - return nil -} - -func (d *dummy) Install(_ *service.Config) error { - return nil -} - -func (d *dummy) Uninstall(_ *service.Config) error { - return nil -} - -func (d *dummy) PreRun() error { - return nil -} - -func (d *dummy) Configure() error { - return nil -} - -func (d *dummy) Setup() error { - return nil -} - -func (d *dummy) Cleanup() error { - return nil -} diff --git a/internal/router/os_freebsd.go b/internal/router/os_freebsd.go new file mode 100644 index 0000000..9d9b738 --- /dev/null +++ b/internal/router/os_freebsd.go @@ -0,0 +1,142 @@ +package router + +import ( + "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=). + // 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.cfg.FirstListener().IsDirectDnsListener() { + _ = exec.Command(unboundRcPath, "onerestart").Run() + _ = exec.Command(dnsmasqRcPath, "onerestart").Run() + } + return nil +} + +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" +command="/usr/sbin/daemon" +daemon_args="-P ${pidfile} -r -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" +` diff --git a/internal/router/os_others.go b/internal/router/os_others.go new file mode 100644 index 0000000..52b41e4 --- /dev/null +++ b/internal/router/os_others.go @@ -0,0 +1,41 @@ +//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 +} diff --git a/internal/router/pfsense/pfsense.go b/internal/router/pfsense/pfsense.go deleted file mode 100644 index 1806ec7..0000000 --- a/internal/router/pfsense/pfsense.go +++ /dev/null @@ -1,96 +0,0 @@ -package pfsense - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - - "github.com/Control-D-Inc/ctrld" - "github.com/kardianos/service" -) - -const ( - Name = "pfsens" - - rcPath = "/usr/local/etc/rc.d" - unboundRcPath = rcPath + "/unbound" - dnsmasqRcPath = rcPath + "/dnsmasq" -) - -const pfsenseInitScript = `#!/bin/sh - -# PROVIDE: {{.Name}} -# REQUIRE: SERVERS -# REQUIRE: unbound dnsmasq securelevel -# KEYWORD: shutdown - -. /etc/rc.subr - -name="{{.Name}}" -{{.Name}}_env="IS_DAEMON=1" -pidfile="/var/run/${name}.pid" -command="/usr/sbin/daemon" -daemon_args="-P ${pidfile} -r -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}" -command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}" - -run_rc_command "$1" -` - -type Pfsense struct { - cfg *ctrld.Config - svcName string -} - -// New returns a router.Router for configuring/setup/run ctrld on Pfsense routers. -func New(cfg *ctrld.Config) *Pfsense { - return &Pfsense{cfg: cfg} -} - -func (p *Pfsense) ConfigureService(svc *service.Config) error { - svc.Option["SysvScript"] = pfsenseInitScript - p.svcName = svc.Name - return nil -} - -func (p *Pfsense) Install(config *service.Config) error { - // 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, p.svcName) - newname := filepath.Join(rcPath, p.svcName+".sh") - _ = os.Remove(newname) - if err := os.Symlink(oldname, newname); err != nil { - return fmt.Errorf("os.Symlink: %w", err) - } - return nil -} - -func (p *Pfsense) Uninstall(config *service.Config) error { - if err := os.Remove(filepath.Join(rcPath, p.svcName+".sh")); err != nil { - return fmt.Errorf("os.Remove: %w", err) - } - return nil -} - -func (p *Pfsense) PreRun() error { - // TODO: remove this hacky solution. - // If Pfsense is in DNS Resolver mode, ensure no unbound processes running. - _ = exec.Command("killall", "unbound").Run() - - // If Pfsense is in DNS Forwarder mode, ensure no dnsmasq processes running. - _ = exec.Command("killall", "dnsmasq").Run() - return nil -} - -func (p *Pfsense) Setup() error { - return nil -} - -func (p *Pfsense) Cleanup() error { - if p.cfg.FirstListener().IsDirectDnsListener() { - _ = exec.Command(unboundRcPath, "onerestart").Run() - _ = exec.Command(dnsmasqRcPath, "onerestart").Run() - } - - return nil -} diff --git a/internal/router/router.go b/internal/router/router.go index b500de6..6cae16e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -19,7 +19,6 @@ import ( "github.com/Control-D-Inc/ctrld/internal/router/firewalla" "github.com/Control-D-Inc/ctrld/internal/router/merlin" "github.com/Control-D-Inc/ctrld/internal/router/openwrt" - "github.com/Control-D-Inc/ctrld/internal/router/pfsense" "github.com/Control-D-Inc/ctrld/internal/router/synology" "github.com/Control-D-Inc/ctrld/internal/router/tomato" "github.com/Control-D-Inc/ctrld/internal/router/ubios" @@ -27,8 +26,11 @@ import ( // 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 } @@ -36,13 +38,17 @@ type Service interface { 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) Router { +func New(cfg *ctrld.Config, cdMode bool) Router { switch Name() { case ddwrt.Name: return ddwrt.New(cfg) @@ -58,12 +64,10 @@ func New(cfg *ctrld.Config) Router { return synology.New(cfg) case tomato.Name: return tomato.New(cfg) - case pfsense.Name: - return pfsense.New(cfg) case firewalla.Name: return firewalla.New(cfg) } - return &dummy{} + return newOsRouter(cfg, cdMode) } // IsGLiNet reports whether the router is an GL.iNet router. @@ -202,12 +206,10 @@ func distroName() string { return edgeos.Name case haveFile("/etc/ubnt/init/vyatta-router"): return edgeos.Name // For 2.x - case isPfsense(): - return pfsense.Name case haveFile("/etc/firewalla_release"): return firewalla.Name } - return "" + return osName } func haveFile(file string) bool {