فهرست منبع

Use resolved in local DNS server if available

世界 2 ماه پیش
والد
کامیت
e8ca0943e4

+ 28 - 3
dns/transport/local/local.go

@@ -16,6 +16,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"
 
@@ -26,9 +27,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) {
@@ -39,20 +42,42 @@ 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 {
+		resolverObject := t.resolved.Object()
+		if resolverObject != nil {
+			return t.resolved.Exchange(resolverObject, ctx, message)
+		}
+	}
 	question := message.Question[0]
 	domain := dns.FqdnToDomain(question.Name)
 	if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {

+ 9 - 5
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) {

+ 14 - 0
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
+	Object() any
+	Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
+}

+ 150 - 0
dns/transport/local/local_resolved_linux.go

@@ -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"
+	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    common.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")
+}

+ 14 - 0
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
+}