Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android && !ts_omit_networkmanager
package dns
import ( "context" "encoding/binary" "errors" "fmt" "net" "net/netip" "sort" "time"
"github.com/godbus/dbus/v5" "tailscale.com/net/tsaddr" "tailscale.com/util/cmpver" "tailscale.com/util/dnsname")
const ( highestPriority = int32(-1 << 31) mediumPriority = int32(1) // Highest priority that doesn't hard-override lowerPriority = int32(200) // lower than all builtin auto priorities)
// nmManager uses the NetworkManager DBus API.type nmManager struct { interfaceName string manager dbus.BusObject dnsManager dbus.BusObject}
func init() { optNewNMManager.Set(newNMManager) optNMIsUsingResolved.Set(nmIsUsingResolved) optNMVersionBetween.Set(nmVersionBetween)}
func newNMManager(interfaceName string) (OSConfigurator, error) { conn, err := dbus.SystemBus() if err != nil { return nil, err }
return &nmManager{ interfaceName: interfaceName, manager: conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager")), dnsManager: conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager")), }, nil}
type nmConnectionSettings map[string]map[string]dbus.Variant
func (m *nmManager) SetDNS(config OSConfig) error { ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout) defer cancel()
// NetworkManager only lets you set DNS settings on "active" // connections, which requires an assigned IP address. This got // configured before the DNS manager was invoked, but it might // take a little time for the netlink notifications to propagate // up. So, keep retrying for the duration of the reconfigTimeout. var err error for ctx.Err() == nil { err = m.trySet(ctx, config) if err == nil { break } time.Sleep(10 * time.Millisecond) }
return err}
func (m *nmManager) trySet(ctx context.Context, config OSConfig) error { conn, err := dbus.SystemBus() if err != nil { return fmt.Errorf("connecting to system bus: %w", err) }
// This is how we get at the DNS settings: // // org.freedesktop.NetworkManager // | // [GetDeviceByIpIface] // | // v // org.freedesktop.NetworkManager.Device <--------\ // (describes a network interface) | // | | // [GetAppliedConnection] [Reapply] // | | // v | // org.freedesktop.NetworkManager.Connection | // (connection settings) ------/ // contains {dns, dns-priority, dns-search} // // Ref: https://developer.gnome.org/NetworkManager/stable/settings-ipv4.html.
nm := conn.Object( "org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager"), )
var devicePath dbus.ObjectPath err = nm.CallWithContext( ctx, "org.freedesktop.NetworkManager.GetDeviceByIpIface", 0, m.interfaceName, ).Store(&devicePath) if err != nil { return fmt.Errorf("getDeviceByIpIface: %w", err) } device := conn.Object("org.freedesktop.NetworkManager", devicePath)
var ( settings nmConnectionSettings version uint64 ) err = device.CallWithContext( ctx, "org.freedesktop.NetworkManager.Device.GetAppliedConnection", 0, uint32(0), ).Store(&settings, &version) if err != nil { return fmt.Errorf("getAppliedConnection: %w", err) }
// Frustratingly, NetworkManager represents IPv4 addresses as uint32s, // although IPv6 addresses are represented as byte arrays. // Perform the conversion here. var ( dnsv4 []uint32 dnsv6 [][]byte ) for _, ip := range config.Nameservers { b := ip.As16() if ip.Is4() { dnsv4 = append(dnsv4, binary.NativeEndian.Uint32(b[12:])) } else { dnsv6 = append(dnsv6, b[:]) } }
// NetworkManager wipes out IPv6 address configuration unless we // tell it explicitly to keep it. Read out the current interface // settings and mirror them out to NetworkManager. var addrs6 []map[string]any if tsIf, err := net.InterfaceByName(m.interfaceName); err == nil { addrs, _ := tsIf.Addrs() for _, a := range addrs { if ipnet, ok := a.(*net.IPNet); ok { nip, ok := netip.AddrFromSlice(ipnet.IP) nip = nip.Unmap() if ok && tsaddr.IsTailscaleIP(nip) && nip.Is6() { addrs6 = append(addrs6, map[string]any{ "address": nip.String(), "prefix": uint32(128), }) } } } }
seen := map[dnsname.FQDN]bool{} var search []string for _, dom := range config.SearchDomains { if seen[dom] { continue } seen[dom] = true search = append(search, dom.WithTrailingDot()) } for _, dom := range config.MatchDomains { if seen[dom] { continue } seen[dom] = true search = append(search, "~"+dom.WithTrailingDot()) } if len(config.MatchDomains) == 0 { // Non-split routing requested, add an all-domains match. search = append(search, "~.") }
// Ideally we would like to disable LLMNR and mdns on the // interface here, but older NetworkManagers don't understand // those settings and choke on them, so we don't. Both LLMNR and // mdns will fail since tailscale0 doesn't do multicast, so it's // effectively fine. We used to try and enforce LLMNR and mdns // settings here, but that led to #1870.
ipv4Map := settings["ipv4"] ipv4Map["dns"] = dbus.MakeVariant(dnsv4) ipv4Map["dns-search"] = dbus.MakeVariant(search) // We should only request priority if we have nameservers to set. if len(dnsv4) == 0 { ipv4Map["dns-priority"] = dbus.MakeVariant(lowerPriority) } else if len(config.MatchDomains) > 0 { // Set a fairly high priority, but don't override all other // configs when in split-DNS mode. ipv4Map["dns-priority"] = dbus.MakeVariant(mediumPriority) } else { // Negative priority means only the settings from the most // negative connection get used. The way this mixes with // per-domain routing is unclear, but it _seems_ that the // priority applies after routing has found possible // candidates for a resolution. ipv4Map["dns-priority"] = dbus.MakeVariant(highestPriority) }
ipv6Map := settings["ipv6"] // In IPv6 settings, you're only allowed to provide additional // static DNS settings in "auto" (SLAAC) or "manual" mode. In // "manual" mode you also have to specify IP addresses, so we use // "auto". // // NM actually documents that to set just DNS servers, you should // use "auto" mode and then set ignore auto routes and DNS, which // basically means "autoconfigure but ignore any autoconfiguration // results you might get". As a safety, we also say that // NetworkManager should never try to make us the default route // (none of its business anyway, we handle our own default // routing). ipv6Map["method"] = dbus.MakeVariant("auto") if len(addrs6) > 0 { ipv6Map["address-data"] = dbus.MakeVariant(addrs6) } ipv6Map["ignore-auto-routes"] = dbus.MakeVariant(true) ipv6Map["ignore-auto-dns"] = dbus.MakeVariant(true) ipv6Map["never-default"] = dbus.MakeVariant(true)
ipv6Map["dns"] = dbus.MakeVariant(dnsv6) ipv6Map["dns-search"] = dbus.MakeVariant(search) if len(dnsv6) == 0 { ipv6Map["dns-priority"] = dbus.MakeVariant(lowerPriority) } else if len(config.MatchDomains) > 0 { // Set a fairly high priority, but don't override all other // configs when in split-DNS mode. ipv6Map["dns-priority"] = dbus.MakeVariant(mediumPriority) } else { ipv6Map["dns-priority"] = dbus.MakeVariant(highestPriority) }
// deprecatedProperties are the properties in interface settings // that are deprecated by NetworkManager. // // In practice, this means that they are returned for reading, // but submitting a settings object with them present fails // with hard-to-diagnose errors. They must be removed. deprecatedProperties := []string{ "addresses", "routes", }
for _, property := range deprecatedProperties { delete(ipv4Map, property) delete(ipv6Map, property) }
if call := device.CallWithContext(ctx, "org.freedesktop.NetworkManager.Device.Reapply", 0, settings, version, uint32(0)); call.Err != nil { return fmt.Errorf("reapply: %w", call.Err) }
return nil}
func (m *nmManager) SupportsSplitDNS() bool { var mode string v, err := m.dnsManager.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode") if err != nil { return false } mode, ok := v.Value().(string) if !ok { return false }
// Per NM's documentation, it only does split-DNS when it's // programming dnsmasq or systemd-resolved. All other modes are // primary-only. return mode == "dnsmasq" || mode == "systemd-resolved"}
func (m *nmManager) GetBaseConfig() (OSConfig, error) { conn, err := dbus.SystemBus() if err != nil { return OSConfig{}, err }
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager")) v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Configuration") if err != nil { return OSConfig{}, err } cfgs, ok := v.Value().([]map[string]dbus.Variant) if !ok { return OSConfig{}, fmt.Errorf("unexpected NM config type %T", v.Value()) }
if len(cfgs) == 0 { return OSConfig{}, nil }
type dnsPrio struct { resolvers []netip.Addr domains []string priority int32 } order := make([]dnsPrio, 0, len(cfgs)-1)
for _, cfg := range cfgs { if name, ok := cfg["interface"]; ok { if s, ok := name.Value().(string); ok && s == m.interfaceName { // Config for the tailscale interface, skip. continue } }
var p dnsPrio
if v, ok := cfg["nameservers"]; ok { if ips, ok := v.Value().([]string); ok { for _, s := range ips { ip, err := netip.ParseAddr(s) if err != nil { // hmm, what do? Shouldn't really happen. continue } p.resolvers = append(p.resolvers, ip) } } } if v, ok := cfg["domains"]; ok { if domains, ok := v.Value().([]string); ok { p.domains = domains } } if v, ok := cfg["priority"]; ok { if prio, ok := v.Value().(int32); ok { p.priority = prio } }
order = append(order, p) }
sort.Slice(order, func(i, j int) bool { return order[i].priority < order[j].priority })
var ( ret OSConfig seenResolvers = map[netip.Addr]bool{} seenSearch = map[string]bool{} )
for _, cfg := range order { for _, resolver := range cfg.resolvers { if seenResolvers[resolver] { continue } ret.Nameservers = append(ret.Nameservers, resolver) seenResolvers[resolver] = true } for _, dom := range cfg.domains { if seenSearch[dom] { continue } fqdn, err := dnsname.ToFQDN(dom) if err != nil { continue } ret.SearchDomains = append(ret.SearchDomains, fqdn) seenSearch[dom] = true } if cfg.priority < 0 { // exclusive configurations preempt all other // configurations, so we're done. break } }
return ret, nil}
func (m *nmManager) Close() error { // No need to do anything on close, NetworkManager will delete our // settings when the tailscale interface goes away. 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}