mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
Refactor local DNS transport
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user