Use libresolv in local DNS server on darwin

This commit is contained in:
世界
2025-08-18 14:19:49 +08:00
parent b1ea333bd4
commit 490daad0a7
14 changed files with 380 additions and 300 deletions
+17 -6
View File
@@ -22,6 +22,10 @@ import (
mDNS "github.com/miekg/dns"
)
func RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport)
}
var _ adapter.DNSTransport = (*Transport)(nil)
type Transport struct {
@@ -30,10 +34,14 @@ type Transport struct {
logger logger.ContextLogger
hosts *hosts.File
dialer N.Dialer
preferGo bool
resolved ResolvedResolver
}
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
if C.IsDarwin && !options.PreferGo {
return NewResolvTransport(ctx, logger, tag)
}
transportDialer, err := dns.NewLocalDialer(ctx, options)
if err != nil {
return nil, err
@@ -44,19 +52,22 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
logger: logger,
hosts: hosts.NewFile(hosts.DefaultPath),
dialer: transportDialer,
preferGo: options.PreferGo,
}, nil
}
func (t *Transport) Start(stage adapter.StartStage) error {
switch stage {
case adapter.StartStateInitialize:
resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger)
if err == nil {
err = resolvedResolver.Start()
if !t.preferGo {
resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger)
if err == nil {
t.resolved = resolvedResolver
} else {
t.logger.Warn(E.Cause(err, "initialize resolved resolver"))
err = resolvedResolver.Start()
if err == nil {
t.resolved = resolvedResolver
} else {
t.logger.Warn(E.Cause(err, "initialize resolved resolver"))
}
}
}
}
-209
View File
@@ -1,209 +0,0 @@
package local
import (
"context"
"errors"
"net"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
)
func RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewFallbackTransport)
}
type FallbackTransport struct {
adapter.DNSTransport
ctx context.Context
fallback bool
resolver net.Resolver
}
func NewFallbackTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
transport, err := NewTransport(ctx, logger, tag, options)
if err != nil {
return nil, err
}
platformInterface := service.FromContext[platform.Interface](ctx)
if platformInterface == nil {
return transport, nil
}
return &FallbackTransport{
DNSTransport: transport,
ctx: ctx,
}, nil
}
func (f *FallbackTransport) Start(stage adapter.StartStage) error {
err := f.DNSTransport.Start(stage)
if err != nil {
return err
}
if stage != adapter.StartStatePostStart {
return nil
}
inboundManager := service.FromContext[adapter.InboundManager](f.ctx)
for _, inbound := range inboundManager.Inbounds() {
if inbound.Type() == C.TypeTun {
// platform tun hijacks DNS, so we can only use cgo resolver here
f.fallback = true
break
}
}
return nil
}
func (f *FallbackTransport) Close() error {
return f.DNSTransport.Close()
}
func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !f.fallback {
return f.DNSTransport.Exchange(ctx, message)
}
question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
var network string
if question.Qtype == mDNS.TypeA {
network = "ip4"
} else {
network = "ip6"
}
addresses, err := f.resolver.LookupNetIP(ctx, network, domain)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
} else if question.Qtype == mDNS.TypeNS {
records, err := f.resolver.LookupNS(ctx, domain)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
response := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeSuccess,
Response: true,
},
Question: []mDNS.Question{question},
}
for _, record := range records {
response.Answer = append(response.Answer, &mDNS.NS{
Hdr: mDNS.RR_Header{
Name: question.Name,
Rrtype: mDNS.TypeNS,
Class: mDNS.ClassINET,
Ttl: C.DefaultDNSTTL,
},
Ns: record.Host,
})
}
return response, nil
} else if question.Qtype == mDNS.TypeCNAME {
cname, err := f.resolver.LookupCNAME(ctx, domain)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeSuccess,
Response: true,
},
Question: []mDNS.Question{question},
Answer: []mDNS.RR{
&mDNS.CNAME{
Hdr: mDNS.RR_Header{
Name: question.Name,
Rrtype: mDNS.TypeCNAME,
Class: mDNS.ClassINET,
Ttl: C.DefaultDNSTTL,
},
Target: cname,
},
},
}, nil
} else if question.Qtype == mDNS.TypeTXT {
records, err := f.resolver.LookupTXT(ctx, domain)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeSuccess,
Response: true,
},
Question: []mDNS.Question{question},
Answer: []mDNS.RR{
&mDNS.TXT{
Hdr: mDNS.RR_Header{
Name: question.Name,
Rrtype: mDNS.TypeCNAME,
Class: mDNS.ClassINET,
Ttl: C.DefaultDNSTTL,
},
Txt: records,
},
},
}, nil
} else if question.Qtype == mDNS.TypeMX {
records, err := f.resolver.LookupMX(ctx, domain)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
response := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeSuccess,
Response: true,
},
Question: []mDNS.Question{question},
}
for _, record := range records {
response.Answer = append(response.Answer, &mDNS.MX{
Hdr: mDNS.RR_Header{
Name: question.Name,
Rrtype: mDNS.TypeA,
Class: mDNS.ClassINET,
Ttl: C.DefaultDNSTTL,
},
Preference: record.Pref,
Mx: record.Host,
})
}
return response, nil
} else {
return nil, E.New("only A, AAAA, NS, CNAME, TXT, MX queries are supported on current platform when using TUN, please switch to a fixed DNS server.")
}
}
+46
View File
@@ -0,0 +1,46 @@
//go:build darwin
package local
import (
"context"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/logger"
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSTransport = (*ResolvTransport)(nil)
type ResolvTransport struct {
dns.TransportAdapter
ctx context.Context
logger logger.ContextLogger
}
func NewResolvTransport(ctx context.Context, logger log.ContextLogger, tag string) (adapter.DNSTransport, error) {
return &ResolvTransport{
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeLocal, tag, nil),
ctx: ctx,
logger: logger,
}, nil
}
func (t *ResolvTransport) Start(stage adapter.StartStage) error {
return nil
}
func (t *ResolvTransport) Close() error {
return nil
}
func (t *ResolvTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
return doBlockingWithCtx(ctx, func() (*mDNS.Msg, error) {
return cgoResSearch(question.Name, int(question.Qtype), int(question.Qclass))
})
}
@@ -0,0 +1,170 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin
package local
import (
"context"
"errors"
"runtime"
"syscall"
"unsafe"
_ "unsafe"
E "github.com/sagernet/sing/common/exceptions"
mDNS "github.com/miekg/dns"
)
type (
_C_char = byte
_C_int = int32
_C_uchar = byte
_C_ushort = uint16
_C_uint = uint32
_C_ulong = uint64
_C_struct___res_state = ResState
_C_struct_sockaddr = syscall.RawSockaddr
)
func _C_free(p unsafe.Pointer) { runtime.KeepAlive(p) }
func _C_malloc(n uintptr) unsafe.Pointer {
if n <= 0 {
n = 1
}
return unsafe.Pointer(&make([]byte, n)[0])
}
const (
MAXNS = 3
MAXDNSRCH = 6
)
type ResState struct {
Retrans _C_int
Retry _C_int
Options _C_ulong
Nscount _C_int
Nsaddrlist [MAXNS]_C_struct_sockaddr
Id _C_ushort
Dnsrch [MAXDNSRCH + 1]*_C_char
Defname [256]_C_char
Pfcode _C_ulong
Ndots _C_uint
Nsort _C_uint
stub [128]byte
}
//go:linkname ResNinit internal/syscall/unix.ResNinit
func ResNinit(state *_C_struct___res_state) error
//go:linkname ResNsearch internal/syscall/unix.ResNsearch
func ResNsearch(state *_C_struct___res_state, dname *byte, class, typ int, ans *byte, anslen int) (int, error)
//go:linkname ResNclose internal/syscall/unix.ResNclose
func ResNclose(state *_C_struct___res_state)
//go:linkname GoString internal/syscall/unix.GoString
func GoString(p *byte) string
// doBlockingWithCtx executes a blocking function in a separate goroutine when the provided
// context is cancellable. It is intended for use with calls that don't support context
// cancellation (cgo, syscalls). blocking func may still be running after this function finishes.
// For the duration of the execution of the blocking function, the thread is 'acquired' using [acquireThread],
// blocking might not be executed when the context gets canceled early.
func doBlockingWithCtx[T any](ctx context.Context, blocking func() (T, error)) (T, error) {
if err := acquireThread(ctx); err != nil {
var zero T
return zero, err
}
if ctx.Done() == nil {
defer releaseThread()
return blocking()
}
type result struct {
res T
err error
}
res := make(chan result, 1)
go func() {
defer releaseThread()
var r result
r.res, r.err = blocking()
res <- r
}()
select {
case r := <-res:
return r.res, r.err
case <-ctx.Done():
var zero T
return zero, ctx.Err()
}
}
//go:linkname acquireThread net.acquireThread
func acquireThread(ctx context.Context) error
//go:linkname releaseThread net.releaseThread
func releaseThread()
func cgoResSearch(hostname string, rtype, class int) (*mDNS.Msg, error) {
resStateSize := unsafe.Sizeof(_C_struct___res_state{})
var state *_C_struct___res_state
if resStateSize > 0 {
mem := _C_malloc(resStateSize)
defer _C_free(mem)
memSlice := unsafe.Slice((*byte)(mem), resStateSize)
clear(memSlice)
state = (*_C_struct___res_state)(unsafe.Pointer(&memSlice[0]))
}
if err := ResNinit(state); err != nil {
return nil, errors.New("res_ninit failure: " + err.Error())
}
defer ResNclose(state)
bufSize := maxDNSPacketSize
buf := (*_C_uchar)(_C_malloc(uintptr(bufSize)))
defer _C_free(unsafe.Pointer(buf))
s, err := syscall.BytePtrFromString(hostname)
if err != nil {
return nil, err
}
var size int
for {
size, _ = ResNsearch(state, s, class, rtype, buf, bufSize)
if size <= bufSize || size > 0xffff {
break
}
// Allocate a bigger buffer to fit the entire msg.
_C_free(unsafe.Pointer(buf))
bufSize = size
buf = (*_C_uchar)(_C_malloc(uintptr(bufSize)))
}
var msg mDNS.Msg
if size == -1 {
// macOS's libresolv seems to directly return -1 for responses that are not success responses but are exchanged.
// However, we still need the response, so we fall back to parsing the entire buffer.
err = msg.Unpack(unsafe.Slice(buf, bufSize))
if err != nil {
return nil, E.New("res_nsearch failure")
}
} else {
err = msg.Unpack(unsafe.Slice(buf, size))
if err != nil {
return nil, err
}
}
return &msg, nil
}
+15
View File
@@ -0,0 +1,15 @@
//go:build !darwin
package local
import (
"context"
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
)
func NewResolvTransport(ctx context.Context, logger log.ContextLogger, tag string) (adapter.DNSTransport, error) {
return nil, os.ErrInvalid
}
+72
View File
@@ -0,0 +1,72 @@
package local
import (
"context"
"net/netip"
"syscall"
"time"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
"github.com/miekg/dns"
)
func dnsReadConfig(_ context.Context, _ string) *dnsConfig {
resStateSize := unsafe.Sizeof(_C_struct___res_state{})
var state *_C_struct___res_state
if resStateSize > 0 {
mem := _C_malloc(resStateSize)
defer _C_free(mem)
memSlice := unsafe.Slice((*byte)(mem), resStateSize)
clear(memSlice)
state = (*_C_struct___res_state)(unsafe.Pointer(&memSlice[0]))
}
if err := ResNinit(state); err != nil {
return &dnsConfig{
servers: defaultNS,
search: dnsDefaultSearch(),
ndots: 1,
timeout: 5 * time.Second,
attempts: 2,
err: E.Cause(err, "libresolv initialization failed"),
}
}
defer ResNclose(state)
conf := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
attempts: int(state.Retry),
}
for i := 0; i < int(state.Nscount); i++ {
addr := parseRawSockaddr(&state.Nsaddrlist[i])
if addr.IsValid() {
conf.servers = append(conf.servers, addr.String())
}
}
for i := 0; ; i++ {
search := state.Dnsrch[i]
if search == nil {
break
}
name := dns.Fqdn(GoString(search))
if name == "" {
continue
}
conf.search = append(conf.search, name)
}
return conf
}
func parseRawSockaddr(rawSockaddr *syscall.RawSockaddr) netip.Addr {
switch rawSockaddr.Family {
case syscall.AF_INET:
sa := (*syscall.RawSockaddrInet4)(unsafe.Pointer(rawSockaddr))
return netip.AddrFrom4(sa.Addr)
case syscall.AF_INET6:
sa := (*syscall.RawSockaddrInet6)(unsafe.Pointer(rawSockaddr))
return netip.AddrFrom16(sa.Addr)
default:
return netip.Addr{}
}
}
-55
View File
@@ -1,55 +0,0 @@
//go:build darwin && cgo
package local
/*
#include <stdlib.h>
#include <stdio.h>
#include <resolv.h>
#include <arpa/inet.h>
*/
import "C"
import (
"context"
"time"
E "github.com/sagernet/sing/common/exceptions"
"github.com/miekg/dns"
)
func dnsReadConfig(_ context.Context, _ string) *dnsConfig {
var state C.struct___res_state
if C.res_ninit(&state) != 0 {
return &dnsConfig{
servers: defaultNS,
search: dnsDefaultSearch(),
ndots: 1,
timeout: 5 * time.Second,
attempts: 2,
err: E.New("libresolv initialization failed"),
}
}
conf := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
attempts: int(state.retry),
}
for i := 0; i < int(state.nscount); i++ {
ns := state.nsaddr_list[i]
addr := C.inet_ntoa(ns.sin_addr)
if addr == nil {
continue
}
conf.servers = append(conf.servers, C.GoString(addr))
}
for i := 0; ; i++ {
search := state.dnsrch[i]
if search == nil {
break
}
conf.search = append(conf.search, dns.Fqdn(C.GoString(search)))
}
return conf
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !windows && !(darwin && cgo)
//go:build !windows && !darwin
package local
+19 -10
View File
@@ -30,7 +30,7 @@ type TransportManager struct {
transportByTag map[string]adapter.DNSTransport
dependByTag map[string][]string
defaultTransport adapter.DNSTransport
defaultTransportFallback adapter.DNSTransport
defaultTransportFallback func() (adapter.DNSTransport, error)
fakeIPTransport adapter.FakeIPTransport
}
@@ -45,7 +45,7 @@ func NewTransportManager(logger logger.ContextLogger, registry adapter.DNSTransp
}
}
func (m *TransportManager) Initialize(defaultTransportFallback adapter.DNSTransport) {
func (m *TransportManager) Initialize(defaultTransportFallback func() (adapter.DNSTransport, error)) {
m.defaultTransportFallback = defaultTransportFallback
}
@@ -56,14 +56,27 @@ func (m *TransportManager) Start(stage adapter.StartStage) error {
}
m.started = true
m.stage = stage
transports := m.transports
m.access.Unlock()
if stage == adapter.StartStateStart {
if m.defaultTag != "" && m.defaultTransport == nil {
m.access.Unlock()
return E.New("default DNS server not found: ", m.defaultTag)
}
return m.startTransports(m.transports)
if m.defaultTransport == nil {
defaultTransport, err := m.defaultTransportFallback()
if err != nil {
m.access.Unlock()
return E.Cause(err, "default DNS server fallback")
}
m.transports = append(m.transports, defaultTransport)
m.transportByTag[defaultTransport.Tag()] = defaultTransport
m.defaultTransport = defaultTransport
}
transports := m.transports
m.access.Unlock()
return m.startTransports(transports)
} else {
transports := m.transports
m.access.Unlock()
for _, outbound := range transports {
err := adapter.LegacyStart(outbound, stage)
if err != nil {
@@ -172,11 +185,7 @@ func (m *TransportManager) Transport(tag string) (adapter.DNSTransport, bool) {
func (m *TransportManager) Default() adapter.DNSTransport {
m.access.RLock()
defer m.access.RUnlock()
if m.defaultTransport != nil {
return m.defaultTransport
} else {
return m.defaultTransportFallback
}
return m.defaultTransport
}
func (m *TransportManager) FakeIP() adapter.FakeIPTransport {