net/dns: add Plan 9 support

This requires the rsc/plan9 ndb DNS changes for now:

https://9fans.topicbox.com/groups/9fans/T9c9d81b5801a0820/ndb-suffix-specific-dns-changes
e8c148ff09
1d0642ae49

Updates #5794

Change-Id: I0e242c1fe7bb4404e23604e03a31f89f0d18e70d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-04-01 04:01:00 -07:00 committed by Brad Fitzpatrick
parent 5e305032a9
commit 84c82ac4be
4 changed files with 269 additions and 2 deletions

View File

@ -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.

View File

@ -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

181
net/dns/manager_plan9.go Normal file
View File

@ -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
}

View File

@ -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)
}
})
}
}