diff --git a/net/dns/resolved.go b/net/dns/resolved.go index 136318238..b83c7628d 100644 --- a/net/dns/resolved.go +++ b/net/dns/resolved.go @@ -13,6 +13,7 @@ import ( "fmt" "net" "strings" + "sync" "github.com/godbus/dbus/v5" "golang.org/x/sys/unix" @@ -84,9 +85,15 @@ func isResolvedActive() bool { // resolvedManager is an OSConfigurator which uses the systemd-resolved DBus API. type resolvedManager struct { - logf logger.Logf - ifidx int - resolved dbus.BusObject + logf logger.Logf + ifidx int + + 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) { @@ -100,19 +107,84 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage return nil, err } - return &resolvedManager{ - logf: logf, - ifidx: iface.Index, - resolved: conn.Object("org.freedesktop.resolve1", dbus.ObjectPath("/org/freedesktop/resolve1")), - }, nil + ctx, cancel := context.WithCancel(context.Background()) + + ret := &resolvedManager{ + logf: logf, + 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 { - 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() - var linkNameservers = make([]resolvedLinkNameserver, len(config.Nameservers)) - for i, server := range config.Nameservers { + var linkNameservers = make([]resolvedLinkNameserver, len(m.config.Nameservers)) + for i, server := range m.config.Nameservers { ip := server.As16() if server.Is4() { linkNameservers[i] = resolvedLinkNameserver{ @@ -135,9 +207,9 @@ func (m *resolvedManager) SetDNS(config OSConfig) error { 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{} - for _, domain := range config.SearchDomains { + for _, domain := range m.config.SearchDomains { if seenDomains[domain] { continue } @@ -147,7 +219,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error { RoutingOnly: false, }) } - for _, domain := range config.MatchDomains { + for _, domain := range m.config.MatchDomains { if seenDomains[domain] { // Search domains act as both search and match in // resolved, so it's correct to skip. @@ -159,7 +231,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error { 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 // routing-only root domain. linkDomains = append(linkDomains, resolvedLinkDomain{ @@ -184,7 +256,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error { 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 { // on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent, // but otherwise it's working good @@ -234,13 +306,20 @@ func (m *resolvedManager) GetBaseConfig() (OSConfig, error) { } func (m *resolvedManager) Close() error { + m.cancelSyncer() + ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout) defer cancel() - if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0, m.ifidx); call.Err != nil { return fmt.Errorf("RevertLink: %w", call.Err) } + select { + case <-m.syncerDone: + case <-ctx.Done(): + m.logf("timeout in systemd-resolved syncer shutdown") + } + return nil }