1
0
Эх сурвалжийг харах

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`.
世界 2 сар өмнө
parent
commit
f61b5b6c8f

+ 18 - 1
.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: |

+ 1 - 1
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)

+ 4 - 3
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 {

+ 7 - 9
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",
 	}
 
+	tags := append(sharedTags, memcTags...)
+	if debugEnabled {
+		tags = append(tags, debugTags...)
+	}
+
 	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...)
-	}
-
 	args = append(args, "-tags", strings.Join(tags, ","))
 	args = append(args, "./experimental/libbox")
 

+ 17 - 183
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)
 }

+ 134 - 0
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.")
+}

+ 16 - 0
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)
+}

+ 15 - 0
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
+}

+ 0 - 208
dns/transport/local/local_fallback.go

@@ -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.")
-	}
-}

+ 191 - 0
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)
+}

+ 0 - 55
dns/transport/local/resolv_darwin_cgo.go

@@ -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
dns/transport/local/resolv_unix.go

@@ -1,4 +1,4 @@
-//go:build !windows && !(darwin && cgo)
+//go:build !windows
 
 package local
 

+ 19 - 10
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 {

+ 9 - 4
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:"-"`