From 48f84b31d679bff95787992cda760d8824fb28b0 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] Improve `local` DNS server on darwin We mistakenly believed that `libresolv`'s `search` function worked correctly in NetworkExtension, but it seems only `getaddrinfo` does. This commit changes the behavior of the `local` DNS server in NetworkExtension to prefer DHCP, falling back to `getaddrinfo` if DHCP servers are unavailable. It's worth noting that `prefer_go` does not disable DHCP since it respects Dial Fields, but `getaddrinfo` does the opposite. The new behavior only applies to NetworkExtension, not to all scenarios (primarily command-line binaries) as it did previously. In addition, this commit also improves the DHCP DNS server to use the same robust query logic as `local`. --- .github/workflows/build.yml | 19 +- Makefile | 2 +- box.go | 7 +- cmd/internal/build_libbox/main.go | 18 +- dns/transport/local/local.go | 200 ++------------------ dns/transport/local/local_darwin.go | 134 +++++++++++++ dns/transport/local/local_darwin_dhcp.go | 16 ++ dns/transport/local/local_darwin_nodhcp.go | 15 ++ dns/transport/local/local_fallback.go | 208 --------------------- dns/transport/local/local_shared.go | 191 +++++++++++++++++++ dns/transport/local/resolv_darwin_cgo.go | 55 ------ dns/transport/local/resolv_unix.go | 2 +- dns/transport_manager.go | 29 ++- option/dns.go | 13 +- 14 files changed, 433 insertions(+), 476 deletions(-) create mode 100644 dns/transport/local/local_darwin.go create mode 100644 dns/transport/local/local_darwin_dhcp.go create mode 100644 dns/transport/local/local_darwin_nodhcp.go delete mode 100644 dns/transport/local/local_fallback.go create mode 100644 dns/transport/local/local_shared.go delete mode 100644 dns/transport/local/resolv_darwin_cgo.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5ef5ae202..0e4da8c03 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 a98faeac4..b92bc6b66 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/box.go b/box.go index 8a38f6aea..d43a3c957 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 c7bdf6cf9..71df1ae40 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 3118b6c5d..f1d67d163 100644 --- a/dns/transport/local/local.go +++ b/dns/transport/local/local.go @@ -1,28 +1,27 @@ +//go:build !darwin + package local import ( "context" - "errors" - "math/rand" - "syscall" - "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" - "github.com/sagernet/sing-box/dns/transport" "github.com/sagernet/sing-box/dns/transport/hosts" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" - M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" 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 { @@ -31,6 +30,7 @@ type Transport struct { logger logger.ContextLogger hosts *hosts.File dialer N.Dialer + preferGo bool resolved ResolvedResolver } @@ -45,19 +45,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")) + } } } } @@ -85,174 +88,5 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil } } - systemConfig := getSystemDNSConfig(t.ctx) - if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) { - return t.exchangeSingleRequest(ctx, systemConfig, message, question.Name) - } else { - return t.exchangeParallel(ctx, systemConfig, message, question.Name) - } -} - -func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { - var lastErr error - for _, fqdn := range systemConfig.nameList(domain) { - response, err := t.tryOneName(ctx, systemConfig, fqdn, message) - if err != nil { - lastErr = err - continue - } - return response, nil - } - return nil, lastErr -} - -func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { - returned := make(chan struct{}) - defer close(returned) - type queryResult struct { - response *mDNS.Msg - err error - } - results := make(chan queryResult) - startRacer := func(ctx context.Context, fqdn string) { - response, err := t.tryOneName(ctx, systemConfig, fqdn, message) - if err == nil { - if response.Rcode != mDNS.RcodeSuccess { - err = dns.RcodeError(response.Rcode) - } else if len(dns.MessageToAddresses(response)) == 0 { - err = dns.RcodeSuccess - } - } - select { - case results <- queryResult{response, err}: - case <-returned: - } - } - queryCtx, queryCancel := context.WithCancel(ctx) - defer queryCancel() - var nameCount int - for _, fqdn := range systemConfig.nameList(domain) { - nameCount++ - go startRacer(queryCtx, fqdn) - } - var errors []error - for { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case result := <-results: - if result.err == nil { - return result.response, nil - } - errors = append(errors, result.err) - if len(errors) == nameCount { - return nil, E.Errors(errors...) - } - } - } -} - -func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) { - serverOffset := config.serverOffset() - sLen := uint32(len(config.servers)) - var lastErr error - for i := 0; i < config.attempts; i++ { - for j := uint32(0); j < sLen; j++ { - server := config.servers[(serverOffset+j)%sLen] - question := message.Question[0] - question.Name = fqdn - response, err := t.exchangeOne(ctx, M.ParseSocksaddr(server), question, config.timeout, config.useTCP, config.trustAD) - if err != nil { - lastErr = err - continue - } - return response, nil - } - } - return nil, E.Cause(lastErr, fqdn) -} - -func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) { - if server.Port == 0 { - server.Port = 53 - } - request := &mDNS.Msg{ - MsgHdr: mDNS.MsgHdr{ - Id: uint16(rand.Uint32()), - RecursionDesired: true, - AuthenticatedData: ad, - }, - Question: []mDNS.Question{question}, - Compress: true, - } - request.SetEdns0(buf.UDPBufferSize, false) - if !useTCP { - return t.exchangeUDP(ctx, server, request, timeout) - } else { - return t.exchangeTCP(ctx, server, request, timeout) - } -} - -func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) { - conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server) - if err != nil { - return nil, err - } - defer conn.Close() - if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { - newDeadline := time.Now().Add(timeout) - if deadline.After(newDeadline) { - deadline = newDeadline - } - conn.SetDeadline(deadline) - } - buffer := buf.Get(buf.UDPBufferSize) - defer buf.Put(buffer) - rawMessage, err := request.PackBuffer(buffer) - if err != nil { - return nil, E.Cause(err, "pack request") - } - _, err = conn.Write(rawMessage) - if err != nil { - if errors.Is(err, syscall.EMSGSIZE) { - return t.exchangeTCP(ctx, server, request, timeout) - } - return nil, E.Cause(err, "write request") - } - n, err := conn.Read(buffer) - if err != nil { - if errors.Is(err, syscall.EMSGSIZE) { - return t.exchangeTCP(ctx, server, request, timeout) - } - return nil, E.Cause(err, "read response") - } - var response mDNS.Msg - err = response.Unpack(buffer[:n]) - if err != nil { - return nil, E.Cause(err, "unpack response") - } - if response.Truncated { - return t.exchangeTCP(ctx, server, request, timeout) - } - return &response, nil -} - -func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) { - conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server) - if err != nil { - return nil, err - } - defer conn.Close() - if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { - newDeadline := time.Now().Add(timeout) - if deadline.After(newDeadline) { - deadline = newDeadline - } - conn.SetDeadline(deadline) - } - err = transport.WriteMessage(conn, 0, request) - if err != nil { - return nil, err - } - return transport.ReadMessage(conn) + return t.exchange(ctx, message, question.Name) } diff --git a/dns/transport/local/local_darwin.go b/dns/transport/local/local_darwin.go new file mode 100644 index 000000000..4026cedbe --- /dev/null +++ b/dns/transport/local/local_darwin.go @@ -0,0 +1,134 @@ +//go:build darwin + +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/dns/transport/hosts" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + + 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 { + dns.TransportAdapter + ctx context.Context + logger logger.ContextLogger + hosts *hosts.File + dialer N.Dialer + preferGo bool + fallback bool + dhcpTransport dhcpTransport + resolver net.Resolver +} + +type dhcpTransport interface { + adapter.DNSTransport + Fetch() ([]M.Socksaddr, error) + Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, 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 { + return nil, err + } + transportAdapter := dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options) + return &Transport{ + TransportAdapter: transportAdapter, + ctx: ctx, + logger: logger, + hosts: hosts.NewFile(hosts.DefaultPath), + dialer: transportDialer, + preferGo: options.PreferGo, + }, nil +} + +func (t *Transport) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + inboundManager := service.FromContext[adapter.InboundManager](t.ctx) + for _, inbound := range inboundManager.Inbounds() { + if inbound.Type() == C.TypeTun { + t.fallback = true + break + } + } + if t.fallback { + t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger) + if t.dhcpTransport != nil { + err := t.dhcpTransport.Start(stage) + if err != nil { + return err + } + } + } + return nil +} + +func (t *Transport) Close() error { + return common.Close( + t.dhcpTransport, + ) +} + +func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + question := message.Question[0] + if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { + addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name)) + if len(addresses) > 0 { + return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil + } + } + if !t.fallback { + return t.exchange(ctx, message, question.Name) + } + if t.dhcpTransport != nil { + dhcpTransports, _ := t.dhcpTransport.Fetch() + if len(dhcpTransports) > 0 { + return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports) + } + } + if t.preferGo { + // Assuming the user knows what they are doing, we still execute the query which will fail. + return t.exchange(ctx, message, 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 := t.resolver.LookupNetIP(ctx, network, question.Name) + 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 + } + return nil, E.New("only A and AAAA queries are supported on Apple platforms when using TUN and DHCP unavailable.") +} diff --git a/dns/transport/local/local_darwin_dhcp.go b/dns/transport/local/local_darwin_dhcp.go new file mode 100644 index 000000000..b228b76a4 --- /dev/null +++ b/dns/transport/local/local_darwin_dhcp.go @@ -0,0 +1,16 @@ +//go:build darwin && with_dhcp + +package local + +import ( + "context" + + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport/dhcp" + "github.com/sagernet/sing-box/log" + N "github.com/sagernet/sing/common/network" +) + +func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport { + return dhcp.NewRawTransport(transportAdapter, ctx, dialer, logger) +} diff --git a/dns/transport/local/local_darwin_nodhcp.go b/dns/transport/local/local_darwin_nodhcp.go new file mode 100644 index 000000000..5ce84690a --- /dev/null +++ b/dns/transport/local/local_darwin_nodhcp.go @@ -0,0 +1,15 @@ +//go:build darwin && !with_dhcp + +package local + +import ( + "context" + + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/log" + N "github.com/sagernet/sing/common/network" +) + +func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport { + return nil +} diff --git a/dns/transport/local/local_fallback.go b/dns/transport/local/local_fallback.go deleted file mode 100644 index fcc01cc9d..000000000 --- a/dns/transport/local/local_fallback.go +++ /dev/null @@ -1,208 +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] - 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, question.Name) - 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, question.Name) - 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, question.Name) - 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, question.Name) - 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, question.Name) - 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_shared.go b/dns/transport/local/local_shared.go new file mode 100644 index 000000000..3b05dac6c --- /dev/null +++ b/dns/transport/local/local_shared.go @@ -0,0 +1,191 @@ +package local + +import ( + "context" + "errors" + "math/rand" + "syscall" + "time" + + "github.com/sagernet/sing-box/dns" + "github.com/sagernet/sing-box/dns/transport" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + mDNS "github.com/miekg/dns" +) + +func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { + systemConfig := getSystemDNSConfig(t.ctx) + if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) { + return t.exchangeSingleRequest(ctx, systemConfig, message, domain) + } else { + return t.exchangeParallel(ctx, systemConfig, message, domain) + } +} + +func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { + var lastErr error + for _, fqdn := range systemConfig.nameList(domain) { + response, err := t.tryOneName(ctx, systemConfig, fqdn, message) + if err != nil { + lastErr = err + continue + } + return response, nil + } + return nil, lastErr +} + +func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { + returned := make(chan struct{}) + defer close(returned) + type queryResult struct { + response *mDNS.Msg + err error + } + results := make(chan queryResult) + startRacer := func(ctx context.Context, fqdn string) { + response, err := t.tryOneName(ctx, systemConfig, fqdn, message) + if err == nil { + if response.Rcode != mDNS.RcodeSuccess { + err = dns.RcodeError(response.Rcode) + } else if len(dns.MessageToAddresses(response)) == 0 { + err = E.New(fqdn, ": empty result") + } + } + select { + case results <- queryResult{response, err}: + case <-returned: + } + } + queryCtx, queryCancel := context.WithCancel(ctx) + defer queryCancel() + var nameCount int + for _, fqdn := range systemConfig.nameList(domain) { + nameCount++ + go startRacer(queryCtx, fqdn) + } + var errors []error + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case result := <-results: + if result.err == nil { + return result.response, nil + } + errors = append(errors, result.err) + if len(errors) == nameCount { + return nil, E.Errors(errors...) + } + } + } +} + +func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) { + serverOffset := config.serverOffset() + sLen := uint32(len(config.servers)) + var lastErr error + for i := 0; i < config.attempts; i++ { + for j := uint32(0); j < sLen; j++ { + server := config.servers[(serverOffset+j)%sLen] + question := message.Question[0] + question.Name = fqdn + response, err := t.exchangeOne(ctx, M.ParseSocksaddr(server), question, config.timeout, config.useTCP, config.trustAD) + if err != nil { + lastErr = err + continue + } + return response, nil + } + } + return nil, E.Cause(lastErr, fqdn) +} + +func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) { + if server.Port == 0 { + server.Port = 53 + } + request := &mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: uint16(rand.Uint32()), + RecursionDesired: true, + AuthenticatedData: ad, + }, + Question: []mDNS.Question{question}, + Compress: true, + } + request.SetEdns0(buf.UDPBufferSize, false) + if !useTCP { + return t.exchangeUDP(ctx, server, request, timeout) + } else { + return t.exchangeTCP(ctx, server, request, timeout) + } +} + +func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) { + conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server) + if err != nil { + return nil, err + } + defer conn.Close() + if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { + newDeadline := time.Now().Add(timeout) + if deadline.After(newDeadline) { + deadline = newDeadline + } + conn.SetDeadline(deadline) + } + buffer := buf.Get(buf.UDPBufferSize) + defer buf.Put(buffer) + rawMessage, err := request.PackBuffer(buffer) + if err != nil { + return nil, E.Cause(err, "pack request") + } + _, err = conn.Write(rawMessage) + if err != nil { + if errors.Is(err, syscall.EMSGSIZE) { + return t.exchangeTCP(ctx, server, request, timeout) + } + return nil, E.Cause(err, "write request") + } + n, err := conn.Read(buffer) + if err != nil { + if errors.Is(err, syscall.EMSGSIZE) { + return t.exchangeTCP(ctx, server, request, timeout) + } + return nil, E.Cause(err, "read response") + } + var response mDNS.Msg + err = response.Unpack(buffer[:n]) + if err != nil { + return nil, E.Cause(err, "unpack response") + } + if response.Truncated { + return t.exchangeTCP(ctx, server, request, timeout) + } + return &response, nil +} + +func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) { + conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server) + if err != nil { + return nil, err + } + defer conn.Close() + if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { + newDeadline := time.Now().Add(timeout) + if deadline.After(newDeadline) { + deadline = newDeadline + } + conn.SetDeadline(deadline) + } + err = transport.WriteMessage(conn, 0, request) + if err != nil { + return nil, err + } + return transport.ReadMessage(conn) +} diff --git a/dns/transport/local/resolv_darwin_cgo.go b/dns/transport/local/resolv_darwin_cgo.go deleted file mode 100644 index bbe4ccfef..000000000 --- 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 f77f35536..51512f65e 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 package local diff --git a/dns/transport_manager.go b/dns/transport_manager.go index f41c9f9eb..e289ccea0 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 422d7b3ba..7a23f2c81 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:"-"`