Reset network on DNS server changes

This commit is contained in:
世界
2026-10-09 12:36:03 +08:00
parent 1a773a425a
commit 78d44d52d9
4 changed files with 88 additions and 281 deletions
+17 -275
View File
@@ -2,129 +2,14 @@
package systemconfig
/*
#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-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
}
+68 -3
View File
@@ -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()
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=