Files
ctrld/internal/router/os_freebsd.go
Cuong Manh Le 43d82cf1a7 cmd/cli,internal/router: detect unbound/dnsmasq status correctly on *BSD
Also detect cd mode for stop/uninstall command correctly, too.
2024-03-22 16:08:40 +07:00

158 lines
3.7 KiB
Go

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"
`