From 470da923b34a8c2a514b552129e89fc8d1e797c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 30 Mar 2025 23:34:15 +0800 Subject: [PATCH] Add resolved service and DNS server --- .fpm | 3 + .goreleaser.fury.yaml | 6 + .goreleaser.yaml | 6 + adapter/dns.go | 11 +- adapter/inbound/manager.go | 5 +- adapter/service/manager.go | 5 +- box.go | 6 +- constant/proxy.go | 1 + dns/client.go | 59 ++- dns/router.go | 5 + dns/transport/tls.go | 10 +- include/registry.go | 9 +- option/resolved.go | 49 ++ release/config/sing-box-split-dns.xml | 15 + release/config/sing-box.rules | 8 + release/config/sing-box.service | 2 + release/config/sing-box.sysusers | 1 + release/config/sing-box@.service | 2 + route/dns.go | 15 +- route/route.go | 32 +- service/resolved/resolve1.go | 625 ++++++++++++++++++++++++++ service/resolved/service.go | 247 ++++++++++ service/resolved/transport.go | 278 ++++++++++++ 23 files changed, 1342 insertions(+), 58 deletions(-) create mode 100644 option/resolved.go create mode 100644 release/config/sing-box-split-dns.xml create mode 100644 release/config/sing-box.rules create mode 100644 release/config/sing-box.sysusers create mode 100644 service/resolved/resolve1.go create mode 100644 service/resolved/service.go create mode 100644 service/resolved/transport.go diff --git a/.fpm b/.fpm index 718244b2..757b1081 100644 --- a/.fpm +++ b/.fpm @@ -11,6 +11,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.sysusers=/usr/lib/sysusers.d/sing-box.conf +release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules +release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish diff --git a/.goreleaser.fury.yaml b/.goreleaser.fury.yaml index beef42ec..f3f3cb9b 100644 --- a/.goreleaser.fury.yaml +++ b/.goreleaser.fury.yaml @@ -56,6 +56,12 @@ nfpms: dst: /usr/lib/systemd/system/sing-box.service - src: release/config/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 dst: /usr/share/bash-completion/completions/sing-box.bash diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 4a7efcf4..eba5dcd9 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -138,6 +138,12 @@ nfpms: dst: /usr/lib/systemd/system/sing-box.service - src: release/config/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 dst: /usr/share/bash-completion/completions/sing-box.bash diff --git a/adapter/dns.go b/adapter/dns.go index 73b77601..4e79d657 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -33,11 +33,12 @@ type DNSClient interface { } type DNSQueryOptions struct { - Transport DNSTransport - Strategy C.DomainStrategy - DisableCache bool - RewriteTTL *uint32 - ClientSubnet netip.Prefix + Transport DNSTransport + Strategy C.DomainStrategy + LookupStrategy C.DomainStrategy + DisableCache bool + RewriteTTL *uint32 + ClientSubnet netip.Prefix } func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) { diff --git a/adapter/inbound/manager.go b/adapter/inbound/manager.go index c690b2c9..89e424ac 100644 --- a/adapter/inbound/manager.go +++ b/adapter/inbound/manager.go @@ -37,13 +37,14 @@ func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endp func (m *Manager) Start(stage adapter.StartStage) error { m.access.Lock() - defer m.access.Unlock() if m.started && m.stage >= stage { panic("already started") } m.started = true m.stage = stage - for _, inbound := range m.inbounds { + inbounds := m.inbounds + m.access.Unlock() + for _, inbound := range inbounds { err := adapter.LegacyStart(inbound, stage) if err != nil { return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]") diff --git a/adapter/service/manager.go b/adapter/service/manager.go index 3ef787cb..d58b1a77 100644 --- a/adapter/service/manager.go +++ b/adapter/service/manager.go @@ -35,13 +35,14 @@ func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Man func (m *Manager) Start(stage adapter.StartStage) error { m.access.Lock() - defer m.access.Unlock() if m.started && m.stage >= stage { panic("already started") } m.started = true m.stage = stage - for _, service := range m.services { + services := m.services + m.access.Unlock() + for _, service := range services { err := adapter.LegacyStart(service, stage) if err != nil { return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]") diff --git a/box.go b/box.go index 9e6257f1..60abee74 100644 --- a/box.go +++ b/box.go @@ -467,11 +467,7 @@ func (s *Box) start() error { if err != nil { return err } - err = s.inbound.Start(adapter.StartStateStart) - if err != nil { - return err - } - err = adapter.Start(adapter.StartStateStart, s.endpoint) + err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service) if err != nil { return err } diff --git a/constant/proxy.go b/constant/proxy.go index 40112d6e..742360ea 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -27,6 +27,7 @@ const ( TypeTailscale = "tailscale" TypeDERP = "derp" TypeDERPSTUN = "derp-stun" + TypeResolved = "resolved" ) const ( diff --git a/dns/client.go b/dns/client.go index f456f878..f6985c46 100644 --- a/dns/client.go +++ b/dns/client.go @@ -243,9 +243,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) { domain = FqdnToDomain(domain) dnsName := dns.Fqdn(domain) - if options.Strategy == C.DomainStrategyIPv4Only { + var strategy C.DomainStrategy + if options.LookupStrategy != C.DomainStrategyAsIS { + strategy = options.LookupStrategy + } else { + strategy = options.Strategy + } + if strategy == C.DomainStrategyIPv4Only { return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker) - } else if options.Strategy == C.DomainStrategyIPv6Only { + } else if strategy == C.DomainStrategyIPv6Only { return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker) } var response4 []netip.Addr @@ -271,7 +277,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom if len(response4) == 0 && len(response6) == 0 { return nil, err } - return sortAddresses(response4, response6, options.Strategy), nil + return sortAddresses(response4, response6, strategy), nil } func (c *Client) ClearCache() { @@ -527,12 +533,26 @@ func transportTagFromContext(ctx context.Context) (string, bool) { return value, loaded } +func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg { + return &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: message.Id, + Rcode: rcode, + Response: true, + }, + Question: message.Question, + } +} + func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg { response := dns.Msg{ MsgHdr: dns.MsgHdr{ - Id: id, - Rcode: dns.RcodeSuccess, - Response: true, + Id: id, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: dns.RcodeSuccess, }, Question: []dns.Question{question}, } @@ -565,9 +585,12 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg { response := dns.Msg{ MsgHdr: dns.MsgHdr{ - Id: id, - Rcode: dns.RcodeSuccess, - Response: true, + Id: id, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: dns.RcodeSuccess, }, Question: []dns.Question{question}, Answer: []dns.RR{ @@ -588,9 +611,12 @@ func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToL func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg { response := dns.Msg{ MsgHdr: dns.MsgHdr{ - Id: id, - Rcode: dns.RcodeSuccess, - Response: true, + Id: id, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: dns.RcodeSuccess, }, Question: []dns.Question{question}, Answer: []dns.RR{ @@ -611,9 +637,12 @@ func FixedResponseTXT(id uint16, question dns.Question, records []string, timeTo func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg { response := dns.Msg{ MsgHdr: dns.MsgHdr{ - Id: id, - Rcode: dns.RcodeSuccess, - Response: true, + Id: id, + Response: true, + Authoritative: true, + RecursionDesired: true, + RecursionAvailable: true, + Rcode: dns.RcodeSuccess, }, Question: []dns.Question{question}, } diff --git a/dns/router.go b/dns/router.go index 44edadbd..92aa3cce 100644 --- a/dns/router.go +++ b/dns/router.go @@ -285,7 +285,12 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte } else if errors.Is(err, ErrResponseRejected) { rejected = true r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String()))) + /*} else if responseCheck!= nil && errors.Is(err, RcodeError(mDNS.RcodeNameError)) { + rejected = true + r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String()))) + */ } else if len(message.Question) > 0 { + rejected = true r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String()))) } else { r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ")) diff --git a/dns/transport/tls.go b/dns/transport/tls.go index a99bf2f7..f61a895c 100644 --- a/dns/transport/tls.go +++ b/dns/transport/tls.go @@ -57,13 +57,17 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o if serverAddr.Port == 0 { serverAddr.Port = 853 } + return NewTLSRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions), transportDialer, serverAddr, tlsConfig), nil +} + +func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport { return &TLSTransport{ - TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions), + TransportAdapter: adapter, logger: logger, - dialer: transportDialer, + dialer: dialer, serverAddr: serverAddr, tlsConfig: tlsConfig, - }, nil + } } func (t *TLSTransport) Start(stage adapter.StartStage) error { diff --git a/include/registry.go b/include/registry.go index 9706a9bb..11069d5a 100644 --- a/include/registry.go +++ b/include/registry.go @@ -34,6 +34,7 @@ import ( "github.com/sagernet/sing-box/protocol/tun" "github.com/sagernet/sing-box/protocol/vless" "github.com/sagernet/sing-box/protocol/vmess" + "github.com/sagernet/sing-box/service/resolved" E "github.com/sagernet/sing/common/exceptions" ) @@ -111,6 +112,7 @@ func DNSTransportRegistry() *dns.TransportRegistry { hosts.RegisterTransport(registry) local.RegisterTransport(registry) fakeip.RegisterTransport(registry) + resolved.RegisterTransport(registry) registerQUICTransports(registry) registerDHCPTransport(registry) @@ -122,16 +124,13 @@ func DNSTransportRegistry() *dns.TransportRegistry { func ServiceRegistry() *service.Registry { registry := service.NewRegistry() + resolved.RegisterService(registry) + registerDERPService(registry) return registry } -func ServiceRegistry() *service.Registry { - registry := service.NewRegistry() - return registry -} - func registerStubForRemovedInbounds(registry *inbound.Registry) { inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0") diff --git a/option/resolved.go b/option/resolved.go new file mode 100644 index 00000000..5121f09d --- /dev/null +++ b/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 SplitDNSServerOptions 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"` +} diff --git a/release/config/sing-box-split-dns.xml b/release/config/sing-box-split-dns.xml new file mode 100644 index 00000000..77f5d96a --- /dev/null +++ b/release/config/sing-box-split-dns.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/release/config/sing-box.rules b/release/config/sing-box.rules new file mode 100644 index 00000000..668b2640 --- /dev/null +++ b/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; + } +}); diff --git a/release/config/sing-box.service b/release/config/sing-box.service index 388d7250..f003f844 100644 --- a/release/config/sing-box.service +++ b/release/config/sing-box.service @@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org After=network.target nss-lookup.target network-online.target [Service] +User=sing-box +StateDirectory=sing-box CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH ExecStart=/usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run diff --git a/release/config/sing-box.sysusers b/release/config/sing-box.sysusers new file mode 100644 index 00000000..ad98c100 --- /dev/null +++ b/release/config/sing-box.sysusers @@ -0,0 +1 @@ +u! sing-box - "sing-box Service" diff --git a/release/config/sing-box@.service b/release/config/sing-box@.service index 38866457..726bbee7 100644 --- a/release/config/sing-box@.service +++ b/release/config/sing-box@.service @@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org After=network.target nss-lookup.target network-online.target [Service] +User=sing-box +StateDirectory=sing-box-%i CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH ExecStart=/usr/bin/sing-box -D /var/lib/sing-box-%i -c /etc/sing-box/%i.json run diff --git a/route/dns.go b/route/dns.go index 7d2b5778..80f271fe 100644 --- a/route/dns.go +++ b/route/dns.go @@ -27,12 +27,16 @@ func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata ad conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) err := dnsOutbound.HandleStreamDNSRequest(ctx, r.dns, conn, metadata) if err != nil { - return err + if !E.IsClosedOrCanceled(err) { + return err + } else { + return nil + } } } } -func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext) { +func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext) error { if natConn, isNatConn := conn.(udpnat.Conn); isNatConn { metadata.Destination = M.Socksaddr{} for _, packet := range packetBuffers { @@ -48,18 +52,19 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB ctx: ctx, metadata: metadata, }) - return + return nil } err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata) if err != nil && !E.IsClosedOrCanceled(err) { - r.logger.ErrorContext(ctx, E.Cause(err, "process DNS packet connection")) + return E.Cause(err, "process DNS packet") } + return nil } func ExchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, logger logger.ContextLogger, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) { err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination) if err != nil && !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")) } } diff --git a/route/route.go b/route/route.go index 531ad039..b1822b16 100644 --- a/route/route.go +++ b/route/route.go @@ -6,7 +6,6 @@ import ( "net" "net/netip" "os" - "os/user" "strings" "time" @@ -61,6 +60,8 @@ func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { if r.pauseManager.IsDevicePaused() { return E.New("reject connection to ", metadata.Destination, " while device paused") + } else if metadata.InboundType == C.TypeResolved { + return r.hijackDNSStream(ctx, conn, metadata) } //nolint:staticcheck @@ -117,14 +118,12 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad } case *rule.RuleActionReject: buf.ReleaseMulti(buffers) - N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) - return nil + return action.Error(ctx) case *rule.RuleActionHijackDNS: for _, buffer := range buffers { conn = bufio.NewCachedConn(conn, buffer) } - r.hijackDNSStream(ctx, conn, metadata) - return nil + return r.hijackDNSStream(ctx, conn, metadata) } } if selectedRule == nil { @@ -187,6 +186,8 @@ func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { if r.pauseManager.IsDevicePaused() { return E.New("reject packet connection to ", metadata.Destination, " while device paused") + } else if metadata.InboundType == C.TypeResolved { + return r.hijackDNSPacket(ctx, conn, nil, metadata) } //nolint:staticcheck if metadata.InboundDetour != "" { @@ -238,11 +239,10 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m } case *rule.RuleActionReject: N.ReleaseMultiPacketBuffer(packetBuffers) - N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) - return nil + return action.Error(ctx) case *rule.RuleActionHijackDNS: - r.hijackDNSPacket(ctx, conn, packetBuffers, metadata) - return nil + return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata) + } } if selectedRule == nil || selectReturn { @@ -305,16 +305,16 @@ func (r *Router) matchRule( r.logger.InfoContext(ctx, "failed to search process: ", fErr) } else { if processInfo.ProcessPath != "" { - r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath) + if processInfo.User != "" { + r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.User) + } else if processInfo.UserId != -1 { + r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId) + } else { + r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath) + } } else if processInfo.PackageName != "" { r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName) } else if processInfo.UserId != -1 { - if /*needUserName &&*/ true { - osUser, _ := user.LookupId(F.ToString(processInfo.UserId)) - if osUser != nil { - processInfo.User = osUser.Username - } - } if processInfo.User != "" { r.logger.InfoContext(ctx, "found user: ", processInfo.User) } else { diff --git a/service/resolved/resolve1.go b/service/resolved/resolve1.go new file mode 100644 index 00000000..c76be9df --- /dev/null +++ b/service/resolved/resolve1.go @@ -0,0 +1,625 @@ +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) 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.logRequest(sender, "SetLinkDNS ", link.iif.Name, " ", strings.Join(common.Map(addresses, func(it LinkDNS) string { + return M.AddrFromIP(it.Address).String() + }), ", ")) + } else { + t.logRequest(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.logRequest(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.logRequest(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.logRequest(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.logRequest(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 + t.defaultRouteSequence = append(t.defaultRouteSequence, ifIndex) + var defaultRouteString string + if defaultRoute { + defaultRouteString = "yes" + } else { + defaultRouteString = "no" + } + t.logRequest(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.logRequest(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.logRequest(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.logRequest(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) +} diff --git a/service/resolved/service.go b/service/resolved/service.go new file mode 100644 index 00000000..b2c696e3 --- /dev/null +++ b/service/resolved/service.go @@ -0,0 +1,247 @@ +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.Mutex + 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 + 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) + } + err = systemBus.Export((*resolve1Manager)(i), "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager") + if err != nil { + return err + } + 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)) + } + 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") +} diff --git a/service/resolved/transport.go b/service/resolved/transport.go new file mode 100644 index 00000000..158d3f46 --- /dev/null +++ b/service/resolved/transport.go @@ -0,0 +1,278 @@ +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.SplitDNSServerOptions](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.SplitDNSServerOptions) (adapter.DNSTransport, error) { + if !C.IsLinux { + return nil, E.New("split DNS server is only supported on Linux") + } + 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 + 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) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + question := message.Question[0] + var selectedLink *TransportLink + for _, link := range t.service.links { + for _, domain := range link.domain { + if strings.HasSuffix(question.Name, domain.Domain) { + selectedLink = link + } + } + } + if selectedLink == nil && t.acceptDefaultResolvers { + for _, link := range t.service.links { + if link.defaultRoute { + selectedLink = link + } + } + } + if selectedLink == nil { + t.logger.DebugContext(ctx, "missing selected interface") + return dns.FixedResponseStatus(message, mDNS.RcodeNameError), nil + } + servers := t.linkServers[selectedLink] + if len(servers.Servers) == 0 { + t.logger.DebugContext(ctx, "missing DNS servers") + 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...) + } + } + } +}