diff --git a/dns/transport/exchange_strategy.go b/dns/transport/exchange_strategy.go index c85d3bb6..633c7fce 100644 --- a/dns/transport/exchange_strategy.go +++ b/dns/transport/exchange_strategy.go @@ -82,6 +82,70 @@ type sequentialCallState struct { continued bool } +func ExchangeParallel(ctx context.Context, exchangers []AsyncExchanger, accept func(response *mDNS.Msg, err error) bool, callback func(response *mDNS.Msg, err error)) { + if len(exchangers) == 0 { + callback(nil, E.New("missing exchangers")) + return + } + exchangeCtx, cancel := context.WithCancel(ctx) + parallel := ¶llelExchange{ + accept: accept, + callback: callback, + cancel: cancel, + responses: make([]*mDNS.Msg, len(exchangers)), + errors: make([]error, len(exchangers)), + pending: len(exchangers), + } + for i, exchanger := range exchangers { + exchanger(exchangeCtx, func(response *mDNS.Msg, err error) { + parallel.complete(i, response, err) + }) + } +} + +type parallelExchange struct { + access sync.Mutex + accept func(response *mDNS.Msg, err error) bool + callback func(response *mDNS.Msg, err error) + cancel context.CancelFunc + responses []*mDNS.Msg + errors []error + pending int + completed bool +} + +func (p *parallelExchange) complete(index int, response *mDNS.Msg, err error) { + p.access.Lock() + if p.completed { + p.access.Unlock() + return + } + accepted := p.accept(response, err) + if !accepted { + p.responses[index] = response + p.errors[index] = err + p.pending-- + if p.pending > 0 { + p.access.Unlock() + return + } + } + p.completed = true + p.access.Unlock() + p.cancel() + if accepted { + p.callback(response, err) + return + } + for i, finalResponse := range p.responses { + if p.errors[i] == nil { + p.callback(finalResponse, nil) + return + } + } + p.callback(nil, E.Errors(p.errors...)) +} + func ExchangeNames(ctx context.Context, names []string, question mDNS.Question, exchangerFor func(fqdn string) AsyncExchanger, callback func(response *mDNS.Msg, err error)) { if len(names) == 0 { callback(nil, E.New("missing name candidates")) diff --git a/dns/transport/local/local_resolved.go b/dns/transport/local/local_resolved.go index b698f6a9..efd5eabd 100644 --- a/dns/transport/local/local_resolved.go +++ b/dns/transport/local/local_resolved.go @@ -11,6 +11,5 @@ type ResolvedResolver interface { Close() error Reset() Environment() []string - Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) } diff --git a/dns/transport/local/local_resolved_linux.go b/dns/transport/local/local_resolved_linux.go index 50d4abce..6a7eb3cb 100644 --- a/dns/transport/local/local_resolved_linux.go +++ b/dns/transport/local/local_resolved_linux.go @@ -6,9 +6,11 @@ import ( "errors" "net/netip" "os" + "slices" "strings" "sync" "sync/atomic" + "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" @@ -17,7 +19,6 @@ import ( "github.com/sagernet/sing-box/dns" dnsTransport "github.com/sagernet/sing-box/dns/transport" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-box/service/resolved" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" @@ -54,8 +55,11 @@ func isSystemdResolvedManaged() bool { 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 @@ -66,23 +70,88 @@ type DBusResolvedResolver struct { } type resolvedServerSet struct { - servers []resolvedServer + 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) { - interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor() + networkManager := service.FromContext[adapter.NetworkManager](ctx) + interfaceMonitor := networkManager.InterfaceMonitor() if interfaceMonitor == nil { return nil, os.ErrInvalid } @@ -93,7 +162,9 @@ func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (Reso return &DBusResolvedResolver{ ctx: ctx, logger: logger, + interfaceFinder: networkManager.InterfaceFinder(), interfaceMonitor: interfaceMonitor, + networkMonitor: networkManager.NetworkMonitor(), systemBus: systemBus, }, nil } @@ -101,6 +172,9 @@ func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (Reso 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", @@ -143,6 +217,9 @@ func (t *DBusResolvedResolver) Close() error { 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() } @@ -155,10 +232,12 @@ func (t *DBusResolvedResolver) Reset() { if serverSet == nil { return } - for _, server := range serverSet.servers { - server.primaryTransport.Reset() - if server.fallbackTransport != nil { - server.fallbackTransport.Reset() + for _, scope := range serverSet.scopes { + for _, server := range scope.servers { + server.primaryTransport.Reset() + if server.fallbackTransport != nil { + server.fallbackTransport.Reset() + } } } } @@ -171,40 +250,20 @@ func (t *DBusResolvedResolver) Environment() []string { return serverSet.signature } -func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { - serverSet := t.savedServerSet.Load() - if serverSet == nil { - var err error - serverSet, err = t.checkResolved(context.Background()) - if err != nil { - return nil, err - } - previousServerSet := t.savedServerSet.Swap(serverSet) - if previousServerSet != nil { - _ = previousServerSet.Close() - } - } - response, err := t.exchangeServerSet(ctx, message, serverSet) - if err == nil { - return response, nil - } - t.updateStatus(t.ctx) - refreshedServerSet := t.savedServerSet.Load() - if refreshedServerSet == nil || refreshedServerSet == serverSet { - return nil, err - } - return t.exchangeServerSet(ctx, message, refreshedServerSet) -} - 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() { - callback(t.Exchange(ctx, message)) + err := t.updateStatus(t.ctx) + if err != nil { + callback(nil, err) + return + } + t.exchangeServerSet(ctx, message, t.savedServerSet.Load(), callback) }() return } - t.exchangeServerSetAsync(ctx, message, serverSet, func(response *mDNS.Msg, err error) { + t.exchangeServerSet(ctx, message, serverSet, func(response *mDNS.Msg, err error) { if err == nil { callback(response, nil) return @@ -216,29 +275,129 @@ func (t *DBusResolvedResolver) ExchangeAsync(ctx context.Context, message *mDNS. callback(nil, err) return } - t.exchangeServerSetAsync(ctx, message, refreshedServerSet, callback) + t.exchangeServerSet(ctx, message, refreshedServerSet, callback) }() }) } -func (t *DBusResolvedResolver) exchangeServerSetAsync(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet, callback func(response *mDNS.Msg, err error)) { - if len(serverSet.servers) == 0 { - callback(nil, E.New("link has no DNS servers configured")) +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 } - serverExchangers := make([]dnsTransport.AsyncExchanger, 0, len(serverSet.servers)) - for _, server := range serverSet.servers { + 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.primaryTransport.ExchangeAsync(exchangeCtx, message, func(response *mDNS.Msg, exchangeErr error) { - if exchangeErr != nil && server.fallbackTransport != nil { - server.fallbackTransport.ExchangeAsync(exchangeCtx, message, exchangeCallback) - return + 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, exchangeErr) + exchangeCallback(response, err) }) }) } - dnsTransport.ExchangeSequential(ctx, serverExchangers, nil, callback) + 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() { @@ -275,22 +434,35 @@ func (t *DBusResolvedResolver) postUpdateStatus() { } 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) { +func (t *DBusResolvedResolver) updateStatus(ctx context.Context) error { t.updateRunAccess.Lock() defer t.updateRunAccess.Unlock() - if t.closed || ctx.Err() != nil { - return + 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() } - return + if t.closed { + return os.ErrClosed + } + return ctx.Err() } oldServerSet := t.savedServerSet.Swap(serverSet) if oldServerSet != nil { @@ -304,34 +476,16 @@ func (t *DBusResolvedResolver) updateStatus(ctx context.Context) { if oldServerSet != nil { t.logger.Debug("systemd-resolved service is gone") } - return + return err } else if oldServerSet == nil { t.logger.Debug("using systemd-resolved service as resolver") } -} - -func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet) (*mDNS.Msg, error) { - if serverSet == nil || len(serverSet.servers) == 0 { - return nil, E.New("link has no DNS servers configured") - } - var lastError error - for _, server := range serverSet.servers { - response, err := server.primaryTransport.Exchange(ctx, message) - if err != nil && server.fallbackTransport != nil { - response, err = server.fallbackTransport.Exchange(ctx, message) - } - if err != nil { - lastError = err - continue - } - return response, nil - } - return nil, lastError + return nil } func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) { - dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1") - err := dbusObject.(*dbus.Object).CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Err + 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 } @@ -339,97 +493,167 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ if defaultInterface == nil { return nil, E.New("missing default interface") } - call := dbusObject.(*dbus.Object).CallWithContext( - ctx, - "org.freedesktop.resolve1.Manager.GetLink", - 0, - int32(defaultInterface.Index), - ) - if call.Err != nil { - return nil, call.Err - } - var linkPath dbus.ObjectPath - err = call.Store(&linkPath) + managerProperties, err := loadResolvedProperties(ctx, managerObject, "org.freedesktop.resolve1.Manager") if err != nil { return nil, err } - linkObject := t.systemBus.Object("org.freedesktop.resolve1", linkPath) - if linkObject == nil { - return nil, E.New("missing link object for default interface") - } - dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject) + servers, err := loadResolvedManagerServers(managerProperties, "DNS") if err != nil { return nil, err } - err = ctx.Err() + var managerDomains []resolvedManagerDomain + err = storeResolvedProperty(managerProperties, "Domains", &managerDomains) if err != nil { return nil, err } - linkDNSEx, err := loadResolvedLinkDNSEx(linkObject) + var dnsOverTLSMode string + err = storeResolvedProperty(managerProperties, "DNSOverTLS", &dnsOverTLSMode) if err != nil { return nil, err } - err = ctx.Err() - if err != nil { - return nil, err + globalScope := resolvedScopeSpecification{ + interfaceName: defaultInterface.Name, + dnsOverTLSMode: dnsOverTLSMode, + defaultRoute: true, } - linkDNS, err := loadResolvedLinkDNS(linkObject) - if err != nil { - return nil, err + 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) + } } - if len(linkDNSEx) == 0 && len(linkDNS) == 0 { - for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() { - if inbound.Type() == C.TypeTun { - return nil, E.New("No appropriate name servers or networks for name found") + 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) } } - return nil, E.New("link has no DNS servers configured") + 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: defaultInterface.Name, + BindInterface: scopeSpecification.interfaceName, UDPFragmentDefault: true, }, }) if err != nil { return nil, err } - var serverSpecifications []resolvedServerSpecification - if len(linkDNSEx) > 0 { - for _, entry := range linkDNSEx { - serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, entry.Port, entry.Name) - if !loaded { - continue - } - serverSpecifications = append(serverSpecifications, serverSpecification) - } - } else { - for _, entry := range linkDNS { - serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, 0, "") - if !loaded { - continue - } - serverSpecifications = append(serverSpecifications, serverSpecification) - } - } - if len(serverSpecifications) == 0 { - return nil, E.New("no valid DNS servers on link") - } - serverSet := &resolvedServerSet{ - servers: make([]resolvedServer, 0, len(serverSpecifications)), - signature: common.Map(serverSpecifications, func(it resolvedServerSpecification) string { - return M.SocksaddrFrom(it.address, it.port).String() - }), - } - for _, serverSpecification := range serverSpecifications { - server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification) + for _, serverSpecification := range scopeSpecification.servers { + server, createErr := t.createResolvedServer(serverDialer, scopeSpecification.dnsOverTLSMode, serverSpecification) if createErr != nil { - _ = serverSet.Close() + _ = scope.Close() return nil, createErr } - serverSet.servers = append(serverSet.servers, server) + scope.servers = append(scope.servers, server) } - return serverSet, nil + return scope, nil } func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) { @@ -501,19 +725,8 @@ func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, se return serverTransport, nil } -func (s *resolvedServerSet) 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 buildResolvedServerSpecification(interfaceName string, rawAddress []byte, port uint16, serverName string) (resolvedServerSpecification, bool) { - address, loaded := netip.AddrFromSlice(rawAddress) +func buildResolvedServerSpecification(interfaceName string, server resolvedManagerServer) (resolvedServerSpecification, bool) { + address, loaded := netip.AddrFromSlice(server.address) if !loaded { return resolvedServerSpecification{}, false } @@ -522,8 +735,8 @@ func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, p } return resolvedServerSpecification{ address: address, - port: port, - serverName: serverName, + port: server.port, + serverName: server.serverName, }, true } @@ -537,57 +750,55 @@ func resolvedServerPort(port uint16, useTLS bool) uint16 { return 53 } -func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) { - dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS") +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 { - if isResolvedUnknownPropertyError(err) { - return nil, nil - } - return nil, err + return nil, E.Cause(err, "load ", interfaceName, " properties") } - var linkDNS []resolved.LinkDNS - err = dnsProperty.Store(&linkDNS) - if err != nil { - return nil, err - } - return linkDNS, nil + return properties, nil } -func loadResolvedLinkDNSEx(linkObject dbus.BusObject) ([]resolved.LinkDNSEx, error) { - dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSEx") - if err != nil { - if isResolvedUnknownPropertyError(err) { - return nil, nil - } - return nil, err +func storeResolvedProperty(properties map[string]dbus.Variant, name string, value any) error { + property, loaded := properties[name] + if !loaded { + return nil } - var linkDNSEx []resolved.LinkDNSEx - err = dnsProperty.Store(&linkDNSEx) + 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 linkDNSEx, nil -} - -func loadResolvedLinkDNSOverTLS(linkObject dbus.BusObject) (string, error) { - dnsOverTLSProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSOverTLS") - if err != nil { - if isResolvedUnknownPropertyError(err) { - return "", nil + return common.Map(servers, func(it resolvedManagerDNS) resolvedManagerServer { + return resolvedManagerServer{ + interfaceIndex: it.InterfaceIndex, + address: it.Address, } - return "", err - } - var dnsOverTLSMode string - err = dnsOverTLSProperty.Store(&dnsOverTLSMode) - if err != nil { - return "", err - } - return dnsOverTLSMode, nil -} - -func isResolvedUnknownPropertyError(err error) bool { - var dbusError dbus.Error - return errors.As(err, &dbusError) && dbusError.Name == "org.freedesktop.DBus.Error.UnknownProperty" + }), nil } func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool {