Refactor local DNS transport

This commit is contained in:
世界
2026-08-30 17:41:45 +08:00
parent 1e2846a1a5
commit 28b25598ed
15 changed files with 834 additions and 542 deletions
+5 -42
View File
@@ -14,9 +14,7 @@ import (
"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"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
)
@@ -37,20 +35,14 @@ type Transport struct {
preferredResolver *PreferredDomainResolver
dialer N.Dialer
preferGo bool
fallback bool
resolved ResolvedResolver
mdnsTransport adapter.DNSTransport
dhcpTransport dhcpTransport
configSource *systemConfigSource
system systemResolver
serverSet atomic.Pointer[localServerSet]
serverSetAccess sync.Mutex
}
type dhcpTransport interface {
adapter.DNSTransport
Fetch() []M.Socksaddr
}
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewLocalDialer(ctx, options)
if err != nil {
@@ -67,6 +59,7 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
preferredResolver: preferredResolver,
dialer: transportDialer,
preferGo: options.PreferGo,
configSource: newSystemConfigSource(ctx),
}, nil
}
@@ -86,28 +79,11 @@ func (t *Transport) Start(stage adapter.StartStage) error {
}
}
case adapter.StartStateStart:
if C.IsDarwin {
inboundManager := service.FromContext[adapter.InboundManager](t.ctx)
for _, inbound := range inboundManager.Inbounds() {
if inbound.Type() == C.TypeTun {
t.fallback = true
break
}
}
if t.fallback {
t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger)
}
} else {
if !C.IsDarwin {
t.mdnsTransport = mdns.NewRawTransport(t.TransportAdapter, t.ctx, t.logger)
}
fallthrough
default:
if t.dhcpTransport != nil {
err := t.dhcpTransport.Start(stage)
if err != nil {
return err
}
}
if t.mdnsTransport != nil {
err := t.mdnsTransport.Start(stage)
if err != nil {
@@ -124,7 +100,7 @@ func (t *Transport) Close() error {
serverSet.Close()
}
t.system.close()
return common.Close(t.resolved, t.dhcpTransport, t.mdnsTransport)
return common.Close(t.resolved, t.mdnsTransport, t.configSource)
}
func (t *Transport) Reset() {
@@ -135,12 +111,10 @@ func (t *Transport) Reset() {
}
}
t.system.reset()
t.configSource.Reset()
if t.resolved != nil {
t.resolved.Reset()
}
if t.dhcpTransport != nil {
t.dhcpTransport.Reset()
}
if t.mdnsTransport != nil {
t.mdnsTransport.Reset()
}
@@ -184,16 +158,5 @@ func (t *Transport) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callba
t.resolved.ExchangeAsync(ctx, message, callback)
return
}
if t.dhcpTransport != nil {
servers := t.dhcpTransport.Fetch()
if len(servers) > 0 {
t.dhcpTransport.ExchangeAsync(ctx, message, callback)
return
}
}
if t.fallback {
t.systemExchangeAsync(ctx, message, callback)
return
}
t.exchangeAsync(ctx, message, question.Name, callback)
}
+1 -1
View File
@@ -372,7 +372,7 @@ func buildQueryRequest(queryId uint64, name string, qtype uint16, qclass uint16)
message = binary.BigEndian.AppendUint32(message, mdnsResponderFlagShareConnection|mdnsResponderFlagReturnIntermediates|mdnsResponderFlagTimeout)
message = binary.BigEndian.AppendUint32(message, 0) // interfaceIndex
message = append(message, name...)
message = append(message, 0) // C string terminator
message = append(message, 0)
message = binary.BigEndian.AppendUint16(message, qtype)
message = binary.BigEndian.AppendUint16(message, qclass)
return message
+2 -4
View File
@@ -14,8 +14,7 @@ import (
mDNS "github.com/miekg/dns"
)
// "localhost" is answered by the mDNSResponder daemon itself, so these tests need
// no external network.
// "localhost" is answered by the mDNSResponder daemon itself.
func requireMDNSResponder(t *testing.T) {
t.Helper()
@@ -87,8 +86,7 @@ func TestSystemExchangeNoData(t *testing.T) {
transport := &Transport{}
defer transport.system.close()
message := new(mDNS.Msg)
// localhost has no MX record, so the daemon reports NoSuchRecord, which must
// surface as an empty NOERROR response rather than an error.
// localhost has no MX record, so the daemon reports NoSuchRecord.
message.SetQuestion("localhost.", mDNS.TypeMX)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
-16
View File
@@ -1,16 +0,0 @@
//go:build with_dhcp
package local
import (
"context"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport/dhcp"
"github.com/sagernet/sing-box/log"
N "github.com/sagernet/sing/common/network"
)
func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport {
return dhcp.NewRawTransport(transportAdapter, ctx, dialer, logger)
}
-15
View File
@@ -1,15 +0,0 @@
//go:build !with_dhcp
package local
import (
"context"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
N "github.com/sagernet/sing/common/network"
)
func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport {
return nil
}
+2 -7
View File
@@ -8,7 +8,6 @@ import (
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
mDNS "github.com/miekg/dns"
)
@@ -36,11 +35,7 @@ func (t *Transport) serverSetFor(systemConfig *dnsConfig) (*localServerSet, erro
return serverSet, nil
}
transports := make([]adapter.DNSTransport, 0, len(systemConfig.servers))
for _, server := range systemConfig.servers {
serverAddr := M.ParseSocksaddr(server)
if serverAddr.Port == 0 {
serverAddr.Port = 53
}
for _, serverAddr := range systemConfig.servers {
var serverTransport adapter.DNSTransport
if systemConfig.useTCP {
serverTransport = transport.NewTCPRaw(dns.NewTransportAdapter(C.DNSTypeTCP, "", nil), t.dialer, serverAddr)
@@ -68,7 +63,7 @@ func (t *Transport) serverSetFor(systemConfig *dnsConfig) (*localServerSet, erro
}
func (t *Transport) exchangeAsync(ctx context.Context, message *mDNS.Msg, domain string, callback func(response *mDNS.Msg, err error)) {
systemConfig := getSystemDNSConfig(t.ctx)
systemConfig := t.configSource.Configuration()
serverSet, err := t.serverSetFor(systemConfig)
if err != nil {
callback(nil, err)
-145
View File
@@ -1,145 +0,0 @@
//nolint:unused
package local
import (
"context"
"os"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
)
type resolverConfig struct {
initOnce sync.Once
ch chan struct{}
lastChecked time.Time
dnsConfig atomic.Pointer[dnsConfig]
}
var resolvConf resolverConfig
func getSystemDNSConfig(ctx context.Context) *dnsConfig {
resolvConf.tryUpdate(ctx, "/etc/resolv.conf")
return resolvConf.dnsConfig.Load()
}
func (conf *resolverConfig) init(ctx context.Context) {
conf.dnsConfig.Store(dnsReadConfig(ctx, "/etc/resolv.conf"))
conf.lastChecked = time.Now()
conf.ch = make(chan struct{}, 1)
}
func (conf *resolverConfig) tryUpdate(ctx context.Context, name string) {
conf.initOnce.Do(func() {
conf.init(ctx)
})
if conf.dnsConfig.Load().noReload {
return
}
if !conf.tryAcquireSema() {
return
}
defer conf.releaseSema()
now := time.Now()
if conf.lastChecked.After(now.Add(-5 * time.Second)) {
return
}
conf.lastChecked = now
if runtime.GOOS != "windows" {
var mtime time.Time
if fi, err := os.Stat(name); err == nil {
mtime = fi.ModTime()
}
if mtime.Equal(conf.dnsConfig.Load().mtime) {
return
}
}
dnsConf := dnsReadConfig(ctx, name)
conf.dnsConfig.Store(dnsConf)
}
func (conf *resolverConfig) tryAcquireSema() bool {
select {
case conf.ch <- struct{}{}:
return true
default:
return false
}
}
func (conf *resolverConfig) releaseSema() {
<-conf.ch
}
type dnsConfig struct {
servers []string
search []string
ndots int
timeout time.Duration
attempts int
rotate bool
unknownOpt bool
lookup []string
err error
mtime time.Time
soffset uint32
singleRequest bool
useTCP bool
trustAD bool
noReload bool
}
func (c *dnsConfig) serverOffset() uint32 {
if c.rotate {
return atomic.AddUint32(&c.soffset, 1) - 1 // return 0 to start
}
return 0
}
func (c *dnsConfig) nameList(name string) []string {
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, ".") >= c.ndots
name += "."
// l++
names := make([]string, 0, 1+len(c.search))
if hasNdots && !avoidDNS(name) {
names = append(names, name)
}
for _, suffix := range c.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")
}
-24
View File
@@ -1,24 +0,0 @@
//nolint:unused
package local
import (
"os"
"strings"
_ "unsafe"
"github.com/miekg/dns"
)
//go:linkname defaultNS net.defaultNS
var defaultNS []string
func dnsDefaultSearch() []string {
hn, err := os.Hostname()
if err != nil {
return nil
}
if i := strings.IndexRune(hn, '.'); i >= 0 && i < len(hn)-1 {
return []string{dns.Fqdn(hn[i+1:])}
}
return nil
}
-13
View File
@@ -1,13 +0,0 @@
package local
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestDNSReadConfig(t *testing.T) {
t.Parallel()
require.NoError(t, dnsReadConfig(context.Background(), "/etc/resolv.conf").err)
}
-156
View File
@@ -1,156 +0,0 @@
//go:build !windows
package local
import (
"bufio"
"context"
"net"
"net/netip"
"os"
"strings"
"time"
"github.com/miekg/dns"
)
func dnsReadConfig(_ context.Context, name string) *dnsConfig {
conf := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
attempts: 2,
}
file, err := os.Open(name)
if err != nil {
conf.servers = defaultNS
conf.search = dnsDefaultSearch()
conf.err = err
return conf
}
defer file.Close()
fi, err := file.Stat()
if err == nil {
conf.mtime = fi.ModTime()
} else {
conf.servers = defaultNS
conf.search = dnsDefaultSearch()
conf.err = err
return conf
}
reader := bufio.NewReader(file)
var (
prefix []byte
line []byte
isPrefix bool
)
for {
line, isPrefix, err = reader.ReadLine()
if err != nil {
break
}
if isPrefix {
prefix = append(prefix, line...)
continue
} else if len(prefix) > 0 {
line = append(prefix, line...)
prefix = nil
}
if len(line) > 0 && (line[0] == ';' || line[0] == '#') {
continue
}
f := strings.Fields(string(line))
if len(f) < 1 {
continue
}
switch f[0] {
case "nameserver":
if len(f) > 1 && len(conf.servers) < 3 {
if _, err := netip.ParseAddr(f[1]); err == nil {
conf.servers = append(conf.servers, net.JoinHostPort(f[1], "53"))
}
}
case "domain":
if len(f) > 1 {
conf.search = []string{dns.Fqdn(f[1])}
}
case "search":
conf.search = make([]string, 0, len(f)-1)
for i := 1; i < len(f); i++ {
name := dns.Fqdn(f[i])
if name == "." {
continue
}
conf.search = append(conf.search, name)
}
case "options":
for _, s := range f[1:] {
switch {
case strings.HasPrefix(s, "ndots:"):
n, _, _ := dtoi(s[6:])
if n < 0 {
n = 0
} else if n > 15 {
n = 15
}
conf.ndots = n
case strings.HasPrefix(s, "timeout:"):
n, _, _ := dtoi(s[8:])
if n < 1 {
n = 1
}
conf.timeout = time.Duration(n) * time.Second
case strings.HasPrefix(s, "attempts:"):
n, _, _ := dtoi(s[9:])
if n < 1 {
n = 1
}
conf.attempts = n
case s == "rotate":
conf.rotate = true
case s == "single-request" || s == "single-request-reopen":
conf.singleRequest = true
case s == "use-vc" || s == "usevc" || s == "tcp":
conf.useTCP = true
case s == "trust-ad":
conf.trustAD = true
case s == "edns0":
case s == "no-reload":
conf.noReload = true
default:
conf.unknownOpt = true
}
}
case "lookup":
conf.lookup = f[1:]
default:
conf.unknownOpt = true
}
}
if len(conf.servers) == 0 {
conf.servers = defaultNS
}
if len(conf.search) == 0 {
conf.search = dnsDefaultSearch()
}
return conf
}
const big = 0xFFFFFF
func dtoi(s string) (n int, i int, ok bool) {
n = 0
for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
n = n*10 + int(s[i]-'0')
if n >= big {
return big, i, false
}
}
if i == 0 {
return 0, 0, false
}
return n, i, true
}
-119
View File
@@ -1,119 +0,0 @@
package local
import (
"context"
"net"
"net/netip"
"os"
"strconv"
"syscall"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/service"
"golang.org/x/sys/windows"
)
func dnsReadConfig(ctx context.Context, _ string) *dnsConfig {
conf := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
attempts: 2,
}
defer func() {
if len(conf.servers) == 0 {
conf.servers = defaultNS
}
}()
addresses, err := adapterAddresses()
if err != nil {
return nil
}
var dnsAddresses []struct {
ifName string
netip.Addr
}
for _, address := range addresses {
if address.OperStatus != windows.IfOperStatusUp {
continue
}
if address.IfType == windows.IF_TYPE_TUNNEL {
continue
}
if address.FirstGatewayAddress == nil {
continue
}
for dnsServerAddress := address.FirstDnsServerAddress; dnsServerAddress != nil; dnsServerAddress = dnsServerAddress.Next {
rawSockaddr, err := dnsServerAddress.Address.Sockaddr.Sockaddr()
if err != nil {
continue
}
var dnsServerAddr netip.Addr
switch sockaddr := rawSockaddr.(type) {
case *syscall.SockaddrInet4:
dnsServerAddr = netip.AddrFrom4(sockaddr.Addr)
case *syscall.SockaddrInet6:
if sockaddr.Addr[0] == 0xfe && sockaddr.Addr[1] == 0xc0 {
// fec0/10 IPv6 addresses are site local anycast DNS
// addresses Microsoft sets by default if no other
// IPv6 DNS address is set. Site local anycast is
// deprecated since 2004, see
// https://datatracker.ietf.org/doc/html/rfc3879
continue
}
dnsServerAddr = netip.AddrFrom16(sockaddr.Addr)
if sockaddr.ZoneId != 0 {
dnsServerAddr = dnsServerAddr.WithZone(strconv.FormatInt(int64(sockaddr.ZoneId), 10))
}
default:
// Unexpected type.
continue
}
dnsAddresses = append(dnsAddresses, struct {
ifName string
netip.Addr
}{ifName: windows.UTF16PtrToString(address.FriendlyName), Addr: dnsServerAddr})
}
}
var myInterfaces []string
if networkManager := service.FromContext[adapter.NetworkManager](ctx); networkManager != nil {
myInterfaces = networkManager.InterfaceMonitor().MyInterfaces()
}
for _, address := range dnsAddresses {
if common.Contains(myInterfaces, address.ifName) {
continue
}
conf.servers = append(conf.servers, net.JoinHostPort(address.String(), "53"))
}
return conf
}
func adapterAddresses() ([]*windows.IpAdapterAddresses, error) {
var b []byte
l := uint32(15000) // recommended initial size
for {
b = make([]byte, l)
const flags = windows.GAA_FLAG_INCLUDE_PREFIX | windows.GAA_FLAG_INCLUDE_GATEWAYS
err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, flags, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l)
if err == nil {
if l == 0 {
return nil, nil
}
break
}
if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW {
return nil, os.NewSyscallError("getadaptersaddresses", err)
}
if l <= uint32(len(b)) {
return nil, os.NewSyscallError("getadaptersaddresses", err)
}
}
var aas []*windows.IpAdapterAddresses
for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next {
aas = append(aas, aa)
}
return aas, nil
}
+103
View File
@@ -0,0 +1,103 @@
package local
import (
"net/netip"
"os"
"slices"
"strings"
"sync/atomic"
"time"
M "github.com/sagernet/sing/common/metadata"
mDNS "github.com/miekg/dns"
)
var defaultNS = []M.Socksaddr{
M.SocksaddrFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 53),
M.SocksaddrFrom(netip.IPv6Loopback(), 53),
}
type dnsConfig struct {
servers []M.Socksaddr
search []string
ndots int
timeout time.Duration
attempts int
rotate bool
soffset uint32
singleRequest bool
useTCP bool
trustAD bool
}
func (c *dnsConfig) equal(other *dnsConfig) bool {
return slices.Equal(c.servers, other.servers) &&
slices.Equal(c.search, other.search) &&
c.ndots == other.ndots &&
c.timeout == other.timeout &&
c.attempts == other.attempts &&
c.rotate == other.rotate &&
c.singleRequest == other.singleRequest &&
c.useTCP == other.useTCP &&
c.trustAD == other.trustAD
}
func (c *dnsConfig) serverOffset() uint32 {
if c.rotate {
return atomic.AddUint32(&c.soffset, 1) - 1
}
return 0
}
func (c *dnsConfig) nameList(name string) []string {
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, ".") >= c.ndots
name += "."
names := make([]string, 0, 1+len(c.search))
if hasNdots && !avoidDNS(name) {
names = append(names, name)
}
for _, suffix := range c.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
}
return strings.HasSuffix(strings.TrimSuffix(name, "."), ".onion")
}
func dnsDefaultSearch() []string {
hostname, err := os.Hostname()
if err != nil {
return nil
}
_, domain, found := strings.Cut(hostname, ".")
if !found || domain == "" {
return nil
}
return []string{mDNS.Fqdn(domain)}
}
+364
View File
@@ -0,0 +1,364 @@
//go:build cgo
package local
/*
#include <dlfcn.h>
#include <notify.h>
#include <stdint.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
// dnsinfo.h is not shipped in any SDK. The layouts below are DNSINFO_VERSION
// 20170629 from apple-oss-distributions/configd (#pragma pack(4)), the format
// libsystem_configuration unpacks into at runtime. dns_configuration_copy,
// dns_configuration_free and dns_configuration_notify_key are private
// libSystem exports. cgo silently drops packed struct fields that fall on
// unaligned offsets.
#pragma pack(4)
typedef struct {
struct in_addr address;
struct in_addr mask;
} box_dns_sortaddr_t;
typedef struct {
char *domain;
int32_t n_nameserver;
struct sockaddr **nameserver;
uint16_t port;
int32_t n_search;
char **search;
int32_t n_sortaddr;
box_dns_sortaddr_t **sortaddr;
char *options;
uint32_t timeout;
uint32_t search_order;
uint32_t if_index;
uint32_t flags;
uint32_t reach_flags;
uint32_t service_identifier;
char *cid;
char *if_name;
} box_dns_resolver_t;
typedef struct {
int32_t n_resolver;
box_dns_resolver_t **resolver;
int32_t n_scoped_resolver;
box_dns_resolver_t **scoped_resolver;
uint64_t generation;
int32_t n_service_specific_resolver;
box_dns_resolver_t **service_specific_resolver;
uint32_t version;
} box_dns_config_t;
#pragma pack()
static box_dns_config_t *(*box_dns_configuration_copy)(void);
static void (*box_dns_configuration_free)(box_dns_config_t *);
static void box_reverse_string(char *s) {
size_t length = strlen(s);
for (size_t i = 0; i < length / 2; i++) {
char tmp = s[i];
s[i] = s[length - 1 - i];
s[length - 1 - i] = tmp;
}
}
static int box_dnsinfo_load(void) {
if (box_dns_configuration_copy != NULL && box_dns_configuration_free != NULL) {
return 1;
}
char copy_name[] = "ypoc_noitarugifnoc_snd";
char free_name[] = "eerf_noitarugifnoc_snd";
box_reverse_string(copy_name);
box_reverse_string(free_name);
box_dns_configuration_copy = (box_dns_config_t * (*)(void)) dlsym(RTLD_DEFAULT, copy_name);
box_dns_configuration_free = (void (*)(box_dns_config_t *))dlsym(RTLD_DEFAULT, free_name);
return box_dns_configuration_copy != NULL && box_dns_configuration_free != NULL;
}
static box_dns_config_t *box_dnsinfo_copy(void) {
return box_dns_configuration_copy();
}
static void box_dnsinfo_free(box_dns_config_t *config) {
box_dns_configuration_free(config);
}
static const char *box_dnsinfo_notify_key(void) {
const char *(*notify_key)(void) = (const char *(*)(void))dlsym(RTLD_DEFAULT, "dns_configuration_notify_key");
if (notify_key != NULL) {
return notify_key();
}
return "com.apple.system.SystemConfiguration.dns_configuration";
}
static box_dns_resolver_t *box_dnsinfo_default_resolver(box_dns_config_t *config, int32_t index) {
return config->resolver[index];
}
static box_dns_resolver_t *box_dnsinfo_scoped_resolver(box_dns_config_t *config, int32_t index) {
return config->scoped_resolver[index];
}
static struct sockaddr *box_dnsinfo_nameserver(box_dns_resolver_t *resolver, int32_t index) {
return resolver->nameserver[index];
}
static const char *box_dnsinfo_search_domain(box_dns_resolver_t *resolver, int32_t index) {
return resolver->search[index];
}
*/
import "C"
import (
"context"
"encoding/binary"
"net/netip"
"strconv"
"sync"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
)
type systemConfigSource struct {
interfaceMonitor tun.DefaultInterfaceMonitor
access sync.Mutex
notifyToken C.int
notifyValid bool
stale bool
interfaceIndex int
config *dnsConfig
}
func newSystemConfigSource(ctx context.Context) *systemConfigSource {
source := &systemConfigSource{
interfaceMonitor: service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor(),
}
if C.box_dnsinfo_load() != 0 {
var token C.int
if C.notify_register_check(C.box_dnsinfo_notify_key(), &token) == 0 {
source.notifyToken = token
source.notifyValid = true
}
}
return source
}
func (s *systemConfigSource) Configuration() *dnsConfig {
interfaceIndex := s.defaultInterfaceIndex()
s.access.Lock()
defer s.access.Unlock()
interfaceChanged := s.interfaceIndex != interfaceIndex
s.interfaceIndex = interfaceIndex
changed := s.changedLocked()
if s.config != nil && !s.stale && !interfaceChanged && !changed {
return s.config
}
s.stale = false
systemInfo := copyDNSInfo()
if systemInfo == nil {
if s.config == nil {
s.config = new(dnsInfoConfig).build(interfaceIndex)
}
return s.config
}
config := systemInfo.build(interfaceIndex)
if s.config != nil && config.equal(s.config) {
return s.config
}
s.config = config
return config
}
func (s *systemConfigSource) changedLocked() bool {
if !s.notifyValid {
return true
}
var changed C.int
status := C.notify_check(s.notifyToken, &changed)
if status != 0 {
return true
}
return changed != 0
}
func (s *systemConfigSource) Reset() {
s.access.Lock()
s.stale = true
s.access.Unlock()
}
func (s *systemConfigSource) Close() error {
s.access.Lock()
defer s.access.Unlock()
if s.notifyValid {
C.notify_cancel(s.notifyToken)
s.notifyValid = false
}
return nil
}
func (s *systemConfigSource) defaultInterfaceIndex() int {
if s.interfaceMonitor == nil {
return 0
}
defaultInterface := s.interfaceMonitor.DefaultInterface()
if defaultInterface == nil {
return 0
}
return defaultInterface.Index
}
type dnsInfoResolver struct {
interfaceIndex int
domain string
servers []M.Socksaddr
search []string
timeout time.Duration
}
type dnsInfoConfig struct {
resolvers []dnsInfoResolver
scopedResolvers []dnsInfoResolver
}
func (c *dnsInfoConfig) build(interfaceIndex int) *dnsConfig {
var selected dnsInfoResolver
if interfaceIndex != 0 {
selected = common.Find(c.scopedResolvers, func(it dnsInfoResolver) bool {
return it.interfaceIndex == interfaceIndex && len(it.servers) > 0
})
}
if len(selected.servers) == 0 {
selected = common.Find(c.resolvers, func(it dnsInfoResolver) bool {
return it.domain == "" && len(it.servers) > 0
})
}
config := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
attempts: 2,
}
if len(selected.servers) == 0 {
config.servers = defaultNS
config.search = dnsDefaultSearch()
return config
}
config.servers = selected.servers
if len(selected.search) > 0 {
config.search = selected.search
} else {
config.search = dnsDefaultSearch()
}
if selected.timeout > 0 {
config.timeout = selected.timeout
}
return config
}
func copyDNSInfo() *dnsInfoConfig {
if C.box_dnsinfo_load() == 0 {
return nil
}
rawConfig := C.box_dnsinfo_copy()
if rawConfig == nil {
return nil
}
defer C.box_dnsinfo_free(rawConfig)
systemInfo := new(dnsInfoConfig)
for i := C.int32_t(0); i < rawConfig.n_resolver; i++ {
rawResolver := C.box_dnsinfo_default_resolver(rawConfig, i)
if rawResolver == nil {
continue
}
systemInfo.resolvers = append(systemInfo.resolvers, parseResolver(rawResolver))
}
for i := C.int32_t(0); i < rawConfig.n_scoped_resolver; i++ {
rawResolver := C.box_dnsinfo_scoped_resolver(rawConfig, i)
if rawResolver == nil {
continue
}
systemInfo.scopedResolvers = append(systemInfo.scopedResolvers, parseResolver(rawResolver))
}
return systemInfo
}
func parseResolver(rawResolver *C.box_dns_resolver_t) dnsInfoResolver {
resolver := dnsInfoResolver{
interfaceIndex: int(rawResolver.if_index),
domain: C.GoString(rawResolver.domain),
timeout: time.Duration(rawResolver.timeout) * time.Second,
}
interfaceName := C.GoString(rawResolver.if_name)
resolverPort := uint16(rawResolver.port)
if resolverPort == 0 {
resolverPort = 53
}
for i := C.int32_t(0); i < rawResolver.n_nameserver; i++ {
rawSockaddr := C.box_dnsinfo_nameserver(rawResolver, i)
if rawSockaddr == nil {
continue
}
serverAddr, loaded := parseSockaddr(rawSockaddr, resolverPort, interfaceName)
if !loaded {
continue
}
resolver.servers = append(resolver.servers, M.SocksaddrFromNetIP(serverAddr))
}
for i := C.int32_t(0); i < rawResolver.n_search; i++ {
searchDomain := C.GoString(C.box_dnsinfo_search_domain(rawResolver, i))
if searchDomain == "" {
continue
}
searchDomain = mDNS.Fqdn(searchDomain)
if searchDomain == "." {
continue
}
resolver.search = append(resolver.search, searchDomain)
}
return resolver
}
func parseSockaddr(rawSockaddr *C.struct_sockaddr, fallbackPort uint16, zone string) (netip.AddrPort, bool) {
switch rawSockaddr.sa_family {
case C.AF_INET:
sockaddrInet := (*C.struct_sockaddr_in)(unsafe.Pointer(rawSockaddr))
addr := netip.AddrFrom4(*(*[4]byte)(unsafe.Pointer(&sockaddrInet.sin_addr)))
return netip.AddrPortFrom(addr, sockaddrPort(unsafe.Pointer(&sockaddrInet.sin_port), fallbackPort)), true
case C.AF_INET6:
sockaddrInet6 := (*C.struct_sockaddr_in6)(unsafe.Pointer(rawSockaddr))
addr := netip.AddrFrom16(*(*[16]byte)(unsafe.Pointer(&sockaddrInet6.sin6_addr)))
if addr.IsLinkLocalUnicast() {
scopeId := uint32(sockaddrInet6.sin6_scope_id)
if zone == "" && scopeId != 0 {
zone = strconv.FormatUint(uint64(scopeId), 10)
}
if zone != "" {
addr = addr.WithZone(zone)
}
}
return netip.AddrPortFrom(addr, sockaddrPort(unsafe.Pointer(&sockaddrInet6.sin6_port), fallbackPort)), true
default:
return netip.AddrPort{}, false
}
}
func sockaddrPort(rawPort unsafe.Pointer, fallbackPort uint16) uint16 {
port := binary.BigEndian.Uint16((*[2]byte)(rawPort)[:])
if port == 0 {
return fallbackPort
}
return port
}
+176
View File
@@ -0,0 +1,176 @@
//go:build !windows && !(darwin && cgo)
package local
import (
"bufio"
"context"
"net/netip"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
M "github.com/sagernet/sing/common/metadata"
mDNS "github.com/miekg/dns"
)
const resolvConfPath = "/etc/resolv.conf"
type systemConfigSource struct {
updateAccess sync.Mutex
lastChecked time.Time
current atomic.Pointer[resolvConfig]
}
type resolvConfig struct {
config *dnsConfig
mtime time.Time
noReload bool
}
func newSystemConfigSource(_ context.Context) *systemConfigSource {
source := &systemConfigSource{lastChecked: time.Now()}
source.current.Store(dnsReadConfig(resolvConfPath))
return source
}
func (s *systemConfigSource) Configuration() *dnsConfig {
s.tryUpdate()
return s.current.Load().config
}
func (s *systemConfigSource) tryUpdate() {
if s.current.Load().noReload {
return
}
if !s.updateAccess.TryLock() {
return
}
defer s.updateAccess.Unlock()
now := time.Now()
if s.lastChecked.After(now.Add(-5 * time.Second)) {
return
}
s.lastChecked = now
var mtime time.Time
fileInfo, err := os.Stat(resolvConfPath)
if err == nil {
mtime = fileInfo.ModTime()
}
current := s.current.Load()
if mtime.Equal(current.mtime) {
return
}
updated := dnsReadConfig(resolvConfPath)
if updated.config.equal(current.config) {
updated.config = current.config
}
s.current.Store(updated)
}
func (s *systemConfigSource) Reset() {
s.updateAccess.Lock()
s.lastChecked = time.Time{}
s.updateAccess.Unlock()
}
func (s *systemConfigSource) Close() error {
return nil
}
func dnsReadConfig(path string) *resolvConfig {
config := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
attempts: 2,
}
result := &resolvConfig{config: config}
file, err := os.Open(path)
if err != nil {
config.servers = defaultNS
config.search = dnsDefaultSearch()
return result
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
config.servers = defaultNS
config.search = dnsDefaultSearch()
return result
}
result.mtime = fileInfo.ModTime()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
fields := strings.Fields(line)
if len(fields) < 1 {
continue
}
switch fields[0] {
case "nameserver":
if len(fields) > 1 && len(config.servers) < 3 {
serverAddr, parseErr := netip.ParseAddr(fields[1])
if parseErr == nil {
config.servers = append(config.servers, M.SocksaddrFrom(serverAddr, 53))
}
}
case "domain":
if len(fields) > 1 {
config.search = []string{mDNS.Fqdn(fields[1])}
}
case "search":
config.search = make([]string, 0, len(fields)-1)
for _, searchDomain := range fields[1:] {
name := mDNS.Fqdn(searchDomain)
if name == "." {
continue
}
config.search = append(config.search, name)
}
case "options":
for _, option := range fields[1:] {
switch {
case strings.HasPrefix(option, "ndots:"):
value, parseErr := strconv.Atoi(option[len("ndots:"):])
if parseErr == nil {
config.ndots = min(max(value, 0), 15)
}
case strings.HasPrefix(option, "timeout:"):
value, parseErr := strconv.Atoi(option[len("timeout:"):])
if parseErr == nil {
config.timeout = time.Duration(max(value, 1)) * time.Second
}
case strings.HasPrefix(option, "attempts:"):
value, parseErr := strconv.Atoi(option[len("attempts:"):])
if parseErr == nil {
config.attempts = max(value, 1)
}
case option == "rotate":
config.rotate = true
case option == "single-request" || option == "single-request-reopen":
config.singleRequest = true
case option == "use-vc" || option == "usevc" || option == "tcp":
config.useTCP = true
case option == "trust-ad":
config.trustAD = true
case option == "no-reload":
result.noReload = true
}
}
}
}
if len(config.servers) == 0 {
config.servers = defaultNS
}
if len(config.search) == 0 {
config.search = dnsDefaultSearch()
}
return result
}
@@ -0,0 +1,181 @@
package local
import (
"context"
"net/netip"
"os"
"slices"
"strconv"
"sync"
"syscall"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
"golang.org/x/sys/windows"
)
type systemConfigSource struct {
interfaceMonitor tun.DefaultInterfaceMonitor
access sync.Mutex
updateCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
stale bool
config *dnsConfig
}
func newSystemConfigSource(ctx context.Context) *systemConfigSource {
source := &systemConfigSource{}
interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor()
if interfaceMonitor != nil {
source.interfaceMonitor = interfaceMonitor
source.updateCallback = interfaceMonitor.RegisterCallback(source.interfaceUpdated)
}
return source
}
func (s *systemConfigSource) Configuration() *dnsConfig {
s.access.Lock()
defer s.access.Unlock()
if s.config != nil && !s.stale && s.updateCallback != nil {
return s.config
}
s.stale = false
config := s.readConfig()
if s.config != nil && config.equal(s.config) {
return s.config
}
s.config = config
return config
}
func (s *systemConfigSource) interfaceUpdated(defaultInterface *control.Interface, flags int) {
s.access.Lock()
s.stale = true
s.access.Unlock()
}
func (s *systemConfigSource) Reset() {
s.access.Lock()
s.stale = true
s.access.Unlock()
}
func (s *systemConfigSource) Close() error {
s.access.Lock()
updateCallback := s.updateCallback
s.updateCallback = nil
s.access.Unlock()
if updateCallback != nil {
s.interfaceMonitor.UnregisterCallback(updateCallback)
}
return nil
}
func (s *systemConfigSource) readConfig() *dnsConfig {
config := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
attempts: 2,
}
defer func() {
if len(config.servers) == 0 {
config.servers = defaultNS
}
if len(config.search) == 0 {
config.search = dnsDefaultSearch()
}
}()
addresses, err := adapterAddresses()
if err != nil {
return config
}
var dnsAddresses []struct {
ifName string
netip.Addr
}
for _, address := range addresses {
if address.OperStatus != windows.IfOperStatusUp {
continue
}
if address.IfType == windows.IF_TYPE_TUNNEL {
continue
}
if address.FirstGatewayAddress == nil {
continue
}
for dnsServerAddress := address.FirstDnsServerAddress; dnsServerAddress != nil; dnsServerAddress = dnsServerAddress.Next {
rawSockaddr, sockaddrErr := dnsServerAddress.Address.Sockaddr.Sockaddr()
if sockaddrErr != nil {
continue
}
var dnsServerAddr netip.Addr
switch sockaddr := rawSockaddr.(type) {
case *syscall.SockaddrInet4:
dnsServerAddr = netip.AddrFrom4(sockaddr.Addr)
case *syscall.SockaddrInet6:
if sockaddr.Addr[0] == 0xfe && sockaddr.Addr[1] == 0xc0 {
// fec0::/10 site local anycast addresses are set by
// Windows itself when no IPv6 DNS server is configured.
continue
}
dnsServerAddr = netip.AddrFrom16(sockaddr.Addr)
if sockaddr.ZoneId != 0 {
dnsServerAddr = dnsServerAddr.WithZone(strconv.FormatInt(int64(sockaddr.ZoneId), 10))
}
default:
continue
}
dnsAddresses = append(dnsAddresses, struct {
ifName string
netip.Addr
}{ifName: windows.UTF16PtrToString(address.FriendlyName), Addr: dnsServerAddr})
}
}
var myInterfaces []string
if s.interfaceMonitor != nil {
myInterfaces = s.interfaceMonitor.MyInterfaces()
}
var servers []M.Socksaddr
for _, address := range dnsAddresses {
if slices.Contains(myInterfaces, address.ifName) {
continue
}
servers = append(servers, M.SocksaddrFrom(address.Addr, 53))
}
config.servers = common.Uniq(servers)
return config
}
func adapterAddresses() ([]*windows.IpAdapterAddresses, error) {
var b []byte
l := uint32(15000)
for {
b = make([]byte, l)
const flags = windows.GAA_FLAG_INCLUDE_PREFIX | windows.GAA_FLAG_INCLUDE_GATEWAYS
err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, flags, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l)
if err == nil {
if l == 0 {
return nil, nil
}
break
}
if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW {
return nil, os.NewSyscallError("getadaptersaddresses", err)
}
if l <= uint32(len(b)) {
return nil, os.NewSyscallError("getadaptersaddresses", err)
}
}
var aas []*windows.IpAdapterAddresses
for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next {
aas = append(aas, aa)
}
return aas, nil
}