From f5ef9b917eb286956d10b7a4db95c1802841bdbd Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 18 Apr 2023 21:16:12 +0700 Subject: [PATCH] all: implement router setup for ubios --- internal/router/dnsmasq.go | 2 +- internal/router/router.go | 13 +- internal/router/service.go | 22 +++ internal/router/service_ubios.go | 320 +++++++++++++++++++++++++++++++ internal/router/ubios.go | 55 ++++++ 5 files changed, 402 insertions(+), 10 deletions(-) create mode 100644 internal/router/service_ubios.go create mode 100644 internal/router/ubios.go diff --git a/internal/router/dnsmasq.go b/internal/router/dnsmasq.go index 9c6fd2b..d90854a 100644 --- a/internal/router/dnsmasq.go +++ b/internal/router/dnsmasq.go @@ -2,7 +2,7 @@ package router const dnsMasqConfigContent = `# GENERATED BY ctrld - DO NOT MODIFY no-resolv -server=127.0.0.1#5353 +server=127.0.0.1#5354 ` const merlinDNSMasqPostConfPath = "/jffs/scripts/dnsmasq.postconf" diff --git a/internal/router/router.go b/internal/router/router.go index bca465c..13ec295 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -3,7 +3,6 @@ package router import ( "bytes" "errors" - "fmt" "os" "os/exec" "sync/atomic" @@ -45,12 +44,10 @@ func Configure(c *ctrld.Config) error { case OpenWrt: return setupOpenWrt() case Ubios: + return setupUbiOS() default: return ErrNotSupported } - // TODO: implement all supported platforms. - fmt.Printf("Configuring router for: %s\n", name) - return nil } // ConfigureService performs necessary setup for running ctrld as a service on router. @@ -78,8 +75,8 @@ func PostInstall() error { return postInstallMerlin() case OpenWrt: return postInstallOpenWrt() - case Ubios: + return postInstallUbiOS() } return nil } @@ -95,6 +92,7 @@ func Cleanup() error { case OpenWrt: return cleanupOpenWrt() case Ubios: + return cleanupUbiOS() } return nil } @@ -103,11 +101,8 @@ func Cleanup() error { func ListenAddress() string { name := Name() switch name { - case DDWrt, OpenWrt: - return "127.0.0.1:5353" - case Merlin: + case DDWrt, Merlin, OpenWrt, Ubios: return "127.0.0.1:5354" - case Ubios: } return "" } diff --git a/internal/router/service.go b/internal/router/service.go index 7c8bb40..e404a2d 100644 --- a/internal/router/service.go +++ b/internal/router/service.go @@ -1,7 +1,9 @@ package router import ( + "bytes" "os" + "os/exec" "github.com/kardianos/service" ) @@ -26,6 +28,26 @@ func init() { }, new: newMerlinService, }, + &linuxSystemService{ + name: "ubios", + detect: func() bool { + if Name() != Ubios { + 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, + }, } systems = append(systems, service.AvailableSystems()...) service.ChooseSystem(systems...) diff --git a/internal/router/service_ubios.go b/internal/router/service_ubios.go new file mode 100644 index 0000000..a779590 --- /dev/null +++ b/internal/router/service_ubios.go @@ -0,0 +1,320 @@ +package router + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "os/signal" + "path/filepath" + "strings" + "syscall" + "text/template" + "time" + + "github.com/kardianos/service" +) + +// 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, + ubiosDNSMasqConfigPath, + } + + 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 + } + 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) + signal.Ignore(sigCHLD) + } + + 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 +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) + }, +} diff --git a/internal/router/ubios.go b/internal/router/ubios.go new file mode 100644 index 0000000..8affb33 --- /dev/null +++ b/internal/router/ubios.go @@ -0,0 +1,55 @@ +package router + +import ( + "bytes" + "os" + "strconv" +) + +const ( + ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf" +) + +func setupUbiOS() error { + // Disable dnsmasq as DNS server. + if err := os.WriteFile(ubiosDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil { + return err + } + // Restart dnsmasq service. + if err := ubiosRestartDNSMasq(); err != nil { + return err + } + return nil +} + +func cleanupUbiOS() error { + // Remove the custom dnsmasq config + if err := os.Remove(ubiosDNSMasqConfigPath); err != nil { + return err + } + // Restart dnsmasq service. + if err := ubiosRestartDNSMasq(); err != nil { + return err + } + return nil +} + +func postInstallUbiOS() error { + return nil +} + +func ubiosRestartDNSMasq() error { + buf, err := os.ReadFile("/run/dnsmasq.pid") + if err != nil { + return err + } + pid, err := strconv.ParseUint(string(bytes.TrimSpace(buf)), 10, 64) + if err != nil { + return err + } + proc, err := os.FindProcess(int(pid)) + if err != nil { + return err + } + return proc.Kill() +}