Use resolved in local DNS server if available

This commit is contained in:
世界 2025-08-15 23:31:01 +08:00
parent eeede11fc1
commit d4fb2d7f09
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
5 changed files with 215 additions and 8 deletions

View File

@ -13,6 +13,7 @@ import (
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@ -24,8 +25,10 @@ var _ adapter.DNSTransport = (*Transport)(nil)
type Transport struct { type Transport struct {
dns.TransportAdapter dns.TransportAdapter
ctx context.Context ctx context.Context
logger logger.ContextLogger
hosts *hosts.File hosts *hosts.File
dialer N.Dialer dialer N.Dialer
resolved ResolvedResolver
} }
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) { func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
@ -36,20 +39,42 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
return &Transport{ return &Transport{
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
ctx: ctx, ctx: ctx,
logger: logger,
hosts: hosts.NewFile(hosts.DefaultPath), hosts: hosts.NewFile(hosts.DefaultPath),
dialer: transportDialer, dialer: transportDialer,
}, nil }, nil
} }
func (t *Transport) Start(stage adapter.StartStage) error { 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 return nil
} }
func (t *Transport) Close() error { func (t *Transport) Close() error {
if t.resolved != nil {
return t.resolved.Close()
}
return nil return nil
} }
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if t.resolved != nil {
resolverObject := t.resolved.Object()
if resolverObject != nil {
return t.resolved.Exchange(resolverObject, ctx, message)
}
}
question := message.Question[0] question := message.Question[0]
domain := dns.FqdnToDomain(question.Name) domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {

View File

@ -33,6 +33,10 @@ func NewFallbackTransport(ctx context.Context, logger log.ContextLogger, tag str
if err != nil { if err != nil {
return nil, err return nil, err
} }
platformInterface := service.FromContext[platform.Interface](ctx)
if platformInterface == nil {
return transport, nil
}
return &FallbackTransport{ return &FallbackTransport{
DNSTransport: transport, DNSTransport: transport,
ctx: ctx, ctx: ctx,
@ -40,11 +44,11 @@ func NewFallbackTransport(ctx context.Context, logger log.ContextLogger, tag str
} }
func (f *FallbackTransport) Start(stage adapter.StartStage) error { func (f *FallbackTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { err := f.DNSTransport.Start(stage)
return nil if err != nil {
return err
} }
platformInterface := service.FromContext[platform.Interface](f.ctx) if stage != adapter.StartStatePostStart {
if platformInterface == nil {
return nil return nil
} }
inboundManager := service.FromContext[adapter.InboundManager](f.ctx) inboundManager := service.FromContext[adapter.InboundManager](f.ctx)
@ -59,7 +63,7 @@ func (f *FallbackTransport) Start(stage adapter.StartStage) error {
} }
func (f *FallbackTransport) Close() 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) { func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {

View File

@ -0,0 +1,14 @@
package local
import (
"context"
mDNS "github.com/miekg/dns"
)
type ResolvedResolver interface {
Start() error
Close() error
Object() any
Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
}

View File

@ -0,0 +1,150 @@
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) Object() any {
return t.resoledObject.Load()
}
func (t *DBusResolvedResolver) Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
defaultInterface := t.interfaceMonitor.DefaultInterface()
if defaultInterface == nil {
return nil, E.New("missing default interface")
}
question := message.Question[0]
call := object.(*dbus.Object).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")
}

View File

@ -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
}