From 507c1afd5934c14ce13b2f2a6c5e76803a27d186 Mon Sep 17 00:00:00 2001 From: Ginder Singh Date: Mon, 18 Sep 2023 11:40:03 +0000 Subject: [PATCH] cmd: allow import/running ctrld as library --- cmd/cli/cli.go | 468 +++++++++++++++++++++----------------- cmd/cli/dns_proxy.go | 6 + cmd/cli/library.go | 15 ++ cmd/cli/main.go | 6 +- cmd/cli/prog.go | 25 +- cmd/ctrld_library/main.go | 75 ++++++ go.mod | 1 + go.sum | 2 + 8 files changed, 379 insertions(+), 219 deletions(-) create mode 100644 cmd/cli/library.go create mode 100644 cmd/ctrld_library/main.go diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 1b94902..cb9ec7a 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -80,7 +80,7 @@ var rootCmd = &cobra.Command{ Short: strings.TrimLeft(rootShortDesc, "\n"), Version: curVersion(), PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() }, } @@ -121,188 +121,10 @@ func initCLI() { Short: "Run the DNS proxy server", Args: cobra.NoArgs, PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() }, Run: func(cmd *cobra.Command, args []string) { - waitCh := make(chan struct{}) - stopCh := make(chan struct{}) - p := &prog{ - waitCh: waitCh, - stopCh: stopCh, - cfg: &cfg, - } - if homedir == "" { - if dir, err := userHomeDir(); err == nil { - homedir = dir - } - } - sockPath := filepath.Join(homedir, ctrldLogUnixSock) - if addr, err := net.ResolveUnixAddr("unix", sockPath); err == nil { - if conn, err := net.Dial(addr.Network(), addr.String()); err == nil { - lc := &logConn{conn: conn} - consoleWriter.Out = io.MultiWriter(os.Stdout, lc) - p.logConn = lc - } - } - - if daemon && runtime.GOOS == "windows" { - mainLog.Load().Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.") - } - - if !daemon { - // We need to call s.Run() as soon as possible to response to the OS manager, so it - // can see ctrld is running and don't mark ctrld as failed service. - go func() { - s, err := newService(p, svcConfig) - if err != nil { - mainLog.Load().Fatal().Err(err).Msg("failed create new service") - } - if err := s.Run(); err != nil { - mainLog.Load().Error().Err(err).Msg("failed to start service") - } - }() - } - noConfigStart := isNoConfigStart(cmd) - writeDefaultConfig := !noConfigStart && configBase64 == "" - tryReadingConfig(writeDefaultConfig) - - readBase64Config(configBase64) - processNoConfigFlags(noConfigStart) - if err := v.Unmarshal(&cfg); err != nil { - mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err) - } - - processLogAndCacheFlags() - - // Log config do not have thing to validate, so it's safe to init log here, - // so it's able to log information in processCDFlags. - initLogging() - - mainLog.Load().Info().Msgf("starting ctrld %s", curVersion()) - mainLog.Load().Info().Msgf("os: %s", osVersion()) - - // Wait for network up. - if !ctrldnet.Up() { - mainLog.Load().Fatal().Msg("network is not up yet") - } - - p.router = router.New(&cfg, cdUID != "") - cs, err := newControlServer(filepath.Join(homedir, ctrldControlUnixSock)) - if err != nil { - mainLog.Load().Warn().Err(err).Msg("could not create control server") - } - p.cs = cs - - // Processing --cd flag require connecting to ControlD API, which needs valid - // time for validating server certificate. Some routers need NTP synchronization - // to set the current time, so this check must happen before processCDFlags. - if err := p.router.PreRun(); err != nil { - mainLog.Load().Fatal().Err(err).Msg("failed to perform router pre-run check") - } - - oldLogPath := cfg.Service.LogPath - if uid := cdUIDFromProvToken(); uid != "" { - cdUID = uid - } - if cdUID != "" { - processCDFlags() - } - - updated := updateListenerConfig() - - if cdUID != "" { - processLogAndCacheFlags() - } - - if updated { - if err := writeConfigFile(); err != nil { - mainLog.Load().Fatal().Err(err).Msg("failed to write config file") - } else { - mainLog.Load().Info().Msg("writing config file to: " + defaultConfigFile) - } - } - - if newLogPath := cfg.Service.LogPath; newLogPath != "" && oldLogPath != newLogPath { - // After processCDFlags, log config may change, so reset mainLog and re-init logging. - l := zerolog.New(io.Discard) - mainLog.Store(&l) - - // Copy logs written so far to new log file if possible. - if buf, err := os.ReadFile(oldLogPath); err == nil { - if err := os.WriteFile(newLogPath, buf, os.FileMode(0o600)); err != nil { - mainLog.Load().Warn().Err(err).Msg("could not copy old log file") - } - } - initLoggingWithBackup(false) - } - - validateConfig(&cfg) - initCache() - - if daemon { - exe, err := os.Executable() - if err != nil { - mainLog.Load().Error().Err(err).Msg("failed to find the binary") - os.Exit(1) - } - curDir, err := os.Getwd() - if err != nil { - mainLog.Load().Error().Err(err).Msg("failed to get current working directory") - os.Exit(1) - } - // If running as daemon, re-run the command in background, with daemon off. - cmd := exec.Command(exe, append(os.Args[1:], "-d=false")...) - cmd.Dir = curDir - if err := cmd.Start(); err != nil { - mainLog.Load().Error().Err(err).Msg("failed to start process as daemon") - os.Exit(1) - } - mainLog.Load().Info().Int("pid", cmd.Process.Pid).Msg("DNS proxy started") - os.Exit(0) - } - - p.onStarted = append(p.onStarted, func() { - for _, lc := range p.cfg.Listener { - if shouldAllocateLoopbackIP(lc.IP) { - if err := allocateIP(lc.IP); err != nil { - mainLog.Load().Error().Err(err).Msgf("could not allocate IP: %s", lc.IP) - } - } - } - }) - p.onStopped = append(p.onStopped, func() { - for _, lc := range p.cfg.Listener { - if shouldAllocateLoopbackIP(lc.IP) { - if err := deAllocateIP(lc.IP); err != nil { - mainLog.Load().Error().Err(err).Msgf("could not de-allocate IP: %s", lc.IP) - } - } - } - }) - if platform := router.Name(); platform != "" { - if cp := router.CertPool(); cp != nil { - rootCertPool = cp - } - p.onStarted = append(p.onStarted, func() { - mainLog.Load().Debug().Msg("router setup on start") - if err := p.router.Setup(); err != nil { - mainLog.Load().Error().Err(err).Msg("could not configure router") - } - }) - p.onStopped = append(p.onStopped, func() { - mainLog.Load().Debug().Msg("router cleanup on stop") - if err := p.router.Cleanup(); err != nil { - mainLog.Load().Error().Err(err).Msg("could not cleanup router") - } - p.resetDNS() - }) - } - - close(waitCh) - <-stopCh - for _, f := range p.onStopped { - f() - } + Run(cmd, nil, nil, nil) }, } runCmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon") @@ -327,7 +149,7 @@ func initCLI() { startCmd := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() checkHasElevatedPrivilege() }, Use: "start", @@ -405,7 +227,7 @@ func initCLI() { mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err) } - initLogging() + InitLogging() // Explicitly passing config, so on system where home directory could not be obtained, // or sub-process env is different with the parent, we still behave correctly and use @@ -475,7 +297,7 @@ func initCLI() { routerCmd := &cobra.Command{ Use: "setup", PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() }, Run: func(cmd *cobra.Command, _ []string) { exe, err := os.Executable() @@ -504,7 +326,7 @@ func initCLI() { stopCmd := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() checkHasElevatedPrivilege() }, Use: "stop", @@ -519,7 +341,7 @@ func initCLI() { mainLog.Load().Error().Msg(err.Error()) return } - initLogging() + InitLogging() if doTasks([]task{{s.Stop, true}}) { p.router.Cleanup() p.resetDNS() @@ -531,7 +353,7 @@ func initCLI() { restartCmd := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() checkHasElevatedPrivilege() }, Use: "restart", @@ -543,7 +365,7 @@ func initCLI() { mainLog.Load().Error().Msg(err.Error()) return } - initLogging() + InitLogging() tasks := []task{ {s.Stop, false}, @@ -569,7 +391,7 @@ func initCLI() { Short: "Show status of the ctrld service", Args: cobra.NoArgs, PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() }, Run: func(cmd *cobra.Command, args []string) { s, err := newService(&prog{}, svcConfig) @@ -598,14 +420,14 @@ func initCLI() { if runtime.GOOS == "darwin" { // On darwin, running status command without privileges may return wrong information. statusCmd.PreRun = func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() checkHasElevatedPrivilege() } } uninstallCmd := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() checkHasElevatedPrivilege() }, Use: "uninstall", @@ -636,7 +458,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, Short: "List network interfaces of the host", Args: cobra.NoArgs, PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() }, Run: func(cmd *cobra.Command, args []string) { err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { @@ -696,7 +518,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, rootCmd.AddCommand(serviceCmd) startCmdAlias := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() checkHasElevatedPrivilege() }, Use: "start", @@ -714,7 +536,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, rootCmd.AddCommand(startCmdAlias) stopCmdAlias := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() checkHasElevatedPrivilege() }, Use: "stop", @@ -733,7 +555,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, restartCmdAlias := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() checkHasElevatedPrivilege() }, Use: "restart", @@ -749,7 +571,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, Short: "Show status of the ctrld service", Args: cobra.NoArgs, PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() }, Run: statusCmd.Run, } @@ -757,7 +579,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, uninstallCmdAlias := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() checkHasElevatedPrivilege() }, Use: "uninstall", @@ -782,7 +604,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, Short: "List clients that ctrld discovered", Args: cobra.NoArgs, PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() + InitConsoleLogging() checkHasElevatedPrivilege() }, Run: func(cmd *cobra.Command, args []string) { @@ -838,6 +660,206 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, rootCmd.AddCommand(clientsCmd) } +// isMobile reports whether the current OS is a mobile platform. +func isMobile() bool { + return runtime.GOOS == "android" || runtime.GOOS == "ios" +} + +func Run(cmd *cobra.Command, appConfig *AppConfig, appCallback *AppCallback, stopCh chan struct{}) { + if appConfig != nil { + homedir = appConfig.HomeDir + verbose = appConfig.Verbose + cdUID = appConfig.CdUID + logPath = appConfig.LogPath + } + if stopCh == nil { + stopCh = make(chan struct{}) + } + waitCh := make(chan struct{}) + p := &prog{ + waitCh: waitCh, + stopCh: stopCh, + cfg: &cfg, + appCallback: appCallback, + } + if homedir == "" { + if dir, err := userHomeDir(); err == nil { + homedir = dir + } + } + sockPath := filepath.Join(homedir, ctrldLogUnixSock) + if addr, err := net.ResolveUnixAddr("unix", sockPath); err == nil { + if conn, err := net.Dial(addr.Network(), addr.String()); err == nil { + lc := &logConn{conn: conn} + consoleWriter.Out = io.MultiWriter(os.Stdout, lc) + p.logConn = lc + } + } + + if daemon && runtime.GOOS == "windows" { + mainLog.Load().Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.") + } + + if !daemon { + // We need to call s.Run() as soon as possible to response to the OS manager, so it + // can see ctrld is running and don't mark ctrld as failed service. + go func() { + s, err := newService(p, svcConfig) + if err != nil { + mainLog.Load().Fatal().Err(err).Msg("failed create new service") + } + if err := s.Run(); err != nil { + mainLog.Load().Error().Err(err).Msg("failed to start service") + } + }() + } + noConfigStart := cmd != nil && isNoConfigStart(cmd) + writeDefaultConfig := !noConfigStart && configBase64 == "" + tryReadingConfig(writeDefaultConfig) + + readBase64Config(configBase64) + processNoConfigFlags(noConfigStart) + if err := v.Unmarshal(&cfg); err != nil { + mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err) + } + + processLogAndCacheFlags() + + // Log config do not have thing to validate, so it's safe to init log here, + // so it's able to log information in processCDFlags. + InitLogging() + + mainLog.Load().Info().Msgf("starting ctrld %s", curVersion()) + mainLog.Load().Info().Msgf("os: %s", osVersion()) + + // Wait for network up. + if !ctrldnet.Up() { + mainLog.Load().Fatal().Msg("network is not up yet") + } + + p.router = router.New(&cfg, cdUID != "") + cs, err := newControlServer(filepath.Join(homedir, ctrldControlUnixSock)) + if err != nil { + mainLog.Load().Warn().Err(err).Msg("could not create control server") + } + p.cs = cs + + // Processing --cd flag require connecting to ControlD API, which needs valid + // time for validating server certificate. Some routers need NTP synchronization + // to set the current time, so this check must happen before processCDFlags. + if err := p.router.PreRun(); err != nil { + mainLog.Load().Fatal().Err(err).Msg("failed to perform router pre-run check") + } + + oldLogPath := cfg.Service.LogPath + if uid := cdUIDFromProvToken(); uid != "" { + cdUID = uid + } + if cdUID != "" { + err := processCDFlags() + if err != nil { + appCallback.Exit(err.Error()) + return + } + } + + updated := updateListenerConfig() + + if cdUID != "" { + processLogAndCacheFlags() + } + + if updated { + if err := writeConfigFile(); err != nil { + mainLog.Load().Fatal().Err(err).Msg("failed to write config file") + } else { + mainLog.Load().Info().Msg("writing config file to: " + defaultConfigFile) + } + } + + if newLogPath := cfg.Service.LogPath; newLogPath != "" && oldLogPath != newLogPath { + // After processCDFlags, log config may change, so reset mainLog and re-init logging. + l := zerolog.New(io.Discard) + mainLog.Store(&l) + + // Copy logs written so far to new log file if possible. + if buf, err := os.ReadFile(oldLogPath); err == nil { + if err := os.WriteFile(newLogPath, buf, os.FileMode(0o600)); err != nil { + mainLog.Load().Warn().Err(err).Msg("could not copy old log file") + } + } + initLoggingWithBackup(false) + } + + validateConfig(&cfg) + initCache() + + if daemon { + exe, err := os.Executable() + if err != nil { + mainLog.Load().Error().Err(err).Msg("failed to find the binary") + os.Exit(1) + } + curDir, err := os.Getwd() + if err != nil { + mainLog.Load().Error().Err(err).Msg("failed to get current working directory") + os.Exit(1) + } + // If running as daemon, re-run the command in background, with daemon off. + cmd := exec.Command(exe, append(os.Args[1:], "-d=false")...) + cmd.Dir = curDir + if err := cmd.Start(); err != nil { + mainLog.Load().Error().Err(err).Msg("failed to start process as daemon") + os.Exit(1) + } + mainLog.Load().Info().Int("pid", cmd.Process.Pid).Msg("DNS proxy started") + os.Exit(0) + } + + p.onStarted = append(p.onStarted, func() { + for _, lc := range p.cfg.Listener { + if shouldAllocateLoopbackIP(lc.IP) { + if err := allocateIP(lc.IP); err != nil { + mainLog.Load().Error().Err(err).Msgf("could not allocate IP: %s", lc.IP) + } + } + } + }) + p.onStopped = append(p.onStopped, func() { + for _, lc := range p.cfg.Listener { + if shouldAllocateLoopbackIP(lc.IP) { + if err := deAllocateIP(lc.IP); err != nil { + mainLog.Load().Error().Err(err).Msgf("could not de-allocate IP: %s", lc.IP) + } + } + } + }) + if platform := router.Name(); platform != "" { + if cp := router.CertPool(); cp != nil { + rootCertPool = cp + } + p.onStarted = append(p.onStarted, func() { + mainLog.Load().Debug().Msg("router setup on start") + if err := p.router.Setup(); err != nil { + mainLog.Load().Error().Err(err).Msg("could not configure router") + } + }) + p.onStopped = append(p.onStopped, func() { + mainLog.Load().Debug().Msg("router cleanup on stop") + if err := p.router.Cleanup(); err != nil { + mainLog.Load().Error().Err(err).Msg("could not cleanup router") + } + p.resetDNS() + }) + } + + close(waitCh) + <-stopCh + for _, f := range p.onStopped { + f() + } +} + func writeConfigFile() error { if cfu := v.ConfigFileUsed(); cfu != "" { defaultConfigFile = cfu @@ -972,7 +994,7 @@ func processNoConfigFlags(noConfigStart bool) { v.Set("upstream", upstream) } -func processCDFlags() { +func processCDFlags() error { logger := mainLog.Load().With().Str("mode", "cd").Logger() logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID) bo := backoff.NewBackoff("processCDFlags", logf, 30*time.Second) @@ -992,12 +1014,12 @@ func processCDFlags() { s, err := newService(&prog{}, svcConfig) if err != nil { logger.Warn().Err(err).Msg("failed to create new service") - return + return nil } if netIface, _ := netInterface(iface); netIface != nil { if err := restoreNetworkManager(); err != nil { logger.Error().Err(err).Msg("could not restore NetworkManager") - return + return nil } logger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS for interface") if err := resetDNS(netIface); err != nil { @@ -1011,11 +1033,16 @@ func processCDFlags() { if doTasks(tasks) { logger.Info().Msg("uninstalled service") } - logger.Fatal().Err(uer).Msg("failed to fetch resolver config") + event := logger.Fatal() + if isMobile() { + event = logger.Warn() + } + event.Err(uer).Msg("failed to fetch resolver config") + return uer } if err != nil { logger.Warn().Err(err).Msg("could not fetch resolver config") - return + return nil } logger.Info().Msg("generating ctrld config from Control-D configuration") @@ -1059,6 +1086,7 @@ func processCDFlags() { "0": {IP: "", Port: 0}, } } + return nil } func processListenFlag() { @@ -1269,6 +1297,10 @@ func userHomeDir() (string, error) { if runtime.GOOS == "windows" { return os.UserHomeDir() } + // Mobile platform should provide a rw dir path for this. + if isMobile() { + return homedir, nil + } dir = "/etc/controld" if err := os.MkdirAll(dir, 0750); err != nil { return os.UserHomeDir() // fallback to user home directory @@ -1321,7 +1353,7 @@ func uninstall(p *prog, s service.Service) { {s.Stop, false}, {s.Uninstall, true}, } - initLogging() + InitLogging() if doTasks(tasks) { if err := p.router.ConfigureService(svcConfig); err != nil { mainLog.Load().Fatal().Err(err).Msg("could not configure service") @@ -1413,6 +1445,14 @@ type listenerConfigCheck struct { Port bool } +// mobileListenerPort returns hardcoded port for mobile platforms. +func mobileListenerPort() int { + if runtime.GOOS == "ios" { + return 53 + } + return 5354 +} + // updateListenerConfig updates the config for listeners if not defined, // or defined but invalid to be used, e.g: using loopback address other // than 127.0.0.1 with systemd-resolved. @@ -1436,7 +1476,25 @@ func updateListenerConfig() (updated bool) { } updated = updated || lcc[n].IP || lcc[n].Port } - + if isMobile() { + // On Mobile, only use first listener, ignore others. + firstLn := cfg.FirstListener() + for k := range cfg.Listener { + if cfg.Listener[k] != firstLn { + delete(cfg.Listener, k) + } + } + // In cd mode, always use 127.0.0.1:5354. + if cdMode { + firstLn.IP = "127.0.0.1" // Mobile platforms allows running listener only on loop back address. + firstLn.Port = mobileListenerPort() + // TODO: use clear(lcc) once upgrading to go 1.21 + for k := range lcc { + delete(lcc, k) + } + updated = true + } + } var closers []io.Closer defer func() { for _, closer := range closers { diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index ca9a4d0..13d967a 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -491,6 +491,12 @@ func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-cha func (p *prog) getClientInfo(remoteIP string, msg *dns.Msg) *ctrld.ClientInfo { ci := &ctrld.ClientInfo{} + if p.appCallback != nil { + ci.IP = p.appCallback.LanIp() + ci.Mac = p.appCallback.MacAddress() + ci.Hostname = p.appCallback.HostName() + return ci + } ci.IP, ci.Mac = ipAndMacFromMsg(msg) switch { case ci.IP != "" && ci.Mac != "": diff --git a/cmd/cli/library.go b/cmd/cli/library.go new file mode 100644 index 0000000..235cb96 --- /dev/null +++ b/cmd/cli/library.go @@ -0,0 +1,15 @@ +package cli + +type AppCallback struct { + HostName func() string + LanIp func() string + MacAddress func() string + Exit func(error string) +} + +type AppConfig struct { + CdUID string + HomeDir string + Verbose int + LogPath string +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index e7376be..53281b5 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -65,7 +65,7 @@ func normalizeLogFilePath(logFilePath string) string { return filepath.Join(dir, logFilePath) } -func initConsoleLogging() { +func InitConsoleLogging() { consoleWriter = zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) { w.TimeFormat = time.StampMilli }) @@ -84,8 +84,8 @@ func initConsoleLogging() { } } -// initLogging initializes global logging setup. -func initLogging() { +// InitLogging initializes global logging setup. +func InitLogging() { initLoggingWithBackup(true) } diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 2a06dc3..2ba24b3 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -49,11 +49,12 @@ type prog struct { logConn net.Conn cs *controlServer - cfg *ctrld.Config - cache dnscache.Cacher - sema semaphore - ciTable *clientinfo.Table - router router.Router + cfg *ctrld.Config + appCallback *AppCallback + cache dnscache.Cacher + sema semaphore + ciTable *clientinfo.Table + router router.Router started chan struct{} onStartedDone chan struct{} @@ -136,12 +137,14 @@ func (p *prog) run() { format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat) p.ciTable.AddLeaseFile(leaseFile, format) } - - go func() { - p.ciTable.Init() - p.ciTable.RefreshLoop(p.stopCh) - }() - go p.watchLinkState() + // Newer versions of android and iOS denies permission which breaks connectivity. + if !isMobile() { + go func() { + p.ciTable.Init() + p.ciTable.RefreshLoop(p.stopCh) + }() + go p.watchLinkState() + } for listenerNum := range p.cfg.Listener { p.cfg.Listener[listenerNum].Init() diff --git a/cmd/ctrld_library/main.go b/cmd/ctrld_library/main.go new file mode 100644 index 0000000..2a28841 --- /dev/null +++ b/cmd/ctrld_library/main.go @@ -0,0 +1,75 @@ +package ctrld_library + +import ( + "github.com/Control-D-Inc/ctrld/cmd/cli" +) + +// Controller holds global state +type Controller struct { + stopCh chan struct{} + AppCallback AppCallback + Config cli.AppConfig +} + +// NewController provides reference to global state to be managed by android vpn service and iOS network extension. +// reference is not safe for concurrent use. +func NewController(appCallback AppCallback) *Controller { + return &Controller{AppCallback: appCallback} +} + +// AppCallback provides access to app instance. +type AppCallback interface { + Hostname() string + LanIp() string + MacAddress() string + Exit(error string) +} + +// Start configures utility with config.toml from provided directory. +// This function will block until Stop is called +// Check port availability prior to calling it. +func (c *Controller) Start(CdUID string, HomeDir string, logLevel int, logPath string) { + if c.stopCh == nil { + c.stopCh = make(chan struct{}) + cli.InitConsoleLogging() + c.Config = cli.AppConfig{ + CdUID: CdUID, + HomeDir: HomeDir, + Verbose: logLevel, + LogPath: logPath, + } + appCallback := mapCallback(c.AppCallback) + cli.Run(nil, &c.Config, &appCallback, c.stopCh) + } +} + +// As workaround to avoid circular dependency between cli and ctrld_library module +func mapCallback(callback AppCallback) cli.AppCallback { + return cli.AppCallback{ + HostName: func() string { + return callback.Hostname() + }, + LanIp: func() string { + return callback.LanIp() + }, + MacAddress: func() string { + return callback.MacAddress() + }, + Exit: func(err string) { + callback.Exit(err) + }, + } +} + +func (c *Controller) Stop() bool { + if c.stopCh != nil { + close(c.stopCh) + c.stopCh = nil + return true + } + return false +} + +func (c *Controller) IsRunning() bool { + return c.stopCh != nil +} diff --git a/go.mod b/go.mod index 14415c8..9f3e934 100644 --- a/go.mod +++ b/go.mod @@ -72,6 +72,7 @@ require ( go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect golang.org/x/crypto v0.9.0 // indirect golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect + golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/text v0.9.0 // indirect golang.org/x/tools v0.9.1 // indirect diff --git a/go.sum b/go.sum index c4772f1..bd7a9af 100644 --- a/go.sum +++ b/go.sum @@ -329,6 +329,8 @@ golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPI golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda h1:O+EUvnBNPwI4eLthn8W5K+cS8zQZfgTABPLNm6Bna34= +golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=