From 7058c2e08aca48b0cca0c7ea916ab133a92a00c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 18 Aug 2025 14:19:49 +0800 Subject: [PATCH] Use libresolv in local DNS server on darwin --- .github/workflows/build.yml | 19 +- Makefile | 2 +- adapter/outbound/manager.go | 16 +- box.go | 7 +- cmd/internal/build_libbox/main.go | 18 +- dns/transport/local/local.go | 23 +- dns/transport/local/local_fallback.go | 209 ------------------- dns/transport/local/local_resolv.go | 46 ++++ dns/transport/local/local_resolv_linkname.go | 170 +++++++++++++++ dns/transport/local/local_resolv_stub.go | 15 ++ dns/transport/local/resolv_darwin.go | 72 +++++++ dns/transport/local/resolv_darwin_cgo.go | 55 ----- dns/transport/local/resolv_unix.go | 2 +- dns/transport_manager.go | 29 ++- option/dns.go | 13 +- 15 files changed, 388 insertions(+), 308 deletions(-) delete mode 100644 dns/transport/local/local_fallback.go create mode 100644 dns/transport/local/local_resolv.go create mode 100644 dns/transport/local/local_resolv_linkname.go create mode 100644 dns/transport/local/local_resolv_stub.go create mode 100644 dns/transport/local/resolv_darwin.go delete mode 100644 dns/transport/local/resolv_darwin_cgo.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25114dff..19643014 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -149,7 +149,7 @@ jobs: TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale' echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" - name: Build - if: matrix.os != 'android' + if: matrix.os != 'darwin' && matrix.os != 'android' run: | set -xeuo pipefail mkdir -p dist @@ -165,6 +165,23 @@ jobs: GOMIPS: ${{ matrix.gomips }} GOMIPS64: ${{ matrix.gomips }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build darwin + if: matrix.os == 'darwin' + run: | + set -xeuo pipefail + mkdir -p dist + go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ + -ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \ + ./cmd/sing-box + env: + CGO_ENABLED: "0" + GOOS: ${{ matrix.os }} + GOARCH: ${{ matrix.arch }} + GO386: ${{ matrix.go386 }} + GOARM: ${{ matrix.goarm }} + GOMIPS: ${{ matrix.gomips }} + GOMIPS64: ${{ matrix.gomips }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build Android if: matrix.os == 'android' run: | diff --git a/Makefile b/Makefile index 43f93c8c..e8d0a831 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTARCH = $(shell go env GOHOSTARCH) VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest) -PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid=" +PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid= -checklinkname=0" MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)" MAIN = ./cmd/sing-box PREFIX ?= $(shell go env GOPATH) diff --git a/adapter/outbound/manager.go b/adapter/outbound/manager.go index b58f5277..0fdeb390 100644 --- a/adapter/outbound/manager.go +++ b/adapter/outbound/manager.go @@ -56,6 +56,14 @@ func (m *Manager) Start(stage adapter.StartStage) error { m.started = true m.stage = stage if stage == adapter.StartStateStart { + if m.defaultTag != "" && m.defaultOutbound == nil { + defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag) + if !loaded { + m.access.Unlock() + return E.New("default outbound not found: ", m.defaultTag) + } + m.defaultOutbound = defaultEndpoint + } if m.defaultOutbound == nil { directOutbound, err := m.defaultOutboundFallback() if err != nil { @@ -66,14 +74,6 @@ func (m *Manager) Start(stage adapter.StartStage) error { m.outboundByTag[directOutbound.Tag()] = directOutbound m.defaultOutbound = directOutbound } - if m.defaultTag != "" && m.defaultOutbound == nil { - defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag) - if !loaded { - m.access.Unlock() - return E.New("default outbound not found: ", m.defaultTag) - } - m.defaultOutbound = defaultEndpoint - } outbounds := m.outbounds m.access.Unlock() return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...)) diff --git a/box.go b/box.go index 8a38f6ae..d43a3c95 100644 --- a/box.go +++ b/box.go @@ -323,13 +323,14 @@ func New(options Options) (*Box, error) { option.DirectOutboundOptions{}, ) }) - dnsTransportManager.Initialize(common.Must1( - local.NewTransport( + dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) { + return local.NewTransport( ctx, logFactory.NewLogger("dns/local"), "local", option.LocalDNSServerOptions{}, - ))) + ) + }) if platformInterface != nil { err = platformInterface.Initialize(networkManager) if err != nil { diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index c7bdf6cf..71df1ae4 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -59,8 +59,8 @@ func init() { if err != nil { currentTag = "unknown" } - sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=") - debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag) + sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid= -checklinkname=0") + debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+"-s -w -buildid= -checklinkname=0") sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack") darwinTags = append(darwinTags, "with_dhcp") @@ -106,19 +106,17 @@ func buildAndroid() { "-libname=box", } - if !debugEnabled { - sharedFlags[3] = sharedFlags[3] + " -checklinkname=0" - args = append(args, sharedFlags...) - } else { - debugFlags[1] = debugFlags[1] + " -checklinkname=0" - args = append(args, debugFlags...) - } - tags := append(sharedTags, memcTags...) if debugEnabled { tags = append(tags, debugTags...) } + if !debugEnabled { + args = append(args, sharedFlags...) + } else { + args = append(args, debugFlags...) + } + args = append(args, "-tags", strings.Join(tags, ",")) args = append(args, "./experimental/libbox") diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go index a7d370df..74c9c702 100644 --- a/dns/transport/local/local.go +++ b/dns/transport/local/local.go @@ -20,6 +20,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 { @@ -28,10 +32,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 @@ -42,19 +50,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")) + } } } } diff --git a/dns/transport/local/local_fallback.go b/dns/transport/local/local_fallback.go deleted file mode 100644 index e584ddce..00000000 --- a/dns/transport/local/local_fallback.go +++ /dev/null @@ -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.") - } -} diff --git a/dns/transport/local/local_resolv.go b/dns/transport/local/local_resolv.go new file mode 100644 index 00000000..cf7bcfba --- /dev/null +++ b/dns/transport/local/local_resolv.go @@ -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)) + }) +} diff --git a/dns/transport/local/local_resolv_linkname.go b/dns/transport/local/local_resolv_linkname.go new file mode 100644 index 00000000..1495ae1d --- /dev/null +++ b/dns/transport/local/local_resolv_linkname.go @@ -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 +} diff --git a/dns/transport/local/local_resolv_stub.go b/dns/transport/local/local_resolv_stub.go new file mode 100644 index 00000000..8486b87a --- /dev/null +++ b/dns/transport/local/local_resolv_stub.go @@ -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 +} diff --git a/dns/transport/local/resolv_darwin.go b/dns/transport/local/resolv_darwin.go new file mode 100644 index 00000000..396e40de --- /dev/null +++ b/dns/transport/local/resolv_darwin.go @@ -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{} + } +} diff --git a/dns/transport/local/resolv_darwin_cgo.go b/dns/transport/local/resolv_darwin_cgo.go deleted file mode 100644 index bbe4ccfe..00000000 --- a/dns/transport/local/resolv_darwin_cgo.go +++ /dev/null @@ -1,55 +0,0 @@ -//go:build darwin && cgo - -package local - -/* -#include -#include -#include -#include -*/ -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 -} diff --git a/dns/transport/local/resolv_unix.go b/dns/transport/local/resolv_unix.go index f77f3553..99eb71e6 100644 --- a/dns/transport/local/resolv_unix.go +++ b/dns/transport/local/resolv_unix.go @@ -1,4 +1,4 @@ -//go:build !windows && !(darwin && cgo) +//go:build !windows && !darwin package local diff --git a/dns/transport_manager.go b/dns/transport_manager.go index f41c9f9e..e289ccea 100644 --- a/dns/transport_manager.go +++ b/dns/transport_manager.go @@ -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 { diff --git a/option/dns.go b/option/dns.go index 422d7b3b..7a23f2c8 100644 --- a/option/dns.go +++ b/option/dns.go @@ -190,7 +190,7 @@ func (o *DNSServerOptions) Upgrade(ctx context.Context) error { } } remoteOptions := RemoteDNSServerOptions{ - LocalDNSServerOptions: LocalDNSServerOptions{ + RawLocalDNSServerOptions: RawLocalDNSServerOptions{ DialerOptions: DialerOptions{ Detour: options.Detour, DomainResolver: &DomainResolveOptions{ @@ -211,7 +211,7 @@ func (o *DNSServerOptions) Upgrade(ctx context.Context) error { switch serverType { case C.DNSTypeLocal: o.Type = C.DNSTypeLocal - o.Options = &remoteOptions.LocalDNSServerOptions + o.Options = &remoteOptions.RawLocalDNSServerOptions case C.DNSTypeUDP: o.Type = C.DNSTypeUDP o.Options = &remoteOptions @@ -363,7 +363,7 @@ type HostsDNSServerOptions struct { Predefined *badjson.TypedMap[string, badoption.Listable[netip.Addr]] `json:"predefined,omitempty"` } -type LocalDNSServerOptions struct { +type RawLocalDNSServerOptions struct { DialerOptions Legacy bool `json:"-"` LegacyStrategy DomainStrategy `json:"-"` @@ -371,8 +371,13 @@ type LocalDNSServerOptions struct { LegacyClientSubnet netip.Prefix `json:"-"` } +type LocalDNSServerOptions struct { + RawLocalDNSServerOptions + PreferGo bool `json:"prefer_go,omitempty"` +} + type RemoteDNSServerOptions struct { - LocalDNSServerOptions + RawLocalDNSServerOptions DNSServerAddressOptions LegacyAddressResolver string `json:"-"` LegacyAddressStrategy DomainStrategy `json:"-"`