diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go index 34897e128..816a59a6e 100644 --- a/dns/transport/local/local.go +++ b/dns/transport/local/local.go @@ -40,6 +40,7 @@ type Transport struct { resolved ResolvedResolver mdnsTransport adapter.DNSTransport dhcpTransport dhcpTransport + systemResolver systemResolver neighborResolver adapter.NeighborResolver neighborSuffixes []string } @@ -50,6 +51,12 @@ type dhcpTransport interface { Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error) } +type systemResolver interface { + Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) + Reset() + Close() error +} + 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 { @@ -91,6 +98,7 @@ func (t *Transport) Start(stage adapter.StartStage) error { } case adapter.StartStateStart: if C.IsDarwin { + t.systemResolver = newSystemResolver() inboundManager := service.FromContext[adapter.InboundManager](t.ctx) for _, inbound := range inboundManager.Inbounds() { if inbound.Type() == C.TypeTun { @@ -127,7 +135,7 @@ func (t *Transport) Start(stage adapter.StartStage) error { } func (t *Transport) Close() error { - return common.Close(t.resolved, t.dhcpTransport, t.mdnsTransport) + return common.Close(t.resolved, t.dhcpTransport, t.mdnsTransport, t.systemResolver) } func (t *Transport) Reset() { @@ -137,6 +145,9 @@ func (t *Transport) Reset() { if t.mdnsTransport != nil { t.mdnsTransport.Reset() } + if t.systemResolver != nil { + t.systemResolver.Reset() + } } func (t *Transport) PreferredDomain(domain string) bool { @@ -162,7 +173,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, } if mdns.IsLocalDomain(question.Name) { if C.IsDarwin { - return t.systemExchange(ctx, message) + return t.systemResolver.Exchange(ctx, message) } return t.mdnsTransport.Exchange(ctx, message) } @@ -176,7 +187,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, } } if t.fallback { - return t.systemExchange(ctx, message) + return t.systemResolver.Exchange(ctx, message) } return t.exchange(ctx, message, question.Name) } diff --git a/dns/transport/local/local_darwin_cgo.go b/dns/transport/local/local_darwin_cgo.go index de2ec56a4..96d7362fb 100644 --- a/dns/transport/local/local_darwin_cgo.go +++ b/dns/transport/local/local_darwin_cgo.go @@ -7,20 +7,14 @@ package local #include #include -static int cgo_dns_search(const char *name, int class, int type, +static int cgo_dns_search(dns_handle_t handle, const char *name, int class, int type, unsigned char *answer, int anslen, int *out_h_errno) { - dns_handle_t handle = (dns_handle_t)dns_open(NULL); - if (handle == NULL) { - *out_h_errno = NO_RECOVERY; - return -1; - } struct sockaddr_storage from; uint32_t fromlen = sizeof(from); h_errno = 0; int n = dns_search(handle, name, class, type, (char *)answer, anslen, (struct sockaddr *)&from, &fromlen); *out_h_errno = h_errno; - dns_free(handle); return n; } */ @@ -29,6 +23,8 @@ import "C" import ( "context" "errors" + "net" + "sync" "unsafe" "github.com/sagernet/sing-box/dns" @@ -44,40 +40,107 @@ const ( darwinResolverNoData = 4 ) -func darwinLookupSystemDNS(name string, class, qtype int) (*mDNS.Msg, error) { - cName := C.CString(name) - defer C.free(unsafe.Pointer(cName)) - - answer := make([]byte, 4096) - var hErrno C.int - n := C.cgo_dns_search(cName, C.int(class), C.int(qtype), - (*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)), - &hErrno) - if n <= 0 { - return nil, darwinResolverHErrno(name, int(hErrno)) - } - var response mDNS.Msg - err := response.Unpack(answer[:int(n)]) - if err != nil { - return nil, E.Cause(err, "unpack dns_search response") - } - return &response, nil +type darwinDNSHandle struct { + handle C.dns_handle_t + generation uint64 } -func darwinResolverHErrno(name string, hErrno int) error { - switch hErrno { - case darwinResolverHostNotFound: - return dns.RcodeNameError - case darwinResolverNoData: - return dns.RcodeSuccess - case darwinResolverTryAgain, darwinResolverNoRecovery: - return dns.RcodeServerFailure - default: - return E.New("dns_search: unknown h_errno ", hErrno, " for ", name) +// darwinSystemResolver pools libresolv handles. iOS 26.5.1 NULL-derefs in +// libresolv when resolver state is rebuilt by concurrent dns_open(NULL) calls, +// and dns_search corrupts state when two queries share one handle. Handles are +// therefore reused across queries and leased exclusively: each in-flight query +// owns one handle, dns_search runs concurrently on independent handles, while +// dns_open and dns_free are serialized by lifecycleAccess. A leased handle is +// removed from idle and owned by its caller, so Reset/Close only ever free idle +// handles and the caller frees its own handle on release — dns_free never races +// an in-flight dns_search and is never called twice for the same handle. +type darwinSystemResolver struct { + access sync.Mutex + lifecycleAccess sync.Mutex + idle []*darwinDNSHandle + generation uint64 + closed bool +} + +func newSystemResolver() systemResolver { + return &darwinSystemResolver{} +} + +func (r *darwinSystemResolver) acquire() (*darwinDNSHandle, error) { + r.access.Lock() + if r.closed { + r.access.Unlock() + return nil, net.ErrClosed + } + if count := len(r.idle); count > 0 { + handle := r.idle[count-1] + r.idle[count-1] = nil + r.idle = r.idle[:count-1] + r.access.Unlock() + return handle, nil + } + generation := r.generation + r.access.Unlock() + + r.lifecycleAccess.Lock() + cHandle := C.dns_open(nil) + r.lifecycleAccess.Unlock() + if cHandle == nil { + return nil, dns.RcodeServerFailure + } + return &darwinDNSHandle{handle: cHandle, generation: generation}, nil +} + +func (r *darwinSystemResolver) release(handle *darwinDNSHandle) { + r.access.Lock() + reuse := !r.closed && handle.generation == r.generation + if reuse { + r.idle = append(r.idle, handle) + } + r.access.Unlock() + if !reuse { + r.free(handle) } } -func (t *Transport) systemExchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { +func (r *darwinSystemResolver) free(handle *darwinDNSHandle) { + r.lifecycleAccess.Lock() + C.dns_free(handle.handle) + r.lifecycleAccess.Unlock() +} + +func (r *darwinSystemResolver) Reset() { + r.access.Lock() + if r.closed { + r.access.Unlock() + return + } + r.generation++ + idle := r.idle + r.idle = nil + r.access.Unlock() + for _, handle := range idle { + r.free(handle) + } +} + +func (r *darwinSystemResolver) Close() error { + r.access.Lock() + if r.closed { + r.access.Unlock() + return nil + } + r.closed = true + idle := r.idle + r.idle = nil + r.access.Unlock() + for _, handle := range idle { + r.free(handle) + } + return nil +} + +func (r *darwinSystemResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { question := message.Question[0] type resolvResult struct { response *mDNS.Msg @@ -85,7 +148,7 @@ func (t *Transport) systemExchange(ctx context.Context, message *mDNS.Msg) (*mDN } resultCh := make(chan resolvResult, 1) go func() { - response, err := darwinLookupSystemDNS(question.Name, int(question.Qclass), int(question.Qtype)) + response, err := r.lookup(question.Name, int(question.Qclass), int(question.Qtype)) resultCh <- resolvResult{response, err} }() var result resolvResult @@ -116,3 +179,46 @@ func (t *Transport) systemExchange(ctx context.Context, message *mDNS.Msg) (*mDN result.response.Response = true return result.response, nil } + +func (r *darwinSystemResolver) lookup(name string, class, qtype int) (*mDNS.Msg, error) { + handle, err := r.acquire() + if err != nil { + return nil, err + } + response, err := darwinSearch(handle.handle, name, class, qtype) + r.release(handle) + return response, err +} + +func darwinSearch(handle C.dns_handle_t, name string, class, qtype int) (*mDNS.Msg, error) { + cName := C.CString(name) + defer C.free(unsafe.Pointer(cName)) + + answer := make([]byte, 4096) + var hErrno C.int + n := C.cgo_dns_search(handle, cName, C.int(class), C.int(qtype), + (*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)), + &hErrno) + if n <= 0 { + return nil, darwinResolverHErrno(name, int(hErrno)) + } + var response mDNS.Msg + err := response.Unpack(answer[:int(n)]) + if err != nil { + return nil, E.Cause(err, "unpack dns_search response") + } + return &response, nil +} + +func darwinResolverHErrno(name string, hErrno int) error { + switch hErrno { + case darwinResolverHostNotFound: + return dns.RcodeNameError + case darwinResolverNoData: + return dns.RcodeSuccess + case darwinResolverTryAgain, darwinResolverNoRecovery: + return dns.RcodeServerFailure + default: + return E.New("dns_search: unknown h_errno ", hErrno, " for ", name) + } +} diff --git a/dns/transport/local/local_darwin_stun.go b/dns/transport/local/local_darwin_stun.go index b99833c26..a817d0c19 100644 --- a/dns/transport/local/local_darwin_stun.go +++ b/dns/transport/local/local_darwin_stun.go @@ -10,6 +10,18 @@ import ( mDNS "github.com/miekg/dns" ) -func (t *Transport) systemExchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { +func newSystemResolver() systemResolver { + return &cgoRequiredResolver{} +} + +type cgoRequiredResolver struct{} + +func (r *cgoRequiredResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { return nil, E.New(`local DNS server requires CGO on darwin, rebuild with CGO_ENABLED=1`) } + +func (r *cgoRequiredResolver) Reset() {} + +func (r *cgoRequiredResolver) Close() error { + return nil +} diff --git a/dns/transport/local/local_other.go b/dns/transport/local/local_other.go index 9bb3d7777..5dfa48ba7 100644 --- a/dns/transport/local/local_other.go +++ b/dns/transport/local/local_other.go @@ -2,13 +2,6 @@ package local -import ( - "context" - "os" - - mDNS "github.com/miekg/dns" -) - -func (t *Transport) systemExchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { - return nil, os.ErrInvalid +func newSystemResolver() systemResolver { + return nil }