package local import ( "context" "errors" "os" "sync" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/service/resolved" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" "github.com/godbus/dbus/v5" mDNS "github.com/miekg/dns" ) type DBusResolvedResolver struct { ctx context.Context logger logger.ContextLogger interfaceMonitor tun.DefaultInterfaceMonitor interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] systemBus *dbus.Conn resoledObject atomic.Pointer[ResolvedObject] closeOnce sync.Once } type ResolvedObject struct { dbus.BusObject InterfaceIndex int32 } 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.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.interfaceCallback != nil { t.interfaceMonitor.UnregisterCallback(t.interfaceCallback) } 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) { question := message.Question[0] resolvedObject := object.(*ResolvedObject) call := resolvedObject.CallWithContext( ctx, "org.freedesktop.resolve1.Manager.ResolveRecord", 0, resolvedObject.InterfaceIndex, question.Name, question.Qclass, question.Qtype, uint64(0), ) if call.Err != nil { var dbusError dbus.Error if errors.As(call.Err, &dbusError) && dbusError.Name == "org.freedesktop.resolve1.NoNameServers" { t.updateStatus() } 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, err := t.checkResolved(context.Background()) oldValue := t.resoledObject.Swap(dbusObject) if err != nil { var dbusErr dbus.Error if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwnerCould" { t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable")) } if oldValue != nil { t.logger.Debug("systemd-resolved service is gone") } return } else if oldValue == nil { t.logger.Debug("using systemd-resolved service as resolver") } } func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObject, 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, 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") } dnsProp, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS") if err != nil { return nil, err } var linkDNS []resolved.LinkDNS err = dnsProp.Store(&linkDNS) if err != nil { return nil, err } if 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 &ResolvedObject{ BusObject: dbusObject, }, nil } else { return &ResolvedObject{ BusObject: dbusObject, InterfaceIndex: int32(defaultInterface.Index), }, nil } } func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) { t.updateStatus() }