diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index a2a8207..f510aa6 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -18,9 +18,8 @@ import ( "sync" "time" - "github.com/fsnotify/fsnotify" - "github.com/cuonglm/osinfo" + "github.com/fsnotify/fsnotify" "github.com/go-playground/validator/v10" "github.com/kardianos/service" "github.com/miekg/dns" @@ -33,6 +32,7 @@ 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" ) const selfCheckFQDN = "verify.controld.com" @@ -200,6 +200,18 @@ func initCLI() { os.Exit(0) } + if runtime.GOOS == "linux" && onRouter { + mainLog.Debug().Msg("Router setup") + err := router.Configure(&cfg) + if errors.Is(err, router.ErrNotSupported) { + unsupportedPlatformHelp(cmd) + os.Exit(1) + } + if err != nil { + mainLog.Fatal().Err(err).Msg("failed to configure router") + } + } + close(waitCh) <-stopCh }, @@ -218,6 +230,8 @@ func initCLI() { _ = runCmd.Flags().MarkHidden("homedir") runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) _ = runCmd.Flags().MarkHidden("iface") + runCmd.Flags().BoolVarP(&onRouter, "router", "", false, `Configure onRouter for running ctrld`) + _ = runCmd.Flags().MarkHidden("router") rootCmd.AddCommand(runCmd) @@ -315,6 +329,8 @@ func initCLI() { startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items") startCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid") startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) + startCmd.Flags().BoolVarP(&onRouter, "router", "", false, `Configure onRouter for running ctrld`) + _ = startCmd.Flags().MarkHidden("router") stopCmd := &cobra.Command{ PreRun: checkHasElevatedPrivilege, @@ -510,11 +526,6 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, stopCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`) stopCmdAlias.Flags().AddFlagSet(stopCmd.Flags()) rootCmd.AddCommand(stopCmdAlias) - - if err := rootCmd.Execute(); err != nil { - stderrMsg(err.Error()) - os.Exit(1) - } } func writeConfigFile() error { @@ -802,3 +813,7 @@ func selfCheckStatus(status service.Status) service.Status { mainLog.Debug().Msgf("self-check against %q failed", selfCheckFQDN) return service.StatusUnknown } + +func unsupportedPlatformHelp(cmd *cobra.Command) { + cmd.PrintErrln("Unsupported or incorrectly chosen onRouter platform. Please open an issue and provide all relevant information: https://github.com/Control-D-Inc/ctrld/issues/new") +} diff --git a/cmd/ctrld/cli_router_linux.go b/cmd/ctrld/cli_router_linux.go new file mode 100644 index 0000000..b1dc4b4 --- /dev/null +++ b/cmd/ctrld/cli_router_linux.go @@ -0,0 +1,70 @@ +package main + +import ( + "log" + "os" + "os/exec" + "strings" + + "github.com/spf13/cobra" + + "github.com/Control-D-Inc/ctrld/internal/router" +) + +func initRouterCLI() { + validArgs := append(router.SupportedPlatforms(), "auto") + var b strings.Builder + b.WriteString("Auto-setup Control D on a onRouter.\n\nSupported platforms:\n\n") + for _, arg := range validArgs { + b.WriteString(" ₒ ") + b.WriteString(arg) + if arg == "auto" { + b.WriteString(" - detect the platform you are running on") + } + b.WriteString("\n") + } + + routerCmd := &cobra.Command{ + Use: "setup", + Short: b.String(), + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + _ = cmd.Help() + return + } + if len(args) != 1 { + _ = cmd.Help() + return + } + platform := args[0] + if platform == "auto" { + platform = router.Name() + } + switch platform { + case router.DDWrt, router.Merlin, router.OpenWrt, router.Ubios: + default: + unsupportedPlatformHelp(cmd) + } + exe, err := os.Executable() + if err != nil { + log.Fatal(err) + os.Exit(1) + } + + cmdArgs := []string{"start"} + cmdArgs = append(cmdArgs, os.Args[3:]...) + cmdArgs = append(cmdArgs, "--router=true") + command := exec.Command(exe, cmdArgs...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + command.Stdin = os.Stdin + if err := command.Run(); err != nil { + log.Fatal(err) + } + }, + } + tmpl := routerCmd.UsageTemplate() + tmpl = strings.Replace(tmpl, "{{.UseLine}}", "{{.UseLine}} [platform]", 1) + routerCmd.SetUsageTemplate(tmpl) + rootCmd.AddCommand(routerCmd) +} diff --git a/cmd/ctrld/cli_router_others.go b/cmd/ctrld/cli_router_others.go new file mode 100644 index 0000000..4a7b8c7 --- /dev/null +++ b/cmd/ctrld/cli_router_others.go @@ -0,0 +1,5 @@ +//go:build !linux + +package main + +func initRouterCLI() {} diff --git a/cmd/ctrld/main.go b/cmd/ctrld/main.go index 57ae02d..078c5db 100644 --- a/cmd/ctrld/main.go +++ b/cmd/ctrld/main.go @@ -26,18 +26,22 @@ var ( cacheSize int cfg ctrld.Config verbose int + cdUID string + iface string + ifaceStartStop string + onRouter bool - rootLogger = zerolog.New(io.Discard) - mainLog = rootLogger - - cdUID string - iface string - ifaceStartStop string + mainLog = zerolog.New(io.Discard) ) func main() { ctrld.InitConfig(v, "ctrld") initCLI() + initRouterCLI() + if err := rootCmd.Execute(); err != nil { + stderrMsg(err.Error()) + os.Exit(1) + } } func normalizeLogFilePath(logFilePath string) string { diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..cb8b2b2 --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,86 @@ +package router + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "sync/atomic" + + "github.com/Control-D-Inc/ctrld" +) + +const ( + OpenWrt = "openwrt" + DDWrt = "ddwrt" + Merlin = "merlin" + Ubios = "ubios" +) + +// ErrNotSupported reports the current router is not supported error. +var ErrNotSupported = errors.New("unsupported platform") + +var routerAtomic atomic.Pointer[router] + +type router struct { + name string +} + +// SupportedPlatforms return all platforms that can be configured to run with ctrld. +func SupportedPlatforms() []string { + return []string{DDWrt, Merlin, OpenWrt, Ubios} +} + +// Configure change the given *ctrld.Config for running on the router. +func Configure(c *ctrld.Config) error { + name := Name() + switch name { + case DDWrt, Merlin, OpenWrt, Ubios: + default: + return ErrNotSupported + } + // TODO: implement all supported platforms. + fmt.Printf("Configuring router for: %s\n", name) + return nil +} + +// Name returns name of the router platform. +func Name() string { + if r := routerAtomic.Load(); r != nil { + return r.name + } + r := &router{} + r.name = distroName() + routerAtomic.Store(r) + return r.name +} + +func distroName() string { + switch { + case bytes.HasPrefix(uname(), []byte("DD-WRT")): + return DDWrt + case bytes.HasPrefix(uname(), []byte("ASUSWRT-Merlin")): + return Merlin + case haveFile("/etc/openwrt_version"): + return OpenWrt + case haveDir("/data/unifi"): + return Ubios + } + return "" +} + +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 uname() []byte { + out, _ := exec.Command("uname", "-o").Output() + return out +}