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

Use libresolv in local DNS server on darwin

世界 2 сар өмнө
parent
commit
330735b999

+ 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 - 6
dns/transport/local/local.go

@@ -23,6 +23,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 {
@@ -31,10 +35,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
@@ -45,19 +53,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"))
+				}
 			}
 		}
 	}

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

@@ -1,209 +0,0 @@
-package local
-
-import (
-	"context"
-	"errors"
-	"net"
-
-	"github.com/sagernet/sing-box/adapter"
-	C "github.com/sagernet/sing-box/constant"
-	"github.com/sagernet/sing-box/dns"
-	"github.com/sagernet/sing-box/experimental/libbox/platform"
-	"github.com/sagernet/sing-box/log"
-	"github.com/sagernet/sing-box/option"
-	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/service"
-
-	mDNS "github.com/miekg/dns"
-)
-
-func RegisterTransport(registry *dns.TransportRegistry) {
-	dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewFallbackTransport)
-}
-
-type FallbackTransport struct {
-	adapter.DNSTransport
-	ctx      context.Context
-	fallback bool
-	resolver net.Resolver
-}
-
-func NewFallbackTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
-	transport, err := NewTransport(ctx, logger, tag, options)
-	if err != nil {
-		return nil, err
-	}
-	platformInterface := service.FromContext[platform.Interface](ctx)
-	if platformInterface == nil {
-		return transport, nil
-	}
-	return &FallbackTransport{
-		DNSTransport: transport,
-		ctx:          ctx,
-	}, nil
-}
-
-func (f *FallbackTransport) Start(stage adapter.StartStage) error {
-	err := f.DNSTransport.Start(stage)
-	if err != nil {
-		return err
-	}
-	if stage != adapter.StartStatePostStart {
-		return nil
-	}
-	inboundManager := service.FromContext[adapter.InboundManager](f.ctx)
-	for _, inbound := range inboundManager.Inbounds() {
-		if inbound.Type() == C.TypeTun {
-			// platform tun hijacks DNS, so we can only use cgo resolver here
-			f.fallback = true
-			break
-		}
-	}
-	return nil
-}
-
-func (f *FallbackTransport) Close() error {
-	return f.DNSTransport.Close()
-}
-
-func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
-	if !f.fallback {
-		return f.DNSTransport.Exchange(ctx, message)
-	}
-	question := message.Question[0]
-	domain := dns.FqdnToDomain(question.Name)
-	if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
-		var network string
-		if question.Qtype == mDNS.TypeA {
-			network = "ip4"
-		} else {
-			network = "ip6"
-		}
-		addresses, err := f.resolver.LookupNetIP(ctx, network, domain)
-		if err != nil {
-			var dnsError *net.DNSError
-			if errors.As(err, &dnsError) && dnsError.IsNotFound {
-				return nil, dns.RcodeRefused
-			}
-			return nil, err
-		}
-		return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
-	} else if question.Qtype == mDNS.TypeNS {
-		records, err := f.resolver.LookupNS(ctx, domain)
-		if err != nil {
-			var dnsError *net.DNSError
-			if errors.As(err, &dnsError) && dnsError.IsNotFound {
-				return nil, dns.RcodeRefused
-			}
-			return nil, err
-		}
-		response := &mDNS.Msg{
-			MsgHdr: mDNS.MsgHdr{
-				Id:       message.Id,
-				Rcode:    mDNS.RcodeSuccess,
-				Response: true,
-			},
-			Question: []mDNS.Question{question},
-		}
-		for _, record := range records {
-			response.Answer = append(response.Answer, &mDNS.NS{
-				Hdr: mDNS.RR_Header{
-					Name:   question.Name,
-					Rrtype: mDNS.TypeNS,
-					Class:  mDNS.ClassINET,
-					Ttl:    C.DefaultDNSTTL,
-				},
-				Ns: record.Host,
-			})
-		}
-		return response, nil
-	} else if question.Qtype == mDNS.TypeCNAME {
-		cname, err := f.resolver.LookupCNAME(ctx, domain)
-		if err != nil {
-			var dnsError *net.DNSError
-			if errors.As(err, &dnsError) && dnsError.IsNotFound {
-				return nil, dns.RcodeRefused
-			}
-			return nil, err
-		}
-		return &mDNS.Msg{
-			MsgHdr: mDNS.MsgHdr{
-				Id:       message.Id,
-				Rcode:    mDNS.RcodeSuccess,
-				Response: true,
-			},
-			Question: []mDNS.Question{question},
-			Answer: []mDNS.RR{
-				&mDNS.CNAME{
-					Hdr: mDNS.RR_Header{
-						Name:   question.Name,
-						Rrtype: mDNS.TypeCNAME,
-						Class:  mDNS.ClassINET,
-						Ttl:    C.DefaultDNSTTL,
-					},
-					Target: cname,
-				},
-			},
-		}, nil
-	} else if question.Qtype == mDNS.TypeTXT {
-		records, err := f.resolver.LookupTXT(ctx, domain)
-		if err != nil {
-			var dnsError *net.DNSError
-			if errors.As(err, &dnsError) && dnsError.IsNotFound {
-				return nil, dns.RcodeRefused
-			}
-			return nil, err
-		}
-		return &mDNS.Msg{
-			MsgHdr: mDNS.MsgHdr{
-				Id:       message.Id,
-				Rcode:    mDNS.RcodeSuccess,
-				Response: true,
-			},
-			Question: []mDNS.Question{question},
-			Answer: []mDNS.RR{
-				&mDNS.TXT{
-					Hdr: mDNS.RR_Header{
-						Name:   question.Name,
-						Rrtype: mDNS.TypeCNAME,
-						Class:  mDNS.ClassINET,
-						Ttl:    C.DefaultDNSTTL,
-					},
-					Txt: records,
-				},
-			},
-		}, nil
-	} else if question.Qtype == mDNS.TypeMX {
-		records, err := f.resolver.LookupMX(ctx, domain)
-		if err != nil {
-			var dnsError *net.DNSError
-			if errors.As(err, &dnsError) && dnsError.IsNotFound {
-				return nil, dns.RcodeRefused
-			}
-			return nil, err
-		}
-		response := &mDNS.Msg{
-			MsgHdr: mDNS.MsgHdr{
-				Id:       message.Id,
-				Rcode:    mDNS.RcodeSuccess,
-				Response: true,
-			},
-			Question: []mDNS.Question{question},
-		}
-		for _, record := range records {
-			response.Answer = append(response.Answer, &mDNS.MX{
-				Hdr: mDNS.RR_Header{
-					Name:   question.Name,
-					Rrtype: mDNS.TypeA,
-					Class:  mDNS.ClassINET,
-					Ttl:    C.DefaultDNSTTL,
-				},
-				Preference: record.Pref,
-				Mx:         record.Host,
-			})
-		}
-		return response, nil
-	} else {
-		return nil, E.New("only A, AAAA, NS, CNAME, TXT, MX queries are supported on current platform when using TUN, please switch to a fixed DNS server.")
-	}
-}

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

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

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

+ 72 - 0
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{}
+	}
+}

+ 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 && !darwin
 
 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:"-"`