diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 6ed93e7..91addb6 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -35,15 +35,9 @@ import ( "tailscale.com/net/interfaces" "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/certs" "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" - "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" - "github.com/Control-D-Inc/ctrld/internal/router/firewalla" - "github.com/Control-D-Inc/ctrld/internal/router/merlin" - "github.com/Control-D-Inc/ctrld/internal/router/tomato" - "github.com/Control-D-Inc/ctrld/internal/router/ubios" ) var ( @@ -278,8 +272,8 @@ func initCLI() { } }) if platform := router.Name(); platform != "" { - if platform == ddwrt.Name { - rootCertPool = certs.CACertPool() + if cp := router.CertPool(); cp != nil { + rootCertPool = cp } // Perform router setup/cleanup if ctrld could not be direct listener. if !couldBeDirectListener(cfg.FirstListener()) { @@ -979,6 +973,9 @@ func netInterface(ifaceName string) (*net.Interface, error) { } func defaultIfaceName() string { + if ifaceName := router.DefaultInterfaceName(); ifaceName != "" { + return ifaceName + } dri, err := interfaces.DefaultRouteInterface() if err != nil { // On WSL 1, the route table does not have any default route. But the fact that @@ -986,10 +983,6 @@ func defaultIfaceName() string { if oi := osinfo.New(); strings.Contains(oi.String(), "Microsoft") { return "lo" } - // Same as WSL case above. - if router.Name() == ubios.Name { - return "lo" - } mainLog.Fatal().Err(err).Msg("failed to get default route interface") } return dri @@ -1057,19 +1050,18 @@ func selfCheckStatus(status service.Status, domain string) service.Status { } func userHomeDir() (string, error) { - switch router.Name() { - case ddwrt.Name, merlin.Name, tomato.Name: - exe, err := os.Executable() - if err != nil { - return "", err - } - return filepath.Dir(exe), nil + dir, err := router.HomeDir() + if err != nil { + return "", err + } + if dir != "" { + return dir, nil } // viper will expand for us. if runtime.GOOS == "windows" { return os.UserHomeDir() } - dir := "/etc/controld" + dir = "/etc/controld" if err := os.MkdirAll(dir, 0750); err != nil { return "", err } @@ -1291,7 +1283,7 @@ func updateListenerConfig() { // On firewalla, we don't need to check localhost, because the lo interface is excluded in dnsmasq // config, so we can always listen on localhost port 53, but no traffic could be routed there. - tryLocalhost := !isLoopback(listener.IP) && router.Name() != firewalla.Name + tryLocalhost := !isLoopback(listener.IP) && router.CanListenLocalhost() tryAllPort53 := true tryOldIPPort5354 := true tryPort5354 := true diff --git a/cmd/ctrld/os_linux.go b/cmd/ctrld/os_linux.go index 55f6e7c..94ab8d1 100644 --- a/cmd/ctrld/os_linux.go +++ b/cmd/ctrld/os_linux.go @@ -111,7 +111,7 @@ func resetDNS(iface *net.Interface) (err error) { } // Start systemd-networkd if present. if exe, _ := exec.LookPath("/lib/systemd/systemd-networkd"); exe != "" { - _ = exec.Command("systemctl", "restart", "systemd-networkd").Run() + _ = exec.Command("systemctl", "start", "systemd-networkd").Run() } if r, oerr := dns.NewOSConfigurator(logf, iface.Name); oerr == nil { _ = r.SetDNS(dns.OSConfig{}) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 9aa7db4..cf3b847 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -16,9 +16,6 @@ import ( "github.com/Control-D-Inc/ctrld/internal/clientinfo" "github.com/Control-D-Inc/ctrld/internal/dnscache" "github.com/Control-D-Inc/ctrld/internal/router" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" - "github.com/Control-D-Inc/ctrld/internal/router/edgeos" - "github.com/Control-D-Inc/ctrld/internal/router/firewalla" ) const ( @@ -249,40 +246,23 @@ func (p *prog) setDNS() { logger.Error().Err(err).Msg("could not patch NetworkManager") return } + logger.Debug().Msg("setting DNS for interface") ns := lc.IP - if couldBeDirectListener(lc) { + switch { + case couldBeDirectListener(lc): // If ctrld is direct listener, use 127.0.0.1 as nameserver. ns = "127.0.0.1" - } else if lc.Port != 53 { - ifaceName := iface - switch router.Name() { - case firewalla.Name: - // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. - // Thus, we use "br0" as the nameserver in /etc/resolv.conf file. - ifaceName = "br0" - logger.Warn().Msg("using br0 interface IP address as DNS server") - case edgeos.Name: - // On EdgeOS, dnsmasq is run with "--local-service", so we need to get - // the proper interface from dnsmasq config. - if name, _ := dnsmasq.InterfaceNameFromConfig("/etc/dnsmasq.conf"); name != "" { - ifaceName = name - logger.Warn().Msgf("using %s interface IP address as DNS server", ifaceName) - } - } - logger.Warn().Msgf("ctrld is not running on port 53, use interface %s IP as DNS server", ifaceName) - netIface, err := net.InterfaceByName(ifaceName) - if err != nil { - mainLog.Fatal().Err(err).Msg("failed to get default route interface") - } - addrs, _ := netIface.Addrs() - for _, addr := range addrs { - if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { - ns = netIP.IP.To4().String() - break - } + case lc.Port != 53: + ns = "127.0.0.1" + if resolver := router.LocalResolverIP(); resolver != "" { + ns = resolver } + default: + // If we ever reach here, it means ctrld is running on lc.IP port 53, + // so we could just use lc.IP as nameserver. } + if err := setDNS(netIface, []string{ns}); err != nil { logger.Error().Err(err).Msgf("could not set DNS for interface") return diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index a3f2823..58332bb 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -5,7 +5,6 @@ import ( "github.com/Control-D-Inc/ctrld/internal/dns" "github.com/Control-D-Inc/ctrld/internal/router" - "github.com/Control-D-Inc/ctrld/internal/router/edgeos" ) func init() { @@ -29,11 +28,8 @@ func setDependencies(svc *service.Config) { "Wants=systemd-networkd-wait-online.service", "After=systemd-networkd-wait-online.service", } - // On EdeOS, ctrld needs to start after vyatta-dhcpd, so it can read leases file. - if router.Name() == edgeos.Name { - svc.Dependencies = append(svc.Dependencies, "Wants=vyatta-dhcpd.service") - svc.Dependencies = append(svc.Dependencies, "After=vyatta-dhcpd.service") - svc.Dependencies = append(svc.Dependencies, "Wants=dnsmasq.service") + if routerDeps := router.ServiceDependencies(); len(routerDeps) > 0 { + svc.Dependencies = append(svc.Dependencies, routerDeps...) } } diff --git a/internal/router/router.go b/internal/router/router.go index 839151e..b500de6 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -2,14 +2,19 @@ package router import ( "bytes" + "crypto/x509" + "net" "os" "os/exec" + "path/filepath" "sync/atomic" "github.com/kardianos/service" "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/certs" "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" "github.com/Control-D-Inc/ctrld/internal/router/edgeos" "github.com/Control-D-Inc/ctrld/internal/router/firewalla" "github.com/Control-D-Inc/ctrld/internal/router/merlin" @@ -99,6 +104,86 @@ func Name() string { return r.name } +// DefaultInterfaceName returns the default interface name of the current router. +func DefaultInterfaceName() string { + switch Name() { + case ubios.Name: + return "lo" + } + return "" +} + +// LocalResolverIP returns the IP that could be used as nameserver in /etc/resolv.conf file. +func LocalResolverIP() string { + var iface string + switch Name() { + case edgeos.Name: + // On EdgeOS, dnsmasq is run with "--local-service", so we need to get + // the proper interface from dnsmasq config. + if name, _ := dnsmasq.InterfaceNameFromConfig("/etc/dnsmasq.conf"); name != "" { + iface = name + } + case firewalla.Name: + // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. + // Thus, we use "br0" as the nameserver in /etc/resolv.conf file. + iface = "br0" + } + if netIface, _ := net.InterfaceByName(iface); netIface != nil { + addrs, _ := netIface.Addrs() + for _, addr := range addrs { + if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { + return netIP.IP.To4().String() + } + } + } + return "" +} + +// HomeDir returns the home directory of ctrld on current router. +func HomeDir() (string, error) { + switch Name() { + case ddwrt.Name, merlin.Name, tomato.Name: + exe, err := os.Executable() + if err != nil { + return "", err + } + return filepath.Dir(exe), nil + } + return "", nil +} + +// CertPool returns the system certificate pool of the current router. +func CertPool() *x509.CertPool { + if Name() == ddwrt.Name { + return certs.CACertPool() + } + return nil +} + +// CanListenLocalhost reports whether the ctrld can listen on localhost with current host. +func CanListenLocalhost() bool { + switch { + case Name() == firewalla.Name: + return false + default: + return true + } +} + +// ServiceDependencies returns list of dependencies that ctrld services needs on this router. +// See https://pkg.go.dev/github.com/kardianos/service#Config for list format. +func ServiceDependencies() []string { + if Name() == edgeos.Name { + // On EdeOS, ctrld needs to start after vyatta-dhcpd, so it can read leases file. + return []string{ + "Wants=vyatta-dhcpd.service", + "After=vyatta-dhcpd.service", + "Wants=dnsmasq.service", + } + } + return nil +} + func distroName() string { switch { case bytes.HasPrefix(unameO(), []byte("DD-WRT")):