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=). // 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" `