net/dns: resync config to systemd-resolved when it restarts.

Fixes #3327

Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
David Anderson 2021-11-18 15:28:46 -08:00 committed by Dave Anderson
parent cf9169e4be
commit 4ef3fed100

View File

@ -13,6 +13,7 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"sync"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
@ -84,9 +85,15 @@ func isResolvedActive() bool {
// resolvedManager is an OSConfigurator which uses the systemd-resolved DBus API. // resolvedManager is an OSConfigurator which uses the systemd-resolved DBus API.
type resolvedManager struct { type resolvedManager struct {
logf logger.Logf logf logger.Logf
ifidx int ifidx int
resolved dbus.BusObject
cancelSyncer context.CancelFunc // run to shut down syncer goroutine
syncerDone chan struct{} // closed when syncer is stopped
resolved dbus.BusObject
mu sync.Mutex // guards RPCs made by syncLocked, and the following
config OSConfig // last SetDNS config
} }
func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) { func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) {
@ -100,19 +107,84 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage
return nil, err return nil, err
} }
return &resolvedManager{ ctx, cancel := context.WithCancel(context.Background())
logf: logf,
ifidx: iface.Index, ret := &resolvedManager{
resolved: conn.Object("org.freedesktop.resolve1", dbus.ObjectPath("/org/freedesktop/resolve1")), logf: logf,
}, nil ifidx: iface.Index,
cancelSyncer: cancel,
syncerDone: make(chan struct{}),
resolved: conn.Object("org.freedesktop.resolve1", dbus.ObjectPath("/org/freedesktop/resolve1")),
}
signals := make(chan *dbus.Signal, 16)
go ret.resync(ctx, signals)
// Only receive the DBus signals we need to resync our config on
// resolved restart. Failure to set filters isn't a fatal error,
// we'll just receive all broadcast signals and have to ignore
// them on our end.
if err := conn.AddMatchSignal(dbus.WithMatchObjectPath("/org/freedesktop/DBus"), dbus.WithMatchInterface("org.freedesktop.DBus"), dbus.WithMatchMember("NameOwnerChanged"), dbus.WithMatchArg(0, "org.freedesktop.resolve1")); err != nil {
logf("[v1] Setting DBus signal filter failed: %v", err)
}
conn.Signal(signals)
return ret, nil
} }
func (m *resolvedManager) SetDNS(config OSConfig) error { func (m *resolvedManager) SetDNS(config OSConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout) m.mu.Lock()
defer m.mu.Unlock()
m.config = config
return m.syncLocked(context.TODO()) // would be nice to plumb context through from SetDNS
}
func (m *resolvedManager) resync(ctx context.Context, signals chan *dbus.Signal) {
defer close(m.syncerDone)
for {
select {
case <-ctx.Done():
return
case signal := <-signals:
// In theory the signal was filtered by DBus, but if
// AddMatchSignal in the constructor failed, we may be
// getting other spam.
if signal.Path != "/org/freedesktop/DBus" || signal.Name != "org.freedesktop.DBus.NameOwnerChanged" {
continue
}
// signal.Body is a []interface{} of 3 strings: bus name, previous owner, new owner.
if len(signal.Body) != 3 {
m.logf("[unexpectected] DBus NameOwnerChanged len(Body) = %d, want 3")
}
if name, ok := signal.Body[0].(string); !ok || name != "org.freedesktop.resolve1" {
continue
}
newOwner, ok := signal.Body[2].(string)
if !ok {
m.logf("[unexpected] DBus NameOwnerChanged.new_owner is a %T, not a string", signal.Body[2])
}
if newOwner == "" {
// systemd-resolved left the bus, no current owner,
// nothing to do.
continue
}
// The resolved bus name has a new owner, meaning resolved
// restarted. Reprogram current config.
m.logf("systemd-resolved restarted, syncing DNS config")
m.mu.Lock()
err := m.syncLocked(ctx)
m.mu.Unlock()
if err != nil {
m.logf("failed to configure systemd-resolved: %v", err)
}
}
}
}
func (m *resolvedManager) syncLocked(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, reconfigTimeout)
defer cancel() defer cancel()
var linkNameservers = make([]resolvedLinkNameserver, len(config.Nameservers)) var linkNameservers = make([]resolvedLinkNameserver, len(m.config.Nameservers))
for i, server := range config.Nameservers { for i, server := range m.config.Nameservers {
ip := server.As16() ip := server.As16()
if server.Is4() { if server.Is4() {
linkNameservers[i] = resolvedLinkNameserver{ linkNameservers[i] = resolvedLinkNameserver{
@ -135,9 +207,9 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
return fmt.Errorf("setLinkDNS: %w", err) return fmt.Errorf("setLinkDNS: %w", err)
} }
linkDomains := make([]resolvedLinkDomain, 0, len(config.SearchDomains)+len(config.MatchDomains)) linkDomains := make([]resolvedLinkDomain, 0, len(m.config.SearchDomains)+len(m.config.MatchDomains))
seenDomains := map[dnsname.FQDN]bool{} seenDomains := map[dnsname.FQDN]bool{}
for _, domain := range config.SearchDomains { for _, domain := range m.config.SearchDomains {
if seenDomains[domain] { if seenDomains[domain] {
continue continue
} }
@ -147,7 +219,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
RoutingOnly: false, RoutingOnly: false,
}) })
} }
for _, domain := range config.MatchDomains { for _, domain := range m.config.MatchDomains {
if seenDomains[domain] { if seenDomains[domain] {
// Search domains act as both search and match in // Search domains act as both search and match in
// resolved, so it's correct to skip. // resolved, so it's correct to skip.
@ -159,7 +231,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
RoutingOnly: true, RoutingOnly: true,
}) })
} }
if len(config.MatchDomains) == 0 && len(config.Nameservers) > 0 { if len(m.config.MatchDomains) == 0 && len(m.config.Nameservers) > 0 {
// Caller requested full DNS interception, install a // Caller requested full DNS interception, install a
// routing-only root domain. // routing-only root domain.
linkDomains = append(linkDomains, resolvedLinkDomain{ linkDomains = append(linkDomains, resolvedLinkDomain{
@ -184,7 +256,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
return fmt.Errorf("setLinkDomains: %w", err) return fmt.Errorf("setLinkDomains: %w", err)
} }
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil { if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDefaultRoute", 0, m.ifidx, len(m.config.MatchDomains) == 0); call.Err != nil {
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name { if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
// on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent, // on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent,
// but otherwise it's working good // but otherwise it's working good
@ -234,13 +306,20 @@ func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
} }
func (m *resolvedManager) Close() error { func (m *resolvedManager) Close() error {
m.cancelSyncer()
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout) ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel() defer cancel()
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0, m.ifidx); call.Err != nil { if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0, m.ifidx); call.Err != nil {
return fmt.Errorf("RevertLink: %w", call.Err) return fmt.Errorf("RevertLink: %w", call.Err)
} }
select {
case <-m.syncerDone:
case <-ctx.Done():
m.logf("timeout in systemd-resolved syncer shutdown")
}
return nil return nil
} }