diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go index 98c8bdfd..7e33a55c 100644 --- a/dns/transport/local/local.go +++ b/dns/transport/local/local.go @@ -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) } diff --git a/dns/transport/local/local_darwin.go b/dns/transport/local/local_darwin.go index e033ba12..3fd60a30 100644 --- a/dns/transport/local/local_darwin.go +++ b/dns/transport/local/local_darwin.go @@ -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 diff --git a/dns/transport/local/local_darwin_test.go b/dns/transport/local/local_darwin_test.go index 52bb3ed6..8211c02d 100644 --- a/dns/transport/local/local_darwin_test.go +++ b/dns/transport/local/local_darwin_test.go @@ -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() diff --git a/dns/transport/local/local_dhcp.go b/dns/transport/local/local_dhcp.go deleted file mode 100644 index bf77ed25..00000000 --- a/dns/transport/local/local_dhcp.go +++ /dev/null @@ -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) -} diff --git a/dns/transport/local/local_nodhcp.go b/dns/transport/local/local_nodhcp.go deleted file mode 100644 index 7893d416..00000000 --- a/dns/transport/local/local_nodhcp.go +++ /dev/null @@ -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 -} diff --git a/dns/transport/local/local_shared.go b/dns/transport/local/local_shared.go index cd011d78..d26e5f80 100644 --- a/dns/transport/local/local_shared.go +++ b/dns/transport/local/local_shared.go @@ -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) diff --git a/dns/transport/local/resolv.go b/dns/transport/local/resolv.go deleted file mode 100644 index 4aa10a64..00000000 --- a/dns/transport/local/resolv.go +++ /dev/null @@ -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") -} diff --git a/dns/transport/local/resolv_default.go b/dns/transport/local/resolv_default.go deleted file mode 100644 index 9c5e8fa2..00000000 --- a/dns/transport/local/resolv_default.go +++ /dev/null @@ -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 -} diff --git a/dns/transport/local/resolv_test.go b/dns/transport/local/resolv_test.go deleted file mode 100644 index 546e8408..00000000 --- a/dns/transport/local/resolv_test.go +++ /dev/null @@ -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) -} diff --git a/dns/transport/local/resolv_unix.go b/dns/transport/local/resolv_unix.go deleted file mode 100644 index 51512f65..00000000 --- a/dns/transport/local/resolv_unix.go +++ /dev/null @@ -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 -} diff --git a/dns/transport/local/resolv_windows.go b/dns/transport/local/resolv_windows.go deleted file mode 100644 index c22e394a..00000000 --- a/dns/transport/local/resolv_windows.go +++ /dev/null @@ -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 -} diff --git a/dns/transport/local/system_config.go b/dns/transport/local/system_config.go new file mode 100644 index 00000000..00f5aa3b --- /dev/null +++ b/dns/transport/local/system_config.go @@ -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)} +} diff --git a/dns/transport/local/system_config_darwin.go b/dns/transport/local/system_config_darwin.go new file mode 100644 index 00000000..5b59f761 --- /dev/null +++ b/dns/transport/local/system_config_darwin.go @@ -0,0 +1,364 @@ +//go:build cgo + +package local + +/* +#include +#include +#include +#include +#include +#include + +// 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 +} diff --git a/dns/transport/local/system_config_resolv.go b/dns/transport/local/system_config_resolv.go new file mode 100644 index 00000000..ad2a530f --- /dev/null +++ b/dns/transport/local/system_config_resolv.go @@ -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 +} diff --git a/dns/transport/local/system_config_windows.go b/dns/transport/local/system_config_windows.go new file mode 100644 index 00000000..ebeeb6c7 --- /dev/null +++ b/dns/transport/local/system_config_windows.go @@ -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 +}