From 4c2d21a8f8486b05e8618fe744f05d6b2ed028e5 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 15 Feb 2023 22:42:33 +0700 Subject: [PATCH] all: add freebsd supports This commit add support for ctrld to run on freebsd, supported platforms are amd64/arm64/armv6/armv7,386. Supporting freebsd also requires adding debian and openresolv resolvconf. Updates #47 --- cmd/ctrld/cli.go | 3 +- ...rk_manager.go => network_manager_linux.go} | 9 -- cmd/ctrld/network_manager_others.go | 13 ++ cmd/ctrld/{os_mac.go => os_darwin.go} | 3 - cmd/ctrld/os_freebsd.go | 47 ++++++ cmd/ctrld/os_others.go | 13 ++ cmd/ctrld/os_windows.go | 13 -- cmd/ctrld/prog_freebsd.go | 20 +++ cmd/ctrld/prog_linux.go | 4 + cmd/ctrld/prog_others.go | 8 +- internal/dns/debian_resolvconf.go | 153 ++++++++++++++++++ internal/dns/direct.go | 4 + internal/dns/manager_freebsd.go | 39 +++++ internal/dns/nm.go | 2 + internal/dns/openresolv.go | 57 +++++++ internal/dns/osconfig.go | 2 + internal/dns/resolvconf-workaround.sh | 63 ++++++++ internal/dns/resolved.go | 2 + 18 files changed, 426 insertions(+), 29 deletions(-) rename cmd/ctrld/{network_manager.go => network_manager_linux.go} (88%) create mode 100644 cmd/ctrld/network_manager_others.go rename cmd/ctrld/{os_mac.go => os_darwin.go} (97%) create mode 100644 cmd/ctrld/os_freebsd.go create mode 100644 cmd/ctrld/os_others.go create mode 100644 cmd/ctrld/prog_freebsd.go create mode 100644 internal/dns/debian_resolvconf.go create mode 100644 internal/dns/manager_freebsd.go create mode 100644 internal/dns/openresolv.go create mode 100644 internal/dns/resolvconf-workaround.sh diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 1ba562e..e3ca198 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -197,8 +197,7 @@ func initCLI() { setDependencies(sc) sc.Arguments = append([]string{"run"}, osArgs...) if dir, err := os.UserHomeDir(); err == nil { - // WorkingDirectory is not supported on Windows. - sc.WorkingDirectory = dir + setWorkingDirectory(sc, dir) // No config path, generating config in HOME directory. noConfigStart := isNoConfigStart(cmd) writeDefaultConfig := !noConfigStart && configBase64 == "" diff --git a/cmd/ctrld/network_manager.go b/cmd/ctrld/network_manager_linux.go similarity index 88% rename from cmd/ctrld/network_manager.go rename to cmd/ctrld/network_manager_linux.go index 670fe9c..fe00f3a 100644 --- a/cmd/ctrld/network_manager.go +++ b/cmd/ctrld/network_manager_linux.go @@ -4,7 +4,6 @@ import ( "context" "os" "path/filepath" - "runtime" "time" "github.com/coreos/go-systemd/v22/dbus" @@ -24,10 +23,6 @@ systemd-resolved=false var networkManagerCtrldConfFile = filepath.Join(nmConfDir, nmCtrldConfFilename) func setupNetworkManager() error { - if runtime.GOOS != "linux" { - mainLog.Debug().Msg("skipping NetworkManager setup, not on Linux") - return nil - } if content, _ := os.ReadFile(nmCtrldConfContent); string(content) == nmCtrldConfContent { mainLog.Debug().Msg("NetworkManager already setup, nothing to do") return nil @@ -48,10 +43,6 @@ func setupNetworkManager() error { } func restoreNetworkManager() error { - if runtime.GOOS != "linux" { - mainLog.Debug().Msg("skipping NetworkManager restoring, not on Linux") - return nil - } err := os.Remove(networkManagerCtrldConfFile) if os.IsNotExist(err) { mainLog.Debug().Msg("NetworkManager is not available") diff --git a/cmd/ctrld/network_manager_others.go b/cmd/ctrld/network_manager_others.go new file mode 100644 index 0000000..3cdb762 --- /dev/null +++ b/cmd/ctrld/network_manager_others.go @@ -0,0 +1,13 @@ +//go:build !linux + +package main + +func setupNetworkManager() error { + return nil +} + +func restoreNetworkManager() error { + return nil +} + +func reloadNetworkManager() {} diff --git a/cmd/ctrld/os_mac.go b/cmd/ctrld/os_darwin.go similarity index 97% rename from cmd/ctrld/os_mac.go rename to cmd/ctrld/os_darwin.go index 95786f3..04bc66b 100644 --- a/cmd/ctrld/os_mac.go +++ b/cmd/ctrld/os_darwin.go @@ -1,6 +1,3 @@ -//go:build darwin -// +build darwin - package main import ( diff --git a/cmd/ctrld/os_freebsd.go b/cmd/ctrld/os_freebsd.go new file mode 100644 index 0000000..b65de54 --- /dev/null +++ b/cmd/ctrld/os_freebsd.go @@ -0,0 +1,47 @@ +package main + +import ( + "net" + "net/netip" + + "github.com/Control-D-Inc/ctrld/internal/dns" + "github.com/Control-D-Inc/ctrld/internal/resolvconffile" +) + +// set the dns server for the provided network interface +func setDNS(iface *net.Interface, nameservers []string) error { + r, err := dns.NewOSConfigurator(logf, iface.Name) + if err != nil { + mainLog.Error().Err(err).Msg("failed to create DNS OS configurator") + return err + } + + ns := make([]netip.Addr, 0, len(nameservers)) + for _, nameserver := range nameservers { + ns = append(ns, netip.MustParseAddr(nameserver)) + } + + if err := r.SetDNS(dns.OSConfig{Nameservers: ns}); err != nil { + mainLog.Error().Err(err).Msg("failed to set DNS") + return err + } + return nil +} + +func resetDNS(iface *net.Interface) error { + r, err := dns.NewOSConfigurator(logf, iface.Name) + if err != nil { + mainLog.Error().Err(err).Msg("failed to create DNS OS configurator") + return err + } + + if err := r.Close(); err != nil { + mainLog.Error().Err(err).Msg("failed to rollback DNS setting") + return err + } + return nil +} + +func currentDNS(_ *net.Interface) []string { + return resolvconffile.NameServers("") +} diff --git a/cmd/ctrld/os_others.go b/cmd/ctrld/os_others.go new file mode 100644 index 0000000..e9f9d61 --- /dev/null +++ b/cmd/ctrld/os_others.go @@ -0,0 +1,13 @@ +//go:build !linux && !darwin + +package main + +// TODO(cuonglm): implement. +func allocateIP(ip string) error { + return nil +} + +// TODO(cuonglm): implement. +func deAllocateIP(ip string) error { + return nil +} diff --git a/cmd/ctrld/os_windows.go b/cmd/ctrld/os_windows.go index bb1631f..0bd7358 100644 --- a/cmd/ctrld/os_windows.go +++ b/cmd/ctrld/os_windows.go @@ -1,6 +1,3 @@ -//go:build windows -// +build windows - package main import ( @@ -14,16 +11,6 @@ import ( ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" ) -// TODO(cuonglm): implement. -func allocateIP(ip string) error { - return nil -} - -// TODO(cuonglm): implement. -func deAllocateIP(ip string) error { - return nil -} - func setDNS(iface *net.Interface, nameservers []string) error { if len(nameservers) == 0 { return errors.New("empty DNS nameservers") diff --git a/cmd/ctrld/prog_freebsd.go b/cmd/ctrld/prog_freebsd.go new file mode 100644 index 0000000..63d8179 --- /dev/null +++ b/cmd/ctrld/prog_freebsd.go @@ -0,0 +1,20 @@ +package main + +import ( + "os" + + "github.com/kardianos/service" +) + +func (p *prog) preRun() { + if !service.Interactive() { + p.setDNS() + } +} + +func setDependencies(svc *service.Config) { + // TODO(cuonglm): remove once https://github.com/kardianos/service/issues/359 fixed. + _ = os.MkdirAll("/usr/local/etc/rc.d", 0755) +} + +func setWorkingDirectory(svc *service.Config, dir string) {} diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 7d4f87a..4ec9416 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -18,3 +18,7 @@ func setDependencies(svc *service.Config) { "After=NetworkManager-wait-online.service", } } + +func setWorkingDirectory(svc *service.Config, dir string) { + svc.WorkingDirectory = dir +} diff --git a/cmd/ctrld/prog_others.go b/cmd/ctrld/prog_others.go index 9d72f91..d790438 100644 --- a/cmd/ctrld/prog_others.go +++ b/cmd/ctrld/prog_others.go @@ -1,5 +1,4 @@ -//go:build !linux -// +build !linux +//go:build !linux && !freebsd package main @@ -8,3 +7,8 @@ import "github.com/kardianos/service" func (p *prog) preRun() {} func setDependencies(svc *service.Config) {} + +func setWorkingDirectory(svc *service.Config, dir string) { + // WorkingDirectory is not supported on Windows. + svc.WorkingDirectory = dir +} diff --git a/internal/dns/debian_resolvconf.go b/internal/dns/debian_resolvconf.go new file mode 100644 index 0000000..f3d736d --- /dev/null +++ b/internal/dns/debian_resolvconf.go @@ -0,0 +1,153 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux || freebsd || openbsd + +package dns + +import ( + "bytes" + _ "embed" + "fmt" + "os" + "os/exec" + "path/filepath" + + "tailscale.com/atomicfile" + "tailscale.com/types/logger" +) + +//go:embed resolvconf-workaround.sh +var workaroundScript []byte + +// resolvconfConfigName is the name of the config submitted to +// resolvconf. +// The name starts with 'tun' in order to match the hardcoded +// interface order in debian resolvconf, which will place this +// configuration ahead of regular network links. In theory, this +// doesn't matter because we then fix things up to ensure our config +// is the only one in use, but in case that fails, this will make our +// configuration slightly preferred. +// The 'inet' suffix has no specific meaning, but conventionally +// resolvconf implementations encourage adding a suffix roughly +// indicating where the config came from, and "inet" is the "none of +// the above" value (rather than, say, "ppp" or "dhcp"). +const resolvconfConfigName = "ctrld.inet" + +// resolvconfLibcHookPath is the directory containing libc update +// scripts, which are run by Debian resolvconf when /etc/resolv.conf +// has been updated. +const resolvconfLibcHookPath = "/etc/resolvconf/update-libc.d" + +// resolvconfHookPath is the name of the libc hook script we install +// to force Ctrld's DNS config to take effect. +var resolvconfHookPath = filepath.Join(resolvconfLibcHookPath, "ctrld") + +// resolvconfManager manages DNS configuration using the Debian +// implementation of the `resolvconf` program, written by Thomas Hood. +type resolvconfManager struct { + logf logger.Logf + listRecordsPath string + interfacesDir string + scriptInstalled bool // libc update script has been installed +} + +var _ OSConfigurator = (*resolvconfManager)(nil) + +func newDebianResolvconfManager(logf logger.Logf) (*resolvconfManager, error) { + ret := &resolvconfManager{ + logf: logf, + listRecordsPath: "/lib/resolvconf/list-records", + interfacesDir: "/etc/resolvconf/run/interface", // panic fallback if nothing seems to work + } + + if _, err := os.Stat(ret.listRecordsPath); os.IsNotExist(err) { + // This might be a Debian system from before the big /usr + // merge, try /usr instead. + ret.listRecordsPath = "/usr" + ret.listRecordsPath + } + // The runtime directory is currently (2020-04) canonically + // /etc/resolvconf/run, but the manpage is making noise about + // switching to /run/resolvconf and dropping the /etc path. So, + // let's probe the possible directories and use the first one + // that works. + for _, path := range []string{ + "/etc/resolvconf/run/interface", + "/run/resolvconf/interface", + "/var/run/resolvconf/interface", + } { + if _, err := os.Stat(path); err == nil { + ret.interfacesDir = path + break + } + } + if ret.interfacesDir == "" { + // None of the paths seem to work, use the canonical location + // that the current manpage says to use. + ret.interfacesDir = "/etc/resolvconf/run/interfaces" + } + + return ret, nil +} + +func (m *resolvconfManager) deleteCtrldConfig() error { + cmd := exec.Command("resolvconf", "-d", resolvconfConfigName) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("running %s: %s", cmd, out) + } + return nil +} + +func (m *resolvconfManager) SetDNS(config OSConfig) error { + if !m.scriptInstalled { + m.logf("injecting resolvconf workaround script") + if err := os.MkdirAll(resolvconfLibcHookPath, 0755); err != nil { + return err + } + if err := atomicfile.WriteFile(resolvconfHookPath, workaroundScript, 0755); err != nil { + return err + } + m.scriptInstalled = true + } + + if config.IsZero() { + if err := m.deleteCtrldConfig(); err != nil { + return err + } + } else { + stdin := new(bytes.Buffer) + writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go + + // This resolvconf implementation doesn't support exclusive + // mode or interface priorities, so it will end up blending + // our configuration with other sources. However, this will + // get fixed up by the script we injected above. + cmd := exec.Command("resolvconf", "-a", resolvconfConfigName) + cmd.Stdin = stdin + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("running %s: %s", cmd, out) + } + } + + return nil +} + +func (m *resolvconfManager) Close() error { + if err := m.deleteCtrldConfig(); err != nil { + return err + } + + if m.scriptInstalled { + m.logf("removing resolvconf workaround script") + os.Remove(resolvconfHookPath) // Best-effort + } + + return nil +} + +func (m *resolvconfManager) Mode() string { + return "resolvconf" +} diff --git a/internal/dns/direct.go b/internal/dns/direct.go index 7258649..e11be05 100644 --- a/internal/dns/direct.go +++ b/internal/dns/direct.go @@ -144,6 +144,10 @@ type directManager struct { lastWarnContents []byte // last resolv.conf contents that we warned about } +func newDirectManager(logf logger.Logf) *directManager { + return newDirectManagerOnFS(logf, directFS{}) +} + func newDirectManagerOnFS(logf logger.Logf, fs wholeFileFS) *directManager { ctx, cancel := context.WithCancel(context.Background()) m := &directManager{ diff --git a/internal/dns/manager_freebsd.go b/internal/dns/manager_freebsd.go new file mode 100644 index 0000000..27a4e7f --- /dev/null +++ b/internal/dns/manager_freebsd.go @@ -0,0 +1,39 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package dns + +import ( + "fmt" + "os" + + "tailscale.com/types/logger" +) + +func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) { + bs, err := os.ReadFile("/etc/resolv.conf") + if os.IsNotExist(err) { + return newDirectManager(logf), nil + } + if err != nil { + return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err) + } + + switch resolvOwner(bs) { + case "resolvconf": + switch resolvconfStyle() { + case "": + return newDirectManager(logf), nil + case "debian": + return newDebianResolvconfManager(logf) + case "openresolv": + return newOpenresolvManager() + default: + logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", resolvconfStyle()) + return newDirectManager(logf), nil + } + default: + return newDirectManager(logf), nil + } +} diff --git a/internal/dns/nm.go b/internal/dns/nm.go index 68ce71b..03e6f4a 100644 --- a/internal/dns/nm.go +++ b/internal/dns/nm.go @@ -31,6 +31,8 @@ type nmManager struct { dnsManager dbus.BusObject } +var _ OSConfigurator = (*nmManager)(nil) + func newNMManager(interfaceName string) (*nmManager, error) { conn, err := dbus.SystemBus() if err != nil { diff --git a/internal/dns/openresolv.go b/internal/dns/openresolv.go new file mode 100644 index 0000000..8c53d87 --- /dev/null +++ b/internal/dns/openresolv.go @@ -0,0 +1,57 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux || freebsd || openbsd + +package dns + +import ( + "bytes" + "fmt" + "os/exec" +) + +// openresolvManager manages DNS configuration using the openresolv +// implementation of the `resolvconf` program. +type openresolvManager struct{} + +var _ OSConfigurator = (*openresolvManager)(nil) + +func newOpenresolvManager() (openresolvManager, error) { + return openresolvManager{}, nil +} + +func (m openresolvManager) deleteTailscaleConfig() error { + cmd := exec.Command("resolvconf", "-f", "-d", "ctrld") + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("running %s: %s", cmd, out) + } + return nil +} + +func (m openresolvManager) SetDNS(config OSConfig) error { + if config.IsZero() { + return m.deleteTailscaleConfig() + } + + var stdin bytes.Buffer + writeResolvConf(&stdin, config.Nameservers, config.SearchDomains) + + cmd := exec.Command("resolvconf", "-m", "0", "-x", "-a", "ctrld") + cmd.Stdin = &stdin + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("running %s: %s", cmd, out) + } + return nil +} + +func (m openresolvManager) Close() error { + return m.deleteTailscaleConfig() +} + +func (m openresolvManager) Mode() string { + return "resolvconf" +} diff --git a/internal/dns/osconfig.go b/internal/dns/osconfig.go index 0f5e91d..36fcaec 100644 --- a/internal/dns/osconfig.go +++ b/internal/dns/osconfig.go @@ -13,6 +13,8 @@ import ( "tailscale.com/util/dnsname" ) +var _ OSConfigurator = (*directManager)(nil) + // An OSConfigurator applies DNS settings to the operating system. type OSConfigurator interface { // SetDNS updates the OS's DNS configuration to match cfg. diff --git a/internal/dns/resolvconf-workaround.sh b/internal/dns/resolvconf-workaround.sh new file mode 100644 index 0000000..d04c723 --- /dev/null +++ b/internal/dns/resolvconf-workaround.sh @@ -0,0 +1,63 @@ +#!/bin/sh +# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +# +# This script is a workaround for a vpn-unfriendly behavior of the +# original resolvconf by Thomas Hood. Unlike the `openresolv` +# implementation (whose binary is also called resolvconf, +# confusingly), the original resolvconf lacks a way to specify +# "exclusive mode" for a provider configuration. In practice, this +# means that if Ctrld wants to install a DNS configuration, that +# config will get "blended" with the configs from other sources, +# rather than override those other sources. +# +# This script gets installed at /etc/resolvconf/update-libc.d, which +# is a directory of hook scripts that get run after resolvconf's libc +# helper has finished rewriting /etc/resolv.conf. It's meant to notify +# consumers of resolv.conf of a new configuration. +# +# Instead, we use that hook mechanism to reach into resolvconf's +# stuff, and rewrite the libc-generated resolv.conf to exclusively +# contain Ctrld's configuration - effectively implementing +# exclusive mode ourselves in post-production. + +set -e + +if [ -n "$CTRLD_RESOLVCONF_HOOK_LOOP" ]; then + # Hook script being invoked by itself, skip. + exit 0 +fi + +if [ ! -f ctrld.inet ]; then + # Ctrld isn't trying to manage DNS, do nothing. + exit 0 +fi + +if ! grep resolvconf /etc/resolv.conf >/dev/null; then + # resolvconf isn't managing /etc/resolv.conf, do nothing. + exit 0 +fi + +# Write out a modified /etc/resolv.conf containing just our config. +( + if [ -f /etc/resolvconf/resolv.conf.d/head ]; then + cat /etc/resolvconf/resolv.conf.d/head + fi + echo "# Ctrld workaround applied to set exclusive DNS configuration." + cat tun-tailscale.inet + if [ -f /etc/resolvconf/resolv.conf.d/base ]; then + # Keep options and sortlist, discard other base things since + # they're the things we're trying to override. + grep -e 'sortlist ' -e 'options ' /etc/resolvconf/resolv.conf.d/base || true + fi + if [ -f /etc/resolvconf/resolv.conf.d/tail ]; then + cat /etc/resolvconf/resolv.conf.d/tail + fi +) >/etc/resolv.conf + +if [ -d /etc/resolvconf/update-libc.d ] ; then + # Re-notify libc watchers that we've changed resolv.conf again. + export CTRLD_RESOLVCONF_HOOK_LOOP=1 + exec run-parts /etc/resolvconf/update-libc.d +fi \ No newline at end of file diff --git a/internal/dns/resolved.go b/internal/dns/resolved.go index 8d03249..02455b5 100644 --- a/internal/dns/resolved.go +++ b/internal/dns/resolved.go @@ -105,6 +105,8 @@ type resolvedManager struct { newManager func(conn *dbus.Conn) dbus.BusObject } +var _ OSConfigurator = (*resolvedManager)(nil) + func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) { iface, err := net.InterfaceByName(interfaceName) if err != nil {