mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-04-04 12:27:36 +03:00
Add resolved service and DNS server
This commit is contained in:
parent
9e6de24f15
commit
470da923b3
23 changed files with 1342 additions and 58 deletions
3
.fpm
3
.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@.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
|
||||||
|
|
|
@ -56,6 +56,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
|
||||||
|
|
|
@ -138,6 +138,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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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(), "]")
|
||||||
|
|
|
@ -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
6
box.go
|
@ -467,11 +467,7 @@ func (s *Box) start() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = s.inbound.Start(adapter.StartStateStart)
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ const (
|
||||||
TypeTailscale = "tailscale"
|
TypeTailscale = "tailscale"
|
||||||
TypeDERP = "derp"
|
TypeDERP = "derp"
|
||||||
TypeDERPSTUN = "derp-stun"
|
TypeDERPSTUN = "derp-stun"
|
||||||
|
TypeResolved = "resolved"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -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) {
|
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
|
||||||
|
@ -271,7 +277,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() {
|
||||||
|
@ -527,12 +533,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},
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
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{
|
||||||
|
@ -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 {
|
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{
|
||||||
|
@ -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 {
|
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},
|
||||||
}
|
}
|
||||||
|
|
|
@ -285,7 +285,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>"))
|
||||||
|
|
|
@ -57,13 +57,17 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
|
||||||
if serverAddr.Port == 0 {
|
if serverAddr.Port == 0 {
|
||||||
serverAddr.Port = 853
|
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{
|
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 {
|
||||||
|
|
|
@ -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,16 +124,13 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServiceRegistry() *service.Registry {
|
|
||||||
registry := service.NewRegistry()
|
|
||||||
return registry
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerStubForRemovedInbounds(registry *inbound.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) {
|
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")
|
return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")
|
||||||
|
|
49
option/resolved.go
Normal file
49
option/resolved.go
Normal 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 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"`
|
||||||
|
}
|
15
release/config/sing-box-split-dns.xml
Normal file
15
release/config/sing-box-split-dns.xml
Normal 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_prefix="org.freedesktop.resolve1"/>
|
||||||
|
<allow send_destination="org.freedesktop.resolve1"/>
|
||||||
|
<allow send_interface="org.freedesktop.resolve1.Manager"/>
|
||||||
|
</policy>
|
||||||
|
<policy user="sing-box">
|
||||||
|
<allow own_prefix="org.freedesktop.resolve1"/>
|
||||||
|
<allow send_destination="org.freedesktop.resolve1"/>
|
||||||
|
<allow send_interface="org.freedesktop.resolve1.Manager"/>
|
||||||
|
</policy>
|
||||||
|
</busconfig>
|
8
release/config/sing-box.rules
Normal file
8
release/config/sing-box.rules
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
|
@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org
|
||||||
After=network.target nss-lookup.target network-online.target
|
After=network.target nss-lookup.target network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
User=sing-box
|
||||||
|
StateDirectory=sing-box
|
||||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||||
ExecStart=/usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run
|
ExecStart=/usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run
|
||||||
|
|
1
release/config/sing-box.sysusers
Normal file
1
release/config/sing-box.sysusers
Normal file
|
@ -0,0 +1 @@
|
||||||
|
u! sing-box - "sing-box Service"
|
|
@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org
|
||||||
After=network.target nss-lookup.target network-online.target
|
After=network.target nss-lookup.target network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
User=sing-box
|
||||||
|
StateDirectory=sing-box-%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
|
||||||
|
|
15
route/dns.go
15
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))
|
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) {
|
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext) error {
|
||||||
if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
|
if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
|
||||||
metadata.Destination = M.Socksaddr{}
|
metadata.Destination = M.Socksaddr{}
|
||||||
for _, packet := range packetBuffers {
|
for _, packet := range packetBuffers {
|
||||||
|
@ -48,18 +52,19 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
})
|
})
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata)
|
err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata)
|
||||||
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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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 {
|
func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
||||||
if r.pauseManager.IsDevicePaused() {
|
if r.pauseManager.IsDevicePaused() {
|
||||||
return E.New("reject connection to ", metadata.Destination, " while device paused")
|
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
|
//nolint:staticcheck
|
||||||
|
@ -117,14 +118,12 @@ 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)
|
||||||
}
|
}
|
||||||
r.hijackDNSStream(ctx, conn, metadata)
|
return r.hijackDNSStream(ctx, conn, metadata)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if selectedRule == nil {
|
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 {
|
func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
||||||
if r.pauseManager.IsDevicePaused() {
|
if r.pauseManager.IsDevicePaused() {
|
||||||
return E.New("reject packet connection to ", metadata.Destination, " while device paused")
|
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
|
//nolint:staticcheck
|
||||||
if metadata.InboundDetour != "" {
|
if metadata.InboundDetour != "" {
|
||||||
|
@ -238,11 +239,10 @@ 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)
|
return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if selectedRule == nil || selectReturn {
|
if selectedRule == nil || selectReturn {
|
||||||
|
@ -305,16 +305,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 {
|
||||||
|
|
625
service/resolved/resolve1.go
Normal file
625
service/resolved/resolve1.go
Normal file
|
@ -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)
|
||||||
|
}
|
247
service/resolved/service.go
Normal file
247
service/resolved/service.go
Normal file
|
@ -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")
|
||||||
|
}
|
278
service/resolved/transport.go
Normal file
278
service/resolved/transport.go
Normal file
|
@ -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...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue