Replace D-Bus ResolveRecord API with direct raw DNS queries to upstream servers obtained from systemd-resolved's per-interface link properties.
502 lines
14 KiB
Go
502 lines
14 KiB
Go
package local
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"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-box/service/resolved"
|
|
"github.com/sagernet/sing-tun"
|
|
"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
|
|
interfaceMonitor tun.DefaultInterfaceMonitor
|
|
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
|
|
systemBus *dbus.Conn
|
|
savedServerSet atomic.Pointer[resolvedServerSet]
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
type resolvedServerSet struct {
|
|
servers []resolvedServer
|
|
}
|
|
|
|
type resolvedServer struct {
|
|
primaryTransport adapter.DNSTransport
|
|
fallbackTransport adapter.DNSTransport
|
|
}
|
|
|
|
type resolvedServerSpecification struct {
|
|
address netip.Addr
|
|
port uint16
|
|
serverName string
|
|
}
|
|
|
|
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
|
|
interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor()
|
|
if interfaceMonitor == nil {
|
|
return nil, os.ErrInvalid
|
|
}
|
|
systemBus, err := dbus.SystemBus()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &DBusResolvedResolver{
|
|
ctx: ctx,
|
|
logger: logger,
|
|
interfaceMonitor: interfaceMonitor,
|
|
systemBus: systemBus,
|
|
}, nil
|
|
}
|
|
|
|
func (t *DBusResolvedResolver) Start() error {
|
|
t.updateStatus()
|
|
t.interfaceCallback = t.interfaceMonitor.RegisterCallback(t.updateDefaultInterface)
|
|
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() {
|
|
serverSet := t.savedServerSet.Swap(nil)
|
|
if serverSet != nil {
|
|
closeErr = serverSet.Close()
|
|
}
|
|
if t.interfaceCallback != nil {
|
|
t.interfaceMonitor.UnregisterCallback(t.interfaceCallback)
|
|
}
|
|
if t.systemBus != nil {
|
|
_ = t.systemBus.Close()
|
|
}
|
|
})
|
|
return closeErr
|
|
}
|
|
|
|
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()
|
|
refreshedServerSet := t.savedServerSet.Load()
|
|
if refreshedServerSet == nil || refreshedServerSet == serverSet {
|
|
return nil, err
|
|
}
|
|
return t.exchangeServerSet(ctx, message, refreshedServerSet)
|
|
}
|
|
|
|
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.updateStatus()
|
|
case "org.freedesktop.DBus.Properties.PropertiesChanged":
|
|
if !shouldUpdateResolvedServerSet(signal) {
|
|
continue
|
|
}
|
|
t.updateStatus()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *DBusResolvedResolver) updateStatus() {
|
|
serverSet, err := t.checkResolved(context.Background())
|
|
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
|
|
} 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
|
|
}
|
|
|
|
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) {
|
|
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
|
|
err := dbusObject.Call("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")
|
|
}
|
|
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)
|
|
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)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
linkDNSEx, err := loadResolvedLinkDNSEx(linkObject)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
linkDNS, err := loadResolvedLinkDNS(linkObject)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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")
|
|
}
|
|
}
|
|
return nil, E.New("link has no DNS servers configured")
|
|
}
|
|
serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{
|
|
BindInterface: defaultInterface.Name,
|
|
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)),
|
|
}
|
|
for _, serverSpecification := range serverSpecifications {
|
|
server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification)
|
|
if createErr != nil {
|
|
_ = serverSet.Close()
|
|
return nil, createErr
|
|
}
|
|
serverSet.servers = append(serverSet.servers, server)
|
|
}
|
|
return serverSet, 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 (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)
|
|
if !loaded {
|
|
return resolvedServerSpecification{}, false
|
|
}
|
|
if address.Is6() && address.IsLinkLocalUnicast() && address.Zone() == "" {
|
|
address = address.WithZone(interfaceName)
|
|
}
|
|
return resolvedServerSpecification{
|
|
address: address,
|
|
port: port,
|
|
serverName: serverName,
|
|
}, true
|
|
}
|
|
|
|
func resolvedServerPort(port uint16, useTLS bool) uint16 {
|
|
if port > 0 {
|
|
return port
|
|
}
|
|
if useTLS {
|
|
return 853
|
|
}
|
|
return 53
|
|
}
|
|
|
|
func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) {
|
|
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
|
|
if err != nil {
|
|
if isResolvedUnknownPropertyError(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
var linkDNS []resolved.LinkDNS
|
|
err = dnsProperty.Store(&linkDNS)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return linkDNS, 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
|
|
}
|
|
var linkDNSEx []resolved.LinkDNSEx
|
|
err = dnsProperty.Store(&linkDNSEx)
|
|
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 "", 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"
|
|
}
|
|
|
|
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.updateStatus()
|
|
}
|