tailscale/net/dns/manager_plan9.go

182 lines
4.4 KiB
Go
Raw Normal View History

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// TODO: man 6 ndb | grep -e 'suffix.*same line'
// to detect Russ's https://9fans.topicbox.com/groups/9fans/T9c9d81b5801a0820/ndb-suffix-specific-dns-changes
package dns
import (
"bufio"
"bytes"
"fmt"
"io"
"net/netip"
"os"
"regexp"
"strings"
"unicode"
"tailscale.com/control/controlknobs"
"tailscale.com/health"
"tailscale.com/types/logger"
"tailscale.com/util/set"
)
func NewOSConfigurator(logf logger.Logf, ht *health.Tracker, knobs *controlknobs.Knobs, interfaceName string) (OSConfigurator, error) {
return &plan9DNSManager{
logf: logf,
ht: ht,
knobs: knobs,
}, nil
}
type plan9DNSManager struct {
logf logger.Logf
ht *health.Tracker
knobs *controlknobs.Knobs
}
// netNDBBytesWithoutTailscale returns raw (the contents of /net/ndb) with any
// Tailscale bits removed.
func netNDBBytesWithoutTailscale(raw []byte) ([]byte, error) {
var ret bytes.Buffer
bs := bufio.NewScanner(bytes.NewReader(raw))
removeLine := set.Set[string]{}
for bs.Scan() {
t := bs.Text()
if rest, ok := strings.CutPrefix(t, "#tailscaled-added-line:"); ok {
removeLine.Add(strings.TrimSpace(rest))
continue
}
trimmed := strings.TrimSpace(t)
if removeLine.Contains(trimmed) {
removeLine.Delete(trimmed)
continue
}
// Also remove any DNS line referencing *.ts.net. This is
// Tailscale-specific (and won't work with, say, Headscale), but
// the Headscale case will be covered by the #tailscaled-added-line
// logic above, assuming the user didn't delete those comments.
if (strings.HasPrefix(trimmed, "dns=") || strings.Contains(trimmed, "dnsdomain=")) &&
strings.HasSuffix(trimmed, ".ts.net") {
continue
}
ret.WriteString(t)
ret.WriteByte('\n')
}
return ret.Bytes(), bs.Err()
}
// setNDBSuffix adds lines to tsFree (the contents of /net/ndb already cleaned
// of Tailscale-added lines) to add the optional DNS search domain (e.g.
// "foo.ts.net") and DNS server to it.
func setNDBSuffix(tsFree []byte, suffix string) []byte {
suffix = strings.TrimSuffix(suffix, ".")
if suffix == "" {
return tsFree
}
var buf bytes.Buffer
bs := bufio.NewScanner(bytes.NewReader(tsFree))
var added []string
addLine := func(s string) {
added = append(added, strings.TrimSpace(s))
buf.WriteString(s)
}
for bs.Scan() {
buf.Write(bs.Bytes())
buf.WriteByte('\n')
t := bs.Text()
if suffix != "" && len(added) == 0 && strings.HasPrefix(t, "\tdns=") {
addLine(fmt.Sprintf("\tdns=100.100.100.100 suffix=%s\n", suffix))
addLine(fmt.Sprintf("\tdnsdomain=%s\n", suffix))
}
}
bufTrim := bytes.TrimLeftFunc(buf.Bytes(), unicode.IsSpace)
if len(added) == 0 {
return bufTrim
}
var ret bytes.Buffer
for _, s := range added {
ret.WriteString("#tailscaled-added-line: ")
ret.WriteString(s)
ret.WriteString("\n")
}
ret.WriteString("\n")
ret.Write(bufTrim)
return ret.Bytes()
}
func (m *plan9DNSManager) SetDNS(c OSConfig) error {
ndbOnDisk, err := os.ReadFile("/net/ndb")
if err != nil {
return err
}
tsFree, err := netNDBBytesWithoutTailscale(ndbOnDisk)
if err != nil {
return err
}
var suffix string
if len(c.SearchDomains) > 0 {
suffix = string(c.SearchDomains[0])
}
newBuf := setNDBSuffix(tsFree, suffix)
if !bytes.Equal(newBuf, ndbOnDisk) {
if err := os.WriteFile("/net/ndb", newBuf, 0644); err != nil {
return fmt.Errorf("writing /net/ndb: %w", err)
}
if f, err := os.OpenFile("/net/dns", os.O_RDWR, 0); err == nil {
if _, err := io.WriteString(f, "refresh\n"); err != nil {
f.Close()
return fmt.Errorf("/net/dns refresh write: %w", err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("/net/dns refresh close: %w", err)
}
}
}
return nil
}
func (m *plan9DNSManager) SupportsSplitDNS() bool { return false }
func (m *plan9DNSManager) Close() error {
// TODO(bradfitz): remove the Tailscale bits from /net/ndb ideally
return nil
}
var dnsRegex = regexp.MustCompile(`\bdns=(\d+\.\d+\.\d+\.\d+)\b`)
func (m *plan9DNSManager) GetBaseConfig() (OSConfig, error) {
var oc OSConfig
f, err := os.Open("/net/ndb")
if err != nil {
return oc, err
}
defer f.Close()
bs := bufio.NewScanner(f)
for bs.Scan() {
m := dnsRegex.FindSubmatch(bs.Bytes())
if m == nil {
continue
}
addr, err := netip.ParseAddr(string(m[1]))
if err != nil {
continue
}
oc.Nameservers = append(oc.Nameservers, addr)
}
if err := bs.Err(); err != nil {
return oc, err
}
return oc, nil
}