net/dns: add more of DNS manager for plan9, tests, plumb netmon/tun name around more

Change-Id: Ia542d7c69f3fbcd2571e2da8b04ad34ec0e2645d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-03-23 13:59:24 -07:00
parent abb77602a4
commit 835c7e1e90
5 changed files with 205 additions and 12 deletions

View File

@ -748,6 +748,12 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
return false, err
}
if runtime.GOOS == "plan9" {
// TODO(bradfitz): why don't we do this on all platforms?
// We should. Doing it just on plan9 for now conservatively.
sys.NetMon.Get().SetTailscaleInterfaceName(devName)
}
r, err := router.New(logf, dev, sys.NetMon.Get(), sys.HealthTracker())
if err != nil {
dev.Close()

View File

@ -9,14 +9,18 @@ package dns
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"net/netip"
"os"
"regexp"
"strings"
"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) {
@ -33,13 +37,113 @@ type plan9DNSManager struct {
knobs *controlknobs.Knobs
}
func (m *plan9DNSManager) SetDNS(c OSConfig) error {
var buf bytes.Buffer
bw := bufio.NewWriter(&buf)
c.WriteToBufioWriter(bw)
bw.Flush()
// netNDBWithoutTailscale returns /net/ndb with any Tailscale
// bits removed.
func netNDBWithoutTailscale() ([]byte, error) {
raw, err := os.ReadFile("/net/ndb")
if err != nil {
return nil, err
}
return netNDBBytesWithoutTailscale(raw)
}
// 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))
}
}
if len(added) == 0 || true {
return buf.Bytes()
}
var ret bytes.Buffer
for _, s := range added {
ret.WriteString("#tailscaled-added-line: ")
ret.WriteString(s)
ret.WriteString("\n")
}
ret.WriteString("\n")
ret.Write(buf.Bytes())
return ret.Bytes()
}
func (m *plan9DNSManager) SetDNS(c OSConfig) error {
tsFree, err := netNDBWithoutTailscale()
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, tsFree) {
log.Printf("XXX need to write /net/ndb of %q", newBuf)
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_WRONLY, 0); err == nil {
defer f.Close()
if _, err := io.WriteString(f, "refresh\n"); err != nil {
return err
}
}
}
log.Printf("XXX: TODO: plan9 SetDNS: %s", buf.Bytes())
return 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)
}
})
}
}

View File

@ -491,7 +491,7 @@ func (m *Monitor) IsMajorChangeFrom(s1, s2 *State) bool {
return true
}
for iname, i := range s1.Interface {
if iname == m.tsIfName || iname == "/net/ipifc/2" {
if iname == m.tsIfName {
// Ignore changes in the Tailscale interface itself.
continue
}

View File

@ -454,9 +454,6 @@ func isTailscaleInterface(name string, ips []netip.Prefix) bool {
// macOS NetworkExtensions and utun devices.
return true
}
if runtime.GOOS == "plan9" && (hasTailscaleIP(ips) || name == "/net/ipifc/2") { // XXX fix; use tun name
return true
}
return name == "Tailscale" || // as it is on Windows
strings.HasPrefix(name, "tailscale") // TODO: use --tun flag value, etc; see TODO in method doc
}
@ -475,11 +472,11 @@ func getState(optTSInterfaceName string) (*State, error) {
Interface: make(map[string]Interface),
}
if err := ForeachInterface(func(ni Interface, pfxs []netip.Prefix) {
isTS := optTSInterfaceName != "" && ni.Name == optTSInterfaceName
isTSInterfaceName := optTSInterfaceName != "" && ni.Name == optTSInterfaceName
ifUp := ni.IsUp()
s.Interface[ni.Name] = ni
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], pfxs...)
if !ifUp || isTS || isTailscaleInterface(ni.Name, pfxs) {
if !ifUp || isTSInterfaceName || isTailscaleInterface(ni.Name, pfxs) {
return
}
for _, pfx := range pfxs {