Add split DNS server

This commit is contained in:
世界 2025-03-30 23:34:15 +08:00
parent 2b18fc4886
commit ea20749a22
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
13 changed files with 381 additions and 0 deletions

3
.fpm
View file

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

View file

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

View file

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

View file

@ -28,6 +28,7 @@ const (
DNSTypeFakeIP = "fakeip"
DNSTypeDHCP = "dhcp"
DNSTypeTailscale = "tailscale"
DNSTypeSplitDNS = "split-dns"
)
const (

View file

@ -0,0 +1,140 @@
package split
import (
"net/netip"
"strings"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/godbus/dbus/v5"
)
type resolve1Manager Transport
type resolve1LinkNameserver struct {
Family int32
Address []byte
}
type resolve1LinkDomain struct {
Domain string
RoutingOnly bool
}
func (t *resolve1Manager) getLink(ifIndex uint32) (*TransportLink, 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, dbus.MakeFailedError(err)
}
link.iif = iif
}
return link, nil
}
func (t *resolve1Manager) SetLinkDNS(sender dbus.Sender, ifIndex uint32, addresses []resolve1LinkNameserver) *dbus.Error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
link, err := t.getLink(ifIndex)
if err != nil {
return dbus.MakeFailedError(err)
}
for _, ns := range link.nameservers {
ns.Close()
}
link.nameservers = link.nameservers[:0]
if len(addresses) > 0 {
serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{
BindInterface: link.iif.Name,
UDPFragmentDefault: true,
}))
var serverAddresses []netip.Addr
for _, address := range addresses {
serverAddr, ok := netip.AddrFromSlice(address.Address)
if !ok {
return dbus.MakeFailedError(E.New("invalid address"))
}
serverAddresses = append(serverAddresses, serverAddr)
}
for _, serverAddress := range serverAddresses {
link.nameservers = append(link.nameservers, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddress, 53)))
}
t.logger.Info(sender, ": SetLinkDNS ", link.iif.Name, " ", strings.Join(common.Map(serverAddresses, netip.Addr.String), ", "))
} else {
t.logger.Info(sender, ": SetLinkDNS ", link.iif.Name, " (empty)")
}
return nil
}
func (t *resolve1Manager) SetLinkDomains(sender dbus.Sender, ifIndex uint32, domains []resolve1LinkDomain) *dbus.Error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
link, err := t.getLink(ifIndex)
if err != nil {
return dbus.MakeFailedError(err)
}
link.domains = domains
if len(domains) > 0 {
t.logger.Info(sender, ": SetLinkDomains ", link.iif.Name, " ", strings.Join(common.Map(domains, func(domain resolve1LinkDomain) string {
if !domain.RoutingOnly {
return domain.Domain
} else {
return domain.Domain + " (routing)"
}
}), ", "))
} else {
t.logger.Info(sender, ": SetLinkDomains ", link.iif.Name, " (empty)")
}
return nil
}
func (t *resolve1Manager) SetLinkDefaultRoute(sender dbus.Sender, ifIndex uint32, defaultRoute bool) *dbus.Error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
link, err := t.getLink(ifIndex)
if err != nil {
return dbus.MakeFailedError(err)
}
link.defaultRoute = defaultRoute
t.logger.Info(sender, ": SetLinkDefaultRoute ", link.iif.Name, " ", defaultRoute)
return nil
}
func (t *resolve1Manager) SetLinkLLMNR(ifIndex uint32, llmnrMode string) {
}
func (t *resolve1Manager) SetLinkMulticastDNS(ifIndex uint32, mdnsMode string) {
}
func (t *resolve1Manager) SetLinkDNSOverTLS(ifIndex uint32, dotMode string) {
}
func (t *resolve1Manager) SetLinkDNSSEC(ifIndex uint32, dnssecMode string) {
}
func (t *resolve1Manager) SetLinkDNSSECNegativeTrustAnchors(ifIndex uint32, domains []string) {
}
func (t *resolve1Manager) RevertLink(sender dbus.Sender, ifIndex uint32) *dbus.Error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
link, err := t.getLink(ifIndex)
if err != nil {
return dbus.MakeFailedError(err)
}
delete(t.links, ifIndex)
t.logger.Info(sender, ": RevertLink ", link.iif.Name)
return nil
}
func (t *resolve1Manager) FlushCaches() {
t.dnsRouter.ClearCache()
}

View file

@ -0,0 +1,191 @@
package split
import (
"context"
"strings"
"sync"
"github.com/sagernet/sing-box/adapter"
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"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/godbus/dbus/v5"
mDNS "github.com/miekg/dns"
)
func RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.SplitDNSServerOptions](registry, C.DNSTypeSplitDNS, NewTransport)
}
var _ adapter.DNSTransport = (*Transport)(nil)
type Transport struct {
dns.TransportAdapter
ctx context.Context
network adapter.NetworkManager
dnsRouter adapter.DNSRouter
logger logger.ContextLogger
acceptDefaultResolvers bool
linkAccess sync.Mutex
links map[uint32]*TransportLink
}
type TransportLink struct {
iif *control.Interface
nameservers []adapter.DNSTransport
domains []resolve1LinkDomain
defaultRoute bool
dnsOverTLS bool
}
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,
acceptDefaultResolvers: options.AcceptDefaultResolvers,
network: service.FromContext[adapter.NetworkManager](ctx),
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
links: make(map[uint32]*TransportLink),
}, nil
}
func (t *Transport) Start(stage adapter.StartStage) error {
switch stage {
case adapter.StartStateInitialize:
dnsTransportManager := service.FromContext[adapter.DNSTransportManager](t.ctx)
for _, transport := range dnsTransportManager.Transports() {
if transport.Type() == C.DNSTypeSplitDNS && transport != t {
return E.New("multiple split DNS server are not supported")
}
}
case adapter.StartStateStart:
systemBus, err := dbus.SystemBus()
if err != nil {
return err
}
reply, err := systemBus.RequestName("org.freedesktop.resolve1", dbus.NameFlagDoNotQueue)
if err != nil {
return err
}
switch reply {
case dbus.RequestNameReplyPrimaryOwner:
case dbus.RequestNameReplyExists:
return E.New("D-Bus object already exists, maybe real resolved is running")
default:
return E.New("unknown request name reply: ", reply)
}
err = systemBus.Export((*resolve1Manager)(t), "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager")
if err != nil {
return err
}
}
return nil
}
func (t *Transport) Close() error {
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.links {
for _, domain := range link.domains {
if domain.RoutingOnly && !t.acceptDefaultResolvers {
continue
}
if strings.HasSuffix(question.Name, domain.Domain) {
selectedLink = link
}
}
}
if selectedLink == nil && t.acceptDefaultResolvers {
for _, link := range t.links {
if link.defaultRoute {
selectedLink = link
}
}
}
if selectedLink == nil {
return nil, dns.RcodeNameError
}
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
return t.exchangeParallel(ctx, selectedLink.nameservers, message)
} else {
return t.exchangeSingleRequest(ctx, selectedLink.nameservers, message)
}
}
func (t *Transport) exchangeSingleRequest(ctx context.Context, transports []adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
var errors []error
for _, transport := range transports {
response, err := transport.Exchange(ctx, message)
if err == nil {
addresses, _ := dns.MessageToAddresses(response)
if len(addresses) == 0 {
err = E.New("empty result")
}
}
if err != nil {
errors = append(errors, err)
} else {
return response, nil
}
}
return nil, E.Errors(errors...)
}
func (t *Transport) exchangeParallel(ctx context.Context, transports []adapter.DNSTransport, 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, transport adapter.DNSTransport) {
response, err := transport.Exchange(ctx, message)
if err == nil {
addresses, _ := dns.MessageToAddresses(response)
if len(addresses) == 0 {
err = E.New("empty result")
}
}
select {
case results <- queryResult{response, err}:
case <-returned:
}
}
queryCtx, queryCancel := context.WithCancel(ctx)
defer queryCancel()
var nameCount int
for _, fqdn := range transports {
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...)
}
}
}
}

View file

@ -13,6 +13,7 @@ import (
"github.com/sagernet/sing-box/dns/transport/fakeip"
"github.com/sagernet/sing-box/dns/transport/hosts"
"github.com/sagernet/sing-box/dns/transport/local"
"github.com/sagernet/sing-box/dns/transport/split"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/anytls"
@ -110,6 +111,7 @@ func DNSTransportRegistry() *dns.TransportRegistry {
hosts.RegisterTransport(registry)
local.RegisterTransport(registry)
fakeip.RegisterTransport(registry)
split.RegisterTransport(registry)
registerQUICTransports(registry)
registerDHCPTransport(registry)

View file

@ -387,3 +387,7 @@ type DHCPDNSServerOptions struct {
LocalDNSServerOptions
Interface string `json:"interface,omitempty"`
}
type SplitDNSServerOptions struct {
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
}

View file

@ -0,0 +1,15 @@
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own_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>

View file

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

View file

@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org
After=network.target nss-lookup.target network-online.target
[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

View file

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

View file

@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org
After=network.target nss-lookup.target network-online.target
[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