From 189f35960991cab8b9260ec16a05a7033ade86bc Mon Sep 17 00:00:00 2001 From: Aaron Bieber Date: Tue, 11 Jan 2022 09:45:50 -0700 Subject: [PATCH] net/dns: teach OpenBSD's manager to talk to resolvd(8). (#2789) OpenBSD 6.9 and up has a daemon which handles nameserver configuration. This PR teaches the OpenBSD dns manager to check if resolvd is being used. If it is, it will use the route(8) command to tell resolvd to add the Tailscale dns entries to resolv.conf Signed-off-by: Aaron Bieber --- net/dns/manager_openbsd.go | 67 +++++++++++++++++- net/dns/resolvd.go | 141 +++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 net/dns/resolvd.go diff --git a/net/dns/manager_openbsd.go b/net/dns/manager_openbsd.go index 2889bbf0b..e1611d3cc 100644 --- a/net/dns/manager_openbsd.go +++ b/net/dns/manager_openbsd.go @@ -4,8 +4,71 @@ package dns -import "tailscale.com/types/logger" +import ( + "bytes" + "fmt" + "os" -func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) { + "tailscale.com/types/logger" +) + +type kv struct { + k, v string +} + +func (kv kv) String() string { + return fmt.Sprintf("%s=%s", kv.k, kv.v) +} + +func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) { + return newOSConfigurator(logf, interfaceName, + newOSConfigEnv{ + rcIsResolvd: rcIsResolvd, + fs: directFS{}, + }) +} + +// newOSConfigEnv are the funcs newOSConfigurator needs, pulled out for testing. +type newOSConfigEnv struct { + fs directFS + rcIsResolvd func(resolvConfContents []byte) bool +} + +func newOSConfigurator(logf logger.Logf, interfaceName string, env newOSConfigEnv) (ret OSConfigurator, err error) { + var debug []kv + dbg := func(k, v string) { + debug = append(debug, kv{k, v}) + } + defer func() { + if ret != nil { + dbg("ret", fmt.Sprintf("%T", ret)) + } + logf("dns: %v", debug) + }() + + bs, err := env.fs.ReadFile(resolvConf) + if os.IsNotExist(err) { + dbg("rc", "missing") + return newDirectManager(logf), nil + } + if err != nil { + return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err) + } + + if env.rcIsResolvd(bs) { + dbg("resolvd", "yes") + return newResolvdManager(logf, interfaceName) + } + + dbg("resolvd", "missing") return newDirectManager(logf), nil } + +func rcIsResolvd(resolvConfContents []byte) bool { + // If we have the string "# resolvd:" in resolv.conf resolvd(8) is + // managing things. + if bytes.Contains(resolvConfContents, []byte("# resolvd:")) { + return true + } + return false +} diff --git a/net/dns/resolvd.go b/net/dns/resolvd.go new file mode 100644 index 000000000..42b09ee5a --- /dev/null +++ b/net/dns/resolvd.go @@ -0,0 +1,141 @@ +// 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 openbsd +// +build openbsd + +package dns + +import ( + "bufio" + "bytes" + "fmt" + "os" + "os/exec" + "regexp" + "strings" + + "tailscale.com/types/logger" + "tailscale.com/util/dnsname" +) + +func newResolvdManager(logf logger.Logf, interfaceName string) (*resolvdManager, error) { + return &resolvdManager{ + logf: logf, + ifName: interfaceName, + fs: directFS{}, + }, nil +} + +// resolvdManager is an OSConfigurator which uses route(1) to teach OpenBSD's +// resolvd(8) about DNS servers. +type resolvdManager struct { + logf logger.Logf + ifName string + fs directFS +} + +func (m *resolvdManager) SetDNS(config OSConfig) error { + args := []string{ + "nameserver", + m.ifName, + } + + origResolv, err := m.readAndCopy(resolvConf, backupConf, 0644) + if err != nil { + return err + } + newResolvConf := removeSearchLines(origResolv) + + for _, ns := range config.Nameservers { + args = append(args, ns.String()) + } + + var newSearch = []string{ + "search", + } + for _, s := range config.SearchDomains { + newSearch = append(newSearch, s.WithoutTrailingDot()) + } + + newResolvConf = append(newResolvConf, []byte(strings.Join(newSearch, " "))...) + + err = m.fs.WriteFile(resolvConf, newResolvConf, 0644) + if err != nil { + return err + } + + cmd := exec.Command("/sbin/route", args...) + return cmd.Run() +} + +func (m *resolvdManager) SupportsSplitDNS() bool { + return false +} + +func (m *resolvdManager) GetBaseConfig() (OSConfig, error) { + cfg, err := m.readResolvConf() + if err != nil { + return OSConfig{}, err + } + + return cfg, nil +} + +func (m *resolvdManager) Close() error { + // resolvd handles teardown of nameservers so we only need to write back the original + // config and be done. + + _, err := m.readAndCopy(backupConf, resolvConf, 0644) + if err != nil { + return err + } + + return m.fs.Remove(backupConf) +} + +func (m *resolvdManager) readAndCopy(a, b string, mode os.FileMode) ([]byte, error) { + orig, err := m.fs.ReadFile(a) + if err != nil { + return nil, err + } + err = m.fs.WriteFile(b, orig, mode) + if err != nil { + return nil, err + } + + return orig, nil +} + +func (m resolvdManager) readResolvConf() (config OSConfig, err error) { + b, err := m.fs.ReadFile(resolvConf) + if err != nil { + return OSConfig{}, err + } + + scanner := bufio.NewScanner(bytes.NewReader(b)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // resolvd manages "nameserver" lines, we only need to handle + // "search". + if strings.HasPrefix(line, "search") { + domain := strings.TrimPrefix(line, "search") + domain = strings.TrimSpace(domain) + fqdn, err := dnsname.ToFQDN(domain) + if err != nil { + return OSConfig{}, fmt.Errorf("parsing search domains %q: %w", line, err) + } + config.SearchDomains = append(config.SearchDomains, fqdn) + continue + } + } + + return config, nil +} + +func removeSearchLines(orig []byte) []byte { + re := regexp.MustCompile(`(?m)^search\s+.+$`) + return re.ReplaceAll(orig, []byte("")) +}