diff --git a/net/dns/manager.go b/net/dns/manager.go index 0bfbaa077..64bf12c6b 100644 --- a/net/dns/manager.go +++ b/net/dns/manager.go @@ -284,7 +284,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig // Deal with trivial configs first. switch { - case !cfg.needsOSResolver(): + case !cfg.needsOSResolver() || runtime.GOOS == "plan9": // Set search domains, but nothing else. This also covers the // case where cfg is entirely zero, in which case these // configs clear all Tailscale DNS settings. diff --git a/net/dns/manager_default.go b/net/dns/manager_default.go index 99ff017da..e14454e76 100644 --- a/net/dns/manager_default.go +++ b/net/dns/manager_default.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -//go:build !linux && !freebsd && !openbsd && !windows && !darwin && !illumos && !solaris +//go:build !linux && !freebsd && !openbsd && !windows && !darwin && !illumos && !solaris && !plan9 package dns diff --git a/net/dns/manager_plan9.go b/net/dns/manager_plan9.go new file mode 100644 index 000000000..ca179f27f --- /dev/null +++ b/net/dns/manager_plan9.go @@ -0,0 +1,181 @@ +// 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 +} diff --git a/net/dns/manager_plan9_test.go b/net/dns/manager_plan9_test.go new file mode 100644 index 000000000..806fdb68e --- /dev/null +++ b/net/dns/manager_plan9_test.go @@ -0,0 +1,86 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build plan9 + +package dns + +import "testing" + +func TestNetNDBBytesWithoutTailscale(t *testing.T) { + tests := []struct { + name string + raw string + want string + }{ + { + name: "empty", + raw: "", + want: "", + }, + { + name: "no-tailscale", + raw: "# This is a comment\nip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2\n\tsys=gnot\n", + want: "# This is a comment\nip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2\n\tsys=gnot\n", + }, + { + name: "remove-by-comments", + raw: "# This is a comment\n#tailscaled-added-line: dns=100.100.100.100\nip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2\n\tdns=100.100.100.100\n\tsys=gnot\n", + want: "# This is a comment\nip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2\n\tsys=gnot\n", + }, + { + name: "remove-by-ts.net", + raw: "Some line\n\tdns=100.100.100.100 suffix=foo.ts.net\n\tfoo=bar\n", + want: "Some line\n\tfoo=bar\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := netNDBBytesWithoutTailscale([]byte(tt.raw)) + if err != nil { + t.Fatal(err) + } + if string(got) != tt.want { + t.Errorf("GOT:\n%s\n\nWANT:\n%s\n", string(got), tt.want) + } + }) + } +} + +func TestSetNDBSuffix(t *testing.T) { + tests := []struct { + name string + raw string + want string + }{ + { + name: "empty", + raw: "", + want: "", + }, + { + name: "set", + raw: "ip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2\n\tsys=gnot\n\tdns=100.100.100.100\n\n# foo\n", + want: `#tailscaled-added-line: dns=100.100.100.100 suffix=foo.ts.net +#tailscaled-added-line: dnsdomain=foo.ts.net + +ip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2 + sys=gnot + dns=100.100.100.100 + dns=100.100.100.100 suffix=foo.ts.net + dnsdomain=foo.ts.net + +# foo +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := setNDBSuffix([]byte(tt.raw), "foo.ts.net") + if string(got) != tt.want { + t.Errorf("wrong value\n GOT %q:\n%s\n\nWANT %q:\n%s\n", got, got, tt.want, tt.want) + } + }) + } + +}