Add dns_server_address and dns_search_domain rule items

This commit is contained in:
世界 2026-09-24 17:30:20 +08:00
parent c8d9ae4acf
commit a2df2cdc1f
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
24 changed files with 722 additions and 20 deletions

View file

@ -73,6 +73,12 @@ type DNSTransportWithPreferredDomain interface {
PreferredDomain(domain string) bool
}
type DNSTransportWithConfiguration interface {
DNSTransport
ServerAddresses() []netip.Addr
SearchDomains() []string
}
type DNSTransportWithEnvironment interface {
DNSTransport
Environment() []string

View file

@ -80,9 +80,10 @@ func NormalizeWIFIBSSID(bssid string) string {
type NetworkInterface struct {
control.Interface
Type C.InterfaceType
DNSServers []string
Gateways []netip.Addr
Expensive bool
Constrained bool
Type C.InterfaceType
DNSServers []string
DNSSearchDomains []string
Gateways []netip.Addr
Expensive bool
Constrained bool
}

View file

@ -5,6 +5,7 @@ import (
"errors"
"io"
"net"
"net/netip"
"runtime"
"strings"
"sync"
@ -38,8 +39,9 @@ func RegisterTransport(registry *dns.TransportRegistry) {
}
var (
_ adapter.DNSTransport = (*Transport)(nil)
_ adapter.DNSTransportWithEnvironment = (*Transport)(nil)
_ adapter.DNSTransport = (*Transport)(nil)
_ adapter.DNSTransportWithConfiguration = (*Transport)(nil)
_ adapter.DNSTransportWithEnvironment = (*Transport)(nil)
)
var errInterfaceIsCellular = E.New("interface is cellular")
@ -139,12 +141,37 @@ func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error
func (t *Transport) Reset() {
t.refreshAccess.Lock()
defer t.refreshAccess.Unlock()
state := t.savedState.Swap(nil)
if state != nil && state.serverScope != nil {
state := t.savedState.Load()
if state == nil {
return
}
t.savedState.Store(&transportState{
search: state.search,
servers: state.servers,
})
if state.serverScope != nil {
state.serverScope.Close()
}
}
func (t *Transport) ServerAddresses() []netip.Addr {
state := t.loadState()
if state == nil {
return nil
}
return common.Map(state.servers, func(it M.Socksaddr) netip.Addr {
return it.Addr
})
}
func (t *Transport) SearchDomains() []string {
state := t.loadState()
if state == nil {
return nil
}
return state.search
}
func (t *Transport) Environment() []string {
state := t.savedState.Load()
if state == nil {
@ -207,14 +234,22 @@ func (t *Transport) exchangeCold(ctx context.Context, message *mDNS.Msg, callbac
}
func (t *Transport) Fetch() []M.Socksaddr {
state := t.loadState()
if state == nil {
return nil
}
return state.servers
}
func (t *Transport) loadState() *transportState {
state := t.savedState.Load()
if state == nil || state.lastError != nil {
return nil
}
if len(state.servers) > 0 && time.Since(state.updatedAt) >= C.DHCPTTL {
if time.Since(state.updatedAt) >= C.DHCPTTL {
t.startRefresh()
}
return state.servers
return state
}
func (t *Transport) fetch(ctx context.Context) error {

View file

@ -2,6 +2,7 @@ package local
import (
"context"
"net/netip"
"sync"
"sync/atomic"
@ -12,8 +13,10 @@ import (
"github.com/sagernet/sing-box/dns/transport/mdns"
"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"
N "github.com/sagernet/sing/common/network"
mDNS "github.com/miekg/dns"
@ -26,6 +29,7 @@ func RegisterTransport(registry *dns.TransportRegistry) {
var (
_ adapter.DNSTransport = (*Transport)(nil)
_ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil)
_ adapter.DNSTransportWithConfiguration = (*Transport)(nil)
_ adapter.DNSTransportWithEnvironment = (*Transport)(nil)
)
@ -132,6 +136,19 @@ func (t *Transport) PreferredDomain(domain string) bool {
return t.preferredResolver.PreferredDomain(domain)
}
func (t *Transport) ServerAddresses() []netip.Addr {
if t.resolved != nil {
return t.resolved.ServerAddresses()
}
return common.Map(t.configSource.Configuration().Servers, func(it M.Socksaddr) netip.Addr {
return it.Addr
})
}
func (t *Transport) SearchDomains() []string {
return t.configSource.Configuration().Search
}
func (t *Transport) Environment() []string {
if t.resolved != nil {
return t.resolved.Environment()

View file

@ -2,6 +2,7 @@ package local
import (
"context"
"net/netip"
mDNS "github.com/miekg/dns"
)
@ -11,5 +12,6 @@ type ResolvedResolver interface {
Close() error
Reset()
Environment() []string
ServerAddresses() []netip.Addr
ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error))
}

View file

@ -69,9 +69,10 @@ type DBusResolvedResolver struct {
}
type resolvedServerSet struct {
scopes []*resolvedScope
signature []string
serverScope *adapter.Scope
scopes []*resolvedScope
serverAddresses []netip.Addr
signature []string
serverScope *adapter.Scope
}
// Match levels of dns_scope_good_domain() in systemd-resolved: a routing or search
@ -244,6 +245,14 @@ func (t *DBusResolvedResolver) Environment() []string {
return serverSet.signature
}
func (t *DBusResolvedResolver) ServerAddresses() []netip.Addr {
serverSet := t.savedServerSet.Load()
if serverSet == nil {
return nil
}
return serverSet.serverAddresses
}
func (t *DBusResolvedResolver) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
serverSet := t.savedServerSet.Load()
if serverSet == nil {
@ -569,6 +578,7 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ
}
serverSet.scopes = append(serverSet.scopes, scope)
for _, serverSpecification := range scopeSpecification.servers {
serverSet.serverAddresses = append(serverSet.serverAddresses, serverSpecification.address)
serverSet.signature = append(serverSet.signature, scopeSpecification.interfaceName+"/"+M.SocksaddrFrom(serverSpecification.address, serverSpecification.port).String())
}
}

View file

@ -2,6 +2,11 @@
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.15.0"
:material-plus: [dns_server_address](#dns_server_address)
:material-plus: [dns_search_domain](#dns_search_domain)
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [source_mac_address](#source_mac_address)
@ -178,6 +183,16 @@ icon: material/alert-decagram
"local",
"ts-dns"
],
"dns_server_address": {
"local": [
"192.168.1.1/32"
]
},
"dns_search_domain": {
"ts-dns": [
"example.ts.net"
]
},
"wifi_ssid": [
"My WIFI"
],
@ -535,6 +550,36 @@ Match specified DNS servers' preferred domains.
| `openconnect` | Match split DNS and search domains pushed by the VPN server |
| `resolved` | Match split DNS and search domains from systemd-resolved links |
#### dns_server_address
!!! question "Since sing-box 1.15.0"
Match specified DNS servers' server addresses.
| Type | Match |
|---------------|-----------------------------------------------|
| `local` | Match system DNS servers |
| `dhcp` | Match DNS servers from DHCP |
| `resolved` | Match DNS servers from systemd-resolved links |
| `tailscale` | Match DNS resolvers of the tailnet |
| `openvpn` | Match DNS servers pushed by the VPN server |
| `openconnect` | Match DNS servers pushed by the VPN server |
#### dns_search_domain
!!! question "Since sing-box 1.15.0"
Match specified DNS servers' search domains.
| Type | Match |
|---------------|--------------------------------------------------|
| `local` | Match system search domains |
| `dhcp` | Match search domains from DHCP |
| `resolved` | Match search domains from systemd-resolved links |
| `tailscale` | Match search domains of the tailnet |
| `openvpn` | Match search domains pushed by the VPN server |
| `openconnect` | Match search domains pushed by the VPN server |
#### wifi_ssid
!!! quote ""

View file

@ -2,6 +2,11 @@
icon: material/alert-decagram
---
!!! quote "sing-box 1.15.0 中的更改"
:material-plus: [dns_server_address](#dns_server_address)
:material-plus: [dns_search_domain](#dns_search_domain)
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [source_mac_address](#source_mac_address)
@ -178,6 +183,16 @@ icon: material/alert-decagram
"local",
"ts-dns"
],
"dns_server_address": {
"local": [
"192.168.1.1/32"
]
},
"dns_search_domain": {
"ts-dns": [
"example.ts.net"
]
},
"wifi_ssid": [
"My WIFI"
],
@ -529,6 +544,36 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
| `openconnect` | 匹配 VPN 服务器推送的分流 DNS 和搜索域 |
| `resolved` | 匹配 systemd-resolved 链路中的分流域名和搜索域 |
#### dns_server_address
!!! question "自 sing-box 1.15.0 起"
匹配指定 DNS 服务器的服务器地址。
| 类型 | 匹配 |
|---------------|----------------------------------|
| `local` | 匹配系统 DNS 服务器 |
| `dhcp` | 匹配通过 DHCP 获取的 DNS 服务器 |
| `resolved` | 匹配 systemd-resolved 链路中的 DNS 服务器 |
| `tailscale` | 匹配 tailnet 的 DNS 解析器 |
| `openvpn` | 匹配 VPN 服务器推送的 DNS 服务器 |
| `openconnect` | 匹配 VPN 服务器推送的 DNS 服务器 |
#### dns_search_domain
!!! question "自 sing-box 1.15.0 起"
匹配指定 DNS 服务器的搜索域。
| 类型 | 匹配 |
|---------------|-------------------------------|
| `local` | 匹配系统搜索域 |
| `dhcp` | 匹配通过 DHCP 获取的搜索域 |
| `resolved` | 匹配 systemd-resolved 链路中的搜索域 |
| `tailscale` | 匹配 tailnet 的搜索域 |
| `openvpn` | 匹配 VPN 服务器推送的搜索域 |
| `openconnect` | 匹配 VPN 服务器推送的搜索域 |
#### wifi_ssid
!!! quote ""

View file

@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.15.0"
:material-plus: [dns_server_address](#dns_server_address)
:material-plus: [dns_search_domain](#dns_search_domain)
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [source_mac_address](#source_mac_address)
@ -168,6 +173,16 @@ icon: material/new-box
"tailscale",
"wireguard"
],
"dns_server_address": {
"local": [
"192.168.1.1/32"
]
},
"dns_search_domain": {
"ts-dns": [
"example.ts.net"
]
},
"source_mac_address": [
"00:11:22:33:44:55"
],
@ -471,6 +486,36 @@ Match specified outbounds' preferred routes.
| `wireguard` | Match peers's allowed IPs |
| `bridge` | Match all addresses except local addresses of the machine, only in [pre-match](/configuration/shared/pre-match/) |
#### dns_server_address
!!! question "Since sing-box 1.15.0"
Match specified DNS servers' server addresses.
| Type | Match |
|---------------|-----------------------------------------------|
| `local` | Match system DNS servers |
| `dhcp` | Match DNS servers from DHCP |
| `resolved` | Match DNS servers from systemd-resolved links |
| `tailscale` | Match DNS resolvers of the tailnet |
| `openvpn` | Match DNS servers pushed by the VPN server |
| `openconnect` | Match DNS servers pushed by the VPN server |
#### dns_search_domain
!!! question "Since sing-box 1.15.0"
Match specified DNS servers' search domains.
| Type | Match |
|---------------|--------------------------------------------------|
| `local` | Match system search domains |
| `dhcp` | Match search domains from DHCP |
| `resolved` | Match search domains from systemd-resolved links |
| `tailscale` | Match search domains of the tailnet |
| `openvpn` | Match search domains pushed by the VPN server |
| `openconnect` | Match search domains pushed by the VPN server |
#### source_mac_address
!!! question "Since sing-box 1.14.0"

View file

@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "sing-box 1.15.0 中的更改"
:material-plus: [dns_server_address](#dns_server_address)
:material-plus: [dns_search_domain](#dns_search_domain)
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [source_mac_address](#source_mac_address)
@ -166,6 +171,16 @@ icon: material/new-box
"tailscale",
"wireguard"
],
"dns_server_address": {
"local": [
"192.168.1.1/32"
]
},
"dns_search_domain": {
"ts-dns": [
"example.ts.net"
]
},
"source_mac_address": [
"00:11:22:33:44:55"
],
@ -469,6 +484,36 @@ icon: material/new-box
| `wireguard` | 匹配对端的 allowed IPs |
| `bridge` | 匹配除本机本地地址外的所有地址,仅在[预匹配](/zh/configuration/shared/pre-match/)中 |
#### dns_server_address
!!! question "自 sing-box 1.15.0 起"
匹配指定 DNS 服务器的服务器地址。
| 类型 | 匹配 |
|---------------|----------------------------------|
| `local` | 匹配系统 DNS 服务器 |
| `dhcp` | 匹配通过 DHCP 获取的 DNS 服务器 |
| `resolved` | 匹配 systemd-resolved 链路中的 DNS 服务器 |
| `tailscale` | 匹配 tailnet 的 DNS 解析器 |
| `openvpn` | 匹配 VPN 服务器推送的 DNS 服务器 |
| `openconnect` | 匹配 VPN 服务器推送的 DNS 服务器 |
#### dns_search_domain
!!! question "自 sing-box 1.15.0 起"
匹配指定 DNS 服务器的搜索域。
| 类型 | 匹配 |
|---------------|-------------------------------|
| `local` | 匹配系统搜索域 |
| `dhcp` | 匹配通过 DHCP 获取的搜索域 |
| `resolved` | 匹配 systemd-resolved 链路中的搜索域 |
| `tailscale` | 匹配 tailnet 的搜索域 |
| `openvpn` | 匹配 VPN 服务器推送的搜索域 |
| `openconnect` | 匹配 VPN 服务器推送的搜索域 |
#### source_mac_address
!!! question "自 sing-box 1.14.0 起"

View file

@ -1513,6 +1513,38 @@
}
]
},
"dns_server_address": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"$ref": "#/$defs/IPAddressOrPrefix"
},
{
"type": "array",
"items": {
"$ref": "#/$defs/IPAddressOrPrefix"
}
}
]
}
},
"dns_search_domain": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
},
"rule_set": {
"anyOf": [
{
@ -9811,6 +9843,38 @@
}
]
},
"dns_server_address": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"$ref": "#/$defs/IPAddressOrPrefix"
},
{
"type": "array",
"items": {
"$ref": "#/$defs/IPAddressOrPrefix"
}
}
]
}
},
"dns_search_domain": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
},
"rule_set": {
"anyOf": [
{
@ -10428,6 +10492,38 @@
}
]
},
"dns_server_address": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"$ref": "#/$defs/IPAddressOrPrefix"
},
{
"type": "array",
"items": {
"$ref": "#/$defs/IPAddressOrPrefix"
}
}
]
}
},
"dns_search_domain": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
},
"rule_set": {
"anyOf": [
{
@ -15601,6 +15697,38 @@
}
]
},
"dns_server_address": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"$ref": "#/$defs/IPAddressOrPrefix"
},
{
"type": "array",
"items": {
"$ref": "#/$defs/IPAddressOrPrefix"
}
}
]
}
},
"dns_search_domain": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
}
},
"rule_set": {
"anyOf": [
{

View file

@ -58,6 +58,35 @@ func (p *platformTransport) PreferredDomain(domain string) bool {
return p.preferredResolver.PreferredDomain(domain)
}
func (p *platformTransport) ServerAddresses() []netip.Addr {
if p.networkManager == nil {
return nil
}
defaultInterface := p.networkManager.DefaultNetworkInterface()
if defaultInterface == nil {
return nil
}
var serverAddresses []netip.Addr
for _, server := range defaultInterface.DNSServers {
serverAddr, err := netip.ParseAddr(server)
if err == nil {
serverAddresses = append(serverAddresses, serverAddr)
}
}
return serverAddresses
}
func (p *platformTransport) SearchDomains() []string {
if p.networkManager == nil {
return nil
}
defaultInterface := p.networkManager.DefaultNetworkInterface()
if defaultInterface == nil {
return nil
}
return defaultInterface.DNSSearchDomains
}
func (p *platformTransport) Environment() []string {
if p.networkManager == nil {
return nil
@ -180,5 +209,6 @@ func (c *ExchangeContext) ErrnoCode(code int32) {
var (
_ adapter.DNSTransport = (*platformTransport)(nil)
_ adapter.DNSTransportWithPreferredDomain = (*platformTransport)(nil)
_ adapter.DNSTransportWithConfiguration = (*platformTransport)(nil)
_ adapter.DNSTransportWithEnvironment = (*platformTransport)(nil)
)

View file

@ -129,10 +129,11 @@ type NetworkInterface struct {
Addresses StringIterator
Flags int32
Type int32
DNSServer StringIterator
Gateway StringIterator
Metered bool
Type int32
DNSServer StringIterator
DNSSearchDomain StringIterator
Gateway StringIterator
Metered bool
}
type WIFIState struct {

View file

@ -143,8 +143,9 @@ func (w *platformInterfaceWrapper) NetworkInterfaces() ([]adapter.NetworkInterfa
Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix),
Flags: linkFlags(uint32(netInterface.Flags)),
},
Type: C.InterfaceType(netInterface.Type),
DNSServers: iteratorToArray[string](netInterface.DNSServer),
Type: C.InterfaceType(netInterface.Type),
DNSServers: iteratorToArray[string](netInterface.DNSServer),
DNSSearchDomains: iteratorToArray[string](netInterface.DNSSearchDomain),
Gateways: common.Filter(common.Map(iteratorToArray[string](netInterface.Gateway), func(it string) netip.Addr {
gateway, _ := netip.ParseAddr(it)
return gateway.Unmap().WithZone("")

View file

@ -178,6 +178,8 @@ type RawDefaultRule struct {
SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"`
SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"`
PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"`
DNSServerAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"dns_server_address,omitempty"`
DNSSearchDomain *badjson.TypedMap[string, badoption.Listable[string]] `json:"dns_search_domain,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty" reference:"rule_set"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
Invert bool `json:"invert,omitempty"`

View file

@ -173,6 +173,8 @@ type RawDefaultDNSRule struct {
SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"`
SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"`
PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"`
DNSServerAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"dns_server_address,omitempty"`
DNSSearchDomain *badjson.TypedMap[string, badoption.Listable[string]] `json:"dns_search_domain,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty" reference:"rule_set"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
MatchResponse *DNSRuleMatchResponse `json:"match_response,omitempty"`

View file

@ -41,6 +41,7 @@ type DNSTransport struct {
closed bool
routes []openConnectDNSRoute
searchDomains []string
serverAddresses []netip.Addr
defaultResolvers []adapter.DNSTransport
resolverScope *adapter.Scope
}
@ -83,6 +84,7 @@ func (t *DNSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) err
t.closed = true
t.routes = nil
t.searchDomains = nil
t.serverAddresses = nil
t.defaultResolvers = nil
t.resolverScope = nil
t.access.Unlock()
@ -116,6 +118,7 @@ func (t *DNSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) err
func (t *DNSTransport) updateConfiguration(configuration openconnecttransport.Configuration) {
resolverByAddress := make(map[netip.Addr]adapter.DNSTransport)
var serverAddresses []netip.Addr
resolverFor := func(address netip.Addr) adapter.DNSTransport {
if !address.IsValid() {
return nil
@ -124,6 +127,7 @@ func (t *DNSTransport) updateConfiguration(configuration openconnecttransport.Co
if loaded {
return resolver
}
serverAddresses = append(serverAddresses, address)
resolver = transport.NewUDPRaw(
t.logger,
dns.NewTransportAdapter(C.DNSTypeUDP, t.Tag()+"/"+address.String(), nil),
@ -217,6 +221,7 @@ func (t *DNSTransport) updateConfiguration(configuration openconnecttransport.Co
oldResolverScope := t.resolverScope
t.routes = routes
t.searchDomains = searchDomains
t.serverAddresses = serverAddresses
t.defaultResolvers = defaultResolvers
t.resolverScope = resolverScope
t.access.Unlock()
@ -239,6 +244,18 @@ func (t *DNSTransport) Reset() {
}
}
func (t *DNSTransport) ServerAddresses() []netip.Addr {
t.access.RLock()
defer t.access.RUnlock()
return t.serverAddresses
}
func (t *DNSTransport) SearchDomains() []string {
t.access.RLock()
defer t.access.RUnlock()
return t.searchDomains
}
func (t *DNSTransport) PreferredDomain(domain string) bool {
canonicalDomain := mDNS.Fqdn(canonicalOpenConnectDomain(domain))
t.access.RLock()
@ -369,3 +386,8 @@ func (t *DNSTransport) collectResolversLocked() []adapter.DNSTransport {
}
return resolvers
}
var (
_ adapter.DNSTransportWithPreferredDomain = (*DNSTransport)(nil)
_ adapter.DNSTransportWithConfiguration = (*DNSTransport)(nil)
)

View file

@ -49,6 +49,7 @@ type DNSTransport struct {
closed bool
routes map[string][]adapter.DNSTransport
searchDomains []string
serverAddresses []netip.Addr
defaultResolvers []adapter.DNSTransport
resolverScope *adapter.Scope
}
@ -88,6 +89,7 @@ func (t *DNSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) err
t.closed = true
t.routes = nil
t.searchDomains = nil
t.serverAddresses = nil
t.defaultResolvers = nil
t.resolverScope = nil
t.access.Unlock()
@ -133,6 +135,7 @@ func (t *DNSTransport) updateResolvers(configuration ovpntransport.Configuration
return left.Priority - right.Priority
})
var selectedResolvers []adapter.DNSTransport
var serverAddresses []netip.Addr
if len(servers) > 0 {
server := servers[0]
if server.DNSSEC == "yes" {
@ -148,6 +151,7 @@ func (t *DNSTransport) updateResolvers(configuration ovpntransport.Configuration
return t.failResolverUpdate(resolverScope, err)
}
selectedResolvers = append(selectedResolvers, resolver)
serverAddresses = append(serverAddresses, address.Addr())
}
if len(selectedResolvers) == 0 {
return t.failResolverUpdate(resolverScope, E.New("DNS server ", server.Priority, " has no addresses"))
@ -170,6 +174,7 @@ func (t *DNSTransport) updateResolvers(configuration ovpntransport.Configuration
return t.failResolverUpdate(resolverScope, err)
}
selectedResolvers = append(selectedResolvers, resolver)
serverAddresses = append(serverAddresses, address)
}
if len(configuration.DNSRoutes) > 0 {
if len(selectedResolvers) == 0 {
@ -196,6 +201,7 @@ func (t *DNSTransport) updateResolvers(configuration ovpntransport.Configuration
oldResolverScope := t.resolverScope
t.routes = routes
t.searchDomains = searchDomains
t.serverAddresses = serverAddresses
t.defaultResolvers = defaultResolvers
t.resolverScope = resolverScope
t.access.Unlock()
@ -213,6 +219,7 @@ func (t *DNSTransport) failResolverUpdate(resolverScope *adapter.Scope, updateEr
oldResolverScope := t.resolverScope
t.routes = nil
t.searchDomains = nil
t.serverAddresses = nil
t.defaultResolvers = nil
t.resolverScope = nil
t.access.Unlock()
@ -290,6 +297,18 @@ func (t *DNSTransport) Raw() bool {
return true
}
func (t *DNSTransport) ServerAddresses() []netip.Addr {
t.access.RLock()
defer t.access.RUnlock()
return t.serverAddresses
}
func (t *DNSTransport) SearchDomains() []string {
t.access.RLock()
defer t.access.RUnlock()
return t.searchDomains
}
func (t *DNSTransport) PreferredDomain(domain string) bool {
t.access.RLock()
defer t.access.RUnlock()
@ -428,3 +447,8 @@ func restoreOpenVPNOriginalQuestion(response *mDNS.Msg, expandedName string, ori
}
}
}
var (
_ adapter.DNSTransportWithPreferredDomain = (*DNSTransport)(nil)
_ adapter.DNSTransportWithConfiguration = (*DNSTransport)(nil)
)

View file

@ -58,6 +58,7 @@ type DNSTransport struct {
hosts map[string][]netip.Addr
magicHosts nDNSResolver.MagicDNSHosts
searchDomains []string
serverAddresses []netip.Addr
defaultResolvers []adapter.DNSTransport
resolverScope *adapter.Scope
}
@ -102,6 +103,7 @@ func (t *DNSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) err
t.routes = nil
t.hosts = nil
t.magicHosts = nil
t.serverAddresses = nil
t.defaultResolvers = nil
t.resolverScope = nil
t.access.Unlock()
@ -137,6 +139,7 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
})
resolverScope := adapter.NewScope(t.ctx, t.logger)
routes := make(map[string][]adapter.DNSTransport)
var serverAddresses []netip.Addr
for domain, resolvers := range dnsConfig.Routes {
var myResolvers []adapter.DNSTransport
for _, resolver := range resolvers {
@ -145,6 +148,10 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
return E.Errors(err, resolverScope.Close())
}
myResolvers = append(myResolvers, myResolver)
serverAddrPort, isIPResolver := resolver.IPPort()
if isIPResolver {
serverAddresses = append(serverAddresses, serverAddrPort.Addr())
}
}
routes[mDNS.CanonicalName(domain.WithTrailingDot())] = myResolvers
}
@ -162,6 +169,10 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
return E.Errors(err, resolverScope.Close())
}
defaultResolvers = append(defaultResolvers, myResolver)
serverAddrPort, isIPResolver := resolver.IPPort()
if isIPResolver {
serverAddresses = append(serverAddresses, serverAddrPort.Addr())
}
}
t.access.Lock()
@ -171,6 +182,7 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n
t.hosts = hosts
t.magicHosts = t.endpoint.server.ExportLocalBackend().ExportMagicDNSHosts()
t.searchDomains = searchDomains
t.serverAddresses = common.Uniq(serverAddresses)
t.defaultResolvers = defaultResolvers
t.resolverScope = resolverScope
t.access.Unlock()
@ -285,6 +297,18 @@ func (t *DNSTransport) Raw() bool {
return true
}
func (t *DNSTransport) ServerAddresses() []netip.Addr {
t.access.RLock()
defer t.access.RUnlock()
return t.serverAddresses
}
func (t *DNSTransport) SearchDomains() []string {
t.access.RLock()
defer t.access.RUnlock()
return t.searchDomains
}
func (t *DNSTransport) PreferredDomain(domain string) bool {
t.access.RLock()
hosts := t.hosts
@ -515,3 +539,8 @@ func (t *DNSTransport) routePrefixesSnapshot() []netip.Prefix {
defer t.access.RUnlock()
return append([]netip.Prefix(nil), t.routePrefixes...)
}
var (
_ adapter.DNSTransportWithPreferredDomain = (*DNSTransport)(nil)
_ adapter.DNSTransportWithConfiguration = (*DNSTransport)(nil)
)

View file

@ -283,6 +283,16 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.DNSServerAddress != nil && options.DNSServerAddress.Size() > 0 {
item := NewDNSServerAddressItem(ctx, options.DNSServerAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.DNSSearchDomain != nil && options.DNSSearchDomain.Size() > 0 {
item := NewDNSSearchDomainItem(ctx, options.DNSSearchDomain)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.RuleSet) > 0 {
//nolint:staticcheck
if options.Deprecated_RulesetIPCIDRMatchSource {

View file

@ -377,6 +377,16 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.DNSServerAddress != nil && options.DNSServerAddress.Size() > 0 {
item := NewDNSServerAddressItem(ctx, options.DNSServerAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.DNSSearchDomain != nil && options.DNSSearchDomain.Size() > 0 {
item := NewDNSSearchDomainItem(ctx, options.DNSSearchDomain)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
if legacyDNSMode {
deprecated.Report(ctx, deprecated.OptionRuleSetIPCIDRAcceptEmpty)

View file

@ -0,0 +1,72 @@
package rule
import (
"context"
"slices"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
)
var _ RuleItem = (*DNSSearchDomainItem)(nil)
type DNSSearchDomainItem struct {
ctx context.Context
transportTags []string
searchDomains [][]string
transports []adapter.DNSTransportWithConfiguration
description string
}
func NewDNSSearchDomainItem(ctx context.Context, searchDomains *badjson.TypedMap[string, badoption.Listable[string]]) *DNSSearchDomainItem {
item := &DNSSearchDomainItem{
ctx: ctx,
}
var entryDescriptions []string
for _, entry := range searchDomains.Entries() {
item.transportTags = append(item.transportTags, entry.Key)
item.searchDomains = append(item.searchDomains, common.Map(entry.Value, mDNS.CanonicalName))
entryDescriptions = append(entryDescriptions, entry.Key+"="+strings.Join(entry.Value, ","))
}
item.description = "dns_search_domain=[" + strings.Join(entryDescriptions, " ") + "]"
return item
}
func (r *DNSSearchDomainItem) Start() error {
transportManager := service.FromContext[adapter.DNSTransportManager](r.ctx)
for _, transportTag := range r.transportTags {
rawTransport, loaded := transportManager.Transport(transportTag)
if !loaded {
return E.New("DNS server not found: ", transportTag)
}
transportWithConfiguration, withConfiguration := rawTransport.(adapter.DNSTransportWithConfiguration)
if !withConfiguration {
return E.New("DNS server type does not support dns_search_domain: ", rawTransport.Type())
}
r.transports = append(r.transports, transportWithConfiguration)
}
return nil
}
func (r *DNSSearchDomainItem) Match(metadata *adapter.InboundContext) bool {
for i, transport := range r.transports {
domains := r.searchDomains[i]
if !slices.ContainsFunc(transport.SearchDomains(), func(searchDomain string) bool {
return slices.Contains(domains, mDNS.CanonicalName(searchDomain))
}) {
return false
}
}
return true
}
func (r *DNSSearchDomainItem) String() string {
return r.description
}

View file

@ -0,0 +1,78 @@
package rule
import (
"context"
"net/netip"
"slices"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
"github.com/sagernet/sing/service"
)
var _ RuleItem = (*DNSServerAddressItem)(nil)
type DNSServerAddressItem struct {
ctx context.Context
transportTags []string
serverAddresses [][]netip.Prefix
transports []adapter.DNSTransportWithConfiguration
description string
}
func NewDNSServerAddressItem(ctx context.Context, serverAddresses *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]]) *DNSServerAddressItem {
item := &DNSServerAddressItem{
ctx: ctx,
}
var entryDescriptions []string
for _, entry := range serverAddresses.Entries() {
prefixes := make([]netip.Prefix, 0, len(entry.Value))
for _, prefixable := range entry.Value {
prefixes = append(prefixes, prefixable.Build(netip.Prefix{}))
}
item.transportTags = append(item.transportTags, entry.Key)
item.serverAddresses = append(item.serverAddresses, prefixes)
entryDescriptions = append(entryDescriptions, entry.Key+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ","))
}
item.description = "dns_server_address=[" + strings.Join(entryDescriptions, " ") + "]"
return item
}
func (r *DNSServerAddressItem) Start() error {
transportManager := service.FromContext[adapter.DNSTransportManager](r.ctx)
for _, transportTag := range r.transportTags {
rawTransport, loaded := transportManager.Transport(transportTag)
if !loaded {
return E.New("DNS server not found: ", transportTag)
}
transportWithConfiguration, withConfiguration := rawTransport.(adapter.DNSTransportWithConfiguration)
if !withConfiguration {
return E.New("DNS server type does not support dns_server_address: ", rawTransport.Type())
}
r.transports = append(r.transports, transportWithConfiguration)
}
return nil
}
func (r *DNSServerAddressItem) Match(metadata *adapter.InboundContext) bool {
for i, transport := range r.transports {
prefixes := r.serverAddresses[i]
if !slices.ContainsFunc(transport.ServerAddresses(), func(address netip.Addr) bool {
unwrappedAddress := address.Unmap().WithZone("")
return slices.ContainsFunc(prefixes, func(prefix netip.Prefix) bool {
return prefix.Contains(unwrappedAddress)
})
}) {
return false
}
}
return true
}
func (r *DNSServerAddressItem) String() string {
return r.description
}

View file

@ -36,6 +36,7 @@ func RegisterTransport(registry *dns.TransportRegistry) {
var (
_ adapter.DNSTransport = (*Transport)(nil)
_ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil)
_ adapter.DNSTransportWithConfiguration = (*Transport)(nil)
_ adapter.DNSTransportWithEnvironment = (*Transport)(nil)
)
@ -165,6 +166,47 @@ func (t *Transport) Environment() []string {
return environment
}
func (t *Transport) ServerAddresses() []netip.Addr {
if t.service == nil {
return nil
}
t.service.linkAccess.RLock()
defer t.service.linkAccess.RUnlock()
var serverAddresses []netip.Addr
for _, link := range t.service.links {
for _, address := range link.address {
serverAddr, loaded := netip.AddrFromSlice(address.Address)
if loaded {
serverAddresses = append(serverAddresses, serverAddr)
}
}
for _, address := range link.addressEx {
serverAddr, loaded := netip.AddrFromSlice(address.Address)
if loaded {
serverAddresses = append(serverAddresses, serverAddr)
}
}
}
return serverAddresses
}
func (t *Transport) SearchDomains() []string {
if t.service == nil {
return nil
}
t.service.linkAccess.RLock()
defer t.service.linkAccess.RUnlock()
var searchDomains []string
for _, link := range t.service.links {
for _, domain := range link.domain {
if !domain.RoutingOnly && domain.Domain != "." {
searchDomains = append(searchDomains, domain.Domain)
}
}
}
return searchDomains
}
func (t *Transport) updateTransports(link *TransportLink) error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()