浏览代码

Add resolved service and DNS server

世界 6 月之前
父节点
当前提交
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/sing-box.service=/usr/lib/systemd/system/sing-box.service
 release/config/[email protected]=/usr/lib/systemd/system/[email protected]
 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.bash=/usr/share/bash-completion/completions/sing-box.bash
 release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
 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
         dst: /usr/lib/systemd/system/sing-box.service
       - src: release/config/[email protected]
       - src: release/config/[email protected]
         dst: /usr/lib/systemd/system/[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
       - src: release/completions/sing-box.bash
         dst: /usr/share/bash-completion/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
         dst: /usr/lib/systemd/system/sing-box.service
       - src: release/config/[email protected]
       - src: release/config/[email protected]
         dst: /usr/lib/systemd/system/[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
       - src: release/completions/sing-box.bash
         dst: /usr/share/bash-completion/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 {
 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) {
 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 {
 func (m *Manager) Start(stage adapter.StartStage) error {
 	m.access.Lock()
 	m.access.Lock()
-	defer m.access.Unlock()
 	if m.started && m.stage >= stage {
 	if m.started && m.stage >= stage {
 		panic("already started")
 		panic("already started")
 	}
 	}
 	m.started = true
 	m.started = true
 	m.stage = stage
 	m.stage = stage
-	for _, inbound := range m.inbounds {
+	inbounds := m.inbounds
+	m.access.Unlock()
+	for _, inbound := range inbounds {
 		err := adapter.LegacyStart(inbound, stage)
 		err := adapter.LegacyStart(inbound, stage)
 		if err != nil {
 		if err != nil {
 			return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
 			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 {
 func (m *Manager) Start(stage adapter.StartStage) error {
 	m.access.Lock()
 	m.access.Lock()
-	defer m.access.Unlock()
 	if m.started && m.stage >= stage {
 	if m.started && m.stage >= stage {
 		panic("already started")
 		panic("already started")
 	}
 	}
 	m.started = true
 	m.started = true
 	m.stage = stage
 	m.stage = stage
-	for _, service := range m.services {
+	services := m.services
+	m.access.Unlock()
+	for _, service := range services {
 		err := adapter.LegacyStart(service, stage)
 		err := adapter.LegacyStart(service, stage)
 		if err != nil {
 		if err != nil {
 			return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
 			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 {
 	if err != nil {
 		return err
 		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 {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 1 - 0
constant/proxy.go

@@ -26,6 +26,7 @@ const (
 	TypeHysteria2    = "hysteria2"
 	TypeHysteria2    = "hysteria2"
 	TypeTailscale    = "tailscale"
 	TypeTailscale    = "tailscale"
 	TypeDERP         = "derp"
 	TypeDERP         = "derp"
+	TypeResolved     = "resolved"
 )
 )
 
 
 const (
 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) {
 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)
 	domain = FqdnToDomain(domain)
 	dnsName := dns.Fqdn(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)
 		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)
 		return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
 	}
 	}
 	var response4 []netip.Addr
 	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 {
 	if len(response4) == 0 && len(response6) == 0 {
 		return nil, err
 		return nil, err
 	}
 	}
-	return sortAddresses(response4, response6, options.Strategy), nil
+	return sortAddresses(response4, response6, strategy), nil
 }
 }
 
 
 func (c *Client) ClearCache() {
 func (c *Client) ClearCache() {
@@ -537,12 +543,26 @@ func transportTagFromContext(ctx context.Context) (string, bool) {
 	return value, loaded
 	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 {
 func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg {
 	response := dns.Msg{
 	response := dns.Msg{
 		MsgHdr: dns.MsgHdr{
 		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},
 		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 {
 func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg {
 	response := dns.Msg{
 	response := dns.Msg{
 		MsgHdr: dns.MsgHdr{
 		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},
 		Question: []dns.Question{question},
 		Answer: []dns.RR{
 		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 {
 func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg {
 	response := dns.Msg{
 	response := dns.Msg{
 		MsgHdr: dns.MsgHdr{
 		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},
 		Question: []dns.Question{question},
 		Answer: []dns.RR{
 		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 {
 func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg {
 	response := dns.Msg{
 	response := dns.Msg{
 		MsgHdr: dns.MsgHdr{
 		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},
 		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) {
 					} else if errors.Is(err, ErrResponseRejected) {
 						rejected = true
 						rejected = true
 						r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
 						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 {
 					} else if len(message.Question) > 0 {
+						rejected = true
 						r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
 						r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
 					} else {
 					} else {
 						r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
 						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() {
 	if !serverAddr.IsValid() {
 		return nil, E.New("invalid server address: ", serverAddr)
 		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{
 	return &TLSTransport{
-		TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions),
+		TransportAdapter: adapter,
 		logger:           logger,
 		logger:           logger,
-		dialer:           transportDialer,
+		dialer:           dialer,
 		serverAddr:       serverAddr,
 		serverAddr:       serverAddr,
 		tlsConfig:        tlsConfig,
 		tlsConfig:        tlsConfig,
-	}, nil
+	}
 }
 }
 
 
 func (t *TLSTransport) Start(stage adapter.StartStage) error {
 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/)           |
 | `dhcp`          | [DHCP](./dhcp/)           |
 | `fakeip`        | [Fake IP](./fakeip/)      |
 | `fakeip`        | [Fake IP](./fakeip/)      |
 | `tailscale`     | [Tailscale](./tailscale/) |
 | `tailscale`     | [Tailscale](./tailscale/) |
+| `resolved`      | [Resolved](./resolved/)   |
 
 
 #### tag
 #### tag
 
 

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

@@ -41,6 +41,7 @@ DNS 服务器的类型。
 | `dhcp`          | [DHCP](./dhcp/)           |
 | `dhcp`          | [DHCP](./dhcp/)           |
 | `fakeip`        | [Fake IP](./fakeip/)      |
 | `fakeip`        | [Fake IP](./fakeip/)      |
 | `tailscale`     | [Tailscale](./tailscale/) |
 | `tailscale`     | [Tailscale](./tailscale/) |
+| `resolved`      | [Resolved](./resolved/)   |
 
 
 #### tag
 #### 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==
 ==Required==
 
 
-The tag of the Tailscale endpoint.
+The tag of the [Tailscale Endpoint](/configuration/endpoint/tailscale).
 
 
 #### accept_default_resolvers
 #### accept_default_resolvers
 
 
 Indicates whether default DNS resolvers should be accepted for fallback queries in addition to MagicDNS。
 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
 ### 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
 ### Fields
 
 
-| Type        | Format                   |
-|-------------|--------------------------|
-| `derp`      | [DERP](./derp)           |
+| Type       | Format                 |
+|------------|------------------------|
+| `derp`     | [DERP](./derp)         |
+| `resolved` | [Resolved](./resolved) |
 
 
 #### tag
 #### 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/cretz/bine v0.2.0
 	github.com/go-chi/chi/v5 v5.2.1
 	github.com/go-chi/chi/v5 v5.2.1
 	github.com/go-chi/render v1.0.3
 	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/gofrs/uuid/v5 v5.3.2
 	github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f
 	github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f
 	github.com/libdns/alidns v1.0.4-libdns.v1.beta1
 	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/go-ole/go-ole v1.3.0 // indirect
 	github.com/gobwas/httphead v0.1.0 // indirect
 	github.com/gobwas/httphead v0.1.0 // indirect
 	github.com/gobwas/pool v0.2.1 // 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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/go-cmp v0.6.0 // 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/tun"
 	"github.com/sagernet/sing-box/protocol/vless"
 	"github.com/sagernet/sing-box/protocol/vless"
 	"github.com/sagernet/sing-box/protocol/vmess"
 	"github.com/sagernet/sing-box/protocol/vmess"
+	"github.com/sagernet/sing-box/service/resolved"
 	E "github.com/sagernet/sing/common/exceptions"
 	E "github.com/sagernet/sing/common/exceptions"
 )
 )
 
 
@@ -111,6 +112,7 @@ func DNSTransportRegistry() *dns.TransportRegistry {
 	hosts.RegisterTransport(registry)
 	hosts.RegisterTransport(registry)
 	local.RegisterTransport(registry)
 	local.RegisterTransport(registry)
 	fakeip.RegisterTransport(registry)
 	fakeip.RegisterTransport(registry)
+	resolved.RegisterTransport(registry)
 
 
 	registerQUICTransports(registry)
 	registerQUICTransports(registry)
 	registerDHCPTransport(registry)
 	registerDHCPTransport(registry)
@@ -122,6 +124,8 @@ func DNSTransportRegistry() *dns.TransportRegistry {
 func ServiceRegistry() *service.Registry {
 func ServiceRegistry() *service.Registry {
 	registry := service.NewRegistry()
 	registry := service.NewRegistry()
 
 
+	resolved.RegisterService(registry)
+
 	registerDERPService(registry)
 	registerDERPService(registry)
 
 
 	return registry
 	return registry

+ 2 - 0
mkdocs.yml

@@ -94,6 +94,7 @@ nav:
               - DHCP: configuration/dns/server/dhcp.md
               - DHCP: configuration/dns/server/dhcp.md
               - FakeIP: configuration/dns/server/fakeip.md
               - FakeIP: configuration/dns/server/fakeip.md
               - Tailscale: configuration/dns/server/tailscale.md
               - Tailscale: configuration/dns/server/tailscale.md
+              - Resolved: configuration/dns/server/resolved.md
           - DNS Rule: configuration/dns/rule.md
           - DNS Rule: configuration/dns/rule.md
           - DNS Rule Action: configuration/dns/rule_action.md
           - DNS Rule Action: configuration/dns/rule_action.md
           - FakeIP: configuration/dns/fakeip.md
           - FakeIP: configuration/dns/fakeip.md
@@ -172,6 +173,7 @@ nav:
       - Service:
       - Service:
           - configuration/service/index.md
           - configuration/service/index.md
           - DERP: configuration/service/derp.md
           - DERP: configuration/service/derp.md
+          - Resolved: configuration/service/resolved.md
 markdown_extensions:
 markdown_extensions:
   - pymdownx.inlinehilite
   - pymdownx.inlinehilite
   - pymdownx.snippets
   - 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
 After=network.target nss-lookup.target network-online.target
 
 
 [Service]
 [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
 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
 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
 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
 After=network.target nss-lookup.target network-online.target
 
 
 [Service]
 [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
 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
 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
 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))
 		conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
 		err := dnsOutbound.HandleStreamDNSRequest(ctx, r.dns, conn, metadata)
 		err := dnsOutbound.HandleStreamDNSRequest(ctx, r.dns, conn, metadata)
 		if err != nil {
 		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 {
 	if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
 		metadata.Destination = M.Socksaddr{}
 		metadata.Destination = M.Socksaddr{}
 		for _, packet := range packetBuffers {
 		for _, packet := range packetBuffers {
@@ -48,19 +52,20 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
 			metadata: metadata,
 			metadata: metadata,
 			onClose:  onClose,
 			onClose:  onClose,
 		})
 		})
-		return
+		return nil
 	}
 	}
 	err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata)
 	err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	N.CloseOnHandshakeFailure(conn, onClose, err)
 	if err != nil && !E.IsClosedOrCanceled(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) {
 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)
 	err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination)
 	if err != nil && !R.IsRejected(err) && !E.IsClosedOrCanceled(err) {
 	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"
 	"net/netip"
 	"net/netip"
 	"os"
 	"os"
-	"os/user"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -113,8 +112,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
 			}
 			}
 		case *R.RuleActionReject:
 		case *R.RuleActionReject:
 			buf.ReleaseMulti(buffers)
 			buf.ReleaseMulti(buffers)
-			N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
-			return nil
+			return action.Error(ctx)
 		case *R.RuleActionHijackDNS:
 		case *R.RuleActionHijackDNS:
 			for _, buffer := range buffers {
 			for _, buffer := range buffers {
 				conn = bufio.NewCachedConn(conn, buffer)
 				conn = bufio.NewCachedConn(conn, buffer)
@@ -229,11 +227,9 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
 			}
 			}
 		case *R.RuleActionReject:
 		case *R.RuleActionReject:
 			N.ReleaseMultiPacketBuffer(packetBuffers)
 			N.ReleaseMultiPacketBuffer(packetBuffers)
-			N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
-			return nil
+			return action.Error(ctx)
 		case *R.RuleActionHijackDNS:
 		case *R.RuleActionHijackDNS:
-			r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
-			return nil
+			return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
 		}
 		}
 	}
 	}
 	if selectedRule == nil || selectReturn {
 	if selectedRule == nil || selectReturn {
@@ -296,16 +292,16 @@ func (r *Router) matchRule(
 			r.logger.InfoContext(ctx, "failed to search process: ", fErr)
 			r.logger.InfoContext(ctx, "failed to search process: ", fErr)
 		} else {
 		} else {
 			if processInfo.ProcessPath != "" {
 			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 != "" {
 			} else if processInfo.PackageName != "" {
 				r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName)
 				r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName)
 			} else if processInfo.UserId != -1 {
 			} 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 != "" {
 				if processInfo.User != "" {
 					r.logger.InfoContext(ctx, "found user: ", processInfo.User)
 					r.logger.InfoContext(ctx, "found user: ", processInfo.User)
 				} else {
 				} 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...)
+			}
+		}
+	}
+}