Ver código fonte

Add hosts transport & Remove bad linkname usages

世界 8 meses atrás
pai
commit
3fa417f283

+ 0 - 1
.golangci.yml

@@ -32,7 +32,6 @@ run:
     - with_reality_server
     - with_acme
     - with_clash_api
-    - badlinkname
 
 issues:
   exclude-dirs:

+ 0 - 1
.goreleaser.fury.yaml

@@ -9,7 +9,6 @@ builds:
       - -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
       - -s
       - -buildid=
-      - -checklinkname=0
     tags:
       - with_gvisor
       - with_quic

+ 0 - 1
.goreleaser.yaml

@@ -11,7 +11,6 @@ builds:
       - -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
       - -s
       - -buildid=
-      - -checklinkname=0
     tags:
       - with_gvisor
       - with_quic

+ 1 - 1
Dockerfile

@@ -15,7 +15,7 @@ RUN set -ex \
     && go build -v -trimpath -tags \
         "with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
         -o /go/bin/sing-box \
-        -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \
+        -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
         ./cmd/sing-box
 FROM --platform=$TARGETPLATFORM alpine AS dist
 LABEL maintainer="nekohasekai <[email protected]>"

+ 2 - 3
Makefile

@@ -2,15 +2,14 @@ NAME = sing-box
 COMMIT = $(shell git rev-parse --short HEAD)
 TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
 TAGS_GO121 = with_ech
-TAGS_GO123 = badlinkname
-TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121),$(TAGS_GO123)
+TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
 TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
 
 GOHOSTOS = $(shell go env GOHOSTOS)
 GOHOSTARCH = $(shell go env GOHOSTARCH)
 VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
 
-PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid= -checklinkname=0"
+PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
 MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
 MAIN = ./cmd/sing-box
 PREFIX ?= $(shell go env GOPATH)

+ 1 - 1
cmd/internal/build_libbox/main.go

@@ -55,7 +55,7 @@ func init() {
 	if err != nil {
 		currentTag = "unknown"
 	}
-	sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid= -checklinkname=0")
+	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)
 
 	sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")

+ 1 - 0
constant/dns.go

@@ -22,6 +22,7 @@ const (
 	DNSTypeHTTPS      = "https"
 	DNSTypeQUIC       = "quic"
 	DNSTypeHTTP3      = "h3"
+	DNSTypeHosts      = "hosts"
 	DNSTypeLocal      = "local"
 	DNSTypePreDefined = "predefined"
 	DNSTypeFakeIP     = "fakeip"

+ 63 - 0
dns/transport/hosts/hosts.go

@@ -0,0 +1,63 @@
+package hosts
+
+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-box/option"
+
+	mDNS "github.com/miekg/dns"
+)
+
+func RegisterTransport(registry *dns.TransportRegistry) {
+	dns.RegisterTransport[option.HostsDNSServerOptions](registry, C.DNSTypeHosts, NewTransport)
+}
+
+var _ adapter.DNSTransport = (*Transport)(nil)
+
+type Transport struct {
+	dns.TransportAdapter
+	files []*File
+}
+
+func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.HostsDNSServerOptions) (adapter.DNSTransport, error) {
+	var files []*File
+	if len(options.Path) == 0 {
+		files = append(files, NewFile(DefaultPath))
+	} else {
+		for _, path := range options.Path {
+			files = append(files, NewFile(path))
+		}
+	}
+	return &Transport{
+		TransportAdapter: dns.NewTransportAdapter(C.DNSTypeHosts, tag, nil),
+		files:            files,
+	}, nil
+}
+
+func (t *Transport) Reset() {
+}
+
+func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
+	question := message.Question[0]
+	domain := dns.FqdnToDomain(question.Name)
+	if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
+		for _, file := range t.files {
+			addresses := file.Lookup(domain)
+			if len(addresses) > 0 {
+				return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
+			}
+		}
+	}
+	return &mDNS.Msg{
+		MsgHdr: mDNS.MsgHdr{
+			Id:       message.Id,
+			Rcode:    mDNS.RcodeNameError,
+			Response: true,
+		},
+		Question: []mDNS.Question{question},
+	}, nil
+}

+ 102 - 0
dns/transport/hosts/hosts_file.go

@@ -0,0 +1,102 @@
+package hosts
+
+import (
+	"bufio"
+	"errors"
+	"io"
+	"net/netip"
+	"os"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/miekg/dns"
+)
+
+const cacheMaxAge = 5 * time.Second
+
+type File struct {
+	path    string
+	access  sync.Mutex
+	byName  map[string][]netip.Addr
+	expire  time.Time
+	modTime time.Time
+	size    int64
+}
+
+func NewFile(path string) *File {
+	return &File{
+		path: path,
+	}
+}
+
+func (f *File) Lookup(name string) []netip.Addr {
+	f.access.Lock()
+	defer f.access.Unlock()
+	f.update()
+	return f.byName[name]
+}
+
+func (f *File) update() {
+	now := time.Now()
+	if now.Before(f.expire) && len(f.byName) > 0 {
+		return
+	}
+	stat, err := os.Stat(f.path)
+	if err != nil {
+		return
+	}
+	if f.modTime.Equal(stat.ModTime()) && f.size == stat.Size() {
+		f.expire = now.Add(cacheMaxAge)
+		return
+	}
+	byName := make(map[string][]netip.Addr)
+	file, err := os.Open(f.path)
+	if err != nil {
+		return
+	}
+	defer file.Close()
+	reader := bufio.NewReader(file)
+	var (
+		prefix   []byte
+		line     []byte
+		isPrefix bool
+	)
+	for {
+		line, isPrefix, err = reader.ReadLine()
+		if err != nil {
+			if errors.Is(err, io.EOF) {
+				break
+			}
+			return
+		}
+		if isPrefix {
+			prefix = append(prefix, line...)
+			continue
+		} else if len(prefix) > 0 {
+			line = append(prefix, line...)
+			prefix = nil
+		}
+		commentIndex := strings.IndexRune(string(line), '#')
+		if commentIndex != -1 {
+			line = line[:commentIndex]
+		}
+		fields := strings.Fields(string(line))
+		if len(fields) < 2 {
+			continue
+		}
+		var addr netip.Addr
+		addr, err = netip.ParseAddr(fields[0])
+		if err != nil {
+			continue
+		}
+		for index := 1; index < len(fields); index++ {
+			canonicalName := dns.CanonicalName(fields[index])
+			byName[canonicalName] = append(byName[canonicalName], addr)
+		}
+	}
+	f.expire = now.Add(cacheMaxAge)
+	f.modTime = stat.ModTime()
+	f.size = stat.Size()
+	f.byName = byName
+}

+ 15 - 0
dns/transport/hosts/hosts_test.go

@@ -0,0 +1,15 @@
+package hosts_test
+
+import (
+	"net/netip"
+	"testing"
+
+	"github.com/sagernet/sing-box/dns/transport/hosts"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestHosts(t *testing.T) {
+	require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, hosts.NewFile("testdata/hosts").Lookup("localhost."))
+	require.NotEmpty(t, hosts.NewFile(hosts.DefaultPath).Lookup("localhost."))
+}

+ 5 - 0
dns/transport/hosts/hosts_unix.go

@@ -0,0 +1,5 @@
+//go:build !windows
+
+package hosts
+
+var DefaultPath = "/etc/hosts"

+ 8 - 0
dns/transport/hosts/hosts_windows.go

@@ -0,0 +1,8 @@
+package hosts
+
+import _ "unsafe"
+
+var DefaultPath = getSystemDirectory() + "/Drivers/etc/hosts"
+
+//go:linkname getSystemDirectory internal/syscall/windows.GetSystemDirectory
+func getSystemDirectory() string

+ 2 - 0
dns/transport/hosts/testdata/hosts

@@ -0,0 +1,2 @@
+127.0.0.1       localhost
+::1             localhost

+ 9 - 7
dns/transport/local/local.go

@@ -7,9 +7,9 @@ import (
 	"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"
 	"github.com/sagernet/sing/common/buf"
 	E "github.com/sagernet/sing/common/exceptions"
 	M "github.com/sagernet/sing/common/metadata"
@@ -26,6 +26,7 @@ var _ adapter.DNSTransport = (*Transport)(nil)
 
 type Transport struct {
 	dns.TransportAdapter
+	hosts  *hosts.File
 	dialer N.Dialer
 }
 
@@ -35,7 +36,8 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
 		return nil, err
 	}
 	return &Transport{
-		TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeTCP, tag, options),
+		TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
+		hosts:            hosts.NewFile(hosts.DefaultPath),
 		dialer:           transportDialer,
 	}, nil
 }
@@ -47,9 +49,9 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
 	question := message.Question[0]
 	domain := dns.FqdnToDomain(question.Name)
 	if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
-		addressStrings, _ := lookupStaticHost(domain)
-		if len(addressStrings) > 0 {
-			return dns.FixedResponse(message.Id, question, common.Map(addressStrings, M.ParseAddr), C.DefaultDNSTTL), nil
+		addresses := t.hosts.Lookup(domain)
+		if len(addresses) > 0 {
+			return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
 		}
 	}
 	systemConfig := getSystemDNSConfig()
@@ -62,7 +64,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
 
 func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
 	var lastErr error
-	for _, fqdn := range nameList(systemConfig, domain) {
+	for _, fqdn := range systemConfig.nameList(domain) {
 		response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
 		if err != nil {
 			lastErr = err
@@ -90,7 +92,7 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
 	}
 	queryCtx, queryCancel := context.WithCancel(ctx)
 	defer queryCancel()
-	for _, fqdn := range nameList(systemConfig, domain) {
+	for _, fqdn := range systemConfig.nameList(domain) {
 		go startRacer(queryCtx, fqdn)
 	}
 	select {

+ 0 - 19
dns/transport/local/local_badlinkname.go

@@ -1,19 +0,0 @@
-//go:build badlinkname
-
-package local
-
-import (
-	_ "unsafe"
-)
-
-//go:linkname getSystemDNSConfig net.getSystemDNSConfig
-func getSystemDNSConfig() *dnsConfig
-
-//go:linkname nameList net.(*dnsConfig).nameList
-func nameList(c *dnsConfig, name string) []string
-
-//go:linkname lookupStaticHost net.lookupStaticHost
-func lookupStaticHost(host string) ([]string, string)
-
-//go:linkname splitHostZone net.splitHostZone
-func splitHostZone(s string) (host, zone string)

+ 0 - 44
dns/transport/local/local_linkname.go

@@ -1,44 +0,0 @@
-package local
-
-import (
-	"sync/atomic"
-	"time"
-	_ "unsafe"
-)
-
-const (
-	// net.maxDNSPacketSize
-	maxDNSPacketSize = 1232
-)
-
-type dnsConfig struct {
-	servers       []string      // server addresses (in host:port form) to use
-	search        []string      // rooted suffixes to append to local name
-	ndots         int           // number of dots in name to trigger absolute lookup
-	timeout       time.Duration // wait before giving up on a query, including retries
-	attempts      int           // lost packets before giving up on server
-	rotate        bool          // round robin among servers
-	unknownOpt    bool          // anything unknown was encountered
-	lookup        []string      // OpenBSD top-level database "lookup" order
-	err           error         // any error that occurs during open of resolv.conf
-	mtime         time.Time     // time of resolv.conf modification
-	soffset       uint32        // used by serverOffset
-	singleRequest bool          // use sequential A and AAAA queries instead of parallel queries
-	useTCP        bool          // force usage of TCP for DNS resolutions
-	trustAD       bool          // add AD flag to queries
-	noReload      bool          // do not check for config file updates
-}
-
-func (c *dnsConfig) serverOffset() uint32 {
-	if c.rotate {
-		return atomic.AddUint32(&c.soffset, 1) - 1 // return 0 to start
-	}
-	return 0
-}
-
-//go:linkname runtime_rand runtime.rand
-func runtime_rand() uint64
-
-func randInt() int {
-	return int(uint(runtime_rand()) >> 1) // clear sign bit
-}

+ 0 - 19
dns/transport/local/local_notbadlinkname.go

@@ -1,19 +0,0 @@
-//go:build !badlinkname
-
-package local
-
-func getSystemDNSConfig() *dnsConfig {
-	panic("stub")
-}
-
-func nameList(c *dnsConfig, name string) []string {
-	panic("stub")
-}
-
-func lookupStaticHost(host string) ([]string, string) {
-	panic("stub")
-}
-
-func splitHostZone(s string) (host, zone string) {
-	panic("stub")
-}

+ 154 - 0
dns/transport/local/resolv.go

@@ -0,0 +1,154 @@
+package local
+
+import (
+	"os"
+	"runtime"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"time"
+	_ "unsafe"
+)
+
+const (
+	// net.maxDNSPacketSize
+	maxDNSPacketSize = 1232
+)
+
+type resolverConfig struct {
+	initOnce    sync.Once
+	ch          chan struct{}
+	lastChecked time.Time
+	dnsConfig   atomic.Pointer[dnsConfig]
+}
+
+var resolvConf resolverConfig
+
+func getSystemDNSConfig() *dnsConfig {
+	resolvConf.tryUpdate("/etc/resolv.conf")
+	return resolvConf.dnsConfig.Load()
+}
+
+func (conf *resolverConfig) init() {
+	conf.dnsConfig.Store(dnsReadConfig("/etc/resolv.conf"))
+	conf.lastChecked = time.Now()
+	conf.ch = make(chan struct{}, 1)
+}
+
+func (conf *resolverConfig) tryUpdate(name string) {
+	conf.initOnce.Do(conf.init)
+
+	if conf.dnsConfig.Load().noReload {
+		return
+	}
+	if !conf.tryAcquireSema() {
+		return
+	}
+	defer conf.releaseSema()
+
+	now := time.Now()
+	if conf.lastChecked.After(now.Add(-5 * time.Second)) {
+		return
+	}
+	conf.lastChecked = now
+	if runtime.GOOS != "windows" {
+		var mtime time.Time
+		if fi, err := os.Stat(name); err == nil {
+			mtime = fi.ModTime()
+		}
+		if mtime.Equal(conf.dnsConfig.Load().mtime) {
+			return
+		}
+	}
+	dnsConf := dnsReadConfig(name)
+	conf.dnsConfig.Store(dnsConf)
+}
+
+func (conf *resolverConfig) tryAcquireSema() bool {
+	select {
+	case conf.ch <- struct{}{}:
+		return true
+	default:
+		return false
+	}
+}
+
+func (conf *resolverConfig) releaseSema() {
+	<-conf.ch
+}
+
+type dnsConfig struct {
+	servers       []string
+	search        []string
+	ndots         int
+	timeout       time.Duration
+	attempts      int
+	rotate        bool
+	unknownOpt    bool
+	lookup        []string
+	err           error
+	mtime         time.Time
+	soffset       uint32
+	singleRequest bool
+	useTCP        bool
+	trustAD       bool
+	noReload      bool
+}
+
+func (c *dnsConfig) serverOffset() uint32 {
+	if c.rotate {
+		return atomic.AddUint32(&c.soffset, 1) - 1 // return 0 to start
+	}
+	return 0
+}
+
+func (conf *dnsConfig) nameList(name string) []string {
+	l := len(name)
+	rooted := l > 0 && name[l-1] == '.'
+	if l > 254 || l == 254 && !rooted {
+		return nil
+	}
+
+	if rooted {
+		if avoidDNS(name) {
+			return nil
+		}
+		return []string{name}
+	}
+
+	hasNdots := strings.Count(name, ".") >= conf.ndots
+	name += "."
+	l++
+
+	names := make([]string, 0, 1+len(conf.search))
+	if hasNdots && !avoidDNS(name) {
+		names = append(names, name)
+	}
+	for _, suffix := range conf.search {
+		fqdn := name + suffix
+		if !avoidDNS(fqdn) && len(fqdn) <= 254 {
+			names = append(names, fqdn)
+		}
+	}
+	if !hasNdots && !avoidDNS(name) {
+		names = append(names, name)
+	}
+	return names
+}
+
+//go:linkname runtime_rand runtime.rand
+func runtime_rand() uint64
+
+func randInt() int {
+	return int(uint(runtime_rand()) >> 1) // clear sign bit
+}
+
+func avoidDNS(name string) bool {
+	if name == "" {
+		return true
+	}
+	if name[len(name)-1] == '.' {
+		name = name[:len(name)-1]
+	}
+	return strings.HasSuffix(name, ".onion")
+}

+ 175 - 0
dns/transport/local/resolv_unix.go

@@ -0,0 +1,175 @@
+//go:build !windows
+
+package local
+
+import (
+	"bufio"
+	"net"
+	"net/netip"
+	"os"
+	"strings"
+	"time"
+	_ "unsafe"
+)
+
+func dnsReadConfig(name string) *dnsConfig {
+	conf := &dnsConfig{
+		ndots:    1,
+		timeout:  5 * time.Second,
+		attempts: 2,
+	}
+	file, err := os.Open(name)
+	if err != nil {
+		conf.servers = defaultNS
+		conf.search = dnsDefaultSearch()
+		conf.err = err
+		return conf
+	}
+	defer file.Close()
+	fi, err := file.Stat()
+	if err == nil {
+		conf.mtime = fi.ModTime()
+	} else {
+		conf.servers = defaultNS
+		conf.search = dnsDefaultSearch()
+		conf.err = err
+		return conf
+	}
+	reader := bufio.NewReader(file)
+	var (
+		prefix   []byte
+		line     []byte
+		isPrefix bool
+	)
+	for {
+		line, isPrefix, err = reader.ReadLine()
+		if err != nil {
+			break
+		}
+		if isPrefix {
+			prefix = append(prefix, line...)
+			continue
+		} else if len(prefix) > 0 {
+			line = append(prefix, line...)
+			prefix = nil
+		}
+		if len(line) > 0 && (line[0] == ';' || line[0] == '#') {
+			continue
+		}
+		f := strings.Fields(string(line))
+		if len(f) < 1 {
+			continue
+		}
+		switch f[0] {
+		case "nameserver":
+			if len(f) > 1 && len(conf.servers) < 3 {
+				if _, err := netip.ParseAddr(f[1]); err == nil {
+					conf.servers = append(conf.servers, net.JoinHostPort(f[1], "53"))
+				}
+			}
+		case "domain":
+			if len(f) > 1 {
+				conf.search = []string{ensureRooted(f[1])}
+			}
+
+		case "search":
+			conf.search = make([]string, 0, len(f)-1)
+			for i := 1; i < len(f); i++ {
+				name := ensureRooted(f[i])
+				if name == "." {
+					continue
+				}
+				conf.search = append(conf.search, name)
+			}
+
+		case "options":
+			for _, s := range f[1:] {
+				switch {
+				case strings.HasPrefix(s, "ndots:"):
+					n, _, _ := dtoi(s[6:])
+					if n < 0 {
+						n = 0
+					} else if n > 15 {
+						n = 15
+					}
+					conf.ndots = n
+				case strings.HasPrefix(s, "timeout:"):
+					n, _, _ := dtoi(s[8:])
+					if n < 1 {
+						n = 1
+					}
+					conf.timeout = time.Duration(n) * time.Second
+				case strings.HasPrefix(s, "attempts:"):
+					n, _, _ := dtoi(s[9:])
+					if n < 1 {
+						n = 1
+					}
+					conf.attempts = n
+				case s == "rotate":
+					conf.rotate = true
+				case s == "single-request" || s == "single-request-reopen":
+					conf.singleRequest = true
+				case s == "use-vc" || s == "usevc" || s == "tcp":
+					conf.useTCP = true
+				case s == "trust-ad":
+					conf.trustAD = true
+				case s == "edns0":
+				case s == "no-reload":
+					conf.noReload = true
+				default:
+					conf.unknownOpt = true
+				}
+			}
+
+		case "lookup":
+			conf.lookup = f[1:]
+
+		default:
+			conf.unknownOpt = true
+		}
+	}
+	if len(conf.servers) == 0 {
+		conf.servers = defaultNS
+	}
+	if len(conf.search) == 0 {
+		conf.search = dnsDefaultSearch()
+	}
+	return conf
+}
+
+//go:linkname defaultNS net.defaultNS
+var defaultNS []string
+
+func dnsDefaultSearch() []string {
+	hn, err := os.Hostname()
+	if err != nil {
+		return nil
+	}
+	if i := strings.IndexRune(hn, '.'); i >= 0 && i < len(hn)-1 {
+		return []string{ensureRooted(hn[i+1:])}
+	}
+	return nil
+}
+
+func ensureRooted(s string) string {
+	if len(s) > 0 && s[len(s)-1] == '.' {
+		return s
+	}
+	return s + "."
+}
+
+const big = 0xFFFFFF
+
+func dtoi(s string) (n int, i int, ok bool) {
+	n = 0
+	for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
+		n = n*10 + int(s[i]-'0')
+		if n >= big {
+			return big, i, false
+		}
+	}
+	if i == 0 {
+		return 0, 0, false
+	}
+	return n, i, true
+}

+ 100 - 0
dns/transport/local/resolv_windows.go

@@ -0,0 +1,100 @@
+package local
+
+import (
+	"net"
+	"net/netip"
+	"os"
+	"syscall"
+	"time"
+	"unsafe"
+
+	"golang.org/x/sys/windows"
+)
+
+func dnsReadConfig(_ string) *dnsConfig {
+	conf := &dnsConfig{
+		ndots:    1,
+		timeout:  5 * time.Second,
+		attempts: 2,
+	}
+	defer func() {
+		if len(conf.servers) == 0 {
+			conf.servers = defaultNS
+		}
+	}()
+	aas, err := adapterAddresses()
+	if err != nil {
+		return nil
+	}
+
+	for _, aa := range aas {
+		// Only take interfaces whose OperStatus is IfOperStatusUp(0x01) into DNS configs.
+		if aa.OperStatus != windows.IfOperStatusUp {
+			continue
+		}
+
+		// Only take interfaces which have at least one gateway
+		if aa.FirstGatewayAddress == nil {
+			continue
+		}
+
+		for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next {
+			sa, err := dns.Address.Sockaddr.Sockaddr()
+			if err != nil {
+				continue
+			}
+			var ip netip.Addr
+			switch sa := sa.(type) {
+			case *syscall.SockaddrInet4:
+				ip = netip.AddrFrom4([4]byte{sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3]})
+			case *syscall.SockaddrInet6:
+				var addr16 [16]byte
+				copy(addr16[:], sa.Addr[:])
+				if addr16[0] == 0xfe && addr16[1] == 0xc0 {
+					// fec0/10 IPv6 addresses are site local anycast DNS
+					// addresses Microsoft sets by default if no other
+					// IPv6 DNS address is set. Site local anycast is
+					// deprecated since 2004, see
+					// https://datatracker.ietf.org/doc/html/rfc3879
+					continue
+				}
+				ip = netip.AddrFrom16(addr16)
+			default:
+				// Unexpected type.
+				continue
+			}
+			conf.servers = append(conf.servers, net.JoinHostPort(ip.String(), "53"))
+		}
+	}
+	return conf
+}
+
+//go:linkname defaultNS net.defaultNS
+var defaultNS []string
+
+func adapterAddresses() ([]*windows.IpAdapterAddresses, error) {
+	var b []byte
+	l := uint32(15000) // recommended initial size
+	for {
+		b = make([]byte, l)
+		const flags = windows.GAA_FLAG_INCLUDE_PREFIX | windows.GAA_FLAG_INCLUDE_GATEWAYS
+		err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, flags, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l)
+		if err == nil {
+			if l == 0 {
+				return nil, nil
+			}
+			break
+		}
+		if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW {
+			return nil, os.NewSyscallError("getadaptersaddresses", err)
+		}
+		if l <= uint32(len(b)) {
+			return nil, os.NewSyscallError("getadaptersaddresses", err)
+		}
+	}
+	var aas []*windows.IpAdapterAddresses
+	for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next {
+		aas = append(aas, aa)
+	}
+	return aas, nil
+}

+ 2 - 0
include/registry.go

@@ -11,6 +11,7 @@ import (
 	"github.com/sagernet/sing-box/dns"
 	"github.com/sagernet/sing-box/dns/transport"
 	"github.com/sagernet/sing-box/dns/transport/fakeip"
+	"github.com/sagernet/sing-box/dns/transport/hosts"
 	"github.com/sagernet/sing-box/dns/transport/local"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
@@ -103,6 +104,7 @@ func DNSTransportRegistry() *dns.TransportRegistry {
 	transport.RegisterTLS(registry)
 	transport.RegisterHTTPS(registry)
 	transport.RegisterPredefined(registry)
+	hosts.RegisterTransport(registry)
 	local.RegisterTransport(registry)
 	fakeip.RegisterTransport(registry)
 

+ 5 - 0
option/dns.go

@@ -259,6 +259,11 @@ type LegacyDNSServerOptions struct {
 	ClientSubnet         *badoption.Prefixable `json:"client_subnet,omitempty"`
 }
 
+type HostsDNSServerOptions struct {
+	Path       badoption.Listable[string]                               `json:"path,omitempty"`
+	Predefined badjson.TypedMap[string, badoption.Listable[netip.Addr]] `json:"predefined,omitempty"`
+}
+
 type LocalDNSServerOptions struct {
 	DialerOptions
 	LegacyStrategy      DomainStrategy `json:"-"`