Add resolved service and DNS server

This commit is contained in:
世界 2025-03-30 23:34:15 +08:00
parent 1a8f6e053d
commit cb3c0829c5
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
32 changed files with 1550 additions and 58 deletions

View File

@ -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/sing-box@.service=/usr/lib/systemd/system/sing-box@.service release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
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

View File

@ -55,6 +55,12 @@ nfpms:
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service - src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service dst: /usr/lib/systemd/system/sing-box@.service
- 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

View File

@ -136,6 +136,12 @@ nfpms:
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service - src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service dst: /usr/lib/systemd/system/sing-box@.service
- 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

View File

@ -33,11 +33,12 @@ type DNSClient interface {
} }
type DNSQueryOptions struct { type DNSQueryOptions struct {
Transport DNSTransport Transport DNSTransport
Strategy C.DomainStrategy Strategy C.DomainStrategy
DisableCache bool LookupStrategy C.DomainStrategy
RewriteTTL *uint32 DisableCache bool
ClientSubnet netip.Prefix 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) {

View File

@ -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(), "]")

View File

@ -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(), "]")

6
box.go
View File

@ -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) err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStart, s.endpoint)
if err != nil { if err != nil {
return err return err
} }

View File

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

View File

@ -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, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
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, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
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, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
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, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
Response: true, Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
}, },
Question: []dns.Question{question}, Question: []dns.Question{question},
} }

View File

@ -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>"))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
go.mod
View File

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

View File

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

View File

@ -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
option/resolved.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,12 +27,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 {
@ -49,19 +53,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 && !errors.Is(err, tun.ErrDrop) && !E.IsClosedOrCanceled(err) { if err != nil && !errors.Is(err, tun.ErrDrop) && !E.IsClosedOrCanceled(err) {
logger.ErrorContext(ctx, E.Cause(err, "process DNS packet connection")) logger.ErrorContext(ctx, E.Cause(err, "process DNS packet"))
} }
} }

View File

@ -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 *rule.RuleActionReject: case *rule.RuleActionReject:
buf.ReleaseMulti(buffers) buf.ReleaseMulti(buffers)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) return action.Error(ctx)
return nil
case *rule.RuleActionHijackDNS: case *rule.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 *rule.RuleActionReject: case *rule.RuleActionReject:
N.ReleaseMultiPacketBuffer(packetBuffers) N.ReleaseMultiPacketBuffer(packetBuffers)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) return action.Error(ctx)
return nil
case *rule.RuleActionHijackDNS: case *rule.RuleActionHijackDNS:
r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose) return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
return nil
} }
} }
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 {

View File

@ -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
service/resolved/service.go Normal file
View File

@ -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
service/resolved/stub.go Normal file
View File

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

View File

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