mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-01-28 19:56:49 +00:00
Add hosts transport & Remove bad linkname usages
This commit is contained in:
parent
314b25bccf
commit
3fa417f283
|
@ -32,7 +32,6 @@ run:
|
|||
- with_reality_server
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- badlinkname
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
|
|
|
@ -9,7 +9,6 @@ builds:
|
|||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||
- -s
|
||||
- -buildid=
|
||||
- -checklinkname=0
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
|
|
|
@ -11,7 +11,6 @@ builds:
|
|||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||
- -s
|
||||
- -buildid=
|
||||
- -checklinkname=0
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
|
|
|
@ -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 <contact-git@sekai.icu>"
|
||||
|
|
5
Makefile
5
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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -22,6 +22,7 @@ const (
|
|||
DNSTypeHTTPS = "https"
|
||||
DNSTypeQUIC = "quic"
|
||||
DNSTypeHTTP3 = "h3"
|
||||
DNSTypeHosts = "hosts"
|
||||
DNSTypeLocal = "local"
|
||||
DNSTypePreDefined = "predefined"
|
||||
DNSTypeFakeIP = "fakeip"
|
||||
|
|
63
dns/transport/hosts/hosts.go
Normal file
63
dns/transport/hosts/hosts.go
Normal file
|
@ -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
dns/transport/hosts/hosts_file.go
Normal file
102
dns/transport/hosts/hosts_file.go
Normal file
|
@ -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
dns/transport/hosts/hosts_test.go
Normal file
15
dns/transport/hosts/hosts_test.go
Normal file
|
@ -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
dns/transport/hosts/hosts_unix.go
Normal file
5
dns/transport/hosts/hosts_unix.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
//go:build !windows
|
||||
|
||||
package hosts
|
||||
|
||||
var DefaultPath = "/etc/hosts"
|
8
dns/transport/hosts/hosts_windows.go
Normal file
8
dns/transport/hosts/hosts_windows.go
Normal file
|
@ -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
dns/transport/hosts/testdata/hosts
vendored
Normal file
2
dns/transport/hosts/testdata/hosts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
127.0.0.1 localhost
|
||||
::1 localhost
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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
dns/transport/local/resolv.go
Normal file
154
dns/transport/local/resolv.go
Normal file
|
@ -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
dns/transport/local/resolv_unix.go
Normal file
175
dns/transport/local/resolv_unix.go
Normal file
|
@ -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
dns/transport/local/resolv_windows.go
Normal file
100
dns/transport/local/resolv_windows.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:"-"`
|
||||
|
|
Loading…
Reference in a new issue