Browse Source

Add resolved service and DNS server

世界 6 months ago
parent
commit
6ee3117755

+ 3 - 0
.fpm_systemd

@@ -13,6 +13,9 @@ release/config/config.json=/etc/sing-box/config.json
 
 release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
 release/config/[email protected]=/usr/lib/systemd/system/[email protected]
+release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
+release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
+release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
 
 release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
 release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish

+ 6 - 0
.goreleaser.fury.yaml

@@ -55,6 +55,12 @@ nfpms:
         dst: /usr/lib/systemd/system/sing-box.service
       - src: release/config/[email protected]
         dst: /usr/lib/systemd/system/[email protected]
+      - src: release/config/sing-box.sysusers
+        dst: /usr/lib/sysusers.d/sing-box.conf
+      - src: release/config/sing-box.rules
+        dst: /usr/share/polkit-1/rules.d/sing-box.rules
+      - src: release/config/sing-box-split-dns.xml
+        dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
 
       - src: release/completions/sing-box.bash
         dst: /usr/share/bash-completion/completions/sing-box.bash

+ 6 - 0
.goreleaser.yaml

@@ -136,6 +136,12 @@ nfpms:
         dst: /usr/lib/systemd/system/sing-box.service
       - src: release/config/[email protected]
         dst: /usr/lib/systemd/system/[email protected]
+      - src: release/config/sing-box.sysusers
+        dst: /usr/lib/sysusers.d/sing-box.conf
+      - src: release/config/sing-box.rules
+        dst: /usr/share/polkit-1/rules.d/sing-box.rules
+      - src: release/config/sing-box-split-dns.xml
+        dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
 
       - src: release/completions/sing-box.bash
         dst: /usr/share/bash-completion/completions/sing-box.bash

+ 6 - 5
adapter/dns.go

@@ -33,11 +33,12 @@ type DNSClient interface {
 }
 
 type DNSQueryOptions struct {
-	Transport    DNSTransport
-	Strategy     C.DomainStrategy
-	DisableCache bool
-	RewriteTTL   *uint32
-	ClientSubnet netip.Prefix
+	Transport      DNSTransport
+	Strategy       C.DomainStrategy
+	LookupStrategy C.DomainStrategy
+	DisableCache   bool
+	RewriteTTL     *uint32
+	ClientSubnet   netip.Prefix
 }
 
 func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {

+ 3 - 2
adapter/inbound/manager.go

@@ -37,13 +37,14 @@ func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endp
 
 func (m *Manager) Start(stage adapter.StartStage) error {
 	m.access.Lock()
-	defer m.access.Unlock()
 	if m.started && m.stage >= stage {
 		panic("already started")
 	}
 	m.started = true
 	m.stage = stage
-	for _, inbound := range m.inbounds {
+	inbounds := m.inbounds
+	m.access.Unlock()
+	for _, inbound := range inbounds {
 		err := adapter.LegacyStart(inbound, stage)
 		if err != nil {
 			return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")

+ 3 - 2
adapter/service/manager.go

@@ -35,13 +35,14 @@ func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Man
 
 func (m *Manager) Start(stage adapter.StartStage) error {
 	m.access.Lock()
-	defer m.access.Unlock()
 	if m.started && m.stage >= stage {
 		panic("already started")
 	}
 	m.started = true
 	m.stage = stage
-	for _, service := range m.services {
+	services := m.services
+	m.access.Unlock()
+	for _, service := range services {
 		err := adapter.LegacyStart(service, stage)
 		if err != nil {
 			return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")

+ 1 - 5
box.go

@@ -467,11 +467,7 @@ func (s *Box) start() error {
 	if err != nil {
 		return err
 	}
-	err = s.inbound.Start(adapter.StartStateStart)
-	if err != nil {
-		return err
-	}
-	err = adapter.Start(adapter.StartStateStart, s.endpoint)
+	err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
 	if err != nil {
 		return err
 	}

+ 1 - 0
constant/proxy.go

@@ -26,6 +26,7 @@ const (
 	TypeHysteria2    = "hysteria2"
 	TypeTailscale    = "tailscale"
 	TypeDERP         = "derp"
+	TypeResolved     = "resolved"
 )
 
 const (

+ 44 - 15
dns/client.go

@@ -253,9 +253,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
 func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
 	domain = FqdnToDomain(domain)
 	dnsName := dns.Fqdn(domain)
-	if options.Strategy == C.DomainStrategyIPv4Only {
+	var strategy C.DomainStrategy
+	if options.LookupStrategy != C.DomainStrategyAsIS {
+		strategy = options.LookupStrategy
+	} else {
+		strategy = options.Strategy
+	}
+	if strategy == C.DomainStrategyIPv4Only {
 		return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
-	} else if options.Strategy == C.DomainStrategyIPv6Only {
+	} else if strategy == C.DomainStrategyIPv6Only {
 		return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
 	}
 	var response4 []netip.Addr
@@ -281,7 +287,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
 	if len(response4) == 0 && len(response6) == 0 {
 		return nil, err
 	}
-	return sortAddresses(response4, response6, options.Strategy), nil
+	return sortAddresses(response4, response6, strategy), nil
 }
 
 func (c *Client) ClearCache() {
@@ -537,12 +543,26 @@ func transportTagFromContext(ctx context.Context) (string, bool) {
 	return value, loaded
 }
 
+func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg {
+	return &dns.Msg{
+		MsgHdr: dns.MsgHdr{
+			Id:       message.Id,
+			Rcode:    rcode,
+			Response: true,
+		},
+		Question: message.Question,
+	}
+}
+
 func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg {
 	response := dns.Msg{
 		MsgHdr: dns.MsgHdr{
-			Id:       id,
-			Rcode:    dns.RcodeSuccess,
-			Response: true,
+			Id:                 id,
+			Response:           true,
+			Authoritative:      true,
+			RecursionDesired:   true,
+			RecursionAvailable: true,
+			Rcode:              dns.RcodeSuccess,
 		},
 		Question: []dns.Question{question},
 	}
@@ -575,9 +595,12 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim
 func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg {
 	response := dns.Msg{
 		MsgHdr: dns.MsgHdr{
-			Id:       id,
-			Rcode:    dns.RcodeSuccess,
-			Response: true,
+			Id:                 id,
+			Response:           true,
+			Authoritative:      true,
+			RecursionDesired:   true,
+			RecursionAvailable: true,
+			Rcode:              dns.RcodeSuccess,
 		},
 		Question: []dns.Question{question},
 		Answer: []dns.RR{
@@ -598,9 +621,12 @@ func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToL
 func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg {
 	response := dns.Msg{
 		MsgHdr: dns.MsgHdr{
-			Id:       id,
-			Rcode:    dns.RcodeSuccess,
-			Response: true,
+			Id:                 id,
+			Response:           true,
+			Authoritative:      true,
+			RecursionDesired:   true,
+			RecursionAvailable: true,
+			Rcode:              dns.RcodeSuccess,
 		},
 		Question: []dns.Question{question},
 		Answer: []dns.RR{
@@ -621,9 +647,12 @@ func FixedResponseTXT(id uint16, question dns.Question, records []string, timeTo
 func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg {
 	response := dns.Msg{
 		MsgHdr: dns.MsgHdr{
-			Id:       id,
-			Rcode:    dns.RcodeSuccess,
-			Response: true,
+			Id:                 id,
+			Response:           true,
+			Authoritative:      true,
+			RecursionDesired:   true,
+			RecursionAvailable: true,
+			Rcode:              dns.RcodeSuccess,
 		},
 		Question: []dns.Question{question},
 	}

+ 5 - 0
dns/router.go

@@ -292,7 +292,12 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
 					} else if errors.Is(err, ErrResponseRejected) {
 						rejected = true
 						r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
+						/*} else if responseCheck!= nil && errors.Is(err, RcodeError(mDNS.RcodeNameError)) {
+						rejected = true
+						r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
+						*/
 					} else if len(message.Question) > 0 {
+						rejected = true
 						r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
 					} else {
 						r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))

+ 7 - 3
dns/transport/tls.go

@@ -60,13 +60,17 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
 	if !serverAddr.IsValid() {
 		return nil, E.New("invalid server address: ", serverAddr)
 	}
+	return NewTLSRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions), transportDialer, serverAddr, tlsConfig), nil
+}
+
+func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport {
 	return &TLSTransport{
-		TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions),
+		TransportAdapter: adapter,
 		logger:           logger,
-		dialer:           transportDialer,
+		dialer:           dialer,
 		serverAddr:       serverAddr,
 		tlsConfig:        tlsConfig,
-	}, nil
+	}
 }
 
 func (t *TLSTransport) Start(stage adapter.StartStage) error {

+ 1 - 0
docs/configuration/dns/server/index.md

@@ -41,6 +41,7 @@ The type of the DNS server.
 | `dhcp`          | [DHCP](./dhcp/)           |
 | `fakeip`        | [Fake IP](./fakeip/)      |
 | `tailscale`     | [Tailscale](./tailscale/) |
+| `resolved`      | [Resolved](./resolved/)   |
 
 #### tag
 

+ 1 - 0
docs/configuration/dns/server/index.zh.md

@@ -41,6 +41,7 @@ DNS 服务器的类型。
 | `dhcp`          | [DHCP](./dhcp/)           |
 | `fakeip`        | [Fake IP](./fakeip/)      |
 | `tailscale`     | [Tailscale](./tailscale/) |
+| `resolved`      | [Resolved](./resolved/)   |
 
 #### tag
 

+ 84 - 0
docs/configuration/dns/server/resolved.md

@@ -0,0 +1,84 @@
+---
+icon: material/new-box
+---
+
+!!! question "Since sing-box 1.12.0"
+
+# Resolved
+
+```json
+{
+  "dns": {
+    "servers": [
+      {
+        "type": "resolved",
+        "tag": "",
+
+        "service": "resolved",
+        "accept_default_resolvers": false
+      }
+    ]
+  }
+}
+```
+
+
+### Fields
+
+#### service
+
+==Required==
+
+The tag of the [Resolved Service](/configuration/service/resolved).
+
+#### accept_default_resolvers
+
+Indicates whether the default DNS resolvers should be accepted for fallback queries in addition to matching domains.
+
+Specifically, default DNS resolvers are DNS servers that have `SetLinkDefaultRoute` or `SetLinkDomains ~.` set.
+
+If not enabled, `NXDOMAIN` will be returned for requests that do not match search or match domains.
+
+### Examples
+
+=== "Split DNS only"
+
+    ```json
+    {
+      "dns": {
+        "servers": [
+          {
+            "type": "local",
+            "tag": "local"
+          },
+          {
+            "type": "resolved",
+            "tag": "resolved",
+            "service": "resolved"
+          }
+        ],
+        "rules": [
+          {
+            "ip_accept_any": true,
+            "server": "resolved"
+          }
+        ]
+      }
+    }
+    ```
+
+=== "Use as global DNS"
+
+    ```json
+    {
+      "dns": {
+        "servers": [
+          {
+            "type": "resolved",
+            "service": "resolved",
+            "accept_default_resolvers": true
+          }
+        ]
+      }
+    }
+    ```

+ 3 - 3
docs/configuration/dns/server/tailscale.md

@@ -30,13 +30,13 @@ icon: material/new-box
 
 ==Required==
 
-The tag of the Tailscale endpoint.
+The tag of the [Tailscale Endpoint](/configuration/endpoint/tailscale).
 
 #### accept_default_resolvers
 
 Indicates whether default DNS resolvers should be accepted for fallback queries in addition to MagicDNS。
 
-if not enabled, NXDOMAIN will be returned for non-Tailscale domain queries.
+if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries.
 
 ### Examples
 
@@ -80,4 +80,4 @@ if not enabled, NXDOMAIN will be returned for non-Tailscale domain queries.
         ]
       }
     }
-    ```
+    ```

+ 4 - 3
docs/configuration/service/index.md

@@ -21,9 +21,10 @@ icon: material/new-box
 
 ### Fields
 
-| Type        | Format                   |
-|-------------|--------------------------|
-| `derp`      | [DERP](./derp)           |
+| Type       | Format                 |
+|------------|------------------------|
+| `derp`     | [DERP](./derp)         |
+| `resolved` | [Resolved](./resolved) |
 
 #### tag
 

+ 44 - 0
docs/configuration/service/resolved.md

@@ -0,0 +1,44 @@
+---
+icon: material/new-box
+---
+
+!!! question "Since sing-box 1.12.0"
+
+# Resolved
+
+Resolved service is a fake systemd-resolved DBUS service to receive DNS settings from other programs
+(e.g. NetworkManager) and provide DNS resolution.
+
+See also: [Resolved DNS Server](/configuration/dns/server/resolved/)
+
+### Structure
+
+```json
+{
+  "type": "resolved",
+  
+  ... // Listen Fields
+}
+```
+
+### Listen Fields
+
+See [Listen Fields](/configuration/shared/listen/) for details.
+
+### Fields
+
+#### listen
+
+==Required==
+
+Listen address.
+
+`127.0.0.53` will be used by default.
+
+#### listen_port
+
+==Required==
+
+Listen port.
+
+`53` will be used by default.

+ 1 - 1
go.mod

@@ -10,6 +10,7 @@ require (
 	github.com/cretz/bine v0.2.0
 	github.com/go-chi/chi/v5 v5.2.1
 	github.com/go-chi/render v1.0.3
+	github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
 	github.com/gofrs/uuid/v5 v5.3.2
 	github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f
 	github.com/libdns/alidns v1.0.4-libdns.v1.beta1
@@ -78,7 +79,6 @@ require (
 	github.com/go-ole/go-ole v1.3.0 // indirect
 	github.com/gobwas/httphead v0.1.0 // indirect
 	github.com/gobwas/pool v0.2.1 // indirect
-	github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect

+ 4 - 0
include/registry.go

@@ -34,6 +34,7 @@ import (
 	"github.com/sagernet/sing-box/protocol/tun"
 	"github.com/sagernet/sing-box/protocol/vless"
 	"github.com/sagernet/sing-box/protocol/vmess"
+	"github.com/sagernet/sing-box/service/resolved"
 	E "github.com/sagernet/sing/common/exceptions"
 )
 
@@ -111,6 +112,7 @@ func DNSTransportRegistry() *dns.TransportRegistry {
 	hosts.RegisterTransport(registry)
 	local.RegisterTransport(registry)
 	fakeip.RegisterTransport(registry)
+	resolved.RegisterTransport(registry)
 
 	registerQUICTransports(registry)
 	registerDHCPTransport(registry)
@@ -122,6 +124,8 @@ func DNSTransportRegistry() *dns.TransportRegistry {
 func ServiceRegistry() *service.Registry {
 	registry := service.NewRegistry()
 
+	resolved.RegisterService(registry)
+
 	registerDERPService(registry)
 
 	return registry

+ 2 - 0
mkdocs.yml

@@ -94,6 +94,7 @@ nav:
               - DHCP: configuration/dns/server/dhcp.md
               - FakeIP: configuration/dns/server/fakeip.md
               - Tailscale: configuration/dns/server/tailscale.md
+              - Resolved: configuration/dns/server/resolved.md
           - DNS Rule: configuration/dns/rule.md
           - DNS Rule Action: configuration/dns/rule_action.md
           - FakeIP: configuration/dns/fakeip.md
@@ -172,6 +173,7 @@ nav:
       - Service:
           - configuration/service/index.md
           - DERP: configuration/service/derp.md
+          - Resolved: configuration/service/resolved.md
 markdown_extensions:
   - pymdownx.inlinehilite
   - pymdownx.snippets

+ 49 - 0
option/resolved.go

@@ -0,0 +1,49 @@
+package option
+
+import (
+	"context"
+	"net/netip"
+
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badoption"
+)
+
+type _ResolvedServiceOptions struct {
+	ListenOptions
+}
+
+type ResolvedServiceOptions _ResolvedServiceOptions
+
+func (r ResolvedServiceOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) {
+	if r.Listen != nil && netip.Addr(*r.Listen) == (netip.AddrFrom4([4]byte{127, 0, 0, 53})) {
+		r.Listen = nil
+	}
+	if r.ListenPort == 53 {
+		r.ListenPort = 0
+	}
+	return json.MarshalContext(ctx, (*_ResolvedServiceOptions)(&r))
+}
+
+func (r *ResolvedServiceOptions) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
+	err := json.UnmarshalContextDisallowUnknownFields(ctx, bytes, (*_ResolvedServiceOptions)(r))
+	if err != nil {
+		return err
+	}
+	if r.Listen == nil {
+		r.Listen = (*badoption.Addr)(common.Ptr(netip.AddrFrom4([4]byte{127, 0, 0, 53})))
+	}
+	if r.ListenPort == 0 {
+		r.ListenPort = 53
+	}
+	return nil
+}
+
+type ResolvedDNSServerOptions struct {
+	Service                string `json:"service"`
+	AcceptDefaultResolvers bool   `json:"accept_default_resolvers,omitempty"`
+	// NDots                  int                `json:"ndots,omitempty"`
+	// Timeout                badoption.Duration `json:"timeout,omitempty"`
+	// Attempts               int                `json:"attempts,omitempty"`
+	// Rotate                 bool               `json:"rotate,omitempty"`
+}

+ 15 - 0
release/config/sing-box-split-dns.xml

@@ -0,0 +1,15 @@
+<!DOCTYPE busconfig PUBLIC
+        "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+        "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+    <policy user="root">
+        <allow own="org.freedesktop.resolve1"/>
+        <allow send_destination="org.freedesktop.resolve1"/>
+        <allow receive_sender="org.freedesktop.resolve1"/>
+    </policy>
+    <policy user="sing-box">
+        <allow own="org.freedesktop.resolve1"/>
+        <allow send_destination="org.freedesktop.resolve1"/>
+        <allow receive_sender="org.freedesktop.resolve1"/>
+    </policy>
+</busconfig>

+ 8 - 0
release/config/sing-box.rules

@@ -0,0 +1,8 @@
+polkit.addRule(function(action, subject) {
+    if ((action.id == "org.freedesktop.resolve1.set-domains" ||
+         action.id == "org.freedesktop.resolve1.set-default-route" ||
+         action.id == "org.freedesktop.resolve1.set-dns-servers") &&
+        subject.user == "sing-box") {
+        return polkit.Result.YES;
+    }
+});

+ 2 - 0
release/config/sing-box.service

@@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org
 After=network.target nss-lookup.target network-online.target
 
 [Service]
+User=sing-box
+StateDirectory=sing-box
 CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
 AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
 ExecStart=/usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run

+ 1 - 0
release/config/sing-box.sysusers

@@ -0,0 +1 @@
+u sing-box - "sing-box Service"

+ 2 - 0
release/config/[email protected]

@@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org
 After=network.target nss-lookup.target network-online.target
 
 [Service]
+User=sing-box
+StateDirectory=sing-box-%i
 CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
 AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
 ExecStart=/usr/bin/sing-box -D /var/lib/sing-box-%i -c /etc/sing-box/%i.json run

+ 10 - 5
route/dns.go

@@ -26,12 +26,16 @@ func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata ad
 		conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
 		err := dnsOutbound.HandleStreamDNSRequest(ctx, r.dns, conn, metadata)
 		if err != nil {
-			return err
+			if !E.IsClosedOrCanceled(err) {
+				return err
+			} else {
+				return nil
+			}
 		}
 	}
 }
 
-func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
 	if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
 		metadata.Destination = M.Socksaddr{}
 		for _, packet := range packetBuffers {
@@ -48,19 +52,20 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
 			metadata: metadata,
 			onClose:  onClose,
 		})
-		return
+		return nil
 	}
 	err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil && !E.IsClosedOrCanceled(err) {
-		r.logger.ErrorContext(ctx, E.Cause(err, "process DNS packet connection"))
+		return E.Cause(err, "process DNS packet")
 	}
+	return nil
 }
 
 func ExchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, logger logger.ContextLogger, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) {
 	err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination)
 	if err != nil && !R.IsRejected(err) && !E.IsClosedOrCanceled(err) {
-		logger.ErrorContext(ctx, E.Cause(err, "process DNS packet connection"))
+		logger.ErrorContext(ctx, E.Cause(err, "process DNS packet"))
 	}
 }
 

+ 10 - 14
route/route.go

@@ -6,7 +6,6 @@ import (
 	"net"
 	"net/netip"
 	"os"
-	"os/user"
 	"strings"
 	"time"
 
@@ -113,8 +112,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
 			}
 		case *R.RuleActionReject:
 			buf.ReleaseMulti(buffers)
-			N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
-			return nil
+			return action.Error(ctx)
 		case *R.RuleActionHijackDNS:
 			for _, buffer := range buffers {
 				conn = bufio.NewCachedConn(conn, buffer)
@@ -229,11 +227,9 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
 			}
 		case *R.RuleActionReject:
 			N.ReleaseMultiPacketBuffer(packetBuffers)
-			N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
-			return nil
+			return action.Error(ctx)
 		case *R.RuleActionHijackDNS:
-			r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
-			return nil
+			return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
 		}
 	}
 	if selectedRule == nil || selectReturn {
@@ -296,16 +292,16 @@ func (r *Router) matchRule(
 			r.logger.InfoContext(ctx, "failed to search process: ", fErr)
 		} else {
 			if processInfo.ProcessPath != "" {
-				r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
+				if processInfo.User != "" {
+					r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.User)
+				} else if processInfo.UserId != -1 {
+					r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId)
+				} else {
+					r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
+				}
 			} else if processInfo.PackageName != "" {
 				r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName)
 			} else if processInfo.UserId != -1 {
-				if /*needUserName &&*/ true {
-					osUser, _ := user.LookupId(F.ToString(processInfo.UserId))
-					if osUser != nil {
-						processInfo.User = osUser.Username
-					}
-				}
 				if processInfo.User != "" {
 					r.logger.InfoContext(ctx, "found user: ", processInfo.User)
 				} else {

+ 648 - 0
service/resolved/resolve1.go

@@ -0,0 +1,648 @@
+//go:build linux
+
+package resolved
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/netip"
+	"os"
+	"os/user"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"syscall"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/process"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/dns"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+	F "github.com/sagernet/sing/common/format"
+	M "github.com/sagernet/sing/common/metadata"
+
+	"github.com/godbus/dbus/v5"
+	mDNS "github.com/miekg/dns"
+)
+
+type resolve1Manager Service
+
+type Address struct {
+	IfIndex int32
+	Family  int32
+	Address []byte
+}
+
+type Name struct {
+	IfIndex  int32
+	Hostname string
+}
+
+type ResourceRecord struct {
+	IfIndex int32
+	Type    uint16
+	Class   uint16
+	Data    []byte
+}
+
+type SRVRecord struct {
+	Priority  uint16
+	Weight    uint16
+	Port      uint16
+	Hostname  string
+	Addresses []Address
+	CNAME     string
+}
+
+type TXTRecord []byte
+
+type LinkDNS struct {
+	Family  int32
+	Address []byte
+}
+
+type LinkDNSEx struct {
+	Family  int32
+	Address []byte
+	Port    uint16
+	Name    string
+}
+
+type LinkDomain struct {
+	Domain      string
+	RoutingOnly bool
+}
+
+func (t *resolve1Manager) getLink(ifIndex int32) (*TransportLink, *dbus.Error) {
+	link, loaded := t.links[ifIndex]
+	if !loaded {
+		link = &TransportLink{}
+		t.links[ifIndex] = link
+		iif, err := t.network.InterfaceFinder().ByIndex(int(ifIndex))
+		if err != nil {
+			return nil, wrapError(err)
+		}
+		link.iif = iif
+	}
+	return link, nil
+}
+
+func (t *resolve1Manager) getSenderProcess(sender dbus.Sender) (int32, error) {
+	var senderPid int32
+	dbusObject := t.systemBus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
+	if dbusObject == nil {
+		return 0, E.New("missing dbus object")
+	}
+	err := dbusObject.Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, string(sender)).Store(&senderPid)
+	if err != nil {
+		return 0, E.Cause(err, "GetConnectionUnixProcessID")
+	}
+	return senderPid, nil
+}
+
+func (t *resolve1Manager) createMetadata(sender dbus.Sender) adapter.InboundContext {
+	var metadata adapter.InboundContext
+	metadata.Inbound = t.Tag()
+	metadata.InboundType = C.TypeResolved
+	senderPid, err := t.getSenderProcess(sender)
+	if err != nil {
+		return metadata
+	}
+	var processInfo process.Info
+	metadata.ProcessInfo = &processInfo
+	processInfo.ProcessID = uint32(senderPid)
+
+	processPath, err := os.Readlink(F.ToString("/proc/", senderPid, "/exe"))
+	if err == nil {
+		processInfo.ProcessPath = processPath
+	} else {
+		processPath, err = os.Readlink(F.ToString("/proc/", senderPid, "/comm"))
+		if err == nil {
+			processInfo.ProcessPath = processPath
+		}
+	}
+
+	var uidFound bool
+	statusContent, err := os.ReadFile(F.ToString("/proc/", senderPid, "/status"))
+	if err == nil {
+		for _, line := range strings.Split(string(statusContent), "\n") {
+			line = strings.TrimSpace(line)
+			if strings.HasPrefix(line, "Uid:") {
+				fields := strings.Fields(line)
+				if len(fields) >= 2 {
+					uid, parseErr := strconv.ParseUint(fields[1], 10, 32)
+					if parseErr != nil {
+						break
+					}
+					processInfo.UserId = int32(uid)
+					uidFound = true
+					if osUser, _ := user.LookupId(F.ToString(uid)); osUser != nil {
+						processInfo.User = osUser.Username
+					}
+					break
+				}
+			}
+		}
+	}
+	if !uidFound {
+		metadata.ProcessInfo.UserId = -1
+	}
+	return metadata
+}
+
+func (t *resolve1Manager) log(sender dbus.Sender, message ...any) {
+	metadata := t.createMetadata(sender)
+	if metadata.ProcessInfo != nil {
+		var prefix string
+		if metadata.ProcessInfo.ProcessPath != "" {
+			prefix = filepath.Base(metadata.ProcessInfo.ProcessPath)
+		} else if metadata.ProcessInfo.User != "" {
+			prefix = F.ToString("user:", metadata.ProcessInfo.User)
+		} else if metadata.ProcessInfo.UserId != 0 {
+			prefix = F.ToString("uid:", metadata.ProcessInfo.UserId)
+		}
+		t.logger.Info("(", prefix, ") ", F.ToString(message...))
+	} else {
+		t.logger.Info(F.ToString(message...))
+	}
+}
+
+func (t *resolve1Manager) logRequest(sender dbus.Sender, message ...any) context.Context {
+	ctx := log.ContextWithNewID(t.ctx)
+	metadata := t.createMetadata(sender)
+	if metadata.ProcessInfo != nil {
+		var prefix string
+		if metadata.ProcessInfo.ProcessPath != "" {
+			prefix = filepath.Base(metadata.ProcessInfo.ProcessPath)
+		} else if metadata.ProcessInfo.User != "" {
+			prefix = F.ToString("user:", metadata.ProcessInfo.User)
+		} else if metadata.ProcessInfo.UserId != 0 {
+			prefix = F.ToString("uid:", metadata.ProcessInfo.UserId)
+		}
+		t.logger.InfoContext(ctx, "(", prefix, ") ", F.ToString(message...))
+	} else {
+		t.logger.InfoContext(ctx, F.ToString(message...))
+	}
+	return adapter.WithContext(ctx, &metadata)
+}
+
+func familyToString(family int32) string {
+	switch family {
+	case syscall.AF_UNSPEC:
+		return "AF_UNSPEC"
+	case syscall.AF_INET:
+		return "AF_INET"
+	case syscall.AF_INET6:
+		return "AF_INET6"
+	default:
+		return F.ToString(family)
+	}
+}
+
+func (t *resolve1Manager) ResolveHostname(sender dbus.Sender, ifIndex int32, hostname string, family int32, flags uint64) (addresses []Address, canonical string, outflags uint64, err *dbus.Error) {
+	t.linkAccess.Lock()
+	link, err := t.getLink(ifIndex)
+	if err != nil {
+		return
+	}
+	t.linkAccess.Unlock()
+	var strategy C.DomainStrategy
+	switch family {
+	case syscall.AF_UNSPEC:
+		strategy = C.DomainStrategyAsIS
+	case syscall.AF_INET:
+		strategy = C.DomainStrategyIPv4Only
+	case syscall.AF_INET6:
+		strategy = C.DomainStrategyIPv6Only
+	}
+	ctx := t.logRequest(sender, "ResolveHostname ", link.iif.Name, " ", hostname, " ", familyToString(family), " ", flags)
+	responseAddresses, lookupErr := t.dnsRouter.Lookup(ctx, hostname, adapter.DNSQueryOptions{
+		LookupStrategy: strategy,
+	})
+	if lookupErr != nil {
+		err = wrapError(err)
+		return
+	}
+	addresses = common.Map(responseAddresses, func(it netip.Addr) Address {
+		var addrFamily int32
+		if it.Is4() {
+			addrFamily = syscall.AF_INET
+		} else {
+			addrFamily = syscall.AF_INET6
+		}
+		return Address{
+			IfIndex: ifIndex,
+			Family:  addrFamily,
+			Address: it.AsSlice(),
+		}
+	})
+	canonical = mDNS.CanonicalName(hostname)
+	return
+}
+
+func (t *resolve1Manager) ResolveAddress(sender dbus.Sender, ifIndex int32, family int32, address []byte, flags uint64) (names []Name, outflags uint64, err *dbus.Error) {
+	t.linkAccess.Lock()
+	link, err := t.getLink(ifIndex)
+	if err != nil {
+		return
+	}
+	t.linkAccess.Unlock()
+	addr, ok := netip.AddrFromSlice(address)
+	if !ok {
+		err = wrapError(E.New("invalid address"))
+		return
+	}
+	var nibbles []string
+	for i := len(address) - 1; i >= 0; i-- {
+		b := address[i]
+		nibbles = append(nibbles, fmt.Sprintf("%x", b&0x0F))
+		nibbles = append(nibbles, fmt.Sprintf("%x", b>>4))
+	}
+	var ptrDomain string
+	if addr.Is4() {
+		ptrDomain = strings.Join(nibbles, ".") + ".in-addr.arpa."
+	} else {
+		ptrDomain = strings.Join(nibbles, ".") + ".ip6.arpa."
+	}
+	request := &mDNS.Msg{
+		MsgHdr: mDNS.MsgHdr{
+			RecursionDesired: true,
+		},
+		Question: []mDNS.Question{
+			{
+				Name:   mDNS.Fqdn(ptrDomain),
+				Qtype:  mDNS.TypePTR,
+				Qclass: mDNS.ClassINET,
+			},
+		},
+	}
+	ctx := t.logRequest(sender, "ResolveAddress ", link.iif.Name, familyToString(family), addr, flags)
+	response, lookupErr := t.dnsRouter.Exchange(ctx, request, adapter.DNSQueryOptions{})
+	if lookupErr != nil {
+		err = wrapError(err)
+		return
+	}
+	if response.Rcode != mDNS.RcodeSuccess {
+		err = rcodeError(response.Rcode)
+		return
+	}
+	for _, rawRR := range response.Answer {
+		switch rr := rawRR.(type) {
+		case *mDNS.PTR:
+			names = append(names, Name{
+				IfIndex:  ifIndex,
+				Hostname: rr.Ptr,
+			})
+		}
+	}
+	return
+}
+
+func (t *resolve1Manager) ResolveRecord(sender dbus.Sender, ifIndex int32, family int32, hostname string, qClass uint16, qType uint16, flags uint64) (records []ResourceRecord, outflags uint64, err *dbus.Error) {
+	t.linkAccess.Lock()
+	link, err := t.getLink(ifIndex)
+	if err != nil {
+		return
+	}
+	t.linkAccess.Unlock()
+	request := &mDNS.Msg{
+		MsgHdr: mDNS.MsgHdr{
+			RecursionDesired: true,
+		},
+		Question: []mDNS.Question{
+			{
+				Name:   mDNS.Fqdn(hostname),
+				Qtype:  qType,
+				Qclass: qClass,
+			},
+		},
+	}
+	ctx := t.logRequest(sender, "ResolveRecord ", link.iif.Name, familyToString(family), hostname, mDNS.Class(qClass), mDNS.Type(qType), flags)
+	response, exchangeErr := t.dnsRouter.Exchange(ctx, request, adapter.DNSQueryOptions{})
+	if exchangeErr != nil {
+		err = wrapError(exchangeErr)
+		return
+	}
+	if response.Rcode != mDNS.RcodeSuccess {
+		err = rcodeError(response.Rcode)
+		return
+	}
+	for _, rr := range response.Answer {
+		var record ResourceRecord
+		record.IfIndex = ifIndex
+		record.Type = rr.Header().Rrtype
+		record.Class = rr.Header().Class
+		data := make([]byte, mDNS.Len(rr))
+		_, unpackErr := mDNS.PackRR(rr, data, 0, nil, false)
+		if unpackErr != nil {
+			err = wrapError(unpackErr)
+		}
+		record.Data = data
+	}
+	return
+}
+
+func (t *resolve1Manager) ResolveService(sender dbus.Sender, ifIndex int32, hostname string, sType string, domain string, family int32, flags uint64) (srvData []SRVRecord, txtData []TXTRecord, canonicalName string, canonicalType string, canonicalDomain string, outflags uint64, err *dbus.Error) {
+	t.linkAccess.Lock()
+	link, err := t.getLink(ifIndex)
+	if err != nil {
+		return
+	}
+	t.linkAccess.Unlock()
+
+	serviceName := hostname
+	if hostname != "" && !strings.HasSuffix(hostname, ".") {
+		serviceName += "."
+	}
+	serviceName += sType
+	if !strings.HasSuffix(serviceName, ".") {
+		serviceName += "."
+	}
+	serviceName += domain
+	if !strings.HasSuffix(serviceName, ".") {
+		serviceName += "."
+	}
+
+	ctx := t.logRequest(sender, "ResolveService ", link.iif.Name, " ", hostname, " ", sType, " ", domain, " ", familyToString(family), " ", flags)
+
+	srvRequest := &mDNS.Msg{
+		MsgHdr: mDNS.MsgHdr{
+			RecursionDesired: true,
+		},
+		Question: []mDNS.Question{
+			{
+				Name:   serviceName,
+				Qtype:  mDNS.TypeSRV,
+				Qclass: mDNS.ClassINET,
+			},
+		},
+	}
+
+	srvResponse, exchangeErr := t.dnsRouter.Exchange(ctx, srvRequest, adapter.DNSQueryOptions{})
+	if exchangeErr != nil {
+		err = wrapError(exchangeErr)
+		return
+	}
+	if srvResponse.Rcode != mDNS.RcodeSuccess {
+		err = rcodeError(srvResponse.Rcode)
+		return
+	}
+
+	txtRequest := &mDNS.Msg{
+		MsgHdr: mDNS.MsgHdr{
+			RecursionDesired: true,
+		},
+		Question: []mDNS.Question{
+			{
+				Name:   serviceName,
+				Qtype:  mDNS.TypeTXT,
+				Qclass: mDNS.ClassINET,
+			},
+		},
+	}
+
+	txtResponse, exchangeErr := t.dnsRouter.Exchange(ctx, txtRequest, adapter.DNSQueryOptions{})
+	if exchangeErr != nil {
+		err = wrapError(exchangeErr)
+		return
+	}
+
+	for _, rawRR := range srvResponse.Answer {
+		switch rr := rawRR.(type) {
+		case *mDNS.SRV:
+			var srvRecord SRVRecord
+			srvRecord.Priority = rr.Priority
+			srvRecord.Weight = rr.Weight
+			srvRecord.Port = rr.Port
+			srvRecord.Hostname = rr.Target
+
+			var strategy C.DomainStrategy
+			switch family {
+			case syscall.AF_UNSPEC:
+				strategy = C.DomainStrategyAsIS
+			case syscall.AF_INET:
+				strategy = C.DomainStrategyIPv4Only
+			case syscall.AF_INET6:
+				strategy = C.DomainStrategyIPv6Only
+			}
+
+			addrs, lookupErr := t.dnsRouter.Lookup(ctx, rr.Target, adapter.DNSQueryOptions{
+				LookupStrategy: strategy,
+			})
+			if lookupErr == nil {
+				srvRecord.Addresses = common.Map(addrs, func(it netip.Addr) Address {
+					var addrFamily int32
+					if it.Is4() {
+						addrFamily = syscall.AF_INET
+					} else {
+						addrFamily = syscall.AF_INET6
+					}
+					return Address{
+						IfIndex: ifIndex,
+						Family:  addrFamily,
+						Address: it.AsSlice(),
+					}
+				})
+			}
+			for _, a := range srvResponse.Answer {
+				if cname, ok := a.(*mDNS.CNAME); ok && cname.Header().Name == rr.Target {
+					srvRecord.CNAME = cname.Target
+					break
+				}
+			}
+			srvData = append(srvData, srvRecord)
+		}
+	}
+	for _, rawRR := range txtResponse.Answer {
+		switch rr := rawRR.(type) {
+		case *mDNS.TXT:
+			data := make([]byte, mDNS.Len(rr))
+			_, packErr := mDNS.PackRR(rr, data, 0, nil, false)
+			if packErr == nil {
+				txtData = append(txtData, data)
+			}
+		}
+	}
+	canonicalName = mDNS.CanonicalName(hostname)
+	canonicalType = mDNS.CanonicalName(sType)
+	canonicalDomain = mDNS.CanonicalName(domain)
+	return
+}
+
+func (t *resolve1Manager) SetLinkDNS(sender dbus.Sender, ifIndex int32, addresses []LinkDNS) *dbus.Error {
+	t.linkAccess.Lock()
+	defer t.linkAccess.Unlock()
+	link, err := t.getLink(ifIndex)
+	if err != nil {
+		return wrapError(err)
+	}
+	link.address = addresses
+	if len(addresses) > 0 {
+		t.log(sender, "SetLinkDNS ", link.iif.Name, " ", strings.Join(common.Map(addresses, func(it LinkDNS) string {
+			return M.AddrFromIP(it.Address).String()
+		}), ", "))
+	} else {
+		t.log(sender, "SetLinkDNS ", link.iif.Name, " (empty)")
+	}
+	return t.postUpdate(link)
+}
+
+func (t *resolve1Manager) SetLinkDNSEx(sender dbus.Sender, ifIndex int32, addresses []LinkDNSEx) *dbus.Error {
+	t.linkAccess.Lock()
+	defer t.linkAccess.Unlock()
+	link, err := t.getLink(ifIndex)
+	if err != nil {
+		return wrapError(err)
+	}
+	link.addressEx = addresses
+	if len(addresses) > 0 {
+		t.log(sender, "SetLinkDNSEx ", link.iif.Name, " ", strings.Join(common.Map(addresses, func(it LinkDNSEx) string {
+			return M.SocksaddrFrom(M.AddrFromIP(it.Address), it.Port).String()
+		}), ", "))
+	} else {
+		t.log(sender, "SetLinkDNSEx ", link.iif.Name, " (empty)")
+	}
+	return t.postUpdate(link)
+}
+
+func (t *resolve1Manager) SetLinkDomains(sender dbus.Sender, ifIndex int32, domains []LinkDomain) *dbus.Error {
+	t.linkAccess.Lock()
+	defer t.linkAccess.Unlock()
+	link, err := t.getLink(ifIndex)
+	if err != nil {
+		return wrapError(err)
+	}
+	link.domain = domains
+	if len(domains) > 0 {
+		t.log(sender, "SetLinkDomains ", link.iif.Name, " ", strings.Join(common.Map(domains, func(domain LinkDomain) string {
+			if !domain.RoutingOnly {
+				return domain.Domain
+			} else {
+				return "~" + domain.Domain
+			}
+		}), ", "))
+	} else {
+		t.log(sender, "SetLinkDomains ", link.iif.Name, " (empty)")
+	}
+	return t.postUpdate(link)
+}
+
+func (t *resolve1Manager) SetLinkDefaultRoute(sender dbus.Sender, ifIndex int32, defaultRoute bool) *dbus.Error {
+	t.linkAccess.Lock()
+	defer t.linkAccess.Unlock()
+	link, err := t.getLink(ifIndex)
+	if err != nil {
+		return err
+	}
+	link.defaultRoute = defaultRoute
+	if defaultRoute {
+		t.defaultRouteSequence = append(common.Filter(t.defaultRouteSequence, func(it int32) bool { return it != ifIndex }), ifIndex)
+	} else {
+		t.defaultRouteSequence = common.Filter(t.defaultRouteSequence, func(it int32) bool { return it != ifIndex })
+	}
+	var defaultRouteString string
+	if defaultRoute {
+		defaultRouteString = "yes"
+	} else {
+		defaultRouteString = "no"
+	}
+	t.log(sender, "SetLinkDefaultRoute ", link.iif.Name, " ", defaultRouteString)
+	return t.postUpdate(link)
+}
+
+func (t *resolve1Manager) SetLinkLLMNR(ifIndex int32, llmnrMode string) *dbus.Error {
+	return nil
+}
+
+func (t *resolve1Manager) SetLinkMulticastDNS(ifIndex int32, mdnsMode string) *dbus.Error {
+	return nil
+}
+
+func (t *resolve1Manager) SetLinkDNSOverTLS(sender dbus.Sender, ifIndex int32, dotMode string) *dbus.Error {
+	t.linkAccess.Lock()
+	defer t.linkAccess.Unlock()
+	link, err := t.getLink(ifIndex)
+	if err != nil {
+		return wrapError(err)
+	}
+	switch dotMode {
+	case "yes":
+		link.dnsOverTLS = true
+	case "":
+		dotMode = "no"
+		fallthrough
+	case "opportunistic", "no":
+		link.dnsOverTLS = false
+	}
+	t.log(sender, "SetLinkDNSOverTLS ", link.iif.Name, " ", dotMode)
+	return t.postUpdate(link)
+}
+
+func (t *resolve1Manager) SetLinkDNSSEC(ifIndex int32, dnssecMode string) *dbus.Error {
+	return nil
+}
+
+func (t *resolve1Manager) SetLinkDNSSECNegativeTrustAnchors(ifIndex int32, domains []string) *dbus.Error {
+	return nil
+}
+
+func (t *resolve1Manager) RevertLink(sender dbus.Sender, ifIndex int32) *dbus.Error {
+	t.linkAccess.Lock()
+	defer t.linkAccess.Unlock()
+	link, err := t.getLink(ifIndex)
+	if err != nil {
+		return wrapError(err)
+	}
+	delete(t.links, ifIndex)
+	t.log(sender, "RevertLink ", link.iif.Name)
+	return t.postUpdate(link)
+}
+
+// TODO: implement RegisterService, UnregisterService
+
+func (t *resolve1Manager) RegisterService(sender dbus.Sender, identifier string, nameTemplate string, serviceType string, port uint16, priority uint16, weight uint16, txtRecords []TXTRecord) (objectPath dbus.ObjectPath, dbusErr *dbus.Error) {
+	return "", wrapError(E.New("not implemented"))
+}
+
+func (t *resolve1Manager) UnregisterService(sender dbus.Sender, servicePath dbus.ObjectPath) error {
+	return wrapError(E.New("not implemented"))
+}
+
+func (t *resolve1Manager) ResetStatistics() *dbus.Error {
+	return nil
+}
+
+func (t *resolve1Manager) FlushCaches(sender dbus.Sender) *dbus.Error {
+	t.dnsRouter.ClearCache()
+	t.log(sender, "FlushCaches")
+	return nil
+}
+
+func (t *resolve1Manager) ResetServerFeatures() *dbus.Error {
+	return nil
+}
+
+func (t *resolve1Manager) postUpdate(link *TransportLink) *dbus.Error {
+	if t.updateCallback != nil {
+		return wrapError(t.updateCallback(link))
+	}
+	return nil
+}
+
+func rcodeError(rcode int) *dbus.Error {
+	return dbus.NewError("org.freedesktop.resolve1.DnsError."+mDNS.RcodeToString[rcode], []any{mDNS.RcodeToString[rcode]})
+}
+
+func wrapError(err error) *dbus.Error {
+	if err == nil {
+		return nil
+	}
+	var rcode dns.RcodeError
+	if errors.As(err, &rcode) {
+		return rcodeError(int(rcode))
+	}
+	return dbus.MakeFailedError(err)
+}

+ 252 - 0
service/resolved/service.go

@@ -0,0 +1,252 @@
+//go:build linux
+
+package resolved
+
+import (
+	"context"
+	"net"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/sagernet/sing-box/adapter"
+	boxService "github.com/sagernet/sing-box/adapter/service"
+	"github.com/sagernet/sing-box/common/listener"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/dns"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	dnsOutbound "github.com/sagernet/sing-box/protocol/dns"
+	tun "github.com/sagernet/sing-tun"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/control"
+	E "github.com/sagernet/sing/common/exceptions"
+	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 RegisterService(registry *boxService.Registry) {
+	boxService.Register[option.ResolvedServiceOptions](registry, C.TypeResolved, NewService)
+}
+
+type Service struct {
+	boxService.Adapter
+	ctx                   context.Context
+	logger                log.ContextLogger
+	network               adapter.NetworkManager
+	dnsRouter             adapter.DNSRouter
+	listener              *listener.Listener
+	systemBus             *dbus.Conn
+	linkAccess            sync.RWMutex
+	links                 map[int32]*TransportLink
+	defaultRouteSequence  []int32
+	networkUpdateCallback *list.Element[tun.NetworkUpdateCallback]
+	updateCallback        func(*TransportLink) error
+	deleteCallback        func(*TransportLink)
+}
+
+type TransportLink struct {
+	iif          *control.Interface
+	address      []LinkDNS
+	addressEx    []LinkDNSEx
+	domain       []LinkDomain
+	defaultRoute bool
+	dnsOverTLS   bool
+	// dnsOverTLSFallback bool
+}
+
+func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedServiceOptions) (adapter.Service, error) {
+	inbound := &Service{
+		Adapter:   boxService.NewAdapter(C.TypeResolved, tag),
+		ctx:       ctx,
+		logger:    logger,
+		network:   service.FromContext[adapter.NetworkManager](ctx),
+		dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
+		links:     make(map[int32]*TransportLink),
+	}
+	inbound.listener = listener.New(listener.Options{
+		Context:                  ctx,
+		Logger:                   logger,
+		Network:                  []string{N.NetworkTCP, N.NetworkUDP},
+		Listen:                   options.ListenOptions,
+		ConnectionHandler:        inbound,
+		OOBPacketHandler:         inbound,
+		ThreadUnsafePacketWriter: true,
+	})
+	return inbound, nil
+}
+
+func (i *Service) Start(stage adapter.StartStage) error {
+	switch stage {
+	case adapter.StartStateInitialize:
+		inboundManager := service.FromContext[adapter.ServiceManager](i.ctx)
+		for _, transport := range inboundManager.Services() {
+			if transport.Type() == C.TypeResolved && transport != i {
+				return E.New("multiple resolved service are not supported")
+			}
+		}
+	case adapter.StartStateStart:
+		err := i.listener.Start()
+		if err != nil {
+			return err
+		}
+		systemBus, err := dbus.SystemBus()
+		if err != nil {
+			return err
+		}
+		i.systemBus = systemBus
+		err = systemBus.Export((*resolve1Manager)(i), "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager")
+		if err != nil {
+			return err
+		}
+		reply, err := systemBus.RequestName("org.freedesktop.resolve1", dbus.NameFlagDoNotQueue)
+		if err != nil {
+			return err
+		}
+		switch reply {
+		case dbus.RequestNameReplyPrimaryOwner:
+		case dbus.RequestNameReplyExists:
+			return E.New("D-Bus object already exists, maybe real resolved is running")
+		default:
+			return E.New("unknown request name reply: ", reply)
+		}
+		i.networkUpdateCallback = i.network.NetworkMonitor().RegisterCallback(i.onNetworkUpdate)
+	}
+	return nil
+}
+
+func (i *Service) Close() error {
+	if i.networkUpdateCallback != nil {
+		i.network.NetworkMonitor().UnregisterCallback(i.networkUpdateCallback)
+	}
+	if i.systemBus != nil {
+		i.systemBus.ReleaseName("org.freedesktop.resolve1")
+		i.systemBus.Close()
+	}
+	return i.listener.Close()
+}
+
+func (i *Service) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	metadata.Inbound = i.Tag()
+	metadata.InboundType = i.Type()
+	metadata.Destination = M.Socksaddr{}
+	for {
+		conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
+		err := dnsOutbound.HandleStreamDNSRequest(ctx, i.dnsRouter, conn, metadata)
+		if err != nil {
+			N.CloseOnHandshakeFailure(conn, onClose, err)
+			return
+		}
+	}
+}
+
+func (i *Service) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) {
+	go i.exchangePacket(buffer, oob, source)
+}
+
+func (i *Service) exchangePacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr) {
+	ctx := log.ContextWithNewID(i.ctx)
+	err := i.exchangePacket0(ctx, buffer, oob, source)
+	if err != nil {
+		i.logger.ErrorContext(ctx, "process DNS packet: ", err)
+	}
+}
+
+func (i *Service) exchangePacket0(ctx context.Context, buffer *buf.Buffer, oob []byte, source M.Socksaddr) error {
+	var message mDNS.Msg
+	err := message.Unpack(buffer.Bytes())
+	buffer.Release()
+	if err != nil {
+		return E.Cause(err, "unpack request")
+	}
+	var metadata adapter.InboundContext
+	metadata.Source = source
+	response, err := i.dnsRouter.Exchange(adapter.WithContext(ctx, &metadata), &message, adapter.DNSQueryOptions{})
+	if err != nil {
+		return err
+	}
+	responseBuffer, err := dns.TruncateDNSMessage(&message, response, 0)
+	if err != nil {
+		return err
+	}
+	defer responseBuffer.Release()
+	_, _, err = i.listener.UDPConn().WriteMsgUDPAddrPort(responseBuffer.Bytes(), oob, source.AddrPort())
+	return err
+}
+
+func (i *Service) onNetworkUpdate() {
+	i.linkAccess.Lock()
+	defer i.linkAccess.Unlock()
+	var deleteIfIndex []int
+	for ifIndex, link := range i.links {
+		iif, err := i.network.InterfaceFinder().ByIndex(int(ifIndex))
+		if err != nil || iif != link.iif {
+			deleteIfIndex = append(deleteIfIndex, int(ifIndex))
+		}
+		i.defaultRouteSequence = common.Filter(i.defaultRouteSequence, func(it int32) bool {
+			return it != ifIndex
+		})
+		if i.deleteCallback != nil {
+			i.deleteCallback(link)
+		}
+	}
+	for _, ifIndex := range deleteIfIndex {
+		delete(i.links, int32(ifIndex))
+	}
+}
+
+func (conf *TransportLink) nameList(ndots int, name string) []string {
+	search := common.Map(common.Filter(conf.domain, func(it LinkDomain) bool {
+		return !it.RoutingOnly
+	}), func(it LinkDomain) string {
+		return it.Domain
+	})
+
+	l := len(name)
+	rooted := l > 0 && name[l-1] == '.'
+	if l > 254 || l == 254 && !rooted {
+		return nil
+	}
+
+	if rooted {
+		if avoidDNS(name) {
+			return nil
+		}
+		return []string{name}
+	}
+
+	hasNdots := strings.Count(name, ".") >= ndots
+	name += "."
+	// l++
+
+	names := make([]string, 0, 1+len(search))
+	if hasNdots && !avoidDNS(name) {
+		names = append(names, name)
+	}
+	for _, suffix := range search {
+		fqdn := name + suffix
+		if !avoidDNS(fqdn) && len(fqdn) <= 254 {
+			names = append(names, fqdn)
+		}
+	}
+	if !hasNdots && !avoidDNS(name) {
+		names = append(names, name)
+	}
+	return names
+}
+
+func avoidDNS(name string) bool {
+	if name == "" {
+		return true
+	}
+	if name[len(name)-1] == '.' {
+		name = name[:len(name)-1]
+	}
+	return strings.HasSuffix(name, ".onion")
+}

+ 27 - 0
service/resolved/stub.go

@@ -0,0 +1,27 @@
+//go:build !linux
+
+package resolved
+
+import (
+	"context"
+
+	"github.com/sagernet/sing-box/adapter"
+	boxService "github.com/sagernet/sing-box/adapter/service"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/dns"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+func RegisterService(registry *boxService.Registry) {
+	boxService.Register[option.ResolvedServiceOptions](registry, C.TypeResolved, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedServiceOptions) (adapter.Service, error) {
+		return nil, E.New("resolved service is only supported on Linux")
+	})
+}
+
+func RegisterTransport(registry *dns.TransportRegistry) {
+	dns.RegisterTransport[option.ResolvedDNSServerOptions](registry, C.TypeResolved, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedDNSServerOptions) (adapter.DNSTransport, error) {
+		return nil, E.New("resolved DNS server is only supported on Linux")
+	})
+}

+ 297 - 0
service/resolved/transport.go

@@ -0,0 +1,297 @@
+//go:build linux
+
+package resolved
+
+import (
+	"context"
+	"net/netip"
+	"os"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"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"
+	"github.com/sagernet/sing-box/dns/transport"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/logger"
+	M "github.com/sagernet/sing/common/metadata"
+	"github.com/sagernet/sing/service"
+
+	mDNS "github.com/miekg/dns"
+)
+
+func RegisterTransport(registry *dns.TransportRegistry) {
+	dns.RegisterTransport[option.ResolvedDNSServerOptions](registry, C.TypeResolved, NewTransport)
+}
+
+var _ adapter.DNSTransport = (*Transport)(nil)
+
+type Transport struct {
+	dns.TransportAdapter
+	ctx                    context.Context
+	logger                 logger.ContextLogger
+	serviceTag             string
+	acceptDefaultResolvers bool
+	ndots                  int
+	timeout                time.Duration
+	attempts               int
+	rotate                 bool
+	service                *Service
+	linkAccess             sync.RWMutex
+	linkServers            map[*TransportLink]*LinkServers
+}
+
+type LinkServers struct {
+	Link         *TransportLink
+	Servers      []adapter.DNSTransport
+	serverOffset uint32
+}
+
+func (c *LinkServers) ServerOffset(rotate bool) uint32 {
+	if rotate {
+		return atomic.AddUint32(&c.serverOffset, 1) - 1
+	}
+	return 0
+}
+
+func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedDNSServerOptions) (adapter.DNSTransport, error) {
+	return &Transport{
+		TransportAdapter:       dns.NewTransportAdapter(C.DNSTypeDHCP, tag, nil),
+		ctx:                    ctx,
+		logger:                 logger,
+		serviceTag:             options.Service,
+		acceptDefaultResolvers: options.AcceptDefaultResolvers,
+		// ndots:                  options.NDots,
+		// timeout:                time.Duration(options.Timeout),
+		// attempts:               options.Attempts,
+		// rotate:                 options.Rotate,
+		ndots:       1,
+		timeout:     5 * time.Second,
+		attempts:    2,
+		linkServers: make(map[*TransportLink]*LinkServers),
+	}, nil
+}
+
+func (t *Transport) Start(stage adapter.StartStage) error {
+	if stage != adapter.StartStateInitialize {
+		return nil
+	}
+	serviceManager := service.FromContext[adapter.ServiceManager](t.ctx)
+	service, loaded := serviceManager.Get(t.serviceTag)
+	if !loaded {
+		return E.New("service not found: ", t.serviceTag)
+	}
+	resolvedInbound, isResolved := service.(*Service)
+	if !isResolved {
+		return E.New("service is not resolved: ", t.serviceTag)
+	}
+	resolvedInbound.updateCallback = t.updateTransports
+	resolvedInbound.deleteCallback = t.deleteTransport
+	t.service = resolvedInbound
+	return nil
+}
+
+func (t *Transport) Close() error {
+	t.linkAccess.RLock()
+	defer t.linkAccess.RUnlock()
+	for _, servers := range t.linkServers {
+		for _, server := range servers.Servers {
+			server.Close()
+		}
+	}
+	return nil
+}
+
+func (t *Transport) updateTransports(link *TransportLink) error {
+	t.linkAccess.Lock()
+	defer t.linkAccess.Unlock()
+	if servers, loaded := t.linkServers[link]; loaded {
+		for _, server := range servers.Servers {
+			server.Close()
+		}
+	}
+	serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{
+		BindInterface:      link.iif.Name,
+		UDPFragmentDefault: true,
+	}))
+	var transports []adapter.DNSTransport
+	for _, address := range link.address {
+		serverAddr, ok := netip.AddrFromSlice(address.Address)
+		if !ok {
+			return os.ErrInvalid
+		}
+		if link.dnsOverTLS {
+			tlsConfig := common.Must1(tls.NewClient(t.ctx, serverAddr.String(), option.OutboundTLSOptions{
+				Enabled:    true,
+				ServerName: serverAddr.String(),
+			}))
+			transports = append(transports, transport.NewTLSRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, 53), tlsConfig))
+
+		} else {
+			transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, 53)))
+		}
+	}
+	for _, address := range link.addressEx {
+		serverAddr, ok := netip.AddrFromSlice(address.Address)
+		if !ok {
+			return os.ErrInvalid
+		}
+		if link.dnsOverTLS {
+			var serverName string
+			if address.Name != "" {
+				serverName = address.Name
+			} else {
+				serverName = serverAddr.String()
+			}
+			tlsConfig := common.Must1(tls.NewClient(t.ctx, serverAddr.String(), option.OutboundTLSOptions{
+				Enabled:    true,
+				ServerName: serverName,
+			}))
+			transports = append(transports, transport.NewTLSRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, address.Port), tlsConfig))
+
+		} else {
+			transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, address.Port)))
+		}
+	}
+	t.linkServers[link] = &LinkServers{
+		Link:    link,
+		Servers: transports,
+	}
+	return nil
+}
+
+func (t *Transport) deleteTransport(link *TransportLink) {
+	t.linkAccess.Lock()
+	defer t.linkAccess.Unlock()
+	servers, loaded := t.linkServers[link]
+	if !loaded {
+		return
+	}
+	for _, server := range servers.Servers {
+		server.Close()
+	}
+	delete(t.linkServers, link)
+}
+
+func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
+	question := message.Question[0]
+	var selectedLink *TransportLink
+	t.service.linkAccess.RLock()
+	for _, link := range t.service.links {
+		for _, domain := range link.domain {
+			if domain.Domain == "." && domain.RoutingOnly && !t.acceptDefaultResolvers {
+				continue
+			}
+			if strings.HasSuffix(question.Name, domain.Domain) {
+				selectedLink = link
+			}
+		}
+	}
+	if selectedLink == nil && t.acceptDefaultResolvers {
+		for l := len(t.service.defaultRouteSequence); l > 0; l-- {
+			selectedLink = t.service.links[t.service.defaultRouteSequence[l-1]]
+			if len(selectedLink.address) > 0 || len(selectedLink.addressEx) > 0 {
+				break
+			}
+		}
+	}
+	t.service.linkAccess.RUnlock()
+	if selectedLink == nil {
+		return dns.FixedResponseStatus(message, mDNS.RcodeNameError), nil
+	}
+	t.linkAccess.RLock()
+	servers := t.linkServers[selectedLink]
+	t.linkAccess.RUnlock()
+	if len(servers.Servers) == 0 {
+		return dns.FixedResponseStatus(message, mDNS.RcodeNameError), nil
+	}
+	if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
+		return t.exchangeParallel(ctx, servers, message)
+	} else {
+		return t.exchangeSingleRequest(ctx, servers, message)
+	}
+}
+
+func (t *Transport) exchangeSingleRequest(ctx context.Context, servers *LinkServers, message *mDNS.Msg) (*mDNS.Msg, error) {
+	var lastErr error
+	for _, fqdn := range servers.Link.nameList(t.ndots, message.Question[0].Name) {
+		response, err := t.tryOneName(ctx, servers, message, fqdn)
+		if err != nil {
+			lastErr = err
+			continue
+		}
+		return response, nil
+	}
+	return nil, lastErr
+}
+
+func (t *Transport) tryOneName(ctx context.Context, servers *LinkServers, message *mDNS.Msg, fqdn string) (*mDNS.Msg, error) {
+	serverOffset := servers.ServerOffset(t.rotate)
+	sLen := uint32(len(servers.Servers))
+	var lastErr error
+	for i := 0; i < t.attempts; i++ {
+		for j := uint32(0); j < sLen; j++ {
+			server := servers.Servers[(serverOffset+j)%sLen]
+			question := message.Question[0]
+			question.Name = fqdn
+			exchangeMessage := *message
+			exchangeMessage.Question = []mDNS.Question{question}
+			exchangeCtx, cancel := context.WithTimeout(ctx, t.timeout)
+			response, err := server.Exchange(exchangeCtx, &exchangeMessage)
+			cancel()
+			if err != nil {
+				lastErr = err
+				continue
+			}
+			return response, nil
+		}
+	}
+	return nil, E.Cause(lastErr, fqdn)
+}
+
+func (t *Transport) exchangeParallel(ctx context.Context, servers *LinkServers, message *mDNS.Msg) (*mDNS.Msg, error) {
+	returned := make(chan struct{})
+	defer close(returned)
+	type queryResult struct {
+		response *mDNS.Msg
+		err      error
+	}
+	results := make(chan queryResult)
+	startRacer := func(ctx context.Context, fqdn string) {
+		response, err := t.tryOneName(ctx, servers, message, fqdn)
+		select {
+		case results <- queryResult{response, err}:
+		case <-returned:
+		}
+	}
+	queryCtx, queryCancel := context.WithCancel(ctx)
+	defer queryCancel()
+	var nameCount int
+	for _, fqdn := range servers.Link.nameList(t.ndots, message.Question[0].Name) {
+		nameCount++
+		go startRacer(queryCtx, fqdn)
+	}
+	var errors []error
+	for {
+		select {
+		case <-ctx.Done():
+			return nil, ctx.Err()
+		case result := <-results:
+			if result.err == nil {
+				return result.response, nil
+			}
+			errors = append(errors, result.err)
+			if len(errors) == nameCount {
+				return nil, E.Errors(errors...)
+			}
+		}
+	}
+}