package local import ( "bufio" "context" "errors" "net/netip" "os" "slices" "strings" "sync" "sync/atomic" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" dnsTransport "github.com/sagernet/sing-box/dns/transport" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/godbus/dbus/v5" mDNS "github.com/miekg/dns" ) func isSystemdResolvedManaged() bool { resolvContent, err := os.Open("/etc/resolv.conf") if err != nil { return false } defer resolvContent.Close() scanner := bufio.NewScanner(resolvContent) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || line[0] != '#' { return false } if strings.Contains(line, "systemd-resolved") { return true } } return false } type DBusResolvedResolver struct { ctx context.Context logger logger.ContextLogger interfaceFinder control.InterfaceFinder interfaceMonitor tun.DefaultInterfaceMonitor interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] networkMonitor tun.NetworkUpdateMonitor networkCallback *list.Element[tun.NetworkUpdateCallback] systemBus *dbus.Conn savedServerSet atomic.Pointer[resolvedServerSet] updateAccess sync.Mutex updateCancel context.CancelFunc updateRunAccess sync.Mutex closed bool closeOnce sync.Once } type resolvedServerSet struct { scopes []*resolvedScope signature []string } // Match levels of dns_scope_good_domain() in systemd-resolved: a routing or search // domain match yields its label count, and only scopes at the best level are queried. const ( resolvedScopeNoMatch = -3 resolvedScopeLastResort = -2 resolvedScopeMaybe = -1 ) // systemd-resolved only signals changes of the DNS server list, while clients such as // NetworkManager and systemd-networkd configure domains, default route and DNS over TLS // of a link in separate calls right after it; link state changes are not signaled at all. const resolvedUpdateDelay = time.Second // SD_RESOLVED_DNS bit of org.freedesktop.resolve1.Link.ScopesMask, set when the link has a unicast DNS scope. const resolvedScopesMaskDNS = 1 << 0 // TRANSACTION_UDP_TIMEOUT_USEC in systemd-resolved (resolved-timeouts.h), after which a transaction // is resent to the next server of the scope. const resolvedServerTimeout = 5 * time.Second type resolvedScope struct { domains []string defaultRoute bool fallback bool servers []resolvedServer currentServer atomic.Uint32 } type resolvedServer struct { primaryTransport adapter.DNSTransport fallbackTransport adapter.DNSTransport } type resolvedScopeSpecification struct { interfaceName string dnsOverTLSMode string domains []string defaultRoute bool fallback bool servers []resolvedServerSpecification } type resolvedServerSpecification struct { address netip.Addr port uint16 serverName string } type resolvedManagerDNS struct { InterfaceIndex int32 Family int32 Address []byte } type resolvedManagerDNSEx struct { InterfaceIndex int32 Family int32 Address []byte Port uint16 Name string } type resolvedManagerDomain struct { InterfaceIndex int32 Domain string RoutingOnly bool } type resolvedManagerServer struct { interfaceIndex int32 address []byte port uint16 serverName string } func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) { networkManager := service.FromContext[adapter.NetworkManager](ctx) interfaceMonitor := networkManager.InterfaceMonitor() if interfaceMonitor == nil { return nil, os.ErrInvalid } systemBus, err := dbus.SystemBus() if err != nil { return nil, err } return &DBusResolvedResolver{ ctx: ctx, logger: logger, interfaceFinder: networkManager.InterfaceFinder(), interfaceMonitor: interfaceMonitor, networkMonitor: networkManager.NetworkMonitor(), systemBus: systemBus, }, nil } func (t *DBusResolvedResolver) Start() error { t.updateStatus(t.ctx) t.interfaceCallback = t.interfaceMonitor.RegisterCallback(t.updateDefaultInterface) if t.networkMonitor != nil { t.networkCallback = t.networkMonitor.RegisterCallback(t.postUpdateStatus) } err := t.systemBus.BusObject().AddMatchSignal( "org.freedesktop.DBus", "NameOwnerChanged", dbus.WithMatchSender("org.freedesktop.DBus"), dbus.WithMatchArg(0, "org.freedesktop.resolve1"), ).Err if err != nil { return E.Cause(err, "configure resolved restart listener") } err = t.systemBus.BusObject().AddMatchSignal( "org.freedesktop.DBus.Properties", "PropertiesChanged", dbus.WithMatchSender("org.freedesktop.resolve1"), dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"), ).Err if err != nil { return E.Cause(err, "configure resolved properties listener") } go t.loopUpdateStatus() return nil } func (t *DBusResolvedResolver) Close() error { var closeErr error t.closeOnce.Do(func() { t.updateAccess.Lock() updateCancel := t.updateCancel t.updateCancel = nil t.updateAccess.Unlock() if updateCancel != nil { updateCancel() } t.updateRunAccess.Lock() t.closed = true serverSet := t.savedServerSet.Swap(nil) t.updateRunAccess.Unlock() if serverSet != nil { closeErr = serverSet.Close() } if t.interfaceCallback != nil { t.interfaceMonitor.UnregisterCallback(t.interfaceCallback) } if t.networkCallback != nil { t.networkMonitor.UnregisterCallback(t.networkCallback) } if t.systemBus != nil { _ = t.systemBus.Close() } }) return closeErr } func (t *DBusResolvedResolver) Reset() { serverSet := t.savedServerSet.Load() if serverSet == nil { return } for _, scope := range serverSet.scopes { for _, server := range scope.servers { server.primaryTransport.Reset() if server.fallbackTransport != nil { server.fallbackTransport.Reset() } } } } func (t *DBusResolvedResolver) Environment() []string { serverSet := t.savedServerSet.Load() if serverSet == nil { return nil } return serverSet.signature } func (t *DBusResolvedResolver) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) { serverSet := t.savedServerSet.Load() if serverSet == nil { go func() { err := t.updateStatus(t.ctx) if err != nil { callback(nil, err) return } t.exchangeServerSet(ctx, message, t.savedServerSet.Load(), callback) }() return } t.exchangeServerSet(ctx, message, serverSet, func(response *mDNS.Msg, err error) { if err == nil { callback(response, nil) return } go func() { t.updateStatus(t.ctx) refreshedServerSet := t.savedServerSet.Load() if refreshedServerSet == nil || refreshedServerSet == serverSet { callback(nil, err) return } t.exchangeServerSet(ctx, message, refreshedServerSet, callback) }() }) } func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet, callback func(response *mDNS.Msg, err error)) { if serverSet == nil { callback(nil, os.ErrClosed) return } scopes := serverSet.selectScopes(message.Question[0].Name) if len(scopes) == 0 { callback(nil, E.New("no appropriate name servers or networks for name found")) return } scopeExchangers := common.Map(scopes, func(scope *resolvedScope) dnsTransport.AsyncExchanger { return func(exchangeCtx context.Context, exchangeCallback func(response *mDNS.Msg, err error)) { scope.exchange(exchangeCtx, message, exchangeCallback) } }) dnsTransport.ExchangeParallel(ctx, scopeExchangers, acceptResolvedScopeResponse, callback) } func (s *resolvedScope) exchange(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) { serverCount := uint32(len(s.servers)) currentServer := s.currentServer.Load() serverExchangers := make([]dnsTransport.AsyncExchanger, 0, serverCount) for i := range serverCount { serverIndex := (currentServer + i) % serverCount server := s.servers[serverIndex] serverExchangers = append(serverExchangers, func(exchangeCtx context.Context, exchangeCallback func(response *mDNS.Msg, err error)) { server.exchange(exchangeCtx, message, func(response *mDNS.Msg, err error) { if exchangeCtx.Err() == nil && !acceptResolvedServerResponse(response, err) { s.currentServer.CompareAndSwap(serverIndex, (serverIndex+1)%serverCount) } exchangeCallback(response, err) }) }) } dnsTransport.ExchangeSequential(ctx, serverExchangers, acceptResolvedServerResponse, callback) } func (s *resolvedServer) exchange(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) { exchangeResolvedTransport(ctx, s.primaryTransport, message, func(response *mDNS.Msg, err error) { if err != nil && s.fallbackTransport != nil { exchangeResolvedTransport(ctx, s.fallbackTransport, message, callback) return } callback(response, err) }) } func exchangeResolvedTransport(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) { exchangeCtx, cancel := context.WithTimeout(ctx, resolvedServerTimeout) transport.ExchangeAsync(exchangeCtx, message, func(response *mDNS.Msg, err error) { cancel() callback(response, err) }) } // systemd-resolved starts a query on every scope at the best match level at once // (resolved-dns-query.c dns_query_go_scopes) and completes it with the first positive answer // (dns_query_ready). Within a scope, SERVFAIL and timeouts move the scope to its next server, which // later queries start from (resolved-dns-transaction.c dns_transaction_retry). func acceptResolvedServerResponse(response *mDNS.Msg, err error) bool { return err == nil && response.Rcode != mDNS.RcodeServerFailure } func acceptResolvedScopeResponse(response *mDNS.Msg, err error) bool { return err == nil && response.Rcode == mDNS.RcodeSuccess } func (s *resolvedServerSet) selectScopes(name string) []*resolvedScope { var ( bestMatch = resolvedScopeNoMatch selectedScopes []*resolvedScope ) for _, scope := range s.scopes { match := scope.match(name) if match == resolvedScopeNoMatch || match < bestMatch { continue } if match > bestMatch { bestMatch = match selectedScopes = selectedScopes[:0] } selectedScopes = append(selectedScopes, scope) } return selectedScopes } func (s *resolvedScope) match(name string) int { bestLabels := -1 for _, domain := range s.domains { if mDNS.IsSubDomain(domain, name) { bestLabels = max(bestLabels, mDNS.CountLabel(domain)) } } if bestLabels >= 0 { return bestLabels } if !s.defaultRoute { return resolvedScopeNoMatch } if s.fallback { return resolvedScopeLastResort } return resolvedScopeMaybe } func (s *resolvedServerSet) Close() error { return E.Errors(common.Map(s.scopes, (*resolvedScope).Close)...) } func (s *resolvedScope) Close() error { var errors []error for _, server := range s.servers { errors = append(errors, server.primaryTransport.Close()) if server.fallbackTransport != nil { errors = append(errors, server.fallbackTransport.Close()) } } return E.Errors(errors...) } func (t *DBusResolvedResolver) loopUpdateStatus() { signalChan := make(chan *dbus.Signal, 1) t.systemBus.Signal(signalChan) for signal := range signalChan { switch signal.Name { case "org.freedesktop.DBus.NameOwnerChanged": if len(signal.Body) != 3 { continue } newOwner, loaded := signal.Body[2].(string) if !loaded || newOwner == "" { continue } t.postUpdateStatus() case "org.freedesktop.DBus.Properties.PropertiesChanged": if !shouldUpdateResolvedServerSet(signal) { continue } t.postUpdateStatus() } } } func (t *DBusResolvedResolver) postUpdateStatus() { updateContext, updateCancel := context.WithCancel(t.ctx) t.updateAccess.Lock() previousCancel := t.updateCancel t.updateCancel = updateCancel t.updateAccess.Unlock() if previousCancel != nil { previousCancel() } go func() { defer updateCancel() timer := time.NewTimer(resolvedUpdateDelay) defer timer.Stop() select { case <-timer.C: case <-updateContext.Done(): return } t.updateStatus(updateContext) }() } func (t *DBusResolvedResolver) updateStatus(ctx context.Context) error { t.updateRunAccess.Lock() defer t.updateRunAccess.Unlock() if t.closed { return os.ErrClosed } if ctx.Err() != nil { return ctx.Err() } serverSet, err := t.checkResolved(ctx) if t.closed || ctx.Err() != nil { if serverSet != nil { _ = serverSet.Close() } if t.closed { return os.ErrClosed } return ctx.Err() } oldServerSet := t.savedServerSet.Swap(serverSet) if oldServerSet != nil { _ = oldServerSet.Close() } if err != nil { var dbusErr dbus.Error if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" { t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable")) } if oldServerSet != nil { t.logger.Debug("systemd-resolved service is gone") } return err } else if oldServerSet == nil { t.logger.Debug("using systemd-resolved service as resolver") } return nil } func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) { managerObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1") err := managerObject.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Err if err != nil { return nil, err } defaultInterface := t.interfaceMonitor.DefaultInterface() if defaultInterface == nil { return nil, E.New("missing default interface") } managerProperties, err := loadResolvedProperties(ctx, managerObject, "org.freedesktop.resolve1.Manager") if err != nil { return nil, err } servers, err := loadResolvedManagerServers(managerProperties, "DNS") if err != nil { return nil, err } var managerDomains []resolvedManagerDomain err = storeResolvedProperty(managerProperties, "Domains", &managerDomains) if err != nil { return nil, err } var dnsOverTLSMode string err = storeResolvedProperty(managerProperties, "DNSOverTLS", &dnsOverTLSMode) if err != nil { return nil, err } globalScope := resolvedScopeSpecification{ interfaceName: defaultInterface.Name, dnsOverTLSMode: dnsOverTLSMode, defaultRoute: true, } var linkScopes []resolvedScopeSpecification linkScopeIndexes := make(map[int32]int) for _, server := range servers { if server.interfaceIndex == 0 { serverSpecification, loaded := buildResolvedServerSpecification(globalScope.interfaceName, server) if loaded { globalScope.servers = append(globalScope.servers, serverSpecification) } continue } scopeIndex, loaded := linkScopeIndexes[server.interfaceIndex] if !loaded { linkScope, linkLoaded, linkErr := t.loadResolvedLinkScope(ctx, managerObject, server.interfaceIndex) if linkErr != nil { return nil, linkErr } if !linkLoaded { linkScopeIndexes[server.interfaceIndex] = -1 continue } scopeIndex = len(linkScopes) linkScopeIndexes[server.interfaceIndex] = scopeIndex linkScopes = append(linkScopes, linkScope) } else if scopeIndex < 0 { continue } serverSpecification, loaded := buildResolvedServerSpecification(linkScopes[scopeIndex].interfaceName, server) if loaded { linkScopes[scopeIndex].servers = append(linkScopes[scopeIndex].servers, serverSpecification) } } for _, domain := range managerDomains { domainName := mDNS.Fqdn(domain.Domain) if domain.InterfaceIndex == 0 { globalScope.domains = append(globalScope.domains, domainName) continue } scopeIndex, loaded := linkScopeIndexes[domain.InterfaceIndex] if loaded && scopeIndex >= 0 { linkScopes[scopeIndex].domains = append(linkScopes[scopeIndex].domains, domainName) } } if len(globalScope.servers) == 0 && !slices.ContainsFunc(linkScopes, func(it resolvedScopeSpecification) bool { return it.defaultRoute && len(it.servers) > 0 }) { fallbackServers, fallbackErr := loadResolvedManagerServers(managerProperties, "FallbackDNS") if fallbackErr != nil { return nil, fallbackErr } for _, server := range fallbackServers { serverSpecification, loaded := buildResolvedServerSpecification(globalScope.interfaceName, server) if loaded { globalScope.servers = append(globalScope.servers, serverSpecification) } } globalScope.fallback = true } scopeSpecifications := common.Filter(append([]resolvedScopeSpecification{globalScope}, linkScopes...), func(it resolvedScopeSpecification) bool { return len(it.servers) > 0 }) if len(scopeSpecifications) == 0 { return nil, E.New("no DNS servers configured") } serverSet := &resolvedServerSet{} for _, scopeSpecification := range scopeSpecifications { scope, createErr := t.createResolvedScope(scopeSpecification) if createErr != nil { _ = serverSet.Close() return nil, createErr } serverSet.scopes = append(serverSet.scopes, scope) for _, serverSpecification := range scopeSpecification.servers { serverSet.signature = append(serverSet.signature, scopeSpecification.interfaceName+"/"+M.SocksaddrFrom(serverSpecification.address, serverSpecification.port).String()) } } return serverSet, nil } func (t *DBusResolvedResolver) loadResolvedLinkScope(ctx context.Context, managerObject dbus.BusObject, interfaceIndex int32) (resolvedScopeSpecification, bool, error) { linkInterface, err := t.interfaceFinder.ByIndex(int(interfaceIndex)) if err != nil || slices.Contains(t.interfaceMonitor.MyInterfaces(), linkInterface.Name) { return resolvedScopeSpecification{}, false, nil } var linkPath dbus.ObjectPath err = managerObject.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.GetLink", 0, interfaceIndex).Store(&linkPath) if err != nil { return resolvedScopeSpecification{}, false, err } linkProperties, err := loadResolvedProperties(ctx, t.systemBus.Object("org.freedesktop.resolve1", linkPath), "org.freedesktop.resolve1.Link") if err != nil { return resolvedScopeSpecification{}, false, err } var scopesMask uint64 err = storeResolvedProperty(linkProperties, "ScopesMask", &scopesMask) if err != nil { return resolvedScopeSpecification{}, false, err } if scopesMask&resolvedScopesMaskDNS == 0 { return resolvedScopeSpecification{}, false, nil } linkScope := resolvedScopeSpecification{ interfaceName: linkInterface.Name, } err = storeResolvedProperty(linkProperties, "DNSOverTLS", &linkScope.dnsOverTLSMode) if err != nil { return resolvedScopeSpecification{}, false, err } err = storeResolvedProperty(linkProperties, "DefaultRoute", &linkScope.defaultRoute) if err != nil { return resolvedScopeSpecification{}, false, err } return linkScope, true, nil } func (t *DBusResolvedResolver) createResolvedScope(scopeSpecification resolvedScopeSpecification) (*resolvedScope, error) { scope := &resolvedScope{ domains: scopeSpecification.domains, defaultRoute: scopeSpecification.defaultRoute, fallback: scopeSpecification.fallback, } serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{ AbstractDialerOptions: option.AbstractDialerOptions{ BindInterface: scopeSpecification.interfaceName, UDPFragmentDefault: true, }, }) if err != nil { return nil, err } for _, serverSpecification := range scopeSpecification.servers { server, createErr := t.createResolvedServer(serverDialer, scopeSpecification.dnsOverTLSMode, serverSpecification) if createErr != nil { _ = scope.Close() return nil, createErr } scope.servers = append(scope.servers, server) } return scope, nil } func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) { if dnsOverTLSMode == "yes" { primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true) if err != nil { return resolvedServer{}, err } return resolvedServer{ primaryTransport: primaryTransport, }, nil } if dnsOverTLSMode == "opportunistic" { primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true) if err != nil { return resolvedServer{}, err } fallbackTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false) if err != nil { _ = primaryTransport.Close() return resolvedServer{}, err } return resolvedServer{ primaryTransport: primaryTransport, fallbackTransport: fallbackTransport, }, nil } primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false) if err != nil { return resolvedServer{}, err } return resolvedServer{ primaryTransport: primaryTransport, }, nil } func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) { serverAddress := M.SocksaddrFrom(serverSpecification.address, resolvedServerPort(serverSpecification.port, useTLS)) if useTLS { tlsAddress := serverSpecification.address if tlsAddress.Zone() != "" { tlsAddress = tlsAddress.WithZone("") } serverName := serverSpecification.serverName if serverName == "" { serverName = tlsAddress.String() } tlsConfig, err := tls.NewClient(t.ctx, t.logger, tlsAddress.String(), option.OutboundTLSOptions{ Enabled: true, ServerName: serverName, }) if err != nil { return nil, err } serverTransport := dnsTransport.NewTLSRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeTLS, "", nil), serverDialer, serverAddress, tlsConfig) err = serverTransport.Start(adapter.StartStateStart) if err != nil { _ = serverTransport.Close() return nil, err } return serverTransport, nil } serverTransport := dnsTransport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), serverDialer, serverAddress) err := serverTransport.Start(adapter.StartStateStart) if err != nil { _ = serverTransport.Close() return nil, err } return serverTransport, nil } func buildResolvedServerSpecification(interfaceName string, server resolvedManagerServer) (resolvedServerSpecification, bool) { address, loaded := netip.AddrFromSlice(server.address) if !loaded { return resolvedServerSpecification{}, false } if address.Is6() && address.IsLinkLocalUnicast() && address.Zone() == "" { address = address.WithZone(interfaceName) } return resolvedServerSpecification{ address: address, port: server.port, serverName: server.serverName, }, true } func resolvedServerPort(port uint16, useTLS bool) uint16 { if port > 0 { return port } if useTLS { return 853 } return 53 } func loadResolvedProperties(ctx context.Context, object dbus.BusObject, interfaceName string) (map[string]dbus.Variant, error) { var properties map[string]dbus.Variant err := object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.GetAll", 0, interfaceName).Store(&properties) if err != nil { return nil, E.Cause(err, "load ", interfaceName, " properties") } return properties, nil } func storeResolvedProperty(properties map[string]dbus.Variant, name string, value any) error { property, loaded := properties[name] if !loaded { return nil } err := property.Store(value) if err != nil { return E.Cause(err, "parse resolved property ", name) } return nil } func loadResolvedManagerServers(properties map[string]dbus.Variant, name string) ([]resolvedManagerServer, error) { _, loaded := properties[name+"Ex"] if loaded { var serversEx []resolvedManagerDNSEx err := storeResolvedProperty(properties, name+"Ex", &serversEx) if err != nil { return nil, err } return common.Map(serversEx, func(it resolvedManagerDNSEx) resolvedManagerServer { return resolvedManagerServer{ interfaceIndex: it.InterfaceIndex, address: it.Address, port: it.Port, serverName: it.Name, } }), nil } var servers []resolvedManagerDNS err := storeResolvedProperty(properties, name, &servers) if err != nil { return nil, err } return common.Map(servers, func(it resolvedManagerDNS) resolvedManagerServer { return resolvedManagerServer{ interfaceIndex: it.InterfaceIndex, address: it.Address, } }), nil } func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool { if len(signal.Body) != 3 { return true } changedProperties, loaded := signal.Body[1].(map[string]dbus.Variant) if !loaded { return true } for propertyName := range changedProperties { switch propertyName { case "DNS", "DNSEx", "DNSOverTLS": return true } } invalidatedProperties, loaded := signal.Body[2].([]string) if !loaded { return true } for _, propertyName := range invalidatedProperties { switch propertyName { case "DNS", "DNSEx", "DNSOverTLS": return true } } return false } func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) { t.postUpdateStatus() }