mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-09-09 21:04:08 +08:00
Compare commits
25 Commits
b585bfdf1b
...
3572acf266
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3572acf266 | ||
![]() |
92d175c615 | ||
![]() |
ca90727e9b | ||
![]() |
9510bc7911 | ||
![]() |
e2281f4f9d | ||
![]() |
b18c002b3c | ||
![]() |
a81bbbd811 | ||
![]() |
9629d6cab7 | ||
![]() |
32f64972e1 | ||
![]() |
dd894adeb5 | ||
![]() |
4f9e334679 | ||
![]() |
fc1204fb8b | ||
![]() |
5ca20e1cf6 | ||
![]() |
8073f299dd | ||
![]() |
005cdadffb | ||
![]() |
8b511086e4 | ||
![]() |
253750a555 | ||
![]() |
213c2be747 | ||
![]() |
5fdc3e3edb | ||
![]() |
97f0dc8a60 | ||
![]() |
ee02532ab5 | ||
![]() |
f1dd0dba78 | ||
![]() |
f4ed684146 | ||
![]() |
83f02d0bfb | ||
![]() |
52fa5f20a3 |
2
Makefile
2
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)
|
||||
|
@ -2,6 +2,7 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@ -18,6 +19,11 @@ type Outbound interface {
|
||||
N.Dialer
|
||||
}
|
||||
|
||||
type OutboundWithPreferredRoutes interface {
|
||||
PreferredDomain(domain string) bool
|
||||
PreferredAddress(address netip.Addr) bool
|
||||
}
|
||||
|
||||
type OutboundRegistry interface {
|
||||
option.OutboundOptionsRegistry
|
||||
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||
|
@ -56,6 +56,14 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
if stage == adapter.StartStateStart {
|
||||
if m.defaultTag != "" && m.defaultOutbound == nil {
|
||||
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
|
||||
if !loaded {
|
||||
m.access.Unlock()
|
||||
return E.New("default outbound not found: ", m.defaultTag)
|
||||
}
|
||||
m.defaultOutbound = defaultEndpoint
|
||||
}
|
||||
if m.defaultOutbound == nil {
|
||||
directOutbound, err := m.defaultOutboundFallback()
|
||||
if err != nil {
|
||||
@ -66,14 +74,6 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.outboundByTag[directOutbound.Tag()] = directOutbound
|
||||
m.defaultOutbound = directOutbound
|
||||
}
|
||||
if m.defaultTag != "" && m.defaultOutbound == nil {
|
||||
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
|
||||
if !loaded {
|
||||
m.access.Unlock()
|
||||
return E.New("default outbound not found: ", m.defaultTag)
|
||||
}
|
||||
m.defaultOutbound = defaultEndpoint
|
||||
}
|
||||
outbounds := m.outbounds
|
||||
m.access.Unlock()
|
||||
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
|
||||
|
7
box.go
7
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 {
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 6db8e06e8d6c77648e79e3f93bdd41a4d48cc319
|
||||
Subproject commit 597c18482f6edc44e0c94b7cc849dd03b2121c45
|
@ -46,7 +46,7 @@ var (
|
||||
sharedFlags []string
|
||||
debugFlags []string
|
||||
sharedTags []string
|
||||
darwinTags []string
|
||||
macOSTags []string
|
||||
memcTags []string
|
||||
notMemcTags []string
|
||||
debugTags []string
|
||||
@ -63,7 +63,7 @@ func init() {
|
||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
|
||||
darwinTags = append(darwinTags, "with_dhcp")
|
||||
macOSTags = append(macOSTags, "with_dhcp")
|
||||
memcTags = append(memcTags, "with_tailscale")
|
||||
notMemcTags = append(notMemcTags, "with_low_memory")
|
||||
debugTags = append(debugTags, "debug")
|
||||
@ -160,7 +160,9 @@ func buildApple() {
|
||||
"-tags-not-macos=with_low_memory",
|
||||
}
|
||||
if !withTailscale {
|
||||
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
|
||||
args = append(args, "-tags-macos="+strings.Join(append(macOSTags, memcTags...), ","))
|
||||
} else {
|
||||
args = append(args, "-tags-macos="+strings.Join(macOSTags, ","))
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
@ -169,7 +171,7 @@ func buildApple() {
|
||||
args = append(args, debugFlags...)
|
||||
}
|
||||
|
||||
tags := append(sharedTags, darwinTags...)
|
||||
tags := sharedTags
|
||||
if withTailscale {
|
||||
tags = append(tags, memcTags...)
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/route/rule"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@ -69,7 +71,7 @@ func compileRuleSet(sourcePath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version)
|
||||
err = srs.Write(outputFile, plainRuleSet.Options, downgradeRuleSetVersion(plainRuleSet.Version, plainRuleSet.Options))
|
||||
if err != nil {
|
||||
outputFile.Close()
|
||||
os.Remove(outputPath)
|
||||
@ -78,3 +80,18 @@ func compileRuleSet(sourcePath string) error {
|
||||
outputFile.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
|
||||
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
||||
len(rule.DefaultInterfaceAddress) > 0
|
||||
}) {
|
||||
version = C.RuleSetVersion3
|
||||
}
|
||||
if version == C.RuleSetVersion3 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||
return len(rule.NetworkType) > 0 || rule.NetworkIsExpensive || rule.NetworkIsConstrained
|
||||
}) {
|
||||
version = C.RuleSetVersion2
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
@ -128,6 +128,10 @@ func (c *ReadWaitConn) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
func (c *ReadWaitConn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
|
||||
|
||||
func init() {
|
||||
|
@ -6,22 +6,26 @@ import (
|
||||
"net"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
|
||||
"github.com/metacubex/utls"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
||||
tlsConn, loaded := common.Cast[*tls.UConn](conn)
|
||||
if !loaded {
|
||||
return
|
||||
switch tlsConn := conn.(type) {
|
||||
case *tls.UConn:
|
||||
return true, func() error {
|
||||
return utlsReadRecord(tlsConn.Conn)
|
||||
}, func() error {
|
||||
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
|
||||
}
|
||||
case *tls.Conn:
|
||||
return true, func() error {
|
||||
return utlsReadRecord(tlsConn)
|
||||
}, func() error {
|
||||
return utlsHandlePostHandshakeMessage(tlsConn)
|
||||
}
|
||||
}
|
||||
return true, func() error {
|
||||
return utlsReadRecord(tlsConn.Conn)
|
||||
}, func() error {
|
||||
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,8 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/domain"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
|
||||
"go4.org/netipx"
|
||||
@ -41,6 +43,8 @@ const (
|
||||
ruleItemNetworkType
|
||||
ruleItemNetworkIsExpensive
|
||||
ruleItemNetworkIsConstrained
|
||||
ruleItemNetworkInterfaceAddress
|
||||
ruleItemDefaultInterfaceAddress
|
||||
ruleItemFinal uint8 = 0xFF
|
||||
)
|
||||
|
||||
@ -230,6 +234,51 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
|
||||
rule.NetworkIsExpensive = true
|
||||
case ruleItemNetworkIsConstrained:
|
||||
rule.NetworkIsConstrained = true
|
||||
case ruleItemNetworkInterfaceAddress:
|
||||
rule.NetworkInterfaceAddress = new(badjson.TypedMap[option.InterfaceType, badoption.Listable[*badoption.Prefixable]])
|
||||
var size uint64
|
||||
size, err = binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for i := uint64(0); i < size; i++ {
|
||||
var key uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var value []*badoption.Prefixable
|
||||
var prefixCount uint64
|
||||
prefixCount, err = binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for j := uint64(0); j < prefixCount; j++ {
|
||||
var prefix netip.Prefix
|
||||
prefix, err = readPrefix(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value = append(value, common.Ptr(badoption.Prefixable(prefix)))
|
||||
}
|
||||
rule.NetworkInterfaceAddress.Put(option.InterfaceType(key), value)
|
||||
}
|
||||
case ruleItemDefaultInterfaceAddress:
|
||||
var value []*badoption.Prefixable
|
||||
var prefixCount uint64
|
||||
prefixCount, err = binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for j := uint64(0); j < prefixCount; j++ {
|
||||
var prefix netip.Prefix
|
||||
prefix, err = readPrefix(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value = append(value, common.Ptr(badoption.Prefixable(prefix)))
|
||||
}
|
||||
rule.DefaultInterfaceAddress = value
|
||||
case ruleItemFinal:
|
||||
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
|
||||
return
|
||||
@ -346,7 +395,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
||||
}
|
||||
if len(rule.NetworkType) > 0 {
|
||||
if generateVersion < C.RuleSetVersion3 {
|
||||
return E.New("network_type rule item is only supported in version 3 or later")
|
||||
return E.New("`network_type` rule item is only supported in version 3 or later")
|
||||
}
|
||||
err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType)
|
||||
if err != nil {
|
||||
@ -354,17 +403,71 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
||||
}
|
||||
}
|
||||
if rule.NetworkIsExpensive {
|
||||
if generateVersion < C.RuleSetVersion3 {
|
||||
return E.New("`network_is_expensive` rule item is only supported in version 3 or later")
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if rule.NetworkIsConstrained {
|
||||
if generateVersion < C.RuleSetVersion3 {
|
||||
return E.New("`network_is_constrained` rule item is only supported in version 3 or later")
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 {
|
||||
if generateVersion < C.RuleSetVersion4 {
|
||||
return E.New("`network_interface_address` rule item is only supported in version 4 or later")
|
||||
}
|
||||
err = writer.WriteByte(ruleItemNetworkInterfaceAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(writer, uint64(rule.NetworkInterfaceAddress.Size()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range rule.NetworkInterfaceAddress.Entries() {
|
||||
err = binary.Write(writer, binary.BigEndian, uint8(entry.Key.Build()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(writer, uint64(len(entry.Value)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rawPrefix := range entry.Value {
|
||||
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rule.DefaultInterfaceAddress) > 0 {
|
||||
if generateVersion < C.RuleSetVersion4 {
|
||||
return E.New("`default_interface_address` rule item is only supported in version 4 or later")
|
||||
}
|
||||
err = writer.WriteByte(ruleItemDefaultInterfaceAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(writer, uint64(len(rule.DefaultInterfaceAddress)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rawPrefix := range rule.DefaultInterfaceAddress {
|
||||
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rule.WIFISSID) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
|
||||
if err != nil {
|
||||
|
33
common/srs/ip_cidr.go
Normal file
33
common/srs/ip_cidr.go
Normal file
@ -0,0 +1,33 @@
|
||||
package srs
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
)
|
||||
|
||||
func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
|
||||
addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, err
|
||||
}
|
||||
prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, err
|
||||
}
|
||||
return netip.PrefixFrom(M.AddrFromIP(addrSlice), int(prefixBits)), nil
|
||||
}
|
||||
|
||||
func writePrefix(writer varbin.Writer, prefix netip.Prefix) error {
|
||||
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@ -2,10 +2,11 @@ package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/badtls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@ -14,7 +15,7 @@ import (
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
func NewDialerFromOptions(ctx context.Context, router adapter.Router, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
||||
func NewDialerFromOptions(ctx context.Context, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
||||
if !options.Enabled {
|
||||
return dialer, nil
|
||||
}
|
||||
@ -53,26 +54,57 @@ func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, e
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
type Dialer struct {
|
||||
type Dialer interface {
|
||||
N.Dialer
|
||||
DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error)
|
||||
}
|
||||
|
||||
type defaultDialer struct {
|
||||
dialer N.Dialer
|
||||
config Config
|
||||
}
|
||||
|
||||
func NewDialer(dialer N.Dialer, config Config) N.Dialer {
|
||||
return &Dialer{dialer, config}
|
||||
func NewDialer(dialer N.Dialer, config Config) Dialer {
|
||||
return &defaultDialer{dialer, config}
|
||||
}
|
||||
|
||||
func (d *Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
if network != N.NetworkTCP {
|
||||
func (d *defaultDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
if N.NetworkName(network) != N.NetworkTCP {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
conn, err := d.dialer.DialContext(ctx, network, destination)
|
||||
return d.DialTLSContext(ctx, destination)
|
||||
}
|
||||
|
||||
func (d *defaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
|
||||
return d.dialContext(ctx, destination, true)
|
||||
}
|
||||
|
||||
func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr, echRetry bool) (Conn, error) {
|
||||
conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ClientHandshake(ctx, conn, d.config)
|
||||
tlsConn, err := ClientHandshake(ctx, conn, d.config)
|
||||
if err == nil {
|
||||
return tlsConn, nil
|
||||
}
|
||||
conn.Close()
|
||||
if echRetry {
|
||||
var echErr *tls.ECHRejectionError
|
||||
if errors.As(err, &echErr) && len(echErr.RetryConfigList) > 0 {
|
||||
if echConfig, isECH := d.config.(ECHCapableConfig); isECH {
|
||||
echConfig.SetECHConfigList(echErr.RetryConfigList)
|
||||
}
|
||||
}
|
||||
return d.dialContext(ctx, destination, false)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (d *Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return nil, os.ErrInvalid
|
||||
func (d *defaultDialer) Upstream() any {
|
||||
return d.dialer
|
||||
}
|
||||
|
@ -206,3 +206,11 @@ func (c *realityConnWrapper) Upstream() any {
|
||||
func (c *realityConnWrapper) CloseWrite() error {
|
||||
return c.Close()
|
||||
}
|
||||
|
||||
func (c *realityConnWrapper) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *realityConnWrapper) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
@ -106,6 +106,14 @@ func (c *utlsConnWrapper) Upstream() any {
|
||||
return c.UConn
|
||||
}
|
||||
|
||||
func (c *utlsConnWrapper) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *utlsConnWrapper) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type utlsALPNWrapper struct {
|
||||
utlsConnWrapper
|
||||
nextProtocols []string
|
||||
|
@ -109,6 +109,9 @@ func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if i != len(splitIndexes) {
|
||||
time.Sleep(c.fallbackDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,10 @@ import (
|
||||
)
|
||||
|
||||
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
||||
_, err := conn.Write(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(fallbackDelay)
|
||||
return nil
|
||||
}
|
||||
|
@ -16,6 +16,9 @@ func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fal
|
||||
err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload)
|
||||
if err != nil {
|
||||
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||
if _, err := conn.Write(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(fallbackDelay)
|
||||
return nil
|
||||
}
|
||||
|
@ -22,7 +22,8 @@ const (
|
||||
RuleSetVersion1 = 1 + iota
|
||||
RuleSetVersion2
|
||||
RuleSetVersion3
|
||||
RuleSetVersionCurrent = RuleSetVersion3
|
||||
RuleSetVersion4
|
||||
RuleSetVersionCurrent = RuleSetVersion4
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -9,10 +9,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
@ -29,6 +27,7 @@ import (
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
mDNS "github.com/miekg/dns"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
@ -45,9 +44,12 @@ type Transport struct {
|
||||
networkManager adapter.NetworkManager
|
||||
interfaceName string
|
||||
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
|
||||
transports []adapter.DNSTransport
|
||||
updateAccess sync.Mutex
|
||||
transportLock sync.RWMutex
|
||||
updatedAt time.Time
|
||||
servers []M.Socksaddr
|
||||
search []string
|
||||
ndots int
|
||||
attempts int
|
||||
}
|
||||
|
||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
@ -62,27 +64,40 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
||||
logger: logger,
|
||||
networkManager: service.FromContext[adapter.NetworkManager](ctx),
|
||||
interfaceName: options.Interface,
|
||||
ndots: 1,
|
||||
attempts: 2,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewRawTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) *Transport {
|
||||
return &Transport{
|
||||
TransportAdapter: transportAdapter,
|
||||
ctx: ctx,
|
||||
dialer: dialer,
|
||||
logger: logger,
|
||||
networkManager: service.FromContext[adapter.NetworkManager](ctx),
|
||||
ndots: 1,
|
||||
attempts: 2,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
err := t.fetchServers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.interfaceName == "" {
|
||||
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
|
||||
}
|
||||
go func() {
|
||||
_, err := t.Fetch()
|
||||
if err != nil {
|
||||
t.logger.Error(E.Cause(err, "fetch DNS servers"))
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
for _, transport := range t.transports {
|
||||
transport.Close()
|
||||
}
|
||||
if t.interfaceCallback != nil {
|
||||
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
|
||||
}
|
||||
@ -90,23 +105,44 @@ func (t *Transport) Close() error {
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
err := t.fetchServers()
|
||||
servers, err := t.Fetch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(t.transports) == 0 {
|
||||
if len(servers) == 0 {
|
||||
return nil, E.New("dhcp: empty DNS servers from response")
|
||||
}
|
||||
return t.Exchange0(ctx, message, servers)
|
||||
}
|
||||
|
||||
var response *mDNS.Msg
|
||||
for _, transport := range t.transports {
|
||||
response, err = transport.Exchange(ctx, message)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
func (t *Transport) Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error) {
|
||||
question := message.Question[0]
|
||||
domain := dns.FqdnToDomain(question.Name)
|
||||
if len(servers) == 1 || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
|
||||
return t.exchangeSingleRequest(ctx, servers, message, domain)
|
||||
} else {
|
||||
return t.exchangeParallel(ctx, servers, message, domain)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (t *Transport) Fetch() ([]M.Socksaddr, error) {
|
||||
t.transportLock.RLock()
|
||||
updatedAt := t.updatedAt
|
||||
servers := t.servers
|
||||
t.transportLock.RUnlock()
|
||||
if time.Since(updatedAt) < C.DHCPTTL {
|
||||
return servers, nil
|
||||
}
|
||||
t.transportLock.Lock()
|
||||
defer t.transportLock.Unlock()
|
||||
if time.Since(t.updatedAt) < C.DHCPTTL {
|
||||
return t.servers, nil
|
||||
}
|
||||
err := t.updateServers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.servers, nil
|
||||
}
|
||||
|
||||
func (t *Transport) fetchInterface() (*control.Interface, error) {
|
||||
@ -124,18 +160,6 @@ func (t *Transport) fetchInterface() (*control.Interface, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) fetchServers() error {
|
||||
if time.Since(t.updatedAt) < C.DHCPTTL {
|
||||
return nil
|
||||
}
|
||||
t.updateAccess.Lock()
|
||||
defer t.updateAccess.Unlock()
|
||||
if time.Since(t.updatedAt) < C.DHCPTTL {
|
||||
return nil
|
||||
}
|
||||
return t.updateServers()
|
||||
}
|
||||
|
||||
func (t *Transport) updateServers() error {
|
||||
iface, err := t.fetchInterface()
|
||||
if err != nil {
|
||||
@ -148,7 +172,7 @@ func (t *Transport) updateServers() error {
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(t.transports) == 0 {
|
||||
} else if len(t.servers) == 0 {
|
||||
return E.New("dhcp: empty DNS servers response")
|
||||
} else {
|
||||
t.updatedAt = time.Now()
|
||||
@ -177,7 +201,11 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *control.Interface)
|
||||
}
|
||||
defer packetConn.Close()
|
||||
|
||||
discovery, err := dhcpv4.NewDiscovery(iface.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer))
|
||||
discovery, err := dhcpv4.NewDiscovery(iface.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(
|
||||
dhcpv4.OptionDomainName,
|
||||
dhcpv4.OptionDomainNameServer,
|
||||
dhcpv4.OptionDNSDomainSearchList,
|
||||
))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -223,31 +251,23 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
|
||||
continue
|
||||
}
|
||||
|
||||
dns := dhcpPacket.DNS()
|
||||
if len(dns) == 0 {
|
||||
return nil
|
||||
}
|
||||
return t.recreateServers(iface, common.Map(dns, func(it net.IP) M.Socksaddr {
|
||||
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
|
||||
}))
|
||||
return t.recreateServers(iface, dhcpPacket)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []M.Socksaddr) error {
|
||||
if len(serverAddrs) > 0 {
|
||||
t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "]")
|
||||
func (t *Transport) recreateServers(iface *control.Interface, dhcpPacket *dhcpv4.DHCPv4) error {
|
||||
searchList := dhcpPacket.DomainSearch()
|
||||
if searchList != nil && len(searchList.Labels) > 0 {
|
||||
t.search = searchList.Labels
|
||||
} else if dhcpPacket.DomainName() != "" {
|
||||
t.search = []string{dhcpPacket.DomainName()}
|
||||
}
|
||||
serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{
|
||||
BindInterface: iface.Name,
|
||||
UDPFragmentDefault: true,
|
||||
}))
|
||||
var transports []adapter.DNSTransport
|
||||
for _, serverAddr := range serverAddrs {
|
||||
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, serverAddr))
|
||||
serverAddrs := common.Map(dhcpPacket.DNS(), func(it net.IP) M.Socksaddr {
|
||||
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
|
||||
})
|
||||
if len(serverAddrs) > 0 && !slices.Equal(t.servers, serverAddrs) {
|
||||
t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "], search: [", strings.Join(t.search, ","), "]")
|
||||
}
|
||||
for _, transport := range t.transports {
|
||||
transport.Close()
|
||||
}
|
||||
t.transports = transports
|
||||
t.servers = serverAddrs
|
||||
return nil
|
||||
}
|
||||
|
202
dns/transport/dhcp/dhcp_shared.go
Normal file
202
dns/transport/dhcp/dhcp_shared.go
Normal file
@ -0,0 +1,202 @@
|
||||
package dhcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
// net.maxDNSPacketSize
|
||||
maxDNSPacketSize = 1232
|
||||
)
|
||||
|
||||
func (t *Transport) exchangeSingleRequest(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||
var lastErr error
|
||||
for _, fqdn := range t.nameList(domain) {
|
||||
response, err := t.tryOneName(ctx, servers, fqdn, message)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
type queryResult struct {
|
||||
response *mDNS.Msg
|
||||
err error
|
||||
}
|
||||
results := make(chan queryResult)
|
||||
startRacer := func(ctx context.Context, fqdn string) {
|
||||
response, err := t.tryOneName(ctx, servers, fqdn, message)
|
||||
if err == nil {
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = dns.RcodeError(response.Rcode)
|
||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||
err = E.New(fqdn, ": empty result")
|
||||
}
|
||||
}
|
||||
select {
|
||||
case results <- queryResult{response, err}:
|
||||
case <-returned:
|
||||
}
|
||||
}
|
||||
queryCtx, queryCancel := context.WithCancel(ctx)
|
||||
defer queryCancel()
|
||||
var nameCount int
|
||||
for _, fqdn := range t.nameList(domain) {
|
||||
nameCount++
|
||||
go startRacer(queryCtx, fqdn)
|
||||
}
|
||||
var errors []error
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case result := <-results:
|
||||
if result.err == nil {
|
||||
return result.response, nil
|
||||
}
|
||||
errors = append(errors, result.err)
|
||||
if len(errors) == nameCount {
|
||||
return nil, E.Errors(errors...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
sLen := len(servers)
|
||||
var lastErr error
|
||||
for i := 0; i < t.attempts; i++ {
|
||||
for j := 0; j < sLen; j++ {
|
||||
server := servers[j]
|
||||
question := message.Question[0]
|
||||
question.Name = fqdn
|
||||
response, err := t.exchangeOne(ctx, server, question, C.DNSTimeout, false, true)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
return nil, E.Cause(lastErr, fqdn)
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) {
|
||||
if server.Port == 0 {
|
||||
server.Port = 53
|
||||
}
|
||||
var networks []string
|
||||
if useTCP {
|
||||
networks = []string{N.NetworkTCP}
|
||||
} else {
|
||||
networks = []string{N.NetworkUDP, N.NetworkTCP}
|
||||
}
|
||||
request := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: uint16(rand.Uint32()),
|
||||
RecursionDesired: true,
|
||||
AuthenticatedData: ad,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
Compress: true,
|
||||
}
|
||||
request.SetEdns0(maxDNSPacketSize, false)
|
||||
buffer := buf.Get(buf.UDPBufferSize)
|
||||
defer buf.Put(buffer)
|
||||
for _, network := range networks {
|
||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
|
||||
defer cancel()
|
||||
conn, err := t.dialer.DialContext(ctx, network, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||
conn.SetDeadline(deadline)
|
||||
}
|
||||
rawMessage, err := request.PackBuffer(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "pack request")
|
||||
}
|
||||
_, err = conn.Write(rawMessage)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read response")
|
||||
}
|
||||
var response mDNS.Msg
|
||||
err = response.Unpack(buffer[:n])
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack response")
|
||||
}
|
||||
if response.Truncated && network == N.NetworkUDP {
|
||||
continue
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
panic("unexpected")
|
||||
}
|
||||
|
||||
func (t *Transport) 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, ".") >= t.ndots
|
||||
name += "."
|
||||
// l++
|
||||
|
||||
names := make([]string, 0, 1+len(t.search))
|
||||
if hasNdots && !avoidDNS(name) {
|
||||
names = append(names, name)
|
||||
}
|
||||
for _, suffix := range t.search {
|
||||
fqdn := name + suffix
|
||||
if !avoidDNS(fqdn) && len(fqdn) <= 254 {
|
||||
names = append(names, fqdn)
|
||||
}
|
||||
}
|
||||
if !hasNdots && !avoidDNS(name) {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func avoidDNS(name string) bool {
|
||||
if name == "" {
|
||||
return true
|
||||
}
|
||||
if name[len(name)-1] == '.' {
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
return strings.HasSuffix(name, ".onion")
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
//go:build !darwin
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
@ -11,21 +11,27 @@ import (
|
||||
"github.com/sagernet/sing-box/dns/transport/hosts"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport)
|
||||
}
|
||||
|
||||
var _ adapter.DNSTransport = (*Transport)(nil)
|
||||
|
||||
type Transport struct {
|
||||
dns.TransportAdapter
|
||||
ctx context.Context
|
||||
hosts *hosts.File
|
||||
dialer N.Dialer
|
||||
ctx context.Context
|
||||
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) {
|
||||
@ -36,20 +42,45 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
||||
return &Transport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
|
||||
ctx: ctx,
|
||||
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:
|
||||
if !t.preferGo {
|
||||
resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger)
|
||||
if err == nil {
|
||||
err = resolvedResolver.Start()
|
||||
if err == nil {
|
||||
t.resolved = resolvedResolver
|
||||
} else {
|
||||
t.logger.Warn(E.Cause(err, "initialize resolved resolver"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
if t.resolved != nil {
|
||||
return t.resolved.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
if t.resolved != nil {
|
||||
resolverObject := t.resolved.Object()
|
||||
if resolverObject != nil {
|
||||
return t.resolved.Exchange(resolverObject, ctx, message)
|
||||
}
|
||||
}
|
||||
question := message.Question[0]
|
||||
domain := dns.FqdnToDomain(question.Name)
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
@ -58,147 +89,5 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
|
||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||
}
|
||||
}
|
||||
systemConfig := getSystemDNSConfig(t.ctx)
|
||||
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
|
||||
return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
|
||||
} else {
|
||||
return t.exchangeParallel(ctx, systemConfig, message, domain)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||
var lastErr error
|
||||
for _, fqdn := range systemConfig.nameList(domain) {
|
||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
type queryResult struct {
|
||||
response *mDNS.Msg
|
||||
err error
|
||||
}
|
||||
results := make(chan queryResult)
|
||||
startRacer := func(ctx context.Context, fqdn string) {
|
||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
||||
if err == nil {
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = dns.RcodeError(response.Rcode)
|
||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||
err = E.New(fqdn, ": empty result")
|
||||
}
|
||||
}
|
||||
select {
|
||||
case results <- queryResult{response, err}:
|
||||
case <-returned:
|
||||
}
|
||||
}
|
||||
queryCtx, queryCancel := context.WithCancel(ctx)
|
||||
defer queryCancel()
|
||||
var nameCount int
|
||||
for _, fqdn := range systemConfig.nameList(domain) {
|
||||
nameCount++
|
||||
go startRacer(queryCtx, fqdn)
|
||||
}
|
||||
var errors []error
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case result := <-results:
|
||||
if result.err == nil {
|
||||
return result.response, nil
|
||||
}
|
||||
errors = append(errors, result.err)
|
||||
if len(errors) == nameCount {
|
||||
return nil, E.Errors(errors...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
serverOffset := config.serverOffset()
|
||||
sLen := uint32(len(config.servers))
|
||||
var lastErr error
|
||||
for i := 0; i < config.attempts; i++ {
|
||||
for j := uint32(0); j < sLen; j++ {
|
||||
server := config.servers[(serverOffset+j)%sLen]
|
||||
question := message.Question[0]
|
||||
question.Name = fqdn
|
||||
response, err := t.exchangeOne(ctx, M.ParseSocksaddr(server), question, config.timeout, config.useTCP, config.trustAD)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
return nil, E.Cause(lastErr, fqdn)
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) {
|
||||
if server.Port == 0 {
|
||||
server.Port = 53
|
||||
}
|
||||
var networks []string
|
||||
if useTCP {
|
||||
networks = []string{N.NetworkTCP}
|
||||
} else {
|
||||
networks = []string{N.NetworkUDP, N.NetworkTCP}
|
||||
}
|
||||
request := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: uint16(rand.Uint32()),
|
||||
RecursionDesired: true,
|
||||
AuthenticatedData: ad,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
Compress: true,
|
||||
}
|
||||
request.SetEdns0(maxDNSPacketSize, false)
|
||||
buffer := buf.Get(buf.UDPBufferSize)
|
||||
defer buf.Put(buffer)
|
||||
for _, network := range networks {
|
||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
|
||||
defer cancel()
|
||||
conn, err := t.dialer.DialContext(ctx, network, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||
conn.SetDeadline(deadline)
|
||||
}
|
||||
rawMessage, err := request.PackBuffer(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "pack request")
|
||||
}
|
||||
_, err = conn.Write(rawMessage)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read response")
|
||||
}
|
||||
var response mDNS.Msg
|
||||
err = response.Unpack(buffer[:n])
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack response")
|
||||
}
|
||||
if response.Truncated && network == N.NetworkUDP {
|
||||
continue
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
panic("unexpected")
|
||||
return t.exchange(ctx, message, domain)
|
||||
}
|
||||
|
142
dns/transport/local/local_darwin.go
Normal file
142
dns/transport/local/local_darwin.go
Normal file
@ -0,0 +1,142 @@
|
||||
//go:build darwin
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport/hosts"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport)
|
||||
}
|
||||
|
||||
var _ adapter.DNSTransport = (*Transport)(nil)
|
||||
|
||||
type Transport struct {
|
||||
dns.TransportAdapter
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
hosts *hosts.File
|
||||
dialer N.Dialer
|
||||
preferGo bool
|
||||
fallback bool
|
||||
dhcpTransport dhcpTransport
|
||||
resolver net.Resolver
|
||||
}
|
||||
|
||||
type dhcpTransport interface {
|
||||
adapter.DNSTransport
|
||||
Fetch() ([]M.Socksaddr, error)
|
||||
Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error)
|
||||
}
|
||||
|
||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewLocalDialer(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transportAdapter := dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options)
|
||||
return &Transport{
|
||||
TransportAdapter: transportAdapter,
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
hosts: hosts.NewFile(hosts.DefaultPath),
|
||||
dialer: transportDialer,
|
||||
preferGo: options.PreferGo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
inboundManager := service.FromContext[adapter.InboundManager](t.ctx)
|
||||
for _, inbound := range inboundManager.Inbounds() {
|
||||
if inbound.Type() == C.TypeTun {
|
||||
t.fallback = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !C.IsIos {
|
||||
if t.fallback {
|
||||
t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger)
|
||||
if t.dhcpTransport != nil {
|
||||
err := t.dhcpTransport.Start(stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
return common.Close(
|
||||
t.dhcpTransport,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
question := message.Question[0]
|
||||
domain := dns.FqdnToDomain(question.Name)
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
addresses := t.hosts.Lookup(domain)
|
||||
if len(addresses) > 0 {
|
||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||
}
|
||||
}
|
||||
if !t.fallback {
|
||||
return t.exchange(ctx, message, domain)
|
||||
}
|
||||
if !C.IsIos {
|
||||
if t.dhcpTransport != nil {
|
||||
dhcpTransports, _ := t.dhcpTransport.Fetch()
|
||||
if len(dhcpTransports) > 0 {
|
||||
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
|
||||
}
|
||||
}
|
||||
}
|
||||
if t.preferGo {
|
||||
// Assuming the user knows what they are doing, we still execute the query which will fail.
|
||||
return t.exchange(ctx, message, domain)
|
||||
}
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
var network string
|
||||
if question.Qtype == mDNS.TypeA {
|
||||
network = "ip4"
|
||||
} else {
|
||||
network = "ip6"
|
||||
}
|
||||
addresses, err := t.resolver.LookupNetIP(ctx, network, 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
|
||||
}
|
||||
if C.IsIos {
|
||||
return nil, E.New("only A and AAAA queries are supported on iOS and tvOS when using NetworkExtension.")
|
||||
} else {
|
||||
return nil, E.New("only A and AAAA queries are supported on macOS when using NetworkExtension and DHCP unavailable.")
|
||||
}
|
||||
}
|
16
dns/transport/local/local_darwin_dhcp.go
Normal file
16
dns/transport/local/local_darwin_dhcp.go
Normal file
@ -0,0 +1,16 @@
|
||||
//go:build darwin && with_dhcp
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport/dhcp"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport {
|
||||
return dhcp.NewRawTransport(transportAdapter, ctx, dialer, logger)
|
||||
}
|
15
dns/transport/local/local_darwin_nodhcp.go
Normal file
15
dns/transport/local/local_darwin_nodhcp.go
Normal file
@ -0,0 +1,15 @@
|
||||
//go:build darwin && !with_dhcp
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport {
|
||||
return nil
|
||||
}
|
@ -1,205 +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
|
||||
}
|
||||
return &FallbackTransport{
|
||||
DNSTransport: transport,
|
||||
ctx: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *FallbackTransport) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
platformInterface := service.FromContext[platform.Interface](f.ctx)
|
||||
if platformInterface == nil {
|
||||
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 nil
|
||||
}
|
||||
|
||||
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.")
|
||||
}
|
||||
}
|
14
dns/transport/local/local_resolved.go
Normal file
14
dns/transport/local/local_resolved.go
Normal file
@ -0,0 +1,14 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type ResolvedResolver interface {
|
||||
Start() error
|
||||
Close() error
|
||||
Object() any
|
||||
Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
|
||||
}
|
150
dns/transport/local/local_resolved_linux.go
Normal file
150
dns/transport/local/local_resolved_linux.go
Normal file
@ -0,0 +1,150 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/service/resolved"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type DBusResolvedResolver struct {
|
||||
logger logger.ContextLogger
|
||||
interfaceMonitor tun.DefaultInterfaceMonitor
|
||||
systemBus *dbus.Conn
|
||||
resoledObject atomic.TypedValue[dbus.BusObject]
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
|
||||
interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor()
|
||||
if interfaceMonitor == nil {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
systemBus, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DBusResolvedResolver{
|
||||
logger: logger,
|
||||
interfaceMonitor: interfaceMonitor,
|
||||
systemBus: systemBus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) Start() error {
|
||||
t.updateStatus()
|
||||
err := t.systemBus.BusObject().AddMatchSignal(
|
||||
"org.freedesktop.DBus",
|
||||
"NameOwnerChanged",
|
||||
dbus.WithMatchSender("org.freedesktop.DBus"),
|
||||
dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"),
|
||||
).Err
|
||||
if err != nil {
|
||||
return E.Cause(err, "configure resolved restart listener")
|
||||
}
|
||||
go t.loopUpdateStatus()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) Close() error {
|
||||
t.closeOnce.Do(func() {
|
||||
if t.systemBus != nil {
|
||||
_ = t.systemBus.Close()
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) Object() any {
|
||||
return t.resoledObject.Load()
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
defaultInterface := t.interfaceMonitor.DefaultInterface()
|
||||
if defaultInterface == nil {
|
||||
return nil, E.New("missing default interface")
|
||||
}
|
||||
question := message.Question[0]
|
||||
call := object.(*dbus.Object).CallWithContext(
|
||||
ctx,
|
||||
"org.freedesktop.resolve1.Manager.ResolveRecord",
|
||||
0,
|
||||
int32(defaultInterface.Index),
|
||||
question.Name,
|
||||
question.Qclass,
|
||||
question.Qtype,
|
||||
uint64(0),
|
||||
)
|
||||
if call.Err != nil {
|
||||
return nil, E.Cause(call.Err, " resolve record via resolved")
|
||||
}
|
||||
var (
|
||||
records []resolved.ResourceRecord
|
||||
outflags uint64
|
||||
)
|
||||
err := call.Store(&records, &outflags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionDesired: true,
|
||||
RecursionAvailable: true,
|
||||
Rcode: mDNS.RcodeSuccess,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
}
|
||||
for _, record := range records {
|
||||
var rr mDNS.RR
|
||||
rr, _, err = mDNS.UnpackRR(record.Data, 0)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack resource record")
|
||||
}
|
||||
response.Answer = append(response.Answer, rr)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) loopUpdateStatus() {
|
||||
signalChan := make(chan *dbus.Signal, 1)
|
||||
t.systemBus.Signal(signalChan)
|
||||
for signal := range signalChan {
|
||||
var restarted bool
|
||||
if signal.Name == "org.freedesktop.DBus.NameOwnerChanged" {
|
||||
if len(signal.Body) != 3 || signal.Body[2].(string) == "" {
|
||||
continue
|
||||
} else {
|
||||
restarted = true
|
||||
}
|
||||
}
|
||||
if restarted {
|
||||
t.updateStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) updateStatus() {
|
||||
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
|
||||
err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err
|
||||
if err != nil {
|
||||
if t.resoledObject.Swap(nil) != nil {
|
||||
t.logger.Debug("systemd-resolved service is gone")
|
||||
}
|
||||
return
|
||||
}
|
||||
t.resoledObject.Store(dbusObject)
|
||||
t.logger.Debug("using systemd-resolved service as resolver")
|
||||
}
|
14
dns/transport/local/local_resolved_stub.go
Normal file
14
dns/transport/local/local_resolved_stub.go
Normal file
@ -0,0 +1,14 @@
|
||||
//go:build !linux
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
161
dns/transport/local/local_shared.go
Normal file
161
dns/transport/local/local_shared.go
Normal file
@ -0,0 +1,161 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||
systemConfig := getSystemDNSConfig(t.ctx)
|
||||
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
|
||||
return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
|
||||
} else {
|
||||
return t.exchangeParallel(ctx, systemConfig, message, domain)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||
var lastErr error
|
||||
for _, fqdn := range systemConfig.nameList(domain) {
|
||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
type queryResult struct {
|
||||
response *mDNS.Msg
|
||||
err error
|
||||
}
|
||||
results := make(chan queryResult)
|
||||
startRacer := func(ctx context.Context, fqdn string) {
|
||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
||||
if err == nil {
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = dns.RcodeError(response.Rcode)
|
||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||
err = E.New(fqdn, ": empty result")
|
||||
}
|
||||
}
|
||||
select {
|
||||
case results <- queryResult{response, err}:
|
||||
case <-returned:
|
||||
}
|
||||
}
|
||||
queryCtx, queryCancel := context.WithCancel(ctx)
|
||||
defer queryCancel()
|
||||
var nameCount int
|
||||
for _, fqdn := range systemConfig.nameList(domain) {
|
||||
nameCount++
|
||||
go startRacer(queryCtx, fqdn)
|
||||
}
|
||||
var errors []error
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case result := <-results:
|
||||
if result.err == nil {
|
||||
return result.response, nil
|
||||
}
|
||||
errors = append(errors, result.err)
|
||||
if len(errors) == nameCount {
|
||||
return nil, E.Errors(errors...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
serverOffset := config.serverOffset()
|
||||
sLen := uint32(len(config.servers))
|
||||
var lastErr error
|
||||
for i := 0; i < config.attempts; i++ {
|
||||
for j := uint32(0); j < sLen; j++ {
|
||||
server := config.servers[(serverOffset+j)%sLen]
|
||||
question := message.Question[0]
|
||||
question.Name = fqdn
|
||||
response, err := t.exchangeOne(ctx, M.ParseSocksaddr(server), question, config.timeout, config.useTCP, config.trustAD)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
return nil, E.Cause(lastErr, fqdn)
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) {
|
||||
if server.Port == 0 {
|
||||
server.Port = 53
|
||||
}
|
||||
var networks []string
|
||||
if useTCP {
|
||||
networks = []string{N.NetworkTCP}
|
||||
} else {
|
||||
networks = []string{N.NetworkUDP, N.NetworkTCP}
|
||||
}
|
||||
request := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: uint16(rand.Uint32()),
|
||||
RecursionDesired: true,
|
||||
AuthenticatedData: ad,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
Compress: true,
|
||||
}
|
||||
request.SetEdns0(maxDNSPacketSize, false)
|
||||
buffer := buf.Get(buf.UDPBufferSize)
|
||||
defer buf.Put(buffer)
|
||||
for _, network := range networks {
|
||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
|
||||
defer cancel()
|
||||
conn, err := t.dialer.DialContext(ctx, network, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||
conn.SetDeadline(deadline)
|
||||
}
|
||||
rawMessage, err := request.PackBuffer(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "pack request")
|
||||
}
|
||||
_, err = conn.Write(rawMessage)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read response")
|
||||
}
|
||||
var response mDNS.Msg
|
||||
err = response.Unpack(buffer[:n])
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack response")
|
||||
}
|
||||
if response.Truncated && network == N.NetworkUDP {
|
||||
continue
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
panic("unexpected")
|
||||
}
|
@ -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,4 +1,4 @@
|
||||
//go:build !windows && !(darwin && cgo)
|
||||
//go:build !windows
|
||||
|
||||
package local
|
||||
|
||||
|
@ -30,7 +30,7 @@ func RegisterTLS(registry *dns.TransportRegistry) {
|
||||
type TLSTransport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
dialer tls.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig tls.Config
|
||||
access sync.Mutex
|
||||
@ -67,7 +67,7 @@ func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer
|
||||
return &TLSTransport{
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: dialer,
|
||||
dialer: tls.NewDialer(dialer, tlsConfig),
|
||||
serverAddr: serverAddr,
|
||||
tlsConfig: tlsConfig,
|
||||
}
|
||||
@ -100,15 +100,10 @@ func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
tcpConn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
|
||||
tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConn, err := tls.ClientHandshake(ctx, tcpConn, t.tlsConfig)
|
||||
if err != nil {
|
||||
tcpConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return t.exchange(message, &tlsDNSConn{Conn: tlsConn})
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -2,11 +2,55 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.13.0-alpha.4
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.2
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.0-alpha.3
|
||||
|
||||
* Improve `local` DNS server **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
On Apple platforms, Windows, and Linux (when using systemd-resolved),
|
||||
`local` DNS server now works with Tun inbound which overrides system DNS servers.
|
||||
|
||||
See [Local DNS Server](/configuration/dns/server/local/).
|
||||
|
||||
#### 1.13.0-alpha.2
|
||||
|
||||
* Add `preferred_by` rule item **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
The new `preferred_by` routing rule item allows you to
|
||||
match preferred domains and addresses for specific outbounds.
|
||||
|
||||
See [Route Rule](/configuration/route/rule/#preferred_by).
|
||||
|
||||
#### 1.13.0-alpha.1
|
||||
|
||||
* Add interface address rule items **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
New interface address rules allow you to dynamically adjust rules based on your network environment.
|
||||
|
||||
See [Route Rule](/configuration/route/rule/), [DNS Route Rule](/configuration/dns/rule/)
|
||||
and [Headless Rule](/configuration/rule-set/headless-rule/).
|
||||
|
||||
#### 1.12.1
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.0
|
||||
### 1.12.0
|
||||
|
||||
* Refactor DNS servers **1**
|
||||
* Add domain resolver options**2**
|
||||
@ -157,7 +201,7 @@ We continue to experience issues updating our sing-box apps on the App Store and
|
||||
Until we rewrite and resubmit the apps, they are considered irrecoverable.
|
||||
Therefore, after this release, we will not be repeating this notice unless there is new information.
|
||||
|
||||
### 1.11.15
|
||||
#### 1.11.15
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -173,7 +217,7 @@ violated the rules (TestFlight users are not affected)._
|
||||
|
||||
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
|
||||
|
||||
### 1.11.14
|
||||
#### 1.11.14
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -223,7 +267,7 @@ You can now choose what the DERP home page shows, just like with derper's `-home
|
||||
|
||||
See [DERP](/configuration/service/derp/#home).
|
||||
|
||||
### 1.11.13
|
||||
#### 1.11.13
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -261,7 +305,7 @@ SSM API service is a RESTful API server for managing Shadowsocks servers.
|
||||
|
||||
See [SSM API Service](/configuration/service/ssm-api/).
|
||||
|
||||
### 1.11.11
|
||||
#### 1.11.11
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -293,7 +337,7 @@ You can now set `bind_interface`, `routing_mark` and `reuse_addr` in Listen Fiel
|
||||
|
||||
See [Listen Fields](/configuration/shared/listen/).
|
||||
|
||||
### 1.11.10
|
||||
#### 1.11.10
|
||||
|
||||
* Undeprecate the `block` outbound **1**
|
||||
* Fixes and improvements
|
||||
@ -311,7 +355,7 @@ violated the rules (TestFlight users are not affected)._
|
||||
* Update quic-go to v0.51.0
|
||||
* Fixes and improvements
|
||||
|
||||
### 1.11.9
|
||||
#### 1.11.9
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -322,7 +366,7 @@ violated the rules (TestFlight users are not affected)._
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
### 1.11.8
|
||||
#### 1.11.8
|
||||
|
||||
* Improve `auto_redirect` **1**
|
||||
* Fixes and improvements
|
||||
@ -339,7 +383,7 @@ violated the rules (TestFlight users are not affected)._
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
### 1.11.7
|
||||
#### 1.11.7
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -355,7 +399,7 @@ violated the rules (TestFlight users are not affected)._
|
||||
Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks,
|
||||
see [Tun](/configuration/inbound/tun/#auto_redirect).
|
||||
|
||||
### 1.11.6
|
||||
#### 1.11.6
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -396,7 +440,7 @@ See [Protocol Sniff](/configuration/route/sniff/).
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/#domain_resolver).
|
||||
|
||||
### 1.11.5
|
||||
#### 1.11.5
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -412,7 +456,7 @@ violated the rules (TestFlight users are not affected)._
|
||||
|
||||
See [DNS Rule Action](/configuration/dns/rule_action/#predefined).
|
||||
|
||||
### 1.11.4
|
||||
#### 1.11.4
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -468,7 +512,7 @@ Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to co
|
||||
|
||||
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
|
||||
|
||||
### 1.11.3
|
||||
#### 1.11.3
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -479,7 +523,7 @@ process._
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
### 1.11.1
|
||||
#### 1.11.1
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -658,7 +702,7 @@ See [Hysteria2](/configuration/outbound/hysteria2/).
|
||||
|
||||
When `up_mbps` and `down_mbps` are set, `ignore_client_bandwidth` instead denies clients from using BBR CC.
|
||||
|
||||
### 1.10.7
|
||||
#### 1.10.7
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@ -753,7 +797,7 @@ and the old outbound will be removed in sing-box 1.13.0.
|
||||
See [Endpoint](/configuration/endpoint/), [WireGuard Endpoint](/configuration/endpoint/wireguard/)
|
||||
and [Migrate WireGuard outbound fields to route options](/migration/#migrate-wireguard-outbound-to-endpoint).
|
||||
|
||||
### 1.10.2
|
||||
#### 1.10.2
|
||||
|
||||
* Add deprecated warnings
|
||||
* Fix proxying websocket connections in HTTP/mixed inbounds
|
||||
@ -890,7 +934,7 @@ See [Rule Action](/configuration/route/rule_action/).
|
||||
* Update quic-go to v0.48.0
|
||||
* Fixes and improvements
|
||||
|
||||
### 1.10.1
|
||||
#### 1.10.1
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
|
@ -2,6 +2,12 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
:material-plus: [network_interface_address](#network_interface_address)
|
||||
:material-plus: [default_interface_address](#default_interface_address)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [ip_accept_any](#ip_accept_any)
|
||||
@ -130,6 +136,19 @@ icon: material/alert-decagram
|
||||
],
|
||||
"network_is_expensive": false,
|
||||
"network_is_constrained": false,
|
||||
"interface_address": {
|
||||
"en0": [
|
||||
"2000::/3"
|
||||
]
|
||||
},
|
||||
"network_interface_address": {
|
||||
"wifi": [
|
||||
"2000::/3"
|
||||
]
|
||||
},
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@ -359,6 +378,36 @@ such as Cellular or a Personal Hotspot (on Apple platforms).
|
||||
|
||||
Match if network is in Low Data Mode.
|
||||
|
||||
#### interface_address
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, Windows, and macOS.
|
||||
|
||||
Match interface address.
|
||||
|
||||
#### network_interface_address
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported in graphical clients on Android and Apple platforms.
|
||||
|
||||
Matches network interface (same values as `network_type`) address.
|
||||
|
||||
#### default_interface_address
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, Windows, and macOS.
|
||||
|
||||
Match default interface address.
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
|
@ -2,6 +2,12 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
:material-plus: [network_interface_address](#network_interface_address)
|
||||
:material-plus: [default_interface_address](#default_interface_address)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [ip_accept_any](#ip_accept_any)
|
||||
@ -130,6 +136,19 @@ icon: material/alert-decagram
|
||||
],
|
||||
"network_is_expensive": false,
|
||||
"network_is_constrained": false,
|
||||
"interface_address": {
|
||||
"en0": [
|
||||
"2000::/3"
|
||||
]
|
||||
},
|
||||
"network_interface_address": {
|
||||
"wifi": [
|
||||
"2000::/3"
|
||||
]
|
||||
},
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@ -358,6 +377,36 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
匹配如果网络在低数据模式下。
|
||||
|
||||
#### interface_address
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、Windows 和 macOS.
|
||||
|
||||
匹配接口地址。
|
||||
|
||||
#### network_interface_address
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅在 Android 与 Apple 平台图形客户端中支持。
|
||||
|
||||
匹配网络接口(可用值同 `network_type`)地址。
|
||||
|
||||
#### default_interface_address
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、Windows 和 macOS.
|
||||
|
||||
匹配默认接口地址。
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
|
@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [prefer_go](#prefer_go)
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
# Local
|
||||
@ -15,6 +19,7 @@ icon: material/new-box
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "",
|
||||
"prefer_go": false
|
||||
|
||||
// Dial Fields
|
||||
}
|
||||
@ -24,10 +29,33 @@ icon: material/new-box
|
||||
```
|
||||
|
||||
!!! info "Difference from legacy local server"
|
||||
|
||||
|
||||
* The old legacy local server only handles IP requests; the new one handles all types of requests and supports concurrent for IP requests.
|
||||
* The old local server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||
|
||||
### Fields
|
||||
|
||||
#### prefer_go
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
When enabled, `local` DNS server will resolve DNS by dialing itself whenever possible.
|
||||
|
||||
Specifically, it disables following behaviors which was added as features in sing-box 1.13.0:
|
||||
|
||||
1. On Apple platforms: Attempt to resolve A/AAAA requests using `getaddrinfo` in NetworkExtension.
|
||||
2. On Linux: Resolve through `systemd-resolvd`'s DBus interface when available.
|
||||
|
||||
As a sole exception, it cannot disable the following behavior:
|
||||
|
||||
1. In the Android graphical client,
|
||||
`local` will always resolve DNS through the platform interface,
|
||||
as there is no other way to obtain upstream DNS servers;
|
||||
On devices running Android versions lower than 10, this interface can only resolve A/AAAA requests.
|
||||
|
||||
2. On macOS, `local` will try DHCP first in Network Extension, since DHCP respects DIal Fields,
|
||||
it will not be disabled by `prefer_go`.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||
|
@ -1,7 +1,3 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.11.0"
|
||||
|
||||
# Endpoint
|
||||
|
@ -1,7 +1,3 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.11.0 起"
|
||||
|
||||
# 端点
|
||||
|
@ -1,7 +1,3 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.11.0"
|
||||
|
||||
### Structure
|
||||
|
@ -1,7 +1,3 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.11.0 起"
|
||||
|
||||
### 结构
|
||||
|
@ -1,7 +1,3 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.11.0"
|
||||
|
||||
:material-plus: [server_ports](#server_ports)
|
||||
|
@ -1,7 +1,3 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-plus: [server_ports](#server_ports)
|
||||
|
@ -2,6 +2,13 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
:material-plus: [network_interface_address](#network_interface_address)
|
||||
:material-plus: [default_interface_address](#default_interface_address)
|
||||
:material-plus: [preferred_by](#preferred_by)
|
||||
|
||||
!!! quote "Changes in sing-box 1.11.0"
|
||||
|
||||
:material-plus: [action](#action)
|
||||
@ -128,12 +135,29 @@ icon: material/new-box
|
||||
],
|
||||
"network_is_expensive": false,
|
||||
"network_is_constrained": false,
|
||||
"interface_address": {
|
||||
"en0": [
|
||||
"2000::/3"
|
||||
]
|
||||
},
|
||||
"network_interface_address": {
|
||||
"wifi": [
|
||||
"2000::/3"
|
||||
]
|
||||
},
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
"wifi_bssid": [
|
||||
"00:00:00:00:00:00"
|
||||
],
|
||||
"preferred_by": [
|
||||
"tailscale",
|
||||
"wireguard"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
@ -363,6 +387,36 @@ such as Cellular or a Personal Hotspot (on Apple platforms).
|
||||
|
||||
Match if network is in Low Data Mode.
|
||||
|
||||
#### interface_address
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, Windows, and macOS.
|
||||
|
||||
Match interface address.
|
||||
|
||||
#### network_interface_address
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported in graphical clients on Android and Apple platforms.
|
||||
|
||||
Matches network interface (same values as `network_type`) address.
|
||||
|
||||
#### default_interface_address
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, Windows, and macOS.
|
||||
|
||||
Match default interface address.
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
@ -379,6 +433,17 @@ Match WiFi SSID.
|
||||
|
||||
Match WiFi BSSID.
|
||||
|
||||
#### preferred_by
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
Match specified outbounds' preferred routes.
|
||||
|
||||
| Type | Match |
|
||||
|-------------|-----------------------------------------------|
|
||||
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
|
||||
| `wireguard` | Match peers's allowed IPs |
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
@ -2,6 +2,13 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
:material-plus: [network_interface_address](#network_interface_address)
|
||||
:material-plus: [default_interface_address](#default_interface_address)
|
||||
:material-plus: [preferred_by](#preferred_by)
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-plus: [action](#action)
|
||||
@ -125,12 +132,29 @@ icon: material/new-box
|
||||
],
|
||||
"network_is_expensive": false,
|
||||
"network_is_constrained": false,
|
||||
"interface_address": {
|
||||
"en0": [
|
||||
"2000::/3"
|
||||
]
|
||||
},
|
||||
"network_interface_address": {
|
||||
"wifi": [
|
||||
"2000::/3"
|
||||
]
|
||||
},
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
"wifi_bssid": [
|
||||
"00:00:00:00:00:00"
|
||||
],
|
||||
"preferred_by": [
|
||||
"tailscale",
|
||||
"wireguard"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
@ -337,7 +361,7 @@ icon: material/new-box
|
||||
|
||||
匹配网络类型。
|
||||
|
||||
Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
可用值: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
#### network_is_expensive
|
||||
|
||||
@ -360,6 +384,36 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
匹配如果网络在低数据模式下。
|
||||
|
||||
#### interface_address
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、Windows 和 macOS.
|
||||
|
||||
匹配接口地址。
|
||||
|
||||
#### network_interface_address
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅在 Android 与 Apple 平台图形客户端中支持。
|
||||
|
||||
匹配网络接口(可用值同 `network_type`)地址。
|
||||
|
||||
#### default_interface_address
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、Windows 和 macOS.
|
||||
|
||||
匹配默认接口地址。
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
@ -376,6 +430,17 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
匹配 WiFi BSSID。
|
||||
|
||||
#### preferred_by
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
匹配制定出站的首选路由。
|
||||
|
||||
| 类型 | 匹配 |
|
||||
|-------------|--------------------------------|
|
||||
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
|
||||
| `wireguard` | 匹配对端的 allowed IPs |
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [network_interface_address](#network_interface_address)
|
||||
:material-plus: [default_interface_address](#default_interface_address)
|
||||
|
||||
!!! quote "Changes in sing-box 1.11.0"
|
||||
|
||||
:material-plus: [network_type](#network_type)
|
||||
@ -78,6 +83,14 @@ icon: material/new-box
|
||||
],
|
||||
"network_is_expensive": false,
|
||||
"network_is_constrained": false,
|
||||
"network_interface_address": {
|
||||
"wifi": [
|
||||
"2000::/3"
|
||||
]
|
||||
},
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@ -225,6 +238,26 @@ such as Cellular or a Personal Hotspot (on Apple platforms).
|
||||
|
||||
Match if network is in Low Data Mode.
|
||||
|
||||
#### network_interface_address
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported in graphical clients on Android and Apple platforms.
|
||||
|
||||
Matches network interface (same values as `network_type`) address.
|
||||
|
||||
#### default_interface_address
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, Windows, and macOS.
|
||||
|
||||
Match default interface address.
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
|
@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [network_interface_address](#network_interface_address)
|
||||
:material-plus: [default_interface_address](#default_interface_address)
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-plus: [network_type](#network_type)
|
||||
@ -78,6 +83,14 @@ icon: material/new-box
|
||||
],
|
||||
"network_is_expensive": false,
|
||||
"network_is_constrained": false,
|
||||
"network_interface_address": {
|
||||
"wifi": [
|
||||
"2000::/3"
|
||||
]
|
||||
},
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@ -221,6 +234,26 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
匹配如果网络在低数据模式下。
|
||||
|
||||
#### network_interface_address
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅在 Android 与 Apple 平台图形客户端中支持。
|
||||
|
||||
匹配网络接口(可用值同 `network_type`)地址。
|
||||
|
||||
#### default_interface_address
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、Windows 和 macOS.
|
||||
|
||||
匹配默认接口地址。
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
|
@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: version `4`
|
||||
|
||||
!!! quote "Changes in sing-box 1.11.0"
|
||||
|
||||
:material-plus: version `3`
|
||||
@ -36,6 +40,7 @@ Version of rule-set.
|
||||
* 1: sing-box 1.8.0: Initial rule-set version.
|
||||
* 2: sing-box 1.10.0: Optimized memory usages of `domain_suffix` rules in binary rule-sets.
|
||||
* 3: sing-box 1.11.0: Added `network_type`, `network_is_expensive` and `network_is_constrainted` rule items.
|
||||
* 4: sing-box 1.13.0: Added `network_interface_address` and `default_interface_address` rule items.
|
||||
|
||||
#### rules
|
||||
|
||||
|
@ -2,6 +2,10 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: version `4`
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-plus: version `3`
|
||||
@ -36,6 +40,7 @@ icon: material/new-box
|
||||
* 1: sing-box 1.8.0: 初始规则集版本。
|
||||
* 2: sing-box 1.10.0: 优化了二进制规则集中 `domain_suffix` 规则的内存使用。
|
||||
* 3: sing-box 1.11.0: 添加了 `network_type`、 `network_is_expensive` 和 `network_is_constrainted` 规则项。
|
||||
* 4: sing-box 1.13.0: 添加了 `network_interface_address` 和 `default_interface_address` 规则项。
|
||||
|
||||
#### rules
|
||||
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
func cacheRouter(ctx context.Context) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Post("/fakeip/flush", flushFakeip(ctx))
|
||||
r.Post("/dns/flush", flushDNS(ctx))
|
||||
return r
|
||||
}
|
||||
|
||||
@ -31,3 +32,13 @@ func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Reques
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func flushDNS(ctx context.Context) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
|
||||
if dnsRouter != nil {
|
||||
dnsRouter.ClearCache()
|
||||
}
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
}
|
||||
|
2
go.mod
2
go.mod
@ -34,7 +34,7 @@ require (
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
||||
github.com/sagernet/sing-tun v0.7.0-beta.1
|
||||
github.com/sagernet/sing-vmess v0.2.6
|
||||
github.com/sagernet/sing-vmess v0.2.7
|
||||
github.com/sagernet/smux v1.5.34-mod.2
|
||||
github.com/sagernet/tailscale v1.80.3-mod.5
|
||||
github.com/sagernet/wireguard-go v0.0.1-beta.7
|
||||
|
4
go.sum
4
go.sum
@ -181,8 +181,8 @@ github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||
github.com/sagernet/sing-tun v0.7.0-beta.1 h1:mBIFXYAnGO5ey/HcCYanqnBx61E7yF8zTFGRZonGYmY=
|
||||
github.com/sagernet/sing-tun v0.7.0-beta.1/go.mod h1:AHJuRrLbNRJuivuFZ2VhXwDj4ViYp14szG5EkkKAqRQ=
|
||||
github.com/sagernet/sing-vmess v0.2.6 h1:1c4dGzeGy0kpBXXrT1sgiMZtHhdJylIT8eWrGhJYZec=
|
||||
github.com/sagernet/sing-vmess v0.2.6/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
|
||||
github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=
|
||||
github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
|
||||
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
|
||||
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
|
||||
github.com/sagernet/tailscale v1.80.3-mod.5 h1:7V7z+p2C//TGtff20pPnDCt3qP6uFyY62peJoKF9z/A=
|
||||
|
@ -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,9 @@ func (o *DNSServerOptions) Upgrade(ctx context.Context) error {
|
||||
switch serverType {
|
||||
case C.DNSTypeLocal:
|
||||
o.Type = C.DNSTypeLocal
|
||||
o.Options = &remoteOptions.LocalDNSServerOptions
|
||||
o.Options = &LocalDNSServerOptions{
|
||||
RawLocalDNSServerOptions: remoteOptions.RawLocalDNSServerOptions,
|
||||
}
|
||||
case C.DNSTypeUDP:
|
||||
o.Type = C.DNSTypeUDP
|
||||
o.Options = &remoteOptions
|
||||
@ -363,7 +365,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 +373,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:"-"`
|
||||
|
@ -67,42 +67,46 @@ func (r Rule) IsValid() bool {
|
||||
}
|
||||
|
||||
type RawDefaultRule struct {
|
||||
Inbound badoption.Listable[string] `json:"inbound,omitempty"`
|
||||
IPVersion int `json:"ip_version,omitempty"`
|
||||
Network badoption.Listable[string] `json:"network,omitempty"`
|
||||
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
|
||||
Protocol badoption.Listable[string] `json:"protocol,omitempty"`
|
||||
Client badoption.Listable[string] `json:"client,omitempty"`
|
||||
Domain badoption.Listable[string] `json:"domain,omitempty"`
|
||||
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
|
||||
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
|
||||
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
|
||||
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
|
||||
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
|
||||
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
|
||||
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
|
||||
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
||||
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
||||
User badoption.Listable[string] `json:"user,omitempty"`
|
||||
UserID badoption.Listable[int32] `json:"user_id,omitempty"`
|
||||
ClashMode string `json:"clash_mode,omitempty"`
|
||||
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
||||
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
|
||||
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
|
||||
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
Inbound badoption.Listable[string] `json:"inbound,omitempty"`
|
||||
IPVersion int `json:"ip_version,omitempty"`
|
||||
Network badoption.Listable[string] `json:"network,omitempty"`
|
||||
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
|
||||
Protocol badoption.Listable[string] `json:"protocol,omitempty"`
|
||||
Client badoption.Listable[string] `json:"client,omitempty"`
|
||||
Domain badoption.Listable[string] `json:"domain,omitempty"`
|
||||
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
|
||||
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
|
||||
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
|
||||
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
|
||||
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
|
||||
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
|
||||
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
|
||||
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
||||
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
||||
User badoption.Listable[string] `json:"user,omitempty"`
|
||||
UserID badoption.Listable[int32] `json:"user_id,omitempty"`
|
||||
ClashMode string `json:"clash_mode,omitempty"`
|
||||
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
||||
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
|
||||
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
|
||||
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"`
|
||||
NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"`
|
||||
DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"`
|
||||
PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"`
|
||||
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
|
||||
// Deprecated: renamed to rule_set_ip_cidr_match_source
|
||||
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
|
||||
|
@ -68,45 +68,48 @@ func (r DNSRule) IsValid() bool {
|
||||
}
|
||||
|
||||
type RawDefaultDNSRule struct {
|
||||
Inbound badoption.Listable[string] `json:"inbound,omitempty"`
|
||||
IPVersion int `json:"ip_version,omitempty"`
|
||||
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
|
||||
Network badoption.Listable[string] `json:"network,omitempty"`
|
||||
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
|
||||
Protocol badoption.Listable[string] `json:"protocol,omitempty"`
|
||||
Domain badoption.Listable[string] `json:"domain,omitempty"`
|
||||
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
|
||||
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
|
||||
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
|
||||
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
|
||||
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
|
||||
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
|
||||
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
||||
IPAcceptAny bool `json:"ip_accept_any,omitempty"`
|
||||
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
|
||||
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
||||
User badoption.Listable[string] `json:"user,omitempty"`
|
||||
UserID badoption.Listable[int32] `json:"user_id,omitempty"`
|
||||
Outbound badoption.Listable[string] `json:"outbound,omitempty"`
|
||||
ClashMode string `json:"clash_mode,omitempty"`
|
||||
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
||||
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
|
||||
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
|
||||
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
|
||||
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
Inbound badoption.Listable[string] `json:"inbound,omitempty"`
|
||||
IPVersion int `json:"ip_version,omitempty"`
|
||||
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
|
||||
Network badoption.Listable[string] `json:"network,omitempty"`
|
||||
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
|
||||
Protocol badoption.Listable[string] `json:"protocol,omitempty"`
|
||||
Domain badoption.Listable[string] `json:"domain,omitempty"`
|
||||
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
|
||||
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
|
||||
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
|
||||
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
|
||||
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
|
||||
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
|
||||
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
||||
IPAcceptAny bool `json:"ip_accept_any,omitempty"`
|
||||
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
|
||||
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
||||
User badoption.Listable[string] `json:"user,omitempty"`
|
||||
UserID badoption.Listable[int32] `json:"user_id,omitempty"`
|
||||
Outbound badoption.Listable[string] `json:"outbound,omitempty"`
|
||||
ClashMode string `json:"clash_mode,omitempty"`
|
||||
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
||||
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
|
||||
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
|
||||
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"`
|
||||
NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"`
|
||||
DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"`
|
||||
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
|
||||
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
|
||||
// Deprecated: renamed to rule_set_ip_cidr_match_source
|
||||
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
|
||||
|
@ -182,28 +182,31 @@ func (r HeadlessRule) IsValid() bool {
|
||||
}
|
||||
|
||||
type DefaultHeadlessRule struct {
|
||||
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
|
||||
Network badoption.Listable[string] `json:"network,omitempty"`
|
||||
Domain badoption.Listable[string] `json:"domain,omitempty"`
|
||||
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
|
||||
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
|
||||
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
|
||||
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
||||
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
||||
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
|
||||
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
|
||||
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
|
||||
Network badoption.Listable[string] `json:"network,omitempty"`
|
||||
Domain badoption.Listable[string] `json:"domain,omitempty"`
|
||||
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
|
||||
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
|
||||
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
|
||||
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
|
||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
||||
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
||||
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
|
||||
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
|
||||
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
|
||||
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
|
||||
NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"`
|
||||
DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"`
|
||||
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
|
||||
DomainMatcher *domain.Matcher `json:"-"`
|
||||
SourceIPSet *netipx.IPSet `json:"-"`
|
||||
@ -240,7 +243,7 @@ type PlainRuleSetCompat _PlainRuleSetCompat
|
||||
func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) {
|
||||
var v any
|
||||
switch r.Version {
|
||||
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3:
|
||||
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4:
|
||||
v = r.Options
|
||||
default:
|
||||
return nil, E.New("unknown rule-set version: ", r.Version)
|
||||
@ -255,7 +258,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
|
||||
}
|
||||
var v any
|
||||
switch r.Version {
|
||||
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3:
|
||||
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4:
|
||||
v = &r.Options
|
||||
case 0:
|
||||
return E.New("missing rule-set version")
|
||||
@ -272,7 +275,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error {
|
||||
|
||||
func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) {
|
||||
switch r.Version {
|
||||
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3:
|
||||
case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3, C.RuleSetVersion4:
|
||||
default:
|
||||
return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version))
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ func RegisterOutbound(registry *outbound.Registry) {
|
||||
|
||||
type Outbound struct {
|
||||
outbound.Adapter
|
||||
dialer N.Dialer
|
||||
dialer tls.Dialer
|
||||
server M.Socksaddr
|
||||
tlsConfig tls.Config
|
||||
client *anytls.Client
|
||||
@ -58,7 +58,8 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outbound.dialer = outboundDialer
|
||||
|
||||
outbound.dialer = tls.NewDialer(outboundDialer, tlsConfig)
|
||||
|
||||
client, err := anytls.NewClient(ctx, anytls.ClientConfig{
|
||||
Password: options.Password,
|
||||
@ -91,16 +92,7 @@ func (d anytlsDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
|
||||
}
|
||||
|
||||
func (h *Outbound) dialOut(ctx context.Context) (net.Conn, error) {
|
||||
conn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConn, err := tls.ClientHandshake(ctx, conn, h.tlsConfig)
|
||||
if err != nil {
|
||||
common.Close(tlsConn, conn)
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
return h.dialer.DialTLSContext(ctx, h.server)
|
||||
}
|
||||
|
||||
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
|
@ -34,7 +34,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
detour, err := tls.NewDialerFromOptions(ctx, router, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||
detour, err := tls.NewDialerFromOptions(ctx, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -47,8 +46,6 @@ type DNSTransport struct {
|
||||
acceptDefaultResolvers bool
|
||||
dnsRouter adapter.DNSRouter
|
||||
endpointManager adapter.EndpointManager
|
||||
cfg *wgcfg.Config
|
||||
dnsCfg *nDNS.Config
|
||||
endpoint *Endpoint
|
||||
routePrefixes []netip.Prefix
|
||||
routes map[string][]adapter.DNSTransport
|
||||
@ -83,10 +80,10 @@ func (t *DNSTransport) Start(stage adapter.StartStage) error {
|
||||
if !isTailscale {
|
||||
return E.New("endpoint is not Tailscale: ", t.endpointTag)
|
||||
}
|
||||
if ep.onReconfig != nil {
|
||||
if ep.onReconfigHook != nil {
|
||||
return E.New("only one Tailscale DNS server is allowed for single endpoint")
|
||||
}
|
||||
ep.onReconfig = t.onReconfig
|
||||
ep.onReconfigHook = t.onReconfig
|
||||
t.endpoint = ep
|
||||
return nil
|
||||
}
|
||||
@ -95,14 +92,6 @@ func (t *DNSTransport) Reset() {
|
||||
}
|
||||
|
||||
func (t *DNSTransport) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *nDNS.Config) {
|
||||
if cfg == nil || dnsCfg == nil {
|
||||
return
|
||||
}
|
||||
if (t.cfg != nil && reflect.DeepEqual(t.cfg, cfg)) && (t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg)) {
|
||||
return
|
||||
}
|
||||
t.cfg = cfg
|
||||
t.dnsCfg = dnsCfg
|
||||
err := t.updateDNSServers(routerCfg, dnsCfg)
|
||||
if err != nil {
|
||||
t.logger.Error(E.Cause(err, "update DNS servers"))
|
||||
|
@ -10,9 +10,9 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@ -31,6 +31,7 @@ import (
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@ -49,8 +50,14 @@ import (
|
||||
"github.com/sagernet/tailscale/version"
|
||||
"github.com/sagernet/tailscale/wgengine"
|
||||
"github.com/sagernet/tailscale/wgengine/filter"
|
||||
"github.com/sagernet/tailscale/wgengine/router"
|
||||
"github.com/sagernet/tailscale/wgengine/wgcfg"
|
||||
|
||||
"go4.org/netipx"
|
||||
)
|
||||
|
||||
var _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil)
|
||||
|
||||
func init() {
|
||||
version.SetVersion("sing-box " + C.Version)
|
||||
}
|
||||
@ -70,7 +77,12 @@ type Endpoint struct {
|
||||
server *tsnet.Server
|
||||
stack *stack.Stack
|
||||
filter *atomic.Pointer[filter.Filter]
|
||||
onReconfig wgengine.ReconfigListener
|
||||
onReconfigHook wgengine.ReconfigListener
|
||||
|
||||
cfg *wgcfg.Config
|
||||
dnsCfg *tsDNS.Config
|
||||
routeDomains atomic.TypedValue[map[string]bool]
|
||||
routePrefixes atomic.Pointer[netipx.IPSet]
|
||||
|
||||
acceptRoutes bool
|
||||
exitNode string
|
||||
@ -216,9 +228,7 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.onReconfig != nil {
|
||||
t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine).SetOnReconfigListener(t.onReconfig)
|
||||
}
|
||||
t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine).SetOnReconfigListener(t.onReconfig)
|
||||
|
||||
ipStack := t.server.ExportNetstack().ExportIPStack()
|
||||
gErr := ipStack.SetSpoofing(tun.DefaultNIC, true)
|
||||
@ -253,8 +263,7 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
|
||||
if err != nil {
|
||||
return E.Cause(err, "update prefs")
|
||||
}
|
||||
t.filter = localBackend.ExportFilter()
|
||||
|
||||
t.filter = atomic.PointerForm(localBackend.ExportFilter())
|
||||
go t.watchState()
|
||||
return nil
|
||||
}
|
||||
@ -473,10 +482,58 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
||||
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
func (t *Endpoint) PreferredDomain(domain string) bool {
|
||||
routeDomains := t.routeDomains.Load()
|
||||
if routeDomains == nil {
|
||||
return false
|
||||
}
|
||||
return routeDomains[strings.ToLower(domain)]
|
||||
}
|
||||
|
||||
func (t *Endpoint) PreferredAddress(address netip.Addr) bool {
|
||||
routePrefixes := t.routePrefixes.Load()
|
||||
if routePrefixes == nil {
|
||||
return false
|
||||
}
|
||||
return routePrefixes.Contains(address)
|
||||
}
|
||||
|
||||
func (t *Endpoint) Server() *tsnet.Server {
|
||||
return t.server
|
||||
}
|
||||
|
||||
func (t *Endpoint) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *tsDNS.Config) {
|
||||
if cfg == nil || dnsCfg == nil {
|
||||
return
|
||||
}
|
||||
if (t.cfg != nil && reflect.DeepEqual(t.cfg, cfg)) && (t.dnsCfg != nil && reflect.DeepEqual(t.dnsCfg, dnsCfg)) {
|
||||
return
|
||||
}
|
||||
t.cfg = cfg
|
||||
t.dnsCfg = dnsCfg
|
||||
|
||||
routeDomains := make(map[string]bool)
|
||||
for fqdn := range dnsCfg.Routes {
|
||||
routeDomains[fqdn.WithoutTrailingDot()] = true
|
||||
}
|
||||
for _, fqdn := range dnsCfg.SearchDomains {
|
||||
routeDomains[fqdn.WithoutTrailingDot()] = true
|
||||
}
|
||||
t.routeDomains.Store(routeDomains)
|
||||
|
||||
var builder netipx.IPSetBuilder
|
||||
for _, peer := range cfg.Peers {
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
builder.AddPrefix(allowedIP)
|
||||
}
|
||||
}
|
||||
t.routePrefixes.Store(common.Must1(builder.IPSet()))
|
||||
|
||||
if t.onReconfigHook != nil {
|
||||
t.onReconfigHook(cfg, routerCfg, dnsCfg)
|
||||
}
|
||||
}
|
||||
|
||||
func addressFromAddr(destination netip.Addr) tcpip.Address {
|
||||
if destination.Is6() {
|
||||
return tcpip.AddrFrom16(destination.As16())
|
||||
|
@ -34,6 +34,7 @@ type Outbound struct {
|
||||
key [56]byte
|
||||
multiplexDialer *mux.Client
|
||||
tlsConfig tls.Config
|
||||
tlsDialer tls.Dialer
|
||||
transport adapter.V2RayClientTransport
|
||||
}
|
||||
|
||||
@ -54,6 +55,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig)
|
||||
}
|
||||
if options.Transport != nil {
|
||||
outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig)
|
||||
@ -121,11 +123,10 @@ func (h *trojanDialer) DialContext(ctx context.Context, network string, destinat
|
||||
var err error
|
||||
if h.transport != nil {
|
||||
conn, err = h.transport.DialContext(ctx)
|
||||
} else if h.tlsDialer != nil {
|
||||
conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
|
||||
} else {
|
||||
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
|
||||
if err == nil && h.tlsConfig != nil {
|
||||
conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
common.Close(conn)
|
||||
|
@ -35,6 +35,7 @@ type Outbound struct {
|
||||
serverAddr M.Socksaddr
|
||||
multiplexDialer *mux.Client
|
||||
tlsConfig tls.Config
|
||||
tlsDialer tls.Dialer
|
||||
transport adapter.V2RayClientTransport
|
||||
packetAddr bool
|
||||
xudp bool
|
||||
@ -56,6 +57,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig)
|
||||
}
|
||||
if options.Transport != nil {
|
||||
outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig)
|
||||
@ -140,11 +142,10 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
var err error
|
||||
if h.transport != nil {
|
||||
conn, err = h.transport.DialContext(ctx)
|
||||
} else if h.tlsDialer != nil {
|
||||
conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
|
||||
} else {
|
||||
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
|
||||
if err == nil && h.tlsConfig != nil {
|
||||
conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -183,11 +184,10 @@ func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
|
||||
var err error
|
||||
if h.transport != nil {
|
||||
conn, err = h.transport.DialContext(ctx)
|
||||
} else if h.tlsDialer != nil {
|
||||
conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
|
||||
} else {
|
||||
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
|
||||
if err == nil && h.tlsConfig != nil {
|
||||
conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
common.Close(conn)
|
||||
|
@ -35,6 +35,7 @@ type Outbound struct {
|
||||
serverAddr M.Socksaddr
|
||||
multiplexDialer *mux.Client
|
||||
tlsConfig tls.Config
|
||||
tlsDialer tls.Dialer
|
||||
transport adapter.V2RayClientTransport
|
||||
packetAddr bool
|
||||
xudp bool
|
||||
@ -56,6 +57,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outbound.tlsDialer = tls.NewDialer(outboundDialer, outbound.tlsConfig)
|
||||
}
|
||||
if options.Transport != nil {
|
||||
outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig)
|
||||
@ -154,11 +156,10 @@ func (h *vmessDialer) DialContext(ctx context.Context, network string, destinati
|
||||
var err error
|
||||
if h.transport != nil {
|
||||
conn, err = h.transport.DialContext(ctx)
|
||||
} else if h.tlsDialer != nil {
|
||||
conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
|
||||
} else {
|
||||
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
|
||||
if err == nil && h.tlsConfig != nil {
|
||||
conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
common.Close(conn)
|
||||
@ -182,11 +183,10 @@ func (h *vmessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr)
|
||||
var err error
|
||||
if h.transport != nil {
|
||||
conn, err = h.transport.DialContext(ctx)
|
||||
} else if h.tlsDialer != nil {
|
||||
conn, err = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
|
||||
} else {
|
||||
conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
|
||||
if err == nil && h.tlsConfig != nil {
|
||||
conn, err = tls.ClientHandshake(ctx, conn, h.tlsConfig)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -22,6 +22,8 @@ import (
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil)
|
||||
|
||||
func RegisterEndpoint(registry *endpoint.Registry) {
|
||||
endpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, NewEndpoint)
|
||||
}
|
||||
@ -210,3 +212,11 @@ func (w *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
|
||||
}
|
||||
return w.endpoint.ListenPacket(ctx, destination)
|
||||
}
|
||||
|
||||
func (w *Endpoint) PreferredDomain(domain string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *Endpoint) PreferredAddress(address netip.Addr) bool {
|
||||
return w.endpoint.Lookup(address) != nil
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ import (
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ adapter.OutboundWithPreferredRoutes = (*Outbound)(nil)
|
||||
|
||||
func RegisterOutbound(registry *outbound.Registry) {
|
||||
outbound.Register[option.LegacyWireGuardOutboundOptions](registry, C.TypeWireGuard, NewOutbound)
|
||||
}
|
||||
@ -158,3 +160,11 @@ func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
|
||||
}
|
||||
return o.endpoint.ListenPacket(ctx, destination)
|
||||
}
|
||||
|
||||
func (o *Outbound) PreferredDomain(domain string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (o *Outbound) PreferredAddress(address netip.Addr) bool {
|
||||
return o.endpoint.Lookup(address) != nil
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
|
||||
if len(options.DomainRegex) > 0 {
|
||||
item, err := NewDomainRegexItem(options.DomainRegex)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "domain_regex")
|
||||
return nil, err
|
||||
}
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
@ -246,6 +246,26 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if options.InterfaceAddress != nil && options.InterfaceAddress.Size() > 0 {
|
||||
item := NewInterfaceAddressItem(networkManager, options.InterfaceAddress)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 {
|
||||
item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.DefaultInterfaceAddress) > 0 {
|
||||
item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.PreferredBy) > 0 {
|
||||
item := NewPreferredByItem(ctx, options.PreferredBy)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.RuleSet) > 0 {
|
||||
var matchSource bool
|
||||
if options.RuleSetIPCIDRMatchSource {
|
||||
|
56
route/rule/rule_default_interface_address.go
Normal file
56
route/rule/rule_default_interface_address.go
Normal file
@ -0,0 +1,56 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*DefaultInterfaceAddressItem)(nil)
|
||||
|
||||
type DefaultInterfaceAddressItem struct {
|
||||
interfaceMonitor tun.DefaultInterfaceMonitor
|
||||
interfaceAddresses []netip.Prefix
|
||||
}
|
||||
|
||||
func NewDefaultInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses badoption.Listable[*badoption.Prefixable]) *DefaultInterfaceAddressItem {
|
||||
item := &DefaultInterfaceAddressItem{
|
||||
interfaceMonitor: networkManager.InterfaceMonitor(),
|
||||
interfaceAddresses: make([]netip.Prefix, 0, len(interfaceAddresses)),
|
||||
}
|
||||
for _, prefixable := range interfaceAddresses {
|
||||
item.interfaceAddresses = append(item.interfaceAddresses, prefixable.Build(netip.Prefix{}))
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func (r *DefaultInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool {
|
||||
defaultInterface := r.interfaceMonitor.DefaultInterface()
|
||||
if defaultInterface == nil {
|
||||
return false
|
||||
}
|
||||
for _, address := range r.interfaceAddresses {
|
||||
if common.All(defaultInterface.Addresses, func(it netip.Prefix) bool {
|
||||
return !address.Overlaps(it)
|
||||
}) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *DefaultInterfaceAddressItem) String() string {
|
||||
addressLen := len(r.interfaceAddresses)
|
||||
switch {
|
||||
case addressLen == 1:
|
||||
return "default_interface_address=" + r.interfaceAddresses[0].String()
|
||||
case addressLen > 3:
|
||||
return "default_interface_address=[" + strings.Join(common.Map(r.interfaceAddresses[:3], netip.Prefix.String), " ") + "...]"
|
||||
default:
|
||||
return "default_interface_address=[" + strings.Join(common.Map(r.interfaceAddresses, netip.Prefix.String), " ") + "]"
|
||||
}
|
||||
}
|
@ -247,6 +247,21 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if options.InterfaceAddress != nil && options.InterfaceAddress.Size() > 0 {
|
||||
item := NewInterfaceAddressItem(networkManager, options.InterfaceAddress)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 {
|
||||
item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.DefaultInterfaceAddress) > 0 {
|
||||
item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.RuleSet) > 0 {
|
||||
var matchSource bool
|
||||
if options.RuleSetIPCIDRMatchSource {
|
||||
|
@ -164,13 +164,21 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR
|
||||
item := NewWIFISSIDItem(networkManager, options.WIFISSID)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
|
||||
}
|
||||
if len(options.WIFIBSSID) > 0 {
|
||||
item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
|
||||
}
|
||||
if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 {
|
||||
item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.DefaultInterfaceAddress) > 0 {
|
||||
item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
}
|
||||
if len(options.AdGuardDomain) > 0 {
|
||||
|
62
route/rule/rule_interface_address.go
Normal file
62
route/rule/rule_interface_address.go
Normal file
@ -0,0 +1,62 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*InterfaceAddressItem)(nil)
|
||||
|
||||
type InterfaceAddressItem struct {
|
||||
networkManager adapter.NetworkManager
|
||||
interfaceAddresses map[string][]netip.Prefix
|
||||
description string
|
||||
}
|
||||
|
||||
func NewInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]]) *InterfaceAddressItem {
|
||||
item := &InterfaceAddressItem{
|
||||
networkManager: networkManager,
|
||||
interfaceAddresses: make(map[string][]netip.Prefix, interfaceAddresses.Size()),
|
||||
}
|
||||
var entryDescriptions []string
|
||||
for _, entry := range interfaceAddresses.Entries() {
|
||||
prefixes := make([]netip.Prefix, 0, len(entry.Value))
|
||||
for _, prefixable := range entry.Value {
|
||||
prefixes = append(prefixes, prefixable.Build(netip.Prefix{}))
|
||||
}
|
||||
item.interfaceAddresses[entry.Key] = prefixes
|
||||
entryDescriptions = append(entryDescriptions, entry.Key+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ","))
|
||||
}
|
||||
item.description = "interface_address=[" + strings.Join(entryDescriptions, " ") + "]"
|
||||
return item
|
||||
}
|
||||
|
||||
func (r *InterfaceAddressItem) Match(metadata *adapter.InboundContext) bool {
|
||||
interfaces := r.networkManager.InterfaceFinder().Interfaces()
|
||||
for ifName, addresses := range r.interfaceAddresses {
|
||||
iface := common.Find(interfaces, func(it control.Interface) bool {
|
||||
return it.Name == ifName
|
||||
})
|
||||
if iface.Name == "" {
|
||||
return false
|
||||
}
|
||||
if common.All(addresses, func(address netip.Prefix) bool {
|
||||
return common.All(iface.Addresses, func(it netip.Prefix) bool {
|
||||
return !address.Overlaps(it)
|
||||
})
|
||||
}) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *InterfaceAddressItem) String() string {
|
||||
return r.description
|
||||
}
|
86
route/rule/rule_item_preferred_by.go
Normal file
86
route/rule/rule_item_preferred_by.go
Normal file
@ -0,0 +1,86 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*PreferredByItem)(nil)
|
||||
|
||||
type PreferredByItem struct {
|
||||
ctx context.Context
|
||||
outboundTags []string
|
||||
outbounds []adapter.OutboundWithPreferredRoutes
|
||||
}
|
||||
|
||||
func NewPreferredByItem(ctx context.Context, outboundTags []string) *PreferredByItem {
|
||||
return &PreferredByItem{
|
||||
ctx: ctx,
|
||||
outboundTags: outboundTags,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PreferredByItem) Start() error {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](r.ctx)
|
||||
for _, outboundTag := range r.outboundTags {
|
||||
rawOutbound, loaded := outboundManager.Outbound(outboundTag)
|
||||
if !loaded {
|
||||
return E.New("outbound not found: ", outboundTag)
|
||||
}
|
||||
outboundWithPreferredRoutes, withRoutes := rawOutbound.(adapter.OutboundWithPreferredRoutes)
|
||||
if !withRoutes {
|
||||
return E.New("outbound type does not support preferred routes: ", rawOutbound.Type())
|
||||
}
|
||||
r.outbounds = append(r.outbounds, outboundWithPreferredRoutes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PreferredByItem) Match(metadata *adapter.InboundContext) bool {
|
||||
var domainHost string
|
||||
if metadata.Domain != "" {
|
||||
domainHost = metadata.Domain
|
||||
} else {
|
||||
domainHost = metadata.Destination.Fqdn
|
||||
}
|
||||
if domainHost != "" {
|
||||
for _, outbound := range r.outbounds {
|
||||
if outbound.PreferredDomain(domainHost) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if metadata.Destination.IsIP() {
|
||||
for _, outbound := range r.outbounds {
|
||||
if outbound.PreferredAddress(metadata.Destination.Addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(metadata.DestinationAddresses) > 0 {
|
||||
for _, address := range metadata.DestinationAddresses {
|
||||
for _, outbound := range r.outbounds {
|
||||
if outbound.PreferredAddress(address) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *PreferredByItem) String() string {
|
||||
description := "preferred_by="
|
||||
pLen := len(r.outboundTags)
|
||||
if pLen == 1 {
|
||||
description += F.ToString(r.outboundTags[0])
|
||||
} else {
|
||||
description += "[" + strings.Join(F.MapToString(r.outboundTags), " ") + "]"
|
||||
}
|
||||
return description
|
||||
}
|
64
route/rule/rule_network_interface_address.go
Normal file
64
route/rule/rule_network_interface_address.go
Normal file
@ -0,0 +1,64 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*NetworkInterfaceAddressItem)(nil)
|
||||
|
||||
type NetworkInterfaceAddressItem struct {
|
||||
networkManager adapter.NetworkManager
|
||||
interfaceAddresses map[C.InterfaceType][]netip.Prefix
|
||||
description string
|
||||
}
|
||||
|
||||
func NewNetworkInterfaceAddressItem(networkManager adapter.NetworkManager, interfaceAddresses *badjson.TypedMap[option.InterfaceType, badoption.Listable[*badoption.Prefixable]]) *NetworkInterfaceAddressItem {
|
||||
item := &NetworkInterfaceAddressItem{
|
||||
networkManager: networkManager,
|
||||
interfaceAddresses: make(map[C.InterfaceType][]netip.Prefix, interfaceAddresses.Size()),
|
||||
}
|
||||
var entryDescriptions []string
|
||||
for _, entry := range interfaceAddresses.Entries() {
|
||||
prefixes := make([]netip.Prefix, 0, len(entry.Value))
|
||||
for _, prefixable := range entry.Value {
|
||||
prefixes = append(prefixes, prefixable.Build(netip.Prefix{}))
|
||||
}
|
||||
item.interfaceAddresses[entry.Key.Build()] = prefixes
|
||||
entryDescriptions = append(entryDescriptions, entry.Key.Build().String()+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ","))
|
||||
}
|
||||
item.description = "network_interface_address=[" + strings.Join(entryDescriptions, " ") + "]"
|
||||
return item
|
||||
}
|
||||
|
||||
func (r *NetworkInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool {
|
||||
interfaces := r.networkManager.NetworkInterfaces()
|
||||
match:
|
||||
for ifType, addresses := range r.interfaceAddresses {
|
||||
for _, networkInterface := range interfaces {
|
||||
if networkInterface.Type != ifType {
|
||||
continue
|
||||
}
|
||||
if common.Any(networkInterface.Addresses, func(it netip.Prefix) bool {
|
||||
return common.Any(addresses, func(prefix netip.Prefix) bool {
|
||||
return prefix.Overlaps(it)
|
||||
})
|
||||
}) {
|
||||
continue match
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *NetworkInterfaceAddressItem) String() string {
|
||||
return r.description
|
||||
}
|
@ -42,7 +42,7 @@ func extractIPSetFromRule(rawRule adapter.HeadlessRule) []*netipx.IPSet {
|
||||
}
|
||||
}
|
||||
|
||||
func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool {
|
||||
func HasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool {
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
@ -50,7 +50,7 @@ func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultH
|
||||
return true
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
if hasHeadlessRule(rule.LogicalOptions.Rules, cond) {
|
||||
if HasHeadlessRule(rule.LogicalOptions.Rules, cond) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -138,9 +138,9 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
|
||||
}
|
||||
}
|
||||
var metadata adapter.RuleSetMetadata
|
||||
metadata.ContainsProcessRule = hasHeadlessRule(headlessRules, isProcessHeadlessRule)
|
||||
metadata.ContainsWIFIRule = hasHeadlessRule(headlessRules, isWIFIHeadlessRule)
|
||||
metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
|
||||
metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule)
|
||||
metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule)
|
||||
metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
|
||||
s.rules = rules
|
||||
s.metadata = metadata
|
||||
s.callbackAccess.Lock()
|
||||
|
@ -185,9 +185,9 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
|
||||
return E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||
}
|
||||
}
|
||||
s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
|
||||
s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
|
||||
s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
|
||||
s.metadata.ContainsProcessRule = HasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
|
||||
s.metadata.ContainsWIFIRule = HasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
|
||||
s.metadata.ContainsIPCIDRRule = HasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
|
||||
s.rules = rules
|
||||
s.callbackAccess.Lock()
|
||||
callbacks := s.callbacks.Array()
|
||||
|
@ -29,7 +29,6 @@ var defaultClientHeader = http.Header{
|
||||
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
dialer N.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
transport *http2.Transport
|
||||
options option.V2RayGRPCOptions
|
||||
@ -46,7 +45,6 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
}
|
||||
client := &Client{
|
||||
ctx: ctx,
|
||||
dialer: dialer,
|
||||
serverAddr: serverAddr,
|
||||
options: options,
|
||||
transport: &http2.Transport{
|
||||
@ -62,7 +60,6 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
},
|
||||
host: host,
|
||||
}
|
||||
|
||||
if tlsConfig == nil {
|
||||
client.transport.DialTLSContext = func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
@ -71,12 +68,9 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
if len(tlsConfig.NextProtos()) == 0 {
|
||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
|
||||
}
|
||||
tlsDialer := tls.NewDialer(dialer, tlsConfig)
|
||||
client.transport.DialTLSContext = func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) {
|
||||
conn, err := dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tls.ClientHandshake(ctx, conn, tlsConfig)
|
||||
return tlsDialer.DialTLSContext(ctx, M.ParseSocksaddr(addr))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,15 +47,12 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
if len(tlsConfig.NextProtos()) == 0 {
|
||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
|
||||
}
|
||||
tlsDialer := tls.NewDialer(dialer, tlsConfig)
|
||||
transport = &http2.Transport{
|
||||
ReadIdleTimeout: time.Duration(options.IdleTimeout),
|
||||
PingTimeout: time.Duration(options.PingTimeout),
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.STDConfig) (net.Conn, error) {
|
||||
conn, err := dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tls.ClientHandshake(ctx, conn, tlsConfig)
|
||||
return tlsDialer.DialTLSContext(ctx, M.ParseSocksaddr(addr))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ var _ adapter.V2RayClientTransport = (*Client)(nil)
|
||||
|
||||
type Client struct {
|
||||
dialer N.Dialer
|
||||
tlsConfig tls.Config
|
||||
serverAddr M.Socksaddr
|
||||
requestURL url.URL
|
||||
headers http.Header
|
||||
@ -35,6 +34,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
if len(tlsConfig.NextProtos()) == 0 {
|
||||
tlsConfig.SetNextProtos([]string{"http/1.1"})
|
||||
}
|
||||
dialer = tls.NewDialer(dialer, tlsConfig)
|
||||
}
|
||||
var host string
|
||||
if options.Host != "" {
|
||||
@ -65,7 +65,6 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
}
|
||||
return &Client{
|
||||
dialer: dialer,
|
||||
tlsConfig: tlsConfig,
|
||||
serverAddr: serverAddr,
|
||||
requestURL: requestURL,
|
||||
headers: headers,
|
||||
@ -78,12 +77,6 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.tlsConfig != nil {
|
||||
conn, err = tls.ClientHandshake(ctx, conn, c.tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
request := &http.Request{
|
||||
Method: http.MethodGet,
|
||||
URL: &c.requestURL,
|
||||
|
@ -26,7 +26,6 @@ var _ adapter.V2RayClientTransport = (*Client)(nil)
|
||||
|
||||
type Client struct {
|
||||
dialer N.Dialer
|
||||
tlsConfig tls.Config
|
||||
serverAddr M.Socksaddr
|
||||
requestURL url.URL
|
||||
headers http.Header
|
||||
@ -39,6 +38,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
if len(tlsConfig.NextProtos()) == 0 {
|
||||
tlsConfig.SetNextProtos([]string{"http/1.1"})
|
||||
}
|
||||
dialer = tls.NewDialer(dialer, tlsConfig)
|
||||
}
|
||||
var requestURL url.URL
|
||||
if tlsConfig == nil {
|
||||
@ -65,7 +65,6 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
||||
}
|
||||
return &Client{
|
||||
dialer,
|
||||
tlsConfig,
|
||||
serverAddr,
|
||||
requestURL,
|
||||
headers,
|
||||
@ -79,12 +78,6 @@ func (c *Client) dialContext(ctx context.Context, requestURL *url.URL, headers h
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.tlsConfig != nil {
|
||||
conn, err = tls.ClientHandshake(ctx, conn, c.tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var deadlineConn net.Conn
|
||||
if deadline.NeedAdditionalReadDeadline(conn) {
|
||||
deadlineConn = deadline.NewConn(conn)
|
||||
|
@ -8,7 +8,9 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@ -30,6 +32,7 @@ type Endpoint struct {
|
||||
allowedAddress []netip.Prefix
|
||||
tunDevice Device
|
||||
device *device.Device
|
||||
allowedIPs *device.AllowedIPs
|
||||
pause pause.Manager
|
||||
pauseCallback *list.Element[pause.Callback]
|
||||
}
|
||||
@ -191,6 +194,7 @@ func (e *Endpoint) Start(resolve bool) error {
|
||||
if e.pause != nil {
|
||||
e.pauseCallback = e.pause.RegisterCallback(e.onPauseUpdated)
|
||||
}
|
||||
e.allowedIPs = (*device.AllowedIPs)(unsafe.Pointer(reflect.Indirect(reflect.ValueOf(wgDevice)).FieldByName("allowedips").UnsafeAddr()))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -218,6 +222,13 @@ func (e *Endpoint) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Endpoint) Lookup(address netip.Addr) *device.Peer {
|
||||
if e.allowedIPs == nil {
|
||||
return nil
|
||||
}
|
||||
return e.allowedIPs.Lookup(address.AsSlice())
|
||||
}
|
||||
|
||||
func (e *Endpoint) onPauseUpdated(event int) {
|
||||
switch event {
|
||||
case pause.EventDevicePaused, pause.EventNetworkPause:
|
||||
|
Loading…
x
Reference in New Issue
Block a user