mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-04-02 03:17:35 +03:00
Add split DNS server
This commit is contained in:
parent
2b18fc4886
commit
ea20749a22
13 changed files with 381 additions and 0 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.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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -28,6 +28,7 @@ const (
|
|||
DNSTypeFakeIP = "fakeip"
|
||||
DNSTypeDHCP = "dhcp"
|
||||
DNSTypeTailscale = "tailscale"
|
||||
DNSTypeSplitDNS = "split-dns"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
140
dns/transport/split/resolve1.go
Normal file
140
dns/transport/split/resolve1.go
Normal 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()
|
||||
}
|
191
dns/transport/split/split.go
Normal file
191
dns/transport/split/split.go
Normal 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...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -387,3 +387,7 @@ type DHCPDNSServerOptions struct {
|
|||
LocalDNSServerOptions
|
||||
Interface string `json:"interface,omitempty"`
|
||||
}
|
||||
|
||||
type SplitDNSServerOptions struct {
|
||||
AcceptDefaultResolvers bool `json:"accept_default_resolvers,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
|
||||
|
||||
[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
|
||||
|
|
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
|
||||
|
||||
[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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue