From 3fa417f2835598d2d9c19643e4a6a30b386b6e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 25 Jan 2025 19:04:12 +0800 Subject: [PATCH] Add hosts transport & Remove bad linkname usages --- .golangci.yml | 1 - .goreleaser.fury.yaml | 1 - .goreleaser.yaml | 1 - Dockerfile | 2 +- Makefile | 5 +- cmd/internal/build_libbox/main.go | 2 +- constant/dns.go | 1 + dns/transport/hosts/hosts.go | 63 +++++++ dns/transport/hosts/hosts_file.go | 102 ++++++++++++ dns/transport/hosts/hosts_test.go | 15 ++ dns/transport/hosts/hosts_unix.go | 5 + dns/transport/hosts/hosts_windows.go | 8 + dns/transport/hosts/testdata/hosts | 2 + dns/transport/local/local.go | 16 +- dns/transport/local/local_badlinkname.go | 19 --- dns/transport/local/local_linkname.go | 44 ----- dns/transport/local/local_notbadlinkname.go | 19 --- dns/transport/local/resolv.go | 154 +++++++++++++++++ dns/transport/local/resolv_unix.go | 175 ++++++++++++++++++++ dns/transport/local/resolv_windows.go | 100 +++++++++++ include/registry.go | 2 + option/dns.go | 5 + 22 files changed, 645 insertions(+), 97 deletions(-) create mode 100644 dns/transport/hosts/hosts.go create mode 100644 dns/transport/hosts/hosts_file.go create mode 100644 dns/transport/hosts/hosts_test.go create mode 100644 dns/transport/hosts/hosts_unix.go create mode 100644 dns/transport/hosts/hosts_windows.go create mode 100644 dns/transport/hosts/testdata/hosts delete mode 100644 dns/transport/local/local_badlinkname.go delete mode 100644 dns/transport/local/local_linkname.go delete mode 100644 dns/transport/local/local_notbadlinkname.go create mode 100644 dns/transport/local/resolv.go create mode 100644 dns/transport/local/resolv_unix.go create mode 100644 dns/transport/local/resolv_windows.go diff --git a/.golangci.yml b/.golangci.yml index fcc8b7fd..cdd53eca 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -32,7 +32,6 @@ run: - with_reality_server - with_acme - with_clash_api - - badlinkname issues: exclude-dirs: diff --git a/.goreleaser.fury.yaml b/.goreleaser.fury.yaml index 599dfa9b..d80dd408 100644 --- a/.goreleaser.fury.yaml +++ b/.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 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 095a94d5..62136cfc 100644 --- a/.goreleaser.yaml +++ b/.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 diff --git a/Dockerfile b/Dockerfile index 0b1ac735..e82b81ce 100644 --- a/Dockerfile +++ b/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 " diff --git a/Makefile b/Makefile index 87595b8b..46123327 100644 --- a/Makefile +++ b/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) diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index 72d06e7a..e37a9ff4 100644 --- a/cmd/internal/build_libbox/main.go +++ b/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") diff --git a/constant/dns.go b/constant/dns.go index d3346de9..99461a27 100644 --- a/constant/dns.go +++ b/constant/dns.go @@ -22,6 +22,7 @@ const ( DNSTypeHTTPS = "https" DNSTypeQUIC = "quic" DNSTypeHTTP3 = "h3" + DNSTypeHosts = "hosts" DNSTypeLocal = "local" DNSTypePreDefined = "predefined" DNSTypeFakeIP = "fakeip" diff --git a/dns/transport/hosts/hosts.go b/dns/transport/hosts/hosts.go new file mode 100644 index 00000000..29f6778a --- /dev/null +++ b/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 +} diff --git a/dns/transport/hosts/hosts_file.go b/dns/transport/hosts/hosts_file.go new file mode 100644 index 00000000..7ff34f69 --- /dev/null +++ b/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 +} diff --git a/dns/transport/hosts/hosts_test.go b/dns/transport/hosts/hosts_test.go new file mode 100644 index 00000000..55e6f461 --- /dev/null +++ b/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.")) +} diff --git a/dns/transport/hosts/hosts_unix.go b/dns/transport/hosts/hosts_unix.go new file mode 100644 index 00000000..4caed8b4 --- /dev/null +++ b/dns/transport/hosts/hosts_unix.go @@ -0,0 +1,5 @@ +//go:build !windows + +package hosts + +var DefaultPath = "/etc/hosts" diff --git a/dns/transport/hosts/hosts_windows.go b/dns/transport/hosts/hosts_windows.go new file mode 100644 index 00000000..8025aa44 --- /dev/null +++ b/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 diff --git a/dns/transport/hosts/testdata/hosts b/dns/transport/hosts/testdata/hosts new file mode 100644 index 00000000..9ddcc8c1 --- /dev/null +++ b/dns/transport/hosts/testdata/hosts @@ -0,0 +1,2 @@ +127.0.0.1 localhost +::1 localhost diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go index 4449e1f6..66a220e8 100644 --- a/dns/transport/local/local.go +++ b/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 { diff --git a/dns/transport/local/local_badlinkname.go b/dns/transport/local/local_badlinkname.go deleted file mode 100644 index 1c7dcf4d..00000000 --- a/dns/transport/local/local_badlinkname.go +++ /dev/null @@ -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) diff --git a/dns/transport/local/local_linkname.go b/dns/transport/local/local_linkname.go deleted file mode 100644 index 03cdd209..00000000 --- a/dns/transport/local/local_linkname.go +++ /dev/null @@ -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 -} diff --git a/dns/transport/local/local_notbadlinkname.go b/dns/transport/local/local_notbadlinkname.go deleted file mode 100644 index d2bb92cf..00000000 --- a/dns/transport/local/local_notbadlinkname.go +++ /dev/null @@ -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") -} diff --git a/dns/transport/local/resolv.go b/dns/transport/local/resolv.go new file mode 100644 index 00000000..2a9e3bc5 --- /dev/null +++ b/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") +} diff --git a/dns/transport/local/resolv_unix.go b/dns/transport/local/resolv_unix.go new file mode 100644 index 00000000..6594ae41 --- /dev/null +++ b/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 +} diff --git a/dns/transport/local/resolv_windows.go b/dns/transport/local/resolv_windows.go new file mode 100644 index 00000000..577e7a12 --- /dev/null +++ b/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 +} diff --git a/include/registry.go b/include/registry.go index 6f6aab2b..cbf793f4 100644 --- a/include/registry.go +++ b/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) diff --git a/option/dns.go b/option/dns.go index 7260f8f3..2ed765fc 100644 --- a/option/dns.go +++ b/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:"-"`