From 9e0059cc2bd25b0cd0652c70c6b896cd0d63e506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 15 Aug 2025 23:31:01 +0800 Subject: [PATCH] Use resolved in local DNS server if available --- dns/transport/local/local.go | 28 +++- dns/transport/local/local_fallback.go | 14 +- dns/transport/local/local_resolved.go | 14 ++ dns/transport/local/local_resolved_linux.go | 154 ++++++++++++++++++++ dns/transport/local/local_resolved_stub.go | 14 ++ 5 files changed, 216 insertions(+), 8 deletions(-) create mode 100644 dns/transport/local/local_resolved.go create mode 100644 dns/transport/local/local_resolved_linux.go create mode 100644 dns/transport/local/local_resolved_stub.go diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go index 70b495ca..5e748ae9 100644 --- a/dns/transport/local/local.go +++ b/dns/transport/local/local.go @@ -13,6 +13,7 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/buf" 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" @@ -23,9 +24,11 @@ var _ adapter.DNSTransport = (*Transport)(nil) type Transport struct { dns.TransportAdapter - ctx context.Context - hosts *hosts.File - dialer N.Dialer + ctx context.Context + logger logger.ContextLogger + hosts *hosts.File + dialer N.Dialer + resolved ResolvedResolver } func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) { @@ -36,20 +39,39 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt return &Transport{ TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), ctx: ctx, + logger: logger, hosts: hosts.NewFile(hosts.DefaultPath), dialer: transportDialer, }, nil } func (t *Transport) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateInitialize: + resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger) + if err == nil { + err = resolvedResolver.Start() + if err == nil { + t.resolved = resolvedResolver + } else { + t.logger.Warn(E.Cause(err, "initialize resolved resolver")) + } + } + } return nil } func (t *Transport) Close() error { + if t.resolved != nil { + return t.resolved.Close() + } return nil } func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + if t.resolved != nil && t.resolved.Available() { + return t.resolved.Exchange(ctx, message) + } question := message.Question[0] domain := dns.FqdnToDomain(question.Name) if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { diff --git a/dns/transport/local/local_fallback.go b/dns/transport/local/local_fallback.go index cd2d198f..e584ddce 100644 --- a/dns/transport/local/local_fallback.go +++ b/dns/transport/local/local_fallback.go @@ -33,6 +33,10 @@ func NewFallbackTransport(ctx context.Context, logger log.ContextLogger, tag str if err != nil { return nil, err } + platformInterface := service.FromContext[platform.Interface](ctx) + if platformInterface == nil { + return transport, nil + } return &FallbackTransport{ DNSTransport: transport, ctx: ctx, @@ -40,11 +44,11 @@ func NewFallbackTransport(ctx context.Context, logger log.ContextLogger, tag str } func (f *FallbackTransport) Start(stage adapter.StartStage) error { - if stage != adapter.StartStateStart { - return nil + err := f.DNSTransport.Start(stage) + if err != nil { + return err } - platformInterface := service.FromContext[platform.Interface](f.ctx) - if platformInterface == nil { + if stage != adapter.StartStatePostStart { return nil } inboundManager := service.FromContext[adapter.InboundManager](f.ctx) @@ -59,7 +63,7 @@ func (f *FallbackTransport) Start(stage adapter.StartStage) error { } func (f *FallbackTransport) Close() error { - return nil + return f.DNSTransport.Close() } func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { diff --git a/dns/transport/local/local_resolved.go b/dns/transport/local/local_resolved.go new file mode 100644 index 00000000..5694265e --- /dev/null +++ b/dns/transport/local/local_resolved.go @@ -0,0 +1,14 @@ +package local + +import ( + "context" + + mDNS "github.com/miekg/dns" +) + +type ResolvedResolver interface { + Start() error + Close() error + Available() bool + Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) +} diff --git a/dns/transport/local/local_resolved_linux.go b/dns/transport/local/local_resolved_linux.go new file mode 100644 index 00000000..c7ae34e3 --- /dev/null +++ b/dns/transport/local/local_resolved_linux.go @@ -0,0 +1,154 @@ +package local + +import ( + "context" + "os" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/service/resolved" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/atomic" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" + + "github.com/godbus/dbus/v5" + mDNS "github.com/miekg/dns" +) + +type DBusResolvedResolver struct { + logger logger.ContextLogger + interfaceMonitor tun.DefaultInterfaceMonitor + systemBus *dbus.Conn + resoledObject atomic.TypedValue[dbus.BusObject] + closeOnce sync.Once +} + +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{ + logger: logger, + interfaceMonitor: interfaceMonitor, + systemBus: systemBus, + }, nil +} + +func (t *DBusResolvedResolver) Start() error { + t.updateStatus() + err := t.systemBus.BusObject().AddMatchSignal( + "org.freedesktop.DBus", + "NameOwnerChanged", + dbus.WithMatchSender("org.freedesktop.DBus"), + dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"), + ).Err + if err != nil { + return E.Cause(err, "configure resolved restart listener") + } + go t.loopUpdateStatus() + return nil +} + +func (t *DBusResolvedResolver) Close() error { + t.closeOnce.Do(func() { + if t.systemBus != nil { + _ = t.systemBus.Close() + } + }) + return nil +} + +func (t *DBusResolvedResolver) Available() bool { + return t.resoledObject.Load() == nil +} + +func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + defaultInterface := t.interfaceMonitor.DefaultInterface() + if defaultInterface == nil { + return nil, E.New("missing default interface") + } + resolvedObject := t.resoledObject.Load() + if resolvedObject == nil { + return nil, E.New("DBus interface for resolved is not available") + } + question := message.Question[0] + call := resolvedObject.CallWithContext( + ctx, + "org.freedesktop.resolve1.Manager.ResolveRecord", + 0, + int32(defaultInterface.Index), + question.Name, + question.Qclass, + question.Qtype, + uint64(0), + ) + if call.Err != nil { + return nil, E.Cause(call.Err, " resolve record via resolved") + } + var ( + records []resolved.ResourceRecord + outflags uint64 + ) + err := call.Store(&records, &outflags) + if err != nil { + return nil, err + } + response := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: message.Id, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: mDNS.RcodeSuccess, + }, + Question: []mDNS.Question{question}, + } + for _, record := range records { + var rr mDNS.RR + rr, _, err = mDNS.UnpackRR(record.Data, 0) + if err != nil { + return nil, E.Cause(err, "unpack resource record") + } + response.Answer = append(response.Answer, rr) + } + return response, nil +} + +func (t *DBusResolvedResolver) loopUpdateStatus() { + signalChan := make(chan *dbus.Signal, 1) + t.systemBus.Signal(signalChan) + for signal := range signalChan { + var restarted bool + if signal.Name == "org.freedesktop.DBus.NameOwnerChanged" { + if len(signal.Body) != 3 || signal.Body[2].(string) == "" { + continue + } else { + restarted = true + } + } + if restarted { + t.updateStatus() + } + } +} + +func (t *DBusResolvedResolver) updateStatus() { + dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1") + err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err + if err != nil { + if t.resoledObject.Swap(nil) != nil { + t.logger.Debug("systemd-resolved service is gone") + } + return + } + t.resoledObject.Store(dbusObject) + t.logger.Debug("using systemd-resolved service as resolver") +} diff --git a/dns/transport/local/local_resolved_stub.go b/dns/transport/local/local_resolved_stub.go new file mode 100644 index 00000000..ac23c4f3 --- /dev/null +++ b/dns/transport/local/local_resolved_stub.go @@ -0,0 +1,14 @@ +//go:build !linux + +package local + +import ( + "context" + "os" + + "github.com/sagernet/sing/common/logger" +) + +func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) { + return nil, os.ErrInvalid +}