From 78d44d52d9599770bf2b30351982a7339aa86481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 9 Oct 2026 10:02:50 +0800 Subject: [PATCH] Reset network on DNS server changes --- .../local/systemconfig/source_darwin.go | 292 +----------------- experimental/libbox/monitor.go | 71 ++++- go.mod | 2 +- go.sum | 4 +- 4 files changed, 88 insertions(+), 281 deletions(-) diff --git a/dns/transport/local/systemconfig/source_darwin.go b/dns/transport/local/systemconfig/source_darwin.go index e8bda779..38d74848 100644 --- a/dns/transport/local/systemconfig/source_darwin.go +++ b/dns/transport/local/systemconfig/source_darwin.go @@ -2,129 +2,14 @@ package systemconfig -/* -#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-tun/dnsinfo" "github.com/sagernet/sing/common" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/service" @@ -135,25 +20,15 @@ import ( type Source struct { interfaceMonitor tun.DefaultInterfaceMonitor access sync.Mutex - notifyToken C.int - notifyValid bool stale bool interfaceIndex int config *Config } func NewSource(ctx context.Context) *Source { - source := &Source{ + return &Source{ 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 *Source) Configuration() *Config { @@ -162,19 +37,18 @@ func (s *Source) Configuration() *Config { defer s.access.Unlock() interfaceChanged := s.interfaceIndex != interfaceIndex s.interfaceIndex = interfaceIndex - changed := s.changedLocked() - if s.config != nil && !s.stale && !interfaceChanged && !changed { + if s.config != nil && !s.stale && !interfaceChanged && s.interfaceMonitor != nil { return s.config } s.stale = false - systemInfo := copyDNSInfo() - if systemInfo == nil { + dnsConfiguration := dnsinfo.Copy() + if dnsConfiguration == nil { if s.config == nil { - s.config = new(dnsInfoConfig).build(interfaceIndex) + s.config = newConfig(dnsinfo.Resolver{}) } return s.config } - config := systemInfo.build(interfaceIndex) + config := newConfig(dnsConfiguration.Select(interfaceIndex)) if s.config != nil && config.Equal(s.config) { return s.config } @@ -182,18 +56,6 @@ func (s *Source) Configuration() *Config { return config } -func (s *Source) 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 *Source) Reset() { s.access.Lock() s.stale = true @@ -201,12 +63,6 @@ func (s *Source) Reset() { } func (s *Source) Close() error { - s.access.Lock() - defer s.access.Unlock() - if s.notifyValid { - C.notify_cancel(s.notifyToken) - s.notifyValid = false - } return nil } @@ -221,144 +77,30 @@ func (s *Source) defaultInterfaceIndex() int { 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) *Config { - 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 - }) - } +func newConfig(resolver dnsinfo.Resolver) *Config { config := &Config{ Ndots: 1, Timeout: 5 * time.Second, Attempts: 2, } - if len(selected.servers) == 0 { + if len(resolver.Servers) == 0 { config.Servers = defaultServers config.Search = defaultSearch() return config } - config.Servers = selected.servers - if len(selected.search) > 0 { - config.Search = selected.search - } else { - config.Search = defaultSearch() - } - 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 - } + config.Servers = common.Map(resolver.Servers, M.SocksaddrFromNetIP) + for _, searchDomain := range resolver.Search { searchDomain = mDNS.Fqdn(searchDomain) if searchDomain == "." { continue } - resolver.search = append(resolver.search, searchDomain) + config.Search = append(config.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 + if len(config.Search) == 0 { + config.Search = defaultSearch() } -} - -func sockaddrPort(rawPort unsafe.Pointer, fallbackPort uint16) uint16 { - port := binary.BigEndian.Uint16((*[2]byte)(rawPort)[:]) - if port == 0 { - return fallbackPort + if resolver.Timeout > 0 { + config.Timeout = resolver.Timeout } - return port + return config } diff --git a/experimental/libbox/monitor.go b/experimental/libbox/monitor.go index 1fe21d72..e42ea17d 100644 --- a/experimental/libbox/monitor.go +++ b/experimental/libbox/monitor.go @@ -1,8 +1,15 @@ package libbox import ( + "net/netip" + "slices" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/service/powerreport" tun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun/dnsinfo" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" @@ -20,15 +27,35 @@ type platformDefaultInterfaceMonitor struct { callbacks list.List[tun.DefaultInterfaceUpdateCallback] myInterfaces []string defaultInterfaceInitialized bool + defaultDNSServers []string + dnsWatcher *dnsinfo.Watcher lastNetworkPath string } func (m *platformDefaultInterfaceMonitor) Start() error { - return m.iif.StartDefaultInterfaceMonitor(m) + if C.IsDarwin { + dnsWatcher, err := dnsinfo.NewWatcher(m.updateDNSServers, m.logger) + if err != nil { + return E.Cause(err, "watch DNS configuration") + } + m.dnsWatcher = dnsWatcher + } + err := m.iif.StartDefaultInterfaceMonitor(m) + if err != nil { + if C.IsDarwin { + m.dnsWatcher.Close() + } + return err + } + return nil } func (m *platformDefaultInterfaceMonitor) Close() error { - return m.iif.CloseDefaultInterfaceMonitor(m) + err := m.iif.CloseDefaultInterfaceMonitor(m) + if C.IsDarwin { + err = E.Errors(err, m.dnsWatcher.Close()) + } + return err } func (m *platformDefaultInterfaceMonitor) DefaultInterface() *control.Interface { @@ -113,6 +140,7 @@ func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName s m.defaultInterfaceAccess.Lock() if interfaceIndex32 == -1 { m.defaultInterface = nil + m.defaultDNSServers = nil m.defaultInterfaceInitialized = true callbacks := m.callbacks.Array() m.defaultInterfaceAccess.Unlock() @@ -128,8 +156,10 @@ func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName s m.logger.Error(E.Cause(err, "find updated interface: ", interfaceName)) return } + oldDNSServers := m.defaultDNSServers m.defaultInterface = newInterface - if m.defaultInterfaceInitialized && oldInterface != nil && oldInterface.Name == m.defaultInterface.Name && oldInterface.Index == m.defaultInterface.Index { + m.defaultDNSServers = m.readDNSServers(newInterface.Index) + if m.defaultInterfaceInitialized && oldInterface != nil && oldInterface.Name == m.defaultInterface.Name && oldInterface.Index == m.defaultInterface.Index && slices.Equal(oldDNSServers, m.defaultDNSServers) { m.defaultInterfaceAccess.Unlock() return } @@ -141,6 +171,41 @@ func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName s } } +func (m *platformDefaultInterfaceMonitor) updateDNSServers() { + m.defaultInterfaceAccess.Lock() + defaultInterface := m.defaultInterface + if defaultInterface == nil { + m.defaultInterfaceAccess.Unlock() + return + } + dnsServers := m.readDNSServers(defaultInterface.Index) + if slices.Equal(m.defaultDNSServers, dnsServers) { + m.defaultInterfaceAccess.Unlock() + return + } + m.defaultDNSServers = dnsServers + callbacks := m.callbacks.Array() + m.defaultInterfaceAccess.Unlock() + for _, callback := range callbacks { + callback(defaultInterface, 0) + } +} + +func (m *platformDefaultInterfaceMonitor) readDNSServers(interfaceIndex int) []string { + if C.IsDarwin { + dnsConfiguration := dnsinfo.Copy() + if dnsConfiguration == nil { + return nil + } + return common.Map(dnsConfiguration.Select(interfaceIndex).Servers, func(it netip.AddrPort) string { + return it.Addr().String() + }) + } + return common.Find(m.networkManager.NetworkInterfaces(), func(it adapter.NetworkInterface) bool { + return it.Index == interfaceIndex + }).DNSServers +} + func (m *platformDefaultInterfaceMonitor) RegisterMyInterface(interfaceName string) { m.defaultInterfaceAccess.Lock() defer m.defaultInterfaceAccess.Unlock() diff --git a/go.mod b/go.mod index fdf2cefa..53b8a1ca 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/sagernet/sing-shadowsocks2 v0.2.2-0.20260929152114-a69d1086332b github.com/sagernet/sing-shadowtls v0.2.2-0.20260928201441-a9c0127d5c99 github.com/sagernet/sing-snell v0.0.0-20260904135315-bc5a12ac736f - github.com/sagernet/sing-tun v0.9.7-0.20261007151649-0e9e4a586ece + github.com/sagernet/sing-tun v0.9.7-0.20261009020158-bca4bf029237 github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca github.com/sagernet/sing-vmess v0.2.9-0.20260929152519-9b95ab8c9478 github.com/sagernet/smux v1.5.50-sing-box-mod.1 diff --git a/go.sum b/go.sum index 06f15b8e..ed70f587 100644 --- a/go.sum +++ b/go.sum @@ -338,8 +338,8 @@ github.com/sagernet/sing-shadowtls v0.2.2-0.20260928201441-a9c0127d5c99 h1:l/tdZ github.com/sagernet/sing-shadowtls v0.2.2-0.20260928201441-a9c0127d5c99/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= github.com/sagernet/sing-snell v0.0.0-20260904135315-bc5a12ac736f h1:tbTCI+iis5sTmua7a6Q7vebcSRURok/LGgnEafnQAsc= github.com/sagernet/sing-snell v0.0.0-20260904135315-bc5a12ac736f/go.mod h1:et8Lws4f5QbOrY65DmjevHGup3mijJkhswkto6cwciM= -github.com/sagernet/sing-tun v0.9.7-0.20261007151649-0e9e4a586ece h1://BO1bRUuh7JjQ2NFRvXt+A2VXd1mBbp5G2lhv5fCag= -github.com/sagernet/sing-tun v0.9.7-0.20261007151649-0e9e4a586ece/go.mod h1:3EgPst7agntRO7D6GOsiZ1l9FoqdLeuWmKT5TnWkmf0= +github.com/sagernet/sing-tun v0.9.7-0.20261009020158-bca4bf029237 h1:B9tUcRf1FAatPyzN8sKEvIHMvKNkFMBZikeIOL4gqtQ= +github.com/sagernet/sing-tun v0.9.7-0.20261009020158-bca4bf029237/go.mod h1:3EgPst7agntRO7D6GOsiZ1l9FoqdLeuWmKT5TnWkmf0= github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca h1:5wA+IE0Fq1CGVLOgSpm0gKKZ03HzugcJ2JyhmDqeX6A= github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca/go.mod h1:ADAZZU85MnM91XNhd2TdZRTaIbA7RjTZuQ1KCHcUbNg= github.com/sagernet/sing-vmess v0.2.9-0.20260929152519-9b95ab8c9478 h1:q2eQn4nq8oWGxRm9zXesXSlPr9GEgPrOGzf4iPtzG9A=