net/dns, feature/featuretags: make NetworkManager, systemd-resolved, and DBus modular

Saves 360 KB (19951800 => 19591352 on linux/amd64 --extra-small --box binary)

Updates #12614
Updates #17206

Change-Id: Iafd5b2536dd735111b447546cba335a7a64379ed
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-09-23 14:11:04 -07:00 committed by Brad Fitzpatrick
parent b54cdf9f38
commit b3e9a128af
12 changed files with 284 additions and 113 deletions

View File

@ -161,3 +161,16 @@ func TestOmitOutboundProxy(t *testing.T) {
}, },
}.Check(t) }.Check(t)
} }
func TestOmitDBus(t *testing.T) {
deptest.DepChecker{
GOOS: "linux",
GOARCH: "amd64",
Tags: "ts_omit_networkmanager,ts_omit_dbus,ts_omit_resolved,ts_omit_systray,ts_omit_ssh,ts_include_cli",
OnDep: func(dep string) {
if strings.Contains(dep, "dbus") {
t.Errorf("unexpected DBus dep: %q", dep)
}
},
}.Check(t)
}

View File

@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build ts_omit_dbus
package buildfeatures
// HasDBus is whether the binary was built with support for modular feature "Linux DBus support".
// Specifically, it's whether the binary was NOT built with the "ts_omit_dbus" build tag.
// It's a const so it can be used for dead code elimination.
const HasDBus = false

View File

@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build !ts_omit_dbus
package buildfeatures
// HasDBus is whether the binary was built with support for modular feature "Linux DBus support".
// Specifically, it's whether the binary was NOT built with the "ts_omit_dbus" build tag.
// It's a const so it can be used for dead code elimination.
const HasDBus = true

View File

@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build ts_omit_networkmanager
package buildfeatures
// HasNetworkManager is whether the binary was built with support for modular feature "Linux NetworkManager integration".
// Specifically, it's whether the binary was NOT built with the "ts_omit_networkmanager" build tag.
// It's a const so it can be used for dead code elimination.
const HasNetworkManager = false

View File

@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build !ts_omit_networkmanager
package buildfeatures
// HasNetworkManager is whether the binary was built with support for modular feature "Linux NetworkManager integration".
// Specifically, it's whether the binary was NOT built with the "ts_omit_networkmanager" build tag.
// It's a const so it can be used for dead code elimination.
const HasNetworkManager = true

View File

@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build ts_omit_resolved
package buildfeatures
// HasResolved is whether the binary was built with support for modular feature "Linux systemd-resolved integration".
// Specifically, it's whether the binary was NOT built with the "ts_omit_resolved" build tag.
// It's a const so it can be used for dead code elimination.
const HasResolved = false

View File

@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by gen.go; DO NOT EDIT.
//go:build !ts_omit_resolved
package buildfeatures
// HasResolved is whether the binary was built with support for modular feature "Linux systemd-resolved integration".
// Specifically, it's whether the binary was NOT built with the "ts_omit_resolved" build tag.
// It's a const so it can be used for dead code elimination.
const HasResolved = true

View File

@ -97,6 +97,7 @@ var Features = map[FeatureTag]FeatureMeta{
"capture": {"Capture", "Packet capture", nil}, "capture": {"Capture", "Packet capture", nil},
"cli": {"CLI", "embed the CLI into the tailscaled binary", nil}, "cli": {"CLI", "embed the CLI into the tailscaled binary", nil},
"completion": {"Completion", "CLI shell completion", nil}, "completion": {"Completion", "CLI shell completion", nil},
"dbus": {"DBus", "Linux DBus support", nil},
"debugeventbus": {"DebugEventBus", "eventbus debug support", nil}, "debugeventbus": {"DebugEventBus", "eventbus debug support", nil},
"debugportmapper": { "debugportmapper": {
Sym: "DebugPortMapper", Sym: "DebugPortMapper",
@ -113,9 +114,19 @@ var Features = map[FeatureTag]FeatureMeta{
Desc: "Outbound localhost HTTP/SOCK5 proxy support", Desc: "Outbound localhost HTTP/SOCK5 proxy support",
Deps: []FeatureTag{"netstack"}, Deps: []FeatureTag{"netstack"},
}, },
"portmapper": {"PortMapper", "NAT-PMP/PCP/UPnP port mapping support", nil}, "portmapper": {"PortMapper", "NAT-PMP/PCP/UPnP port mapping support", nil},
"netstack": {"Netstack", "gVisor netstack (userspace networking) support (TODO; not yet omittable)", nil}, "netstack": {"Netstack", "gVisor netstack (userspace networking) support (TODO; not yet omittable)", nil},
"networkmanager": {
Sym: "NetworkManager",
Desc: "Linux NetworkManager integration",
Deps: []FeatureTag{"dbus"},
},
"relayserver": {"RelayServer", "Relay server", nil}, "relayserver": {"RelayServer", "Relay server", nil},
"resolved": {
Sym: "Resolved",
Desc: "Linux systemd-resolved integration",
Deps: []FeatureTag{"dbus"},
},
"serve": { "serve": {
Sym: "Serve", Sym: "Serve",
Desc: "Serve and Funnel support", Desc: "Serve and Funnel support",
@ -124,10 +135,14 @@ var Features = map[FeatureTag]FeatureMeta{
"ssh": { "ssh": {
Sym: "SSH", Sym: "SSH",
Desc: "Tailscale SSH support", Desc: "Tailscale SSH support",
Deps: []FeatureTag{"netstack"}, Deps: []FeatureTag{"dbus", "netstack"},
},
"syspolicy": {"SystemPolicy", "System policy configuration (MDM) support", nil},
"systray": {
Sym: "SysTray",
Desc: "Linux system tray",
Deps: []FeatureTag{"dbus"},
}, },
"syspolicy": {"SystemPolicy", "System policy configuration (MDM) support", nil},
"systray": {"SysTray", "Linux system tray", nil},
"taildrop": {"Taildrop", "Taildrop (file sending) support", nil}, "taildrop": {"Taildrop", "Taildrop (file sending) support", nil},
"tailnetlock": {"TailnetLock", "Tailnet Lock support", nil}, "tailnetlock": {"TailnetLock", "Tailnet Lock support", nil},
"tap": {"Tap", "Experimental Layer 2 (ethernet) support", nil}, "tap": {"Tap", "Experimental Layer 2 (ethernet) support", nil},

59
net/dns/dbus.go Normal file
View File

@ -0,0 +1,59 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android && !ts_omit_dbus
package dns
import (
"context"
"time"
"github.com/godbus/dbus/v5"
)
func init() {
optDBusPing.Set(dbusPing)
optDBusReadString.Set(dbusReadString)
}
func dbusPing(name, objectPath string) error {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
obj := conn.Object(name, dbus.ObjectPath(objectPath))
call := obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0)
return call.Err
}
// dbusReadString reads a string property from the provided name and object
// path. property must be in "interface.member" notation.
func dbusReadString(name, objectPath, iface, member string) (string, error) {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return "", err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
obj := conn.Object(name, dbus.ObjectPath(objectPath))
var result dbus.Variant
err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, iface, member).Store(&result)
if err != nil {
return "", err
}
if s, ok := result.Value().(string); ok {
return s, nil
}
return result.String(), nil
}

View File

@ -7,7 +7,6 @@ package dns
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -15,13 +14,12 @@ import (
"sync" "sync"
"time" "time"
"github.com/godbus/dbus/v5"
"tailscale.com/control/controlknobs" "tailscale.com/control/controlknobs"
"tailscale.com/feature"
"tailscale.com/health" "tailscale.com/health"
"tailscale.com/net/netaddr" "tailscale.com/net/netaddr"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/util/cmpver"
"tailscale.com/util/syspolicy/policyclient" "tailscale.com/util/syspolicy/policyclient"
"tailscale.com/version/distro" "tailscale.com/version/distro"
) )
@ -36,6 +34,31 @@ func (kv kv) String() string {
var publishOnce sync.Once var publishOnce sync.Once
// reconfigTimeout is the time interval within which Manager.{Up,Down} should complete.
//
// This is particularly useful because certain conditions can cause indefinite hangs
// (such as improper dbus auth followed by contextless dbus.Object.Call).
// Such operations should be wrapped in a timeout context.
const reconfigTimeout = time.Second
// Set unless ts_omit_networkmanager
var (
optNewNMManager feature.Hook[func(ifName string) (OSConfigurator, error)]
optNMIsUsingResolved feature.Hook[func() error]
optNMVersionBetween feature.Hook[func(v1, v2 string) (bool, error)]
)
// Set unless ts_omit_resolved
var (
optNewResolvedManager feature.Hook[func(logf logger.Logf, health *health.Tracker, interfaceName string) (OSConfigurator, error)]
)
// Set unless ts_omit_dbus
var (
optDBusPing feature.Hook[func(name, objectPath string) error]
optDBusReadString feature.Hook[func(name, objectPath, iface, member string) (string, error)]
)
// NewOSConfigurator created a new OS configurator. // NewOSConfigurator created a new OS configurator.
// //
// The health tracker may be nil; the knobs may be nil and are ignored on this platform. // The health tracker may be nil; the knobs may be nil and are ignored on this platform.
@ -45,13 +68,25 @@ func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ policyclient.
} }
env := newOSConfigEnv{ env := newOSConfigEnv{
fs: directFS{}, fs: directFS{},
dbusPing: dbusPing, resolvconfStyle: resolvconfStyle,
dbusReadString: dbusReadString,
nmIsUsingResolved: nmIsUsingResolved,
nmVersionBetween: nmVersionBetween,
resolvconfStyle: resolvconfStyle,
} }
if f, ok := optDBusPing.GetOk(); ok {
env.dbusPing = f
} else {
env.dbusPing = func(_, _ string) error { return errors.ErrUnsupported }
}
if f, ok := optDBusReadString.GetOk(); ok {
env.dbusReadString = f
} else {
env.dbusReadString = func(_, _, _, _ string) (string, error) { return "", errors.ErrUnsupported }
}
if f, ok := optNMIsUsingResolved.GetOk(); ok {
env.nmIsUsingResolved = f
} else {
env.nmIsUsingResolved = func() error { return errors.ErrUnsupported }
}
env.nmVersionBetween, _ = optNMVersionBetween.GetOk() // GetOk to not panic if nil; unused if optNMIsUsingResolved returns an error
mode, err := dnsMode(logf, health, env) mode, err := dnsMode(logf, health, env)
if err != nil { if err != nil {
return nil, err return nil, err
@ -66,17 +101,24 @@ func NewOSConfigurator(logf logger.Logf, health *health.Tracker, _ policyclient.
case "direct": case "direct":
return newDirectManagerOnFS(logf, health, env.fs), nil return newDirectManagerOnFS(logf, health, env.fs), nil
case "systemd-resolved": case "systemd-resolved":
return newResolvedManager(logf, health, interfaceName) if f, ok := optNewResolvedManager.GetOk(); ok {
return f(logf, health, interfaceName)
}
return nil, fmt.Errorf("tailscaled was built without DNS %q support", mode)
case "network-manager": case "network-manager":
return newNMManager(interfaceName) if f, ok := optNewNMManager.GetOk(); ok {
return f(interfaceName)
}
return nil, fmt.Errorf("tailscaled was built without DNS %q support", mode)
case "debian-resolvconf": case "debian-resolvconf":
return newDebianResolvconfManager(logf) return newDebianResolvconfManager(logf)
case "openresolv": case "openresolv":
return newOpenresolvManager(logf) return newOpenresolvManager(logf)
default: default:
logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode) logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode)
return newDirectManagerOnFS(logf, health, env.fs), nil
} }
return newDirectManagerOnFS(logf, health, env.fs), nil
} }
// newOSConfigEnv are the funcs newOSConfigurator needs, pulled out for testing. // newOSConfigEnv are the funcs newOSConfigurator needs, pulled out for testing.
@ -292,50 +334,6 @@ func dnsMode(logf logger.Logf, health *health.Tracker, env newOSConfigEnv) (ret
} }
} }
func nmVersionBetween(first, last string) (bool, error) {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return false, err
}
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager"))
v, err := nm.GetProperty("org.freedesktop.NetworkManager.Version")
if err != nil {
return false, err
}
version, ok := v.Value().(string)
if !ok {
return false, fmt.Errorf("unexpected type %T for NM version", v.Value())
}
outside := cmpver.Compare(version, first) < 0 || cmpver.Compare(version, last) > 0
return !outside, nil
}
func nmIsUsingResolved() error {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return err
}
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager"))
v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode")
if err != nil {
return fmt.Errorf("getting NM mode: %w", err)
}
mode, ok := v.Value().(string)
if !ok {
return fmt.Errorf("unexpected type %T for NM DNS mode", v.Value())
}
if mode != "systemd-resolved" {
return errors.New("NetworkManager is not using systemd-resolved for DNS")
}
return nil
}
// resolvedIsActuallyResolver reports whether the system is using // resolvedIsActuallyResolver reports whether the system is using
// systemd-resolved as the resolver. There are two different ways to // systemd-resolved as the resolver. There are two different ways to
// use systemd-resolved: // use systemd-resolved:
@ -396,44 +394,3 @@ func isLibnssResolveUsed(env newOSConfigEnv) error {
} }
return fmt.Errorf("libnss_resolve not used") return fmt.Errorf("libnss_resolve not used")
} }
func dbusPing(name, objectPath string) error {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
obj := conn.Object(name, dbus.ObjectPath(objectPath))
call := obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0)
return call.Err
}
// dbusReadString reads a string property from the provided name and object
// path. property must be in "interface.member" notation.
func dbusReadString(name, objectPath, iface, member string) (string, error) {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return "", err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
obj := conn.Object(name, dbus.ObjectPath(objectPath))
var result dbus.Variant
err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, iface, member).Store(&result)
if err != nil {
return "", err
}
if s, ok := result.Value().(string); ok {
return s, nil
}
return result.String(), nil
}

View File

@ -1,13 +1,14 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android //go:build linux && !android && !ts_omit_networkmanager
package dns package dns
import ( import (
"context" "context"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
@ -16,6 +17,7 @@ import (
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/util/cmpver"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
) )
@ -25,13 +27,6 @@ const (
lowerPriority = int32(200) // lower than all builtin auto priorities lowerPriority = int32(200) // lower than all builtin auto priorities
) )
// reconfigTimeout is the time interval within which Manager.{Up,Down} should complete.
//
// This is particularly useful because certain conditions can cause indefinite hangs
// (such as improper dbus auth followed by contextless dbus.Object.Call).
// Such operations should be wrapped in a timeout context.
const reconfigTimeout = time.Second
// nmManager uses the NetworkManager DBus API. // nmManager uses the NetworkManager DBus API.
type nmManager struct { type nmManager struct {
interfaceName string interfaceName string
@ -39,7 +34,13 @@ type nmManager struct {
dnsManager dbus.BusObject dnsManager dbus.BusObject
} }
func newNMManager(interfaceName string) (*nmManager, error) { func init() {
optNewNMManager.Set(newNMManager)
optNMIsUsingResolved.Set(nmIsUsingResolved)
optNMVersionBetween.Set(nmVersionBetween)
}
func newNMManager(interfaceName string) (OSConfigurator, error) {
conn, err := dbus.SystemBus() conn, err := dbus.SystemBus()
if err != nil { if err != nil {
return nil, err return nil, err
@ -389,3 +390,47 @@ func (m *nmManager) Close() error {
// settings when the tailscale interface goes away. // settings when the tailscale interface goes away.
return nil return nil
} }
func nmVersionBetween(first, last string) (bool, error) {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return false, err
}
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager"))
v, err := nm.GetProperty("org.freedesktop.NetworkManager.Version")
if err != nil {
return false, err
}
version, ok := v.Value().(string)
if !ok {
return false, fmt.Errorf("unexpected type %T for NM version", v.Value())
}
outside := cmpver.Compare(version, first) < 0 || cmpver.Compare(version, last) > 0
return !outside, nil
}
func nmIsUsingResolved() error {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return err
}
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager"))
v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode")
if err != nil {
return fmt.Errorf("getting NM mode: %w", err)
}
mode, ok := v.Value().(string)
if !ok {
return fmt.Errorf("unexpected type %T for NM DNS mode", v.Value())
}
if mode != "systemd-resolved" {
return errors.New("NetworkManager is not using systemd-resolved for DNS")
}
return nil
}

View File

@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android //go:build linux && !android && !ts_omit_resolved
package dns package dns
@ -70,7 +70,11 @@ type resolvedManager struct {
configCR chan changeRequest // tracks OSConfigs changes and error responses configCR chan changeRequest // tracks OSConfigs changes and error responses
} }
func newResolvedManager(logf logger.Logf, health *health.Tracker, interfaceName string) (*resolvedManager, error) { func init() {
optNewResolvedManager.Set(newResolvedManager)
}
func newResolvedManager(logf logger.Logf, health *health.Tracker, interfaceName string) (OSConfigurator, error) {
iface, err := net.InterfaceByName(interfaceName) iface, err := net.InterfaceByName(interfaceName)
if err != nil { if err != nil {
return nil, err return nil, err