Update On Mon Jan 27 19:35:24 CET 2025

This commit is contained in:
github-action[bot]
2025-01-27 19:35:24 +01:00
parent f9b0e13fea
commit 0548201bd4
203 changed files with 29136 additions and 3344 deletions

1
.github/update.log vendored
View File

@@ -895,3 +895,4 @@ Update On Thu Jan 23 19:35:00 CET 2025
Update On Fri Jan 24 19:33:49 CET 2025
Update On Sat Jan 25 19:31:08 CET 2025
Update On Sun Jan 26 19:30:14 CET 2025
Update On Mon Jan 27 19:35:15 CET 2025

View File

@@ -302,7 +302,7 @@ struct gpio_keys_button_dev {
struct device *dev;
struct gpio_keys_platform_data *pdata;
struct gpio_keys_button_data data[0];
struct gpio_keys_button_data data[];
};
static void gpio_keys_polled_queue_work(struct gpio_keys_button_dev *bdev)
@@ -373,7 +373,7 @@ gpio_keys_get_devtree_pdata(struct device *dev)
if (!node)
return NULL;
nbuttons = of_get_child_count(node);
nbuttons = of_get_available_child_count(node);
if (nbuttons == 0)
return ERR_PTR(-EINVAL);
@@ -388,7 +388,7 @@ gpio_keys_get_devtree_pdata(struct device *dev)
pdata->rep = !!of_get_property(node, "autorepeat", NULL);
of_property_read_u32(node, "poll-interval", &pdata->poll_interval);
for_each_child_of_node(node, pp) {
for_each_available_child_of_node(node, pp) {
button = (struct gpio_keys_button *)(&pdata->buttons[i++]);
if (of_property_read_u32(pp, "linux,code", &button->code)) {
@@ -525,10 +525,9 @@ static int gpio_keys_button_probe(struct platform_device *pdev,
button->active_low ? GPIOF_ACTIVE_LOW :
0), desc);
if (error) {
if (error != -EPROBE_DEFER) {
dev_err(dev, "unable to claim gpio %d, err=%d\n",
button->gpio, error);
}
dev_err_probe(dev, error,
"unable to claim gpio %d",
button->gpio);
goto out;
}
@@ -679,7 +678,11 @@ static void gpio_keys_irq_close(struct gpio_keys_button_dev *bdev)
}
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(6,11,0)
static int gpio_keys_remove(struct platform_device *pdev)
#else
static void gpio_keys_remove(struct platform_device *pdev)
#endif
{
struct gpio_keys_button_dev *bdev = platform_get_drvdata(pdev);
@@ -690,29 +693,25 @@ static int gpio_keys_remove(struct platform_device *pdev)
else
gpio_keys_irq_close(bdev);
#if LINUX_VERSION_CODE < KERNEL_VERSION(6,11,0)
return 0;
#endif
}
static struct platform_driver gpio_keys_driver = {
.probe = gpio_keys_probe,
#if LINUX_VERSION_CODE < KERNEL_VERSION(6,12,0)
.remove = gpio_keys_remove,
#endif
.driver = {
.name = "gpio-keys",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(gpio_keys_of_match),
},
};
static struct platform_driver gpio_keys_polled_driver = {
.probe = gpio_keys_polled_probe,
#if LINUX_VERSION_CODE < KERNEL_VERSION(6,12,0)
.remove = gpio_keys_remove,
#endif
.driver = {
.name = "gpio-keys-polled",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(gpio_keys_polled_of_match),
},
};

View File

@@ -1,11 +1,7 @@
package io.nekohasekai.sagernet.fmt
import android.widget.Toast
import io.nekohasekai.sagernet.IPv6Mode
import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.TunImplementation
import io.nekohasekai.sagernet.*
import io.nekohasekai.sagernet.bg.VpnService
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.database.ProxyEntity
@@ -30,30 +26,8 @@ import io.nekohasekai.sagernet.fmt.wireguard.buildSingBoxOutboundWireguardBean
import io.nekohasekai.sagernet.ktx.isIpAddress
import io.nekohasekai.sagernet.ktx.mkPort
import io.nekohasekai.sagernet.utils.PackageCache
import moe.matsuri.nb4a.Protocols
import moe.matsuri.nb4a.SingBoxOptions.CacheFile
import moe.matsuri.nb4a.SingBoxOptions.ClashAPIOptions
import moe.matsuri.nb4a.SingBoxOptions.DNSFakeIPOptions
import moe.matsuri.nb4a.SingBoxOptions.DNSOptions
import moe.matsuri.nb4a.SingBoxOptions.DNSRule_DefaultOptions
import moe.matsuri.nb4a.SingBoxOptions.DNSServerOptions
import moe.matsuri.nb4a.SingBoxOptions.ExperimentalOptions
import moe.matsuri.nb4a.SingBoxOptions.Inbound_DirectOptions
import moe.matsuri.nb4a.SingBoxOptions.Inbound_MixedOptions
import moe.matsuri.nb4a.SingBoxOptions.Inbound_TunOptions
import moe.matsuri.nb4a.SingBoxOptions.LogOptions
import moe.matsuri.nb4a.SingBoxOptions.MultiplexOptions
import moe.matsuri.nb4a.SingBoxOptions.MyOptions
import moe.matsuri.nb4a.SingBoxOptions.Outbound
import moe.matsuri.nb4a.SingBoxOptions.Outbound_SelectorOptions
import moe.matsuri.nb4a.SingBoxOptions.Outbound_SocksOptions
import moe.matsuri.nb4a.SingBoxOptions.RouteOptions
import moe.matsuri.nb4a.SingBoxOptions.RuleSet
import moe.matsuri.nb4a.SingBoxOptions.Rule_DefaultOptions
import moe.matsuri.nb4a.SingBoxOptionsUtil
import moe.matsuri.nb4a.checkEmpty
import moe.matsuri.nb4a.generateRuleSet
import moe.matsuri.nb4a.makeSingBoxRule
import moe.matsuri.nb4a.*
import moe.matsuri.nb4a.SingBoxOptions.*
import moe.matsuri.nb4a.plugin.Plugins
import moe.matsuri.nb4a.proxy.config.ConfigBean
import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSBean
@@ -418,7 +392,7 @@ fun buildConfig(
protocol = when (DataStore.muxType) {
1 -> "smux"
2 -> "yamux"
else -> "smux"
else -> "h2mux"
}
}.asMap()
}

View File

@@ -7,13 +7,13 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=alist
PKG_VERSION:=3.41.0
PKG_WEB_VERSION:=3.41.0
PKG_VERSION:=3.42.0
PKG_WEB_VERSION:=3.42.0
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/AlistGo/alist/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=0336a1c0089d558e7e3efd0337e51e9c45bc80f22ee8a71103e12417a7d647a2
PKG_HASH:=e9d0370c6bbeaf7e3d25b8865b000892bcc1d61f3604b744b6778ba46281dc9a
PKG_LICENSE:=GPL-3.0
PKG_LICENSE_FILE:=LICENSE
@@ -23,7 +23,7 @@ define Download/$(PKG_NAME)-web
FILE:=$(PKG_NAME)-web-$(PKG_WEB_VERSION).tar.gz
URL_FILE:=dist.tar.gz
URL:=https://github.com/AlistGo/alist-web/releases/download/$(PKG_WEB_VERSION)/
HASH:=7fbc3e83874fca15eb6590aad2c09cd6eb4f15aa7febe2b25a961ea56ba5265b
HASH:=685dd13b4da39e253085d82fd11074f051b1cbc40238479421763b3d2e49b2f0
endef
PKG_BUILD_DEPENDS:=golang/host

View File

@@ -352,7 +352,7 @@ get_geoip() {
local geoip_type_flag=""
local geoip_path="$(config_t_get global_rules v2ray_location_asset)"
geoip_path="${geoip_path%*/}/geoip.dat"
[ -e "$geoip_path" ] || { echo ""; return; }
[ -s "$geoip_path" ] || { echo ""; return; }
case "$2" in
"ipv4") geoip_type_flag="-ipv6=false" ;;
"ipv6") geoip_type_flag="-ipv4=false" ;;
@@ -773,9 +773,26 @@ run_redir() {
run_ipt2socks flag=default proto=UDP local_port=${local_port} socks_address=${_socks_address} socks_port=${_socks_port} socks_username=${_socks_username} socks_password=${_socks_password} log_file=${log_file}
;;
sing-box)
local protocol=$(config_n_get $node protocol)
[ "$protocol" = "_shunt" ] && {
local geoip_path="$(config_t_get global_singbox geoip_path)"
local geosite_path="$(config_t_get global_singbox geosite_path)"
if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then
echolog "* 缺少Geo规则文件UDP Sing-Box分流节点无法正常使用"
fi
}
run_singbox flag=UDP node=$node udp_redir_port=$local_port config_file=$config_file log_file=$log_file
;;
xray)
local protocol=$(config_n_get $node protocol)
[ "$protocol" = "_shunt" ] && {
local geo_path="$(config_t_get global_rules v2ray_location_asset)"
local geoip_path="${geo_path%*/}/geoip.dat"
local geosite_path="${geo_path%*/}/geosite.dat"
if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then
echolog "* 缺少Geo规则文件UDP Xray分流节点无法正常使用"
fi
}
run_xray flag=UDP node=$node udp_redir_port=$local_port config_file=$config_file log_file=$log_file
;;
trojan*)
@@ -875,6 +892,14 @@ run_redir() {
echolog "* 当前TCP节点采用Sing-Box分流且默认节点为直连远程DNS过滤模式将默认使用Sing-Box(TCP),防止环回!"
}
[ "$protocol" = "_shunt" ] && {
local geoip_path="$(config_t_get global_singbox geoip_path)"
local geosite_path="$(config_t_get global_singbox geosite_path)"
if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then
echolog "* 缺少Geo规则文件TCP Sing-Box分流节点无法正常使用"
fi
}
[ "${DNS_MODE}" = "sing-box" ] && {
NO_PLUGIN_DNS=1
config_file=$(echo $config_file | sed "s/.json/_DNS.json/g")
@@ -953,6 +978,15 @@ run_redir() {
echolog "* 当前TCP节点采用Xray分流且默认节点为直连远程DNS过滤模式将默认使用Xray(TCP),防止环回!"
}
[ "$protocol" = "_shunt" ] && {
local geo_path="$(config_t_get global_rules v2ray_location_asset)"
local geoip_path="${geo_path%*/}/geoip.dat"
local geosite_path="${geo_path%*/}/geosite.dat"
if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then
echolog "* 缺少Geo规则文件TCP Xray分流节点无法正常使用"
fi
}
[ "${DNS_MODE}" = "xray" ] && {
NO_PLUGIN_DNS=1
config_file=$(echo $config_file | sed "s/.json/_DNS.json/g")

View File

@@ -5,7 +5,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=25.1.8
PKG_VERSION:=25.1.27
PKG_RELEASE:=1
PKG_CONFIG_DEPENDS:= \

View File

@@ -1048,6 +1048,8 @@ gen_include() {
${__ipt}
${__ip6t}
return 0
EOF
return 0
}

View File

@@ -1081,6 +1081,8 @@ gen_include() {
cat <<-EOF >> $FWI
${__nft}
return 0
EOF
return 0
}

View File

@@ -144,7 +144,7 @@ jobs:
~/go/go1.20.14
key: go120
- name: Setup legacy Go
if: matrix.require_legacy_go == 'true' && steps.cache-legacy-go.outputs.cache-hit != 'true'
if: matrix.require_legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
run: |-
wget https://dl.google.com/go/go1.20.14.linux-amd64.tar.gz
tar -xzf go1.20.14.linux-amd64.tar.gz
@@ -159,7 +159,7 @@ jobs:
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
version: 2.5.1
install-only: true
- name: Extract signing key
run: |-
@@ -224,7 +224,7 @@ jobs:
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
ndk-version: r28-beta3
- name: Setup OpenJDK
run: |-
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
@@ -299,7 +299,7 @@ jobs:
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
ndk-version: r28-beta3
- name: Setup OpenJDK
run: |-
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
@@ -548,7 +548,7 @@ jobs:
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
version: 2.5.1
install-only: true
- name: Cache ghr
uses: actions/cache@v4

View File

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

View File

@@ -52,7 +52,7 @@ builds:
env:
- CGO_ENABLED=0
- GOROOT={{ .Env.GOPATH }}/go1.20.14
gobinary: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
tool: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
targets:
- windows_amd64_v1
- windows_386

View File

@@ -61,6 +61,12 @@ proto_install:
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
update_public_suffix:
go generate common/tlsfragment/public_suffix.go
update_certificates:
go run ./cmd/internal/update_certificates
release:
go run ./cmd/internal/build goreleaser release --clean --skip publish
mkdir dist/release

View File

@@ -0,0 +1,21 @@
package adapter
import (
"context"
"crypto/x509"
"github.com/sagernet/sing/service"
)
type CertificateStore interface {
LifecycleService
Pool() *x509.CertPool
}
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
store := service.FromContext[CertificateStore](ctx)
if store == nil {
return nil
}
return store.Pool()
}

73
sing-box/adapter/dns.go Normal file
View File

@@ -0,0 +1,73 @@
package adapter
import (
"context"
"net/netip"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/logger"
"github.com/miekg/dns"
)
type DNSRouter interface {
Lifecycle
Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error)
Lookup(ctx context.Context, domain string, options DNSQueryOptions) ([]netip.Addr, error)
ClearCache()
LookupReverseMapping(ip netip.Addr) (string, bool)
ResetNetwork()
}
type DNSClient interface {
Start()
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
ClearCache()
}
type DNSQueryOptions struct {
Transport DNSTransport
Strategy C.DomainStrategy
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
}
type RDRCStore interface {
LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
SaveRDRC(transportName string, qName string, qType uint16) error
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
}
type DNSTransport interface {
Type() string
Tag() string
Dependencies() []string
Reset()
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
}
type LegacyDNSTransport interface {
LegacyStrategy() C.DomainStrategy
LegacyClientSubnet() netip.Prefix
}
type DNSTransportRegistry interface {
option.DNSTransportOptionsRegistry
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
}
type DNSTransportManager interface {
Lifecycle
Transports() []DNSTransport
Transport(tag string) (DNSTransport, bool)
Default() DNSTransport
FakeIP() FakeIPTransport
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
}

View File

@@ -6,8 +6,6 @@ import (
"encoding/binary"
"time"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/varbin"
)
@@ -16,7 +14,20 @@ type ClashServer interface {
ConnectionTracker
Mode() string
ModeList() []string
HistoryStorage() *urltest.HistoryStorage
HistoryStorage() URLTestHistoryStorage
}
type URLTestHistory struct {
Time time.Time `json:"time"`
Delay uint16 `json:"delay"`
}
type URLTestHistoryStorage interface {
SetHook(hook chan<- struct{})
LoadURLTestHistory(tag string) *URLTestHistory
DeleteURLTestHistory(tag string)
StoreURLTestHistory(tag string, history *URLTestHistory)
Close() error
}
type V2RayServer interface {
@@ -31,7 +42,7 @@ type CacheFile interface {
FakeIPStorage
StoreRDRC() bool
dns.RDRCStore
RDRCStore
LoadMode() string
StoreMode(mode string) error

View File

@@ -3,7 +3,6 @@ package adapter
import (
"net/netip"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/logger"
)
@@ -27,6 +26,6 @@ type FakeIPStorage interface {
}
type FakeIPTransport interface {
dns.Transport
DNSTransport
Store() FakeIPStore
}

View File

@@ -71,14 +71,14 @@ type InboundContext struct {
UDPDisableDomainUnmapping bool
UDPConnect bool
UDPTimeout time.Duration
TLSFragment bool
TLSFragmentFallbackDelay time.Duration
NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
DNSServer string
DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string

View File

@@ -23,7 +23,7 @@ type Manager struct {
registry adapter.OutboundRegistry
endpoint adapter.EndpointManager
defaultTag string
access sync.Mutex
access sync.RWMutex
started bool
stage adapter.StartStage
outbounds []adapter.Outbound
@@ -169,15 +169,15 @@ func (m *Manager) Close() error {
}
func (m *Manager) Outbounds() []adapter.Outbound {
m.access.Lock()
defer m.access.Unlock()
m.access.RLock()
defer m.access.RUnlock()
return m.outbounds
}
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
m.access.Lock()
m.access.RLock()
outbound, found := m.outboundByTag[tag]
m.access.Unlock()
m.access.RUnlock()
if found {
return outbound, true
}
@@ -185,8 +185,8 @@ func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
}
func (m *Manager) Default() adapter.Outbound {
m.access.Lock()
defer m.access.Unlock()
m.access.RLock()
defer m.access.RUnlock()
if m.defaultOutbound != nil {
return m.defaultOutbound
} else {
@@ -196,9 +196,9 @@ func (m *Manager) Default() adapter.Outbound {
func (m *Manager) Remove(tag string) error {
m.access.Lock()
defer m.access.Unlock()
outbound, found := m.outboundByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.outboundByTag, tag)
@@ -232,7 +232,6 @@ func (m *Manager) Remove(tag string) error {
})
}
}
m.access.Unlock()
if started {
return common.Close(outbound)
}

View File

@@ -2,44 +2,29 @@ package adapter
import (
"context"
"crypto/tls"
"net"
"net/http"
"net/netip"
"sync"
"github.com/sagernet/sing-box/common/geoip"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-dns"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/common/x/list"
mdns "github.com/miekg/dns"
"go4.org/netipx"
)
type Router interface {
Lifecycle
FakeIPStore() FakeIPStore
ConnectionRouter
PreMatch(metadata InboundContext) error
ConnectionRouterEx
GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error)
RuleSet(tag string) (RuleSet, bool)
NeedWIFIState() bool
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
ClearDNSCache()
Rules() []Rule
SetTracker(tracker ConnectionTracker)
ResetNetwork()
}
@@ -83,12 +68,14 @@ type RuleSetMetadata struct {
ContainsIPCIDRRule bool
}
type HTTPStartContext struct {
ctx context.Context
access sync.Mutex
httpClientCache map[string]*http.Client
}
func NewHTTPStartContext() *HTTPStartContext {
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
return &HTTPStartContext{
ctx: ctx,
httpClientCache: make(map[string]*http.Client),
}
}
@@ -106,6 +93,10 @@ func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Clie
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
TLSClientConfig: &tls.Config{
Time: ntp.TimeFuncFromContext(c.ctx),
RootCAs: RootPoolFromContext(c.ctx),
},
},
}
c.httpClientCache[detour] = httpClient

View File

@@ -13,7 +13,6 @@ type Rule interface {
HeadlessRule
Service
Type() string
UpdateGeosite() error
Action() RuleAction
}

View File

@@ -12,10 +12,13 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/certificate"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/taskmonitor"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport/local"
"github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/libbox/platform"
@@ -41,6 +44,8 @@ type Box struct {
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
services []adapter.LifecycleService
@@ -58,6 +63,7 @@ func Context(
inboundRegistry adapter.InboundRegistry,
outboundRegistry adapter.OutboundRegistry,
endpointRegistry adapter.EndpointRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry,
) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
@@ -74,6 +80,10 @@ func Context(
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
}
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
}
return ctx
}
@@ -88,6 +98,7 @@ func New(options Options) (*Box, error) {
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
if endpointRegistry == nil {
return nil, E.New("missing endpoint registry in context")
@@ -131,14 +142,32 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "create log factory")
}
var services []adapter.LifecycleService
certificateOptions := common.PtrValueOrDefault(options.Certificate)
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
len(certificateOptions.Certificate) > 0 ||
len(certificateOptions.CertificatePath) > 0 ||
len(certificateOptions.CertificateDirectoryPath) > 0 {
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
if err != nil {
return nil, err
}
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
services = append(services, certificateStore)
}
routeOptions := common.PtrValueOrDefault(options.Route)
dnsOptions := common.PtrValueOrDefault(options.DNS)
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
if err != nil {
return nil, E.Cause(err, "initialize network manager")
@@ -146,18 +175,40 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS))
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
service.MustRegister[adapter.Router](ctx, router)
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
if err != nil {
return nil, E.Cause(err, "initialize router")
}
ntpOptions := common.PtrValueOrDefault(options.NTP)
var timeService *tls.TimeServiceWrapper
if ntpOptions.Enabled {
timeService = new(tls.TimeServiceWrapper)
service.MustRegister[ntp.TimeService](ctx, timeService)
}
for i, transportOptions := range dnsOptions.Servers {
var tag string
if transportOptions.Tag != "" {
tag = transportOptions.Tag
} else {
tag = F.ToString(i)
}
err = dnsTransportManager.Create(
ctx,
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
tag,
transportOptions.Type,
transportOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize inbound[", i, "]")
}
}
err = dnsRouter.Initialize(dnsOptions.Rules)
if err != nil {
return nil, E.Cause(err, "initialize dns router")
}
for i, endpointOptions := range options.Endpoints {
var tag string
if endpointOptions.Tag != "" {
@@ -165,7 +216,8 @@ func New(options Options) (*Box, error) {
} else {
tag = F.ToString(i)
}
err = endpointManager.Create(ctx,
err = endpointManager.Create(
ctx,
router,
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
tag,
@@ -183,7 +235,8 @@ func New(options Options) (*Box, error) {
} else {
tag = F.ToString(i)
}
err = inboundManager.Create(ctx,
err = inboundManager.Create(
ctx,
router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
tag,
@@ -229,13 +282,19 @@ func New(options Options) (*Box, error) {
option.DirectOutboundOptions{},
),
))
dnsTransportManager.Initialize(common.Must1(
local.NewTransport(
ctx,
logFactory.NewLogger("dns/local"),
"local",
option.LocalDNSServerOptions{},
)))
if platformInterface != nil {
err = platformInterface.Initialize(networkManager)
if err != nil {
return nil, E.Cause(err, "initialize platform interface")
}
}
var services []adapter.LifecycleService
if needCacheFile {
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
@@ -264,7 +323,7 @@ func New(options Options) (*Box, error) {
}
}
if ntpOptions.Enabled {
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions)
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
if err != nil {
return nil, E.Cause(err, "create NTP service")
}
@@ -284,6 +343,8 @@ func New(options Options) (*Box, error) {
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
dnsTransport: dnsTransportManager,
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
createdAt: createdAt,
@@ -344,11 +405,11 @@ func (s *Box) preStart() error {
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateInitialize, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.connection, s.router)
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
if err != nil {
return err
}
@@ -372,7 +433,7 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.connection, s.router, s.inbound, s.endpoint)
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint)
if err != nil {
return err
}
@@ -380,7 +441,7 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(adapter.StartStateStarted, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
if err != nil {
return err
}
@@ -399,7 +460,7 @@ func (s *Box) Close() error {
close(s.done)
}
err := common.Close(
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.network,
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
)
for _, lifecycleService := range s.services {
err = E.Append(err, lifecycleService.Close(), func(err error) error {

View File

@@ -12,7 +12,7 @@ android {
namespace "io.nekohasekai.sfa"
compileSdk 35
ndkVersion "28.0.12674087"
ndkVersion "28.0.12916984"
def ndkPathFromEnv = System.getenv("ANDROID_NDK_HOME")
if (ndkPathFromEnv != null) {

View File

@@ -61,19 +61,19 @@ object DefaultNetworkMonitor {
}
if (Bugs.fixAndroidStack) {
GlobalScope.launch(Dispatchers.IO) {
listener.updateDefaultInterface(interfaceName, interfaceIndex)
listener.updateDefaultInterface(interfaceName, interfaceIndex, false, false)
}
} else {
listener.updateDefaultInterface(interfaceName, interfaceIndex)
listener.updateDefaultInterface(interfaceName, interfaceIndex, false, false)
}
}
} else {
if (Bugs.fixAndroidStack) {
GlobalScope.launch(Dispatchers.IO) {
listener.updateDefaultInterface("", -1)
listener.updateDefaultInterface("", -1, false, false)
}
} else {
listener.updateDefaultInterface("", -1)
listener.updateDefaultInterface("", -1, false, false)
}
}
}

View File

@@ -2,10 +2,14 @@ package io.nekohasekai.sfa.bg
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Process
import android.system.OsConstants
import android.util.Log
import androidx.annotation.RequiresApi
import io.nekohasekai.libbox.InterfaceUpdateListener
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.NetworkInterfaceIterator
import io.nekohasekai.libbox.PlatformInterface
import io.nekohasekai.libbox.StringIterator
@@ -16,7 +20,6 @@ import java.net.Inet6Address
import java.net.InetSocketAddress
import java.net.InterfaceAddress
import java.net.NetworkInterface
import java.util.Enumeration
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
interface PlatformInterfaceWrapper : PlatformInterface {
@@ -44,6 +47,7 @@ interface PlatformInterfaceWrapper : PlatformInterface {
destinationAddress: String,
destinationPort: Int
): Int {
try {
val uid = Application.connectivity.getConnectionOwnerUid(
ipProtocol,
InetSocketAddress(sourceAddress, sourcePort),
@@ -51,6 +55,11 @@ interface PlatformInterfaceWrapper : PlatformInterface {
)
if (uid == Process.INVALID_UID) error("android: connection owner not found")
return uid
} catch (e: Exception) {
Log.e("PlatformInterface", "getConnectionOwnerUid", e)
e.printStackTrace(System.err)
throw e
}
}
override fun packageNameByUid(uid: Int): String {
@@ -76,10 +85,6 @@ interface PlatformInterfaceWrapper : PlatformInterface {
}
}
override fun usePlatformDefaultInterfaceMonitor(): Boolean {
return true
}
override fun startDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
DefaultNetworkMonitor.setListener(listener)
}
@@ -88,12 +93,56 @@ interface PlatformInterfaceWrapper : PlatformInterface {
DefaultNetworkMonitor.setListener(null)
}
override fun usePlatformInterfaceGetter(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
}
override fun getInterfaces(): NetworkInterfaceIterator {
return InterfaceArray(NetworkInterface.getNetworkInterfaces())
val networks = Application.connectivity.allNetworks
val networkInterfaces = NetworkInterface.getNetworkInterfaces().toList()
val interfaces = mutableListOf<LibboxNetworkInterface>()
for (network in networks) {
val boxInterface = LibboxNetworkInterface()
val linkProperties = Application.connectivity.getLinkProperties(network) ?: continue
val networkCapabilities =
Application.connectivity.getNetworkCapabilities(network) ?: continue
boxInterface.name = linkProperties.interfaceName
val networkInterface =
networkInterfaces.find { it.name == boxInterface.name } ?: continue
boxInterface.dnsServer =
StringArray(linkProperties.dnsServers.mapNotNull { it.hostAddress }.iterator())
boxInterface.type = when {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> Libbox.InterfaceTypeWIFI
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> Libbox.InterfaceTypeCellular
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> Libbox.InterfaceTypeEthernet
else -> Libbox.InterfaceTypeOther
}
boxInterface.index = networkInterface.index
runCatching {
boxInterface.mtu = networkInterface.mtu
}.onFailure {
Log.e(
"PlatformInterface", "failed to get mtu for interface ${boxInterface.name}", it
)
}
boxInterface.addresses =
StringArray(networkInterface.interfaceAddresses.mapTo(mutableListOf()) { it.toPrefix() }
.iterator())
var dumpFlags = 0
if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
dumpFlags = OsConstants.IFF_UP or OsConstants.IFF_RUNNING
}
if (networkInterface.isLoopback) {
dumpFlags = dumpFlags or OsConstants.IFF_LOOPBACK
}
if (networkInterface.isPointToPoint) {
dumpFlags = dumpFlags or OsConstants.IFF_POINTOPOINT
}
if (networkInterface.supportsMulticast()) {
dumpFlags = dumpFlags or OsConstants.IFF_MULTICAST
}
boxInterface.flags = dumpFlags
boxInterface.metered =
!networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
interfaces.add(boxInterface)
}
return InterfaceArray(interfaces.iterator())
}
override fun underNetworkExtension(): Boolean {
@@ -108,55 +157,29 @@ interface PlatformInterfaceWrapper : PlatformInterface {
}
override fun readWIFIState(): WIFIState? {
@Suppress("DEPRECATION")
val wifiInfo = Application.wifiManager.connectionInfo ?: return null
@Suppress("DEPRECATION") val wifiInfo =
Application.wifiManager.connectionInfo ?: return null
var ssid = wifiInfo.ssid
if (ssid == "<unknown ssid>") {
return WIFIState("", "")
}
if (ssid.startsWith("\"") && ssid.endsWith("\"")) {
ssid = ssid.substring(1, ssid.length - 1)
}
return WIFIState(ssid, wifiInfo.bssid)
}
private class InterfaceArray(private val iterator: Enumeration<NetworkInterface>) :
private class InterfaceArray(private val iterator: Iterator<LibboxNetworkInterface>) :
NetworkInterfaceIterator {
override fun hasNext(): Boolean {
return iterator.hasMoreElements()
return iterator.hasNext()
}
override fun next(): LibboxNetworkInterface {
val element = iterator.nextElement()
return LibboxNetworkInterface().apply {
name = element.name
index = element.index
runCatching {
mtu = element.mtu
}
addresses =
StringArray(
element.interfaceAddresses.mapTo(mutableListOf()) { it.toPrefix() }
.iterator()
)
runCatching {
flags = element.flags
}
}
return iterator.next()
}
private fun InterfaceAddress.toPrefix(): String {
return if (address is Inet6Address) {
"${Inet6Address.getByAddress(address.address).hostAddress}/${networkPrefixLength}"
} else {
"${address.hostAddress}/${networkPrefixLength}"
}
}
private val NetworkInterface.flags: Int
@SuppressLint("SoonBlockedPrivateApi")
get() {
val getFlagsMethod = NetworkInterface::class.java.getDeclaredMethod("getFlags")
return getFlagsMethod.invoke(this) as Int
}
}
private class StringArray(private val iterator: Iterator<String>) : StringIterator {
@@ -175,4 +198,17 @@ interface PlatformInterfaceWrapper : PlatformInterface {
}
}
private fun InterfaceAddress.toPrefix(): String {
return if (address is Inet6Address) {
"${Inet6Address.getByAddress(address.address).hostAddress}/${networkPrefixLength}"
} else {
"${address.hostAddress}/${networkPrefixLength}"
}
}
private val NetworkInterface.flags: Int
@SuppressLint("SoonBlockedPrivateApi") get() {
val getFlagsMethod = NetworkInterface::class.java.getDeclaredMethod("getFlags")
return getFlagsMethod.invoke(this) as Int
}
}

View File

@@ -1,3 +1,3 @@
VERSION_CODE=462
VERSION_NAME=1.10.7
GO_VERSION=go1.23.4
VERSION_CODE=466
VERSION_NAME=1.11.0
GO_VERSION=go1.23.5

View File

@@ -191,7 +191,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
}
public func usePlatformAutoDetectControl() -> Bool {
true
false
}
public func autoDetectControl(_: Int32) throws {}
@@ -219,20 +219,84 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
tunnel.writeMessage(message)
}
public func usePlatformDefaultInterfaceMonitor() -> Bool {
false
private var nwMonitor: NWPathMonitor? = nil
public func startDefaultInterfaceMonitor(_ listener: LibboxInterfaceUpdateListenerProtocol?) throws {
guard let listener else {
return
}
let monitor = NWPathMonitor()
nwMonitor = monitor
let semaphore = DispatchSemaphore(value: 0)
monitor.pathUpdateHandler = { path in
self.onUpdateDefaultInterface(listener, path)
semaphore.signal()
monitor.pathUpdateHandler = { path in
self.onUpdateDefaultInterface(listener, path)
}
}
monitor.start(queue: DispatchQueue.global())
semaphore.wait()
}
public func startDefaultInterfaceMonitor(_: LibboxInterfaceUpdateListenerProtocol?) throws {}
private func onUpdateDefaultInterface(_ listener: LibboxInterfaceUpdateListenerProtocol, _ path: Network.NWPath) {
if path.status == .unsatisfied {
listener.updateDefaultInterface("", interfaceIndex: -1, isExpensive: false, isConstrained: false)
} else {
let defaultInterface = path.availableInterfaces.first!
listener.updateDefaultInterface(defaultInterface.name, interfaceIndex: Int32(defaultInterface.index), isExpensive: path.isExpensive, isConstrained: path.isConstrained)
}
}
public func closeDefaultInterfaceMonitor(_: LibboxInterfaceUpdateListenerProtocol?) throws {}
public func useGetter() -> Bool {
false
public func closeDefaultInterfaceMonitor(_: LibboxInterfaceUpdateListenerProtocol?) throws {
nwMonitor?.cancel()
nwMonitor = nil
}
public func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol {
throw NSError(domain: "not implemented", code: 0)
guard let nwMonitor else {
throw NSError(domain: "NWMonitor not started", code: 0)
}
let path = nwMonitor.currentPath
if path.status == .unsatisfied {
return networkInterfaceArray([])
}
var interfaces: [LibboxNetworkInterface] = []
for it in path.availableInterfaces {
let interface = LibboxNetworkInterface()
interface.name = it.name
interface.index = Int32(it.index)
switch it.type {
case .wifi:
interface.type = LibboxInterfaceTypeWIFI
case .cellular:
interface.type = LibboxInterfaceTypeCellular
case .wiredEthernet:
interface.type = LibboxInterfaceTypeEthernet
default:
interface.type = LibboxInterfaceTypeOther
}
interfaces.append(interface)
}
return networkInterfaceArray(interfaces)
}
class networkInterfaceArray: NSObject, LibboxNetworkInterfaceIteratorProtocol {
private var iterator: IndexingIterator<[LibboxNetworkInterface]>
init(_ array: [LibboxNetworkInterface]) {
iterator = array.makeIterator()
}
private var nextValue: LibboxNetworkInterface? = nil
func hasNext() -> Bool {
nextValue = iterator.next()
return nextValue != nil
}
func next() -> LibboxNetworkInterface? {
nextValue
}
}
public func underNetworkExtension() -> Bool {

View File

@@ -24,7 +24,6 @@
3A27D9002A89BE230031EBCC /* CommandClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A27D8FF2A89BE230031EBCC /* CommandClient.swift */; };
3A27D9022A89C6870031EBCC /* ExtensionEnvironments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A27D9012A89C6870031EBCC /* ExtensionEnvironments.swift */; };
3A2EAEED2A6F4CBB00D00DE3 /* StandaloneApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2EAEEC2A6F4CBB00D00DE3 /* StandaloneApplicationDelegate.swift */; };
3A2F29EB2C998A5D007E024C /* Export.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3A2F29EA2C998A5D007E024C /* Export.plist */; };
3A334ED02C0F621E00E9C577 /* ConnectionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A334ECF2C0F621E00E9C577 /* ConnectionDetailsView.swift */; };
3A3AA7FC2A4EFDAE002F78AB /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
3A3AA7FF2A4EFDB3002F78AB /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
@@ -74,6 +73,7 @@
3A6326A22C0DE64F0076E274 /* ConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6326A12C0DE64F0076E274 /* ConnectionView.swift */; };
3A648D2D2A4EEAA600D95A12 /* Library.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A648D2C2A4EEAA600D95A12 /* Library.swift */; };
3A648D542A4EF4C700D95A12 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; };
3A68FB312D33671800CC613C /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 3A6CA5A52A713AA10027933B /* AppIcon.icns */; };
3A6CA4542BC19FDE0012B238 /* OnDemandRulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6CA4532BC19FDE0012B238 /* OnDemandRulesView.swift */; };
3A6CA5A32A713A580027933B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AEECC0A2A6DF9CA006A0E0C /* Assets.xcassets */; };
3A6CA5A62A713AA10027933B /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 3A6CA5A52A713AA10027933B /* AppIcon.icns */; };
@@ -95,8 +95,6 @@
3A99B42C2A75288C0010D4B0 /* ViewCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */; };
3A99B42E2A752ABB0010D4B0 /* NavigationDestinationCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */; };
3A9E6EBF2C0F20B0005061F3 /* ConnectionListPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9E6EBE2C0F20B0005061F3 /* ConnectionListPage.swift */; };
3AABFD432A9CC5A7005A24A4 /* Upload.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3AABFD422A9CC5A7005A24A4 /* Upload.plist */; };
3AABFD472A9CCC58005A24A4 /* Upload.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3AABFD462A9CCC58005A24A4 /* Upload.plist */; };
3AB1220B2A70FD500087CD55 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB1220A2A70FD500087CD55 /* Alert.swift */; };
3AC03B992A72BF3300B7946F /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC03B982A72BF3300B7946F /* Application.swift */; };
3AC03B9D2A72BF3500B7946F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AC03B9C2A72BF3500B7946F /* Assets.xcassets */; };
@@ -1507,8 +1505,9 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9C37F1812C131100001B9FA8 /* Localizable.xcstrings in Resources */,
3AC03B9D2A72BF3500B7946F /* Assets.xcassets in Resources */,
3A68FB312D33671800CC613C /* AppIcon.icns in Resources */,
9C37F1812C131100001B9FA8 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1531,10 +1530,9 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3A6CA5A92A713C420027933B /* AppIcon.icns in Resources */,
3AABFD432A9CC5A7005A24A4 /* Upload.plist in Resources */,
9C37F17D2C131100001B9FA8 /* Localizable.xcstrings in Resources */,
3AEC20FA2A459AB500A63465 /* Assets.xcassets in Resources */,
3A6CA5A92A713C420027933B /* AppIcon.icns in Resources */,
9C37F17D2C131100001B9FA8 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1542,10 +1540,10 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3A6CA5A72A713ABA0027933B /* AppIcon.icns in Resources */,
3A6CA5A32A713A580027933B /* Assets.xcassets in Resources */,
9C37F17E2C131100001B9FA8 /* Localizable.xcstrings in Resources */,
3A251C122A52D09700651082 /* Assets.xcassets in Resources */,
3A6CA5A72A713ABA0027933B /* AppIcon.icns in Resources */,
9C37F17E2C131100001B9FA8 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1568,12 +1566,10 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3AABFD472A9CCC58005A24A4 /* Upload.plist in Resources */,
9C37F17F2C131100001B9FA8 /* Localizable.xcstrings in Resources */,
3A6CA5A82A713B340027933B /* AppIcon.icns in Resources */,
3AEC8A4F2A6E5E18003702E1 /* Assets.xcassets in Resources */,
3A2F29EB2C998A5D007E024C /* Export.plist in Resources */,
3AEECC0B2A6DF9CA006A0E0C /* Assets.xcassets in Resources */,
3AEC8A4F2A6E5E18003702E1 /* Assets.xcassets in Resources */,
3A6CA5A82A713B340027933B /* AppIcon.icns in Resources */,
9C37F17F2C131100001B9FA8 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1581,8 +1577,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9C37F1842C131100001B9FA8 /* Localizable.xcstrings in Resources */,
3A6CA5A62A713AA10027933B /* AppIcon.icns in Resources */,
9C37F1842C131100001B9FA8 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2167,7 +2163,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.10.7;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt;
PRODUCT_NAME = "sing-box";
SDKROOT = appletvos;
@@ -2202,7 +2198,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.10.7;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt;
PRODUCT_NAME = "sing-box";
SDKROOT = appletvos;
@@ -2507,7 +2503,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.10.7;
MARKETING_VERSION = 1.11.0;
OTHER_CODE_SIGN_FLAGS = "--deep";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt;
PRODUCT_NAME = "sing-box";
@@ -2549,7 +2545,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.10.7;
MARKETING_VERSION = 1.11.0;
OTHER_CODE_SIGN_FLAGS = "--deep";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt;
PRODUCT_NAME = "sing-box";
@@ -2572,7 +2568,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 301;
CURRENT_PROJECT_VERSION = 302;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 287TTNZF8L;
ENABLE_HARDENED_RUNTIME = YES;
@@ -2590,7 +2586,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.7;
MARKETING_VERSION = 1.11.0;
OTHER_CODE_SIGN_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt;
PRODUCT_NAME = "sing-box";
@@ -2612,7 +2608,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 301;
CURRENT_PROJECT_VERSION = 302;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 287TTNZF8L;
ENABLE_HARDENED_RUNTIME = YES;
@@ -2630,7 +2626,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.7;
MARKETING_VERSION = 1.11.0;
OTHER_CODE_SIGN_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt;
PRODUCT_NAME = "sing-box";
@@ -2758,7 +2754,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.7;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt.system;
PRODUCT_NAME = "$(inherited)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2794,7 +2790,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.7;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt.system;
PRODUCT_NAME = "$(inherited)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2836,7 +2832,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.7;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt.standalone;
PRODUCT_NAME = SFM;
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2877,7 +2873,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.10.7;
MARKETING_VERSION = 1.11.0;
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt.standalone;
PRODUCT_NAME = SFM;
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@@ -24,8 +24,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/orchetect/MacControlCenterUI",
"state" : {
"revision" : "2d03811c2b4b499d22e034157ef8c2a6f74f569b",
"version" : "2.0.9"
"revision" : "30bff7163905641bb2b80898e7191bec0ef507f9",
"version" : "2.3.3"
}
},
{
@@ -33,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/orchetect/MenuBarExtraAccess",
"state" : {
"revision" : "f5896b47e15e114975897354c7e1082c51a2bffd",
"version" : "1.0.5"
"revision" : "e911e6454f8cbfe34a52136fc48e1ceb989a60e7",
"version" : "1.2.1"
}
},
{

View File

@@ -48,7 +48,7 @@ func FindSDK() {
}
func findNDK() bool {
const fixedVersion = "28.0.12674087"
const fixedVersion = "28.0.12916984"
const versionFile = "source.properties"
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
androidNDKPath = fixedPath

View File

@@ -0,0 +1,68 @@
package main
import (
"encoding/csv"
"io"
"net/http"
"os"
"strings"
"github.com/sagernet/sing-box/log"
"golang.org/x/exp/slices"
)
func main() {
err := updateMozillaIncludedRootCAs()
if err != nil {
log.Error(err)
}
}
func updateMozillaIncludedRootCAs() error {
response, err := http.Get("https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV")
if err != nil {
return err
}
defer response.Body.Close()
reader := csv.NewReader(response.Body)
header, err := reader.Read()
if err != nil {
return err
}
geoIndex := slices.Index(header, "Geographic Focus")
nameIndex := slices.Index(header, "Common Name or Certificate Name")
certIndex := slices.Index(header, "PEM Info")
generated := strings.Builder{}
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
package certificate
import "crypto/x509"
var mozillaIncluded *x509.CertPool
func init() {
mozillaIncluded = x509.NewCertPool()
`)
for {
record, err := reader.Read()
if err == io.EOF {
break
} else if err != nil {
return err
}
if record[geoIndex] == "China" {
continue
}
generated.WriteString("\n // ")
generated.WriteString(record[nameIndex])
generated.WriteString("\n")
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
generated.WriteString(record[certIndex])
generated.WriteString("`))\n")
}
generated.WriteString("}\n")
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
}

View File

@@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) {
configPaths = append(configPaths, "config.json")
}
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry())
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry())
}

View File

@@ -61,14 +61,15 @@ func upgradeRuleSet(sourcePath string) error {
log.Info("already up-to-date")
return nil
}
plainRuleSet, err := plainRuleSetCompat.Upgrade()
plainRuleSetCompat.Options, err = plainRuleSetCompat.Upgrade()
if err != nil {
return err
}
plainRuleSetCompat.Version = C.RuleSetVersionCurrent
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
err = encoder.Encode(plainRuleSet)
err = encoder.Encode(plainRuleSetCompat)
if err != nil {
return E.Cause(err, "encode config")
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,184 @@
package certificate
import (
"context"
"crypto/x509"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
)
var _ adapter.CertificateStore = (*Store)(nil)
type Store struct {
systemPool *x509.CertPool
currentPool *x509.CertPool
certificate string
certificatePaths []string
certificateDirectoryPaths []string
watcher *fswatch.Watcher
}
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
var systemPool *x509.CertPool
switch options.Store {
case C.CertificateStoreSystem:
platformInterface := service.FromContext[platform.Interface](ctx)
systemCertificates := platformInterface.SystemCertificates()
if len(systemCertificates) > 0 {
systemPool = x509.NewCertPool()
for _, cert := range systemCertificates {
if !systemPool.AppendCertsFromPEM([]byte(cert)) {
return nil, E.New("invalid system certificate PEM: ", cert)
}
}
} else {
certPool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
systemPool = certPool
}
case C.CertificateStoreMozilla:
systemPool = mozillaIncluded
case C.CertificateStoreNone:
systemPool = nil
default:
return nil, E.New("unknown certificate store: ", options.Store)
}
store := &Store{
systemPool: systemPool,
certificate: strings.Join(options.Certificate, "\n"),
certificatePaths: options.CertificatePath,
certificateDirectoryPaths: options.CertificateDirectoryPath,
}
var watchPaths []string
for _, target := range options.CertificatePath {
watchPaths = append(watchPaths, target)
}
for _, target := range options.CertificateDirectoryPath {
watchPaths = append(watchPaths, target)
}
if len(watchPaths) > 0 {
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPaths,
Logger: logger,
Callback: func(_ string) {
err := store.update()
if err != nil {
logger.Error(E.Cause(err, "reload certificates"))
}
},
})
if err != nil {
return nil, E.Cause(err, "fswatch: create fsnotify watcher")
}
store.watcher = watcher
}
err := store.update()
if err != nil {
return nil, E.Cause(err, "initializing certificate store")
}
return store, nil
}
func (s *Store) Name() string {
return "certificate"
}
func (s *Store) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
if s.watcher != nil {
return s.watcher.Start()
}
return nil
}
func (s *Store) Close() error {
if s.watcher != nil {
return s.watcher.Close()
}
return nil
}
func (s *Store) Pool() *x509.CertPool {
return s.currentPool
}
func (s *Store) update() error {
var currentPool *x509.CertPool
if s.systemPool == nil {
currentPool = x509.NewCertPool()
} else {
currentPool = s.systemPool.Clone()
}
if s.certificate != "" {
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
return E.New("invalid certificate PEM strings")
}
}
for _, path := range s.certificatePaths {
pemContent, err := os.ReadFile(path)
if err != nil {
return err
}
if !currentPool.AppendCertsFromPEM(pemContent) {
return E.New("invalid certificate PEM file: ", path)
}
}
var firstErr error
for _, directoryPath := range s.certificateDirectoryPaths {
directoryEntries, err := readUniqueDirectoryEntries(directoryPath)
if err != nil {
if firstErr == nil && !os.IsNotExist(err) {
firstErr = E.Cause(err, "invalid certificate directory: ", directoryPath)
}
continue
}
for _, directoryEntry := range directoryEntries {
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
if err == nil {
currentPool.AppendCertsFromPEM(pemContent)
}
}
}
if firstErr != nil {
return firstErr
}
s.currentPool = currentPool
return nil
}
func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) {
files, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
uniq := files[:0]
for _, f := range files {
if !isSameDirSymlink(f, dir) {
uniq = append(uniq, f)
}
}
return uniq, nil
}
func isSameDirSymlink(f fs.DirEntry, dir string) bool {
if f.Type()&fs.ModeSymlink == 0 {
return false
}
target, err := os.Readlink(filepath.Join(dir, f.Name()))
return err == nil && !strings.Contains(target, "/")
}

View File

@@ -8,15 +8,15 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-dns"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) {
func New(ctx context.Context, options option.DialerOptions, remoteIsDomain bool) (N.Dialer, error) {
if options.IsWireGuardListener {
return NewDefault(ctx, options)
}
@@ -36,14 +36,26 @@ func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) {
}
dialer = NewDetour(outboundManager, options.Detour)
}
if options.Detour == "" {
router := service.FromContext[adapter.Router](ctx)
if remoteIsDomain && options.Detour == "" && options.DomainResolver == "" {
deprecated.Report(ctx, deprecated.OptionMissingDomainResolverInDialOptions)
}
if (options.Detour == "" && remoteIsDomain) || options.DomainResolver != "" {
router := service.FromContext[adapter.DNSRouter](ctx)
if router != nil {
var resolveTransport adapter.DNSTransport
if options.DomainResolver != "" {
transport, loaded := service.FromContext[adapter.DNSTransportManager](ctx).Transport(options.DomainResolver)
if !loaded {
return nil, E.New("DNS server not found: " + options.DomainResolver)
}
resolveTransport = transport
}
dialer = NewResolveDialer(
router,
dialer,
options.Detour == "" && !options.TCPFastOpen,
dns.DomainStrategy(options.DomainStrategy),
resolveTransport,
C.DomainStrategy(options.DomainStrategy),
time.Duration(options.FallbackDelay))
}
}
@@ -61,11 +73,20 @@ func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInter
if err != nil {
return nil, err
}
var resolveTransport adapter.DNSTransport
if options.DomainResolver != "" {
transport, loaded := service.FromContext[adapter.DNSTransportManager](ctx).Transport(options.DomainResolver)
if !loaded {
return nil, E.New("DNS server not found: " + options.DomainResolver)
}
resolveTransport = transport
}
return NewResolveParallelInterfaceDialer(
service.FromContext[adapter.Router](ctx),
service.FromContext[adapter.DNSRouter](ctx),
dialer,
true,
dns.DomainStrategy(options.DomainStrategy),
resolveTransport,
C.DomainStrategy(options.DomainStrategy),
time.Duration(options.FallbackDelay),
), nil
}

View File

@@ -3,13 +3,11 @@ package dialer
import (
"context"
"net"
"net/netip"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@@ -23,16 +21,18 @@ var (
type resolveDialer struct {
dialer N.Dialer
parallel bool
router adapter.Router
strategy dns.DomainStrategy
router adapter.DNSRouter
transport adapter.DNSTransport
strategy C.DomainStrategy
fallbackDelay time.Duration
}
func NewResolveDialer(router adapter.Router, dialer N.Dialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) N.Dialer {
func NewResolveDialer(router adapter.DNSRouter, dialer N.Dialer, parallel bool, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) N.Dialer {
return &resolveDialer{
dialer,
parallel,
router,
transport,
strategy,
fallbackDelay,
}
@@ -43,12 +43,13 @@ type resolveParallelNetworkDialer struct {
dialer ParallelInterfaceDialer
}
func NewResolveParallelInterfaceDialer(router adapter.Router, dialer ParallelInterfaceDialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) ParallelInterfaceDialer {
func NewResolveParallelInterfaceDialer(router adapter.DNSRouter, dialer ParallelInterfaceDialer, parallel bool, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) ParallelInterfaceDialer {
return &resolveParallelNetworkDialer{
resolveDialer{
dialer,
parallel,
router,
transport,
strategy,
fallbackDelay,
},
@@ -60,22 +61,13 @@ func (d *resolveDialer) DialContext(ctx context.Context, network string, destina
if !destination.IsFqdn() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx, metadata := adapter.ExtendContext(ctx)
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
metadata.Destination = destination
metadata.Domain = ""
var addresses []netip.Addr
var err error
if d.strategy == dns.DomainStrategyAsIS {
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
} else {
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
}
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
if err != nil {
return nil, err
}
if d.parallel {
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, d.fallbackDelay)
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
} else {
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
}
@@ -85,17 +77,8 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
if !destination.IsFqdn() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx, metadata := adapter.ExtendContext(ctx)
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
metadata.Destination = destination
metadata.Domain = ""
var addresses []netip.Addr
var err error
if d.strategy == dns.DomainStrategyAsIS {
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
} else {
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
}
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
if err != nil {
return nil, err
}
@@ -110,17 +93,8 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
if !destination.IsFqdn() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx, metadata := adapter.ExtendContext(ctx)
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
metadata.Destination = destination
metadata.Domain = ""
var addresses []netip.Addr
var err error
if d.strategy == dns.DomainStrategyAsIS {
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
} else {
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
}
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
if err != nil {
return nil, err
}
@@ -128,7 +102,7 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
fallbackDelay = d.fallbackDelay
}
if d.parallel {
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
} else {
return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
}
@@ -138,17 +112,8 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
if !destination.IsFqdn() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx, metadata := adapter.ExtendContext(ctx)
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
metadata.Destination = destination
metadata.Domain = ""
var addresses []netip.Addr
var err error
if d.strategy == dns.DomainStrategyAsIS {
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
} else {
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
}
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
if err != nil {
return nil, err
}

View File

@@ -7,24 +7,27 @@ import (
"github.com/sagernet/sing-box/adapter"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
type DefaultOutboundDialer struct {
outboundManager adapter.OutboundManager
outbound adapter.OutboundManager
}
func NewDefaultOutbound(outboundManager adapter.OutboundManager) N.Dialer {
return &DefaultOutboundDialer{outboundManager: outboundManager}
func NewDefaultOutbound(ctx context.Context) N.Dialer {
return &DefaultOutboundDialer{
outbound: service.FromContext[adapter.OutboundManager](ctx),
}
}
func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
return d.outboundManager.Default().DialContext(ctx, network, destination)
return d.outbound.Default().DialContext(ctx, network, destination)
}
func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return d.outboundManager.Default().ListenPacket(ctx, destination)
return d.outbound.Default().ListenPacket(ctx, destination)
}
func (d *DefaultOutboundDialer) Upstream() any {
return d.outboundManager.Default()
return d.outbound.Default()
}

View File

@@ -15,8 +15,8 @@ import (
cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-dns"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
@@ -100,6 +100,7 @@ func NewECHClient(ctx context.Context, serverAddress string, options option.Outb
var tlsConfig cftls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
@@ -215,7 +216,7 @@ func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverNam
},
},
}
response, err := service.FromContext[adapter.Router](ctx).Exchange(ctx, message)
response, err := service.FromContext[adapter.DNSRouter](ctx).Exchange(ctx, message, adapter.DNSQueryOptions{})
if err != nil {
return nil, err
}

View File

@@ -90,7 +90,7 @@ func (c *echServerConfig) startWatcher() error {
Callback: func(path string) {
err := c.credentialsUpdated(path)
if err != nil {
c.logger.Error(E.Cause(err, "reload credentials from ", path))
c.logger.Error(E.Cause(err, "reload credentials"))
}
},
})

View File

@@ -27,9 +27,11 @@ import (
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
aTLS "github.com/sagernet/sing/common/tls"
utls "github.com/sagernet/utls"
@@ -40,6 +42,7 @@ import (
var _ ConfigCompat = (*RealityClientConfig)(nil)
type RealityClientConfig struct {
ctx context.Context
uClient *UTLSClientConfig
publicKey []byte
shortID [8]byte
@@ -70,7 +73,7 @@ func NewRealityClient(ctx context.Context, serverAddress string, options option.
if decodedLen > 8 {
return nil, E.New("invalid short_id")
}
return &RealityClientConfig{uClient, publicKey, shortID}, nil
return &RealityClientConfig{ctx, uClient, publicKey, shortID}, nil
}
func (e *RealityClientConfig) ServerName() string {
@@ -180,20 +183,24 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
}
if !verifier.verified {
go realityClientFallback(uConn, e.uClient.ServerName(), e.uClient.id)
go realityClientFallback(e.ctx, uConn, e.uClient.ServerName(), e.uClient.id)
return nil, E.New("reality verification failed")
}
return &realityClientConnWrapper{uConn}, nil
}
func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
func realityClientFallback(ctx context.Context, uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
defer uConn.Close()
client := &http.Client{
Transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, network, addr string, config *tls.Config) (net.Conn, error) {
return uConn, nil
},
TLSClientConfig: &tls.Config{
Time: ntp.TimeFuncFromContext(ctx),
RootCAs: adapter.RootPoolFromContext(ctx),
},
},
}
request, _ := http.NewRequest("GET", "https://"+serverName, nil)
@@ -213,6 +220,7 @@ func (e *RealityClientConfig) SetSessionIDGenerator(generator func(clientHello [
func (e *RealityClientConfig) Clone() Config {
return &RealityClientConfig{
e.ctx,
e.uClient.Clone().(*UTLSClientConfig),
e.publicKey,
e.shortID,

View File

@@ -101,7 +101,7 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
tlsConfig.ShortIds[shortID] = true
}
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions)
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions, options.Reality.Handshake.ServerIsDomain())
if err != nil {
return nil, err
}

View File

@@ -5,10 +5,10 @@ import (
"crypto/tls"
"crypto/x509"
"net"
"net/netip"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
@@ -51,16 +51,15 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
if options.ServerName != "" {
serverName = options.ServerName
} else if serverAddress != "" {
if _, err := netip.ParseAddr(serverName); err != nil {
serverName = serverAddress
}
}
if serverName == "" && !options.Insecure {
return nil, E.New("missing server_name or insecure=true")
}
var tlsConfig tls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {

View File

@@ -99,7 +99,7 @@ func (c *STDServerConfig) startWatcher() error {
Callback: func(path string) {
err := c.certificateUpdated(path)
if err != nil {
c.logger.Error(err)
c.logger.Error(E.Cause(err, "reload certificate"))
}
},
})

View File

@@ -12,6 +12,7 @@ import (
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
@@ -130,6 +131,7 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
var tlsConfig utls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {

View File

@@ -0,0 +1,125 @@
package tf
import (
"context"
"math/rand"
"net"
"strings"
"syscall"
"time"
"github.com/sagernet/sing/common"
N "github.com/sagernet/sing/common/network"
)
type Conn struct {
net.Conn
syscallConn syscall.Conn
ctx context.Context
firstPacketWritten bool
fallbackDelay time.Duration
}
func NewConn(conn net.Conn, ctx context.Context, fallbackDelay time.Duration) (*Conn, error) {
syscallConn, _ := N.UnwrapReader(conn).(syscall.Conn)
return &Conn{
Conn: conn,
syscallConn: syscallConn,
ctx: ctx,
fallbackDelay: fallbackDelay,
}, nil
}
func (c *Conn) Write(b []byte) (n int, err error) {
if !c.firstPacketWritten {
defer func() {
c.firstPacketWritten = true
}()
serverName := indexTLSServerName(b)
if serverName != nil {
tcpConn, isTCPConn := c.syscallConn.(interface {
SetNoDelay(bool) error
})
if isTCPConn {
err = tcpConn.SetNoDelay(true)
if err != nil {
return
}
}
splits := strings.Split(string(b[serverName.Index:serverName.Index+serverName.Length]), ".")
currentIndex := serverName.Index
var striped bool
if len(splits) > 3 {
suffix := splits[len(splits)-3] + "." + splits[len(splits)-2] + "." + splits[len(splits)-1]
if publicSuffixMatcher().Match(suffix) {
splits = splits[:len(splits)-3]
}
striped = true
}
if !striped && len(splits) > 2 {
suffix := splits[len(splits)-2] + "." + splits[len(splits)-1]
if publicSuffixMatcher().Match(suffix) {
splits = splits[:len(splits)-2]
}
striped = true
}
if !striped && len(splits) > 1 {
suffix := splits[len(splits)-1]
if publicSuffixMatcher().Match(suffix) {
splits = splits[:len(splits)-1]
}
}
if len(splits) > 1 && common.Contains(publicPrefix, splits[0]) {
currentIndex += len(splits[0]) + 1
splits = splits[1:]
}
var splitIndexes []int
for i, split := range splits {
splitAt := rand.Intn(len(split))
splitIndexes = append(splitIndexes, currentIndex+splitAt)
currentIndex += len(split)
if i != len(splits)-1 {
currentIndex++
}
}
for i := 0; i <= len(splitIndexes); i++ {
if i == 0 {
_, err = c.Conn.Write(b[:splitIndexes[i]])
} else if i == len(splitIndexes) {
_, err = c.Conn.Write(b[splitIndexes[i-1]:])
} else {
_, err = c.Conn.Write(b[splitIndexes[i-1]:splitIndexes[i]])
}
if err != nil {
return
}
if c.syscallConn != nil && i != len(splitIndexes) {
err = waitAck(c.ctx, c.syscallConn, c.fallbackDelay)
if err != nil {
return
}
}
}
if isTCPConn {
err = tcpConn.SetNoDelay(false)
if err != nil {
return
}
}
return len(b), nil
}
}
return c.Conn.Write(b)
}
func (c *Conn) ReaderReplaceable() bool {
return true
}
func (c *Conn) WriterReplaceable() bool {
return c.firstPacketWritten
}
func (c *Conn) Upstream() any {
return c.Conn
}

View File

@@ -0,0 +1,131 @@
package tf
import (
"encoding/binary"
)
const (
recordLayerHeaderLen int = 5
handshakeHeaderLen int = 6
randomDataLen int = 32
sessionIDHeaderLen int = 1
cipherSuiteHeaderLen int = 2
compressMethodHeaderLen int = 1
extensionsHeaderLen int = 2
extensionHeaderLen int = 4
sniExtensionHeaderLen int = 5
contentType uint8 = 22
handshakeType uint8 = 1
sniExtensionType uint16 = 0
sniNameDNSHostnameType uint8 = 0
tlsVersionBitmask uint16 = 0xFFFC
tls13 uint16 = 0x0304
)
type myServerName struct {
Index int
Length int
sex []byte
}
func indexTLSServerName(payload []byte) *myServerName {
if len(payload) < recordLayerHeaderLen || payload[0] != contentType {
return nil
}
segmentLen := binary.BigEndian.Uint16(payload[3:5])
if len(payload) < recordLayerHeaderLen+int(segmentLen) {
return nil
}
serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)])
if serverName == nil {
return nil
}
serverName.Length += recordLayerHeaderLen
return serverName
}
func indexTLSServerNameFromHandshake(hs []byte) *myServerName {
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
return nil
}
if hs[0] != handshakeType {
return nil
}
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
if len(hs[4:]) != int(handshakeLen) {
return nil
}
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
return nil
}
sessionIDLen := hs[38]
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
return nil
}
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
if len(cs) < cipherSuiteHeaderLen {
return nil
}
csLen := uint16(cs[0])<<8 | uint16(cs[1])
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
return nil
}
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
return nil
}
currentIndex := cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen)
serverName := indexTLSServerNameFromExtensions(cs[currentIndex:])
if serverName == nil {
return nil
}
serverName.Index += currentIndex
return serverName
}
func indexTLSServerNameFromExtensions(exs []byte) *myServerName {
if len(exs) == 0 {
return nil
}
if len(exs) < extensionsHeaderLen {
return nil
}
exsLen := uint16(exs[0])<<8 | uint16(exs[1])
exs = exs[extensionsHeaderLen:]
if len(exs) < int(exsLen) {
return nil
}
for currentIndex := extensionsHeaderLen; len(exs) > 0; {
if len(exs) < extensionHeaderLen {
return nil
}
exType := uint16(exs[0])<<8 | uint16(exs[1])
exLen := uint16(exs[2])<<8 | uint16(exs[3])
if len(exs) < extensionHeaderLen+int(exLen) {
return nil
}
sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)]
switch exType {
case sniExtensionType:
if len(sex) < sniExtensionHeaderLen {
return nil
}
sniType := sex[2]
if sniType != sniNameDNSHostnameType {
return nil
}
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
sex = sex[sniExtensionHeaderLen:]
return &myServerName{
Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen,
Length: int(sniLen),
sex: sex,
}
}
exs = exs[4+exLen:]
currentIndex += 4 + int(exLen)
}
return nil
}

View File

@@ -0,0 +1,55 @@
package tf
import (
"bufio"
"bytes"
_ "embed"
"io"
"strings"
"sync"
"github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
var publicPrefix = []string{
"www",
}
//go:generate wget -O public_suffix_list.dat https://publicsuffix.org/list/public_suffix_list.dat
//go:embed public_suffix_list.dat
var publicSuffix []byte
var publicSuffixMatcher = sync.OnceValue(func() *domain.Matcher {
matcher, err := initPublicSuffixMatcher()
if err != nil {
panic(F.ToString("error in initialize public suffix matcher"))
}
return matcher
})
func initPublicSuffixMatcher() (*domain.Matcher, error) {
reader := bufio.NewReader(bytes.NewReader(publicSuffix))
var domainList []string
for {
line, isPrefix, err := reader.ReadLine()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
if isPrefix {
return nil, E.New("unexpected prefix line")
}
lineStr := string(line)
lineStr = strings.TrimSpace(lineStr)
if lineStr == "" || strings.HasPrefix(lineStr, "//") {
continue
}
domainList = append(domainList, lineStr)
}
return domain.NewMatcher(domainList, nil, false), nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,89 @@
package tf
import (
"context"
"syscall"
"time"
"github.com/sagernet/sing/common/control"
"golang.org/x/sys/unix"
)
/*
const tcpMaxNotifyAck = 10
type tcpNotifyAckID uint32
type tcpNotifyAckComplete struct {
NotifyPending uint32
NotifyCompleteCount uint32
NotifyCompleteID [tcpMaxNotifyAck]tcpNotifyAckID
}
var sizeOfTCPNotifyAckComplete = int(unsafe.Sizeof(tcpNotifyAckComplete{}))
func getsockoptTCPNotifyAckComplete(fd, level, opt int) (*tcpNotifyAckComplete, error) {
var value tcpNotifyAckComplete
vallen := uint32(sizeOfTCPNotifyAckComplete)
err := getsockopt(fd, level, opt, unsafe.Pointer(&value), &vallen)
return &value, err
}
//go:linkname getsockopt golang.org/x/sys/unix.getsockopt
func getsockopt(s int, level int, name int, val unsafe.Pointer, vallen *uint32) error
func waitAck(ctx context.Context, conn *net.TCPConn, _ time.Duration) error {
const TCP_NOTIFY_ACKNOWLEDGEMENT = 0x212
return control.Conn(conn, func(fd uintptr) error {
err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT, 1)
if err != nil {
if errors.Is(err, unix.EINVAL) {
return waitAckFallback(ctx, conn, 0)
}
return err
}
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
var ackComplete *tcpNotifyAckComplete
ackComplete, err = getsockoptTCPNotifyAckComplete(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT)
if err != nil {
return err
}
if ackComplete.NotifyPending == 0 {
return nil
}
time.Sleep(10 * time.Millisecond)
}
})
}
*/
func waitAck(ctx context.Context, conn syscall.Conn, fallbackDelay time.Duration) error {
return control.Conn(conn, func(fd uintptr) error {
start := time.Now()
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
unacked, err := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_NWRITE)
if err != nil {
return err
}
if unacked == 0 {
if time.Since(start) <= 20*time.Millisecond {
// under transparent proxy
time.Sleep(fallbackDelay)
}
return nil
}
time.Sleep(10 * time.Millisecond)
}
})
}

View File

@@ -0,0 +1,36 @@
package tf
import (
"context"
"syscall"
"time"
"github.com/sagernet/sing/common/control"
"golang.org/x/sys/unix"
)
func waitAck(ctx context.Context, conn syscall.Conn, fallbackDelay time.Duration) error {
return control.Conn(conn, func(fd uintptr) error {
start := time.Now()
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
tcpInfo, err := unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
if err != nil {
return err
}
if tcpInfo.Unacked == 0 {
if time.Since(start) <= 20*time.Millisecond {
// under transparent proxy
time.Sleep(fallbackDelay)
}
return nil
}
time.Sleep(10 * time.Millisecond)
}
})
}

View File

@@ -0,0 +1,14 @@
//go:build !(linux || darwin)
package tf
import (
"context"
"syscall"
"time"
)
func waitAck(ctx context.Context, conn syscall.Conn, fallbackDelay time.Duration) error {
time.Sleep(fallbackDelay)
return nil
}

View File

@@ -2,32 +2,32 @@ package urltest
import (
"context"
"crypto/tls"
"net"
"net/http"
"net/url"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
)
type History struct {
Time time.Time `json:"time"`
Delay uint16 `json:"delay"`
}
var _ adapter.URLTestHistoryStorage = (*HistoryStorage)(nil)
type HistoryStorage struct {
access sync.RWMutex
delayHistory map[string]*History
delayHistory map[string]*adapter.URLTestHistory
updateHook chan<- struct{}
}
func NewHistoryStorage() *HistoryStorage {
return &HistoryStorage{
delayHistory: make(map[string]*History),
delayHistory: make(map[string]*adapter.URLTestHistory),
}
}
@@ -35,7 +35,7 @@ func (s *HistoryStorage) SetHook(hook chan<- struct{}) {
s.updateHook = hook
}
func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory {
if s == nil {
return nil
}
@@ -51,7 +51,7 @@ func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
s.notifyUpdated()
}
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
s.access.Lock()
s.delayHistory[tag] = history
s.access.Unlock()
@@ -110,6 +110,10 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return instance, nil
},
TLSClientConfig: &tls.Config{
Time: ntp.TimeFuncFromContext(ctx),
RootCAs: adapter.RootPoolFromContext(ctx),
},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse

View File

@@ -0,0 +1,7 @@
package constant
const (
CertificateStoreSystem = "system"
CertificateStoreMozilla = "mozilla"
CertificateStoreNone = "none"
)

View File

@@ -1,5 +1,34 @@
package constant
const (
DefaultDNSTTL = 600
)
type DomainStrategy = uint8
const (
DomainStrategyAsIS DomainStrategy = iota
DomainStrategyPreferIPv4
DomainStrategyPreferIPv6
DomainStrategyIPv4Only
DomainStrategyIPv6Only
)
const (
DNSTypeLegacy = "legacy"
DNSTypeUDP = "udp"
DNSTypeTCP = "tcp"
DNSTypeTLS = "tls"
DNSTypeHTTPS = "https"
DNSTypeQUIC = "quic"
DNSTypeHTTP3 = "h3"
DNSTypeHosts = "hosts"
DNSTypeLocal = "local"
DNSTypePreDefined = "predefined"
DNSTypeFakeIP = "fakeip"
DNSTypeDHCP = "dhcp"
)
const (
DNSProviderAliDNS = "alidns"
DNSProviderCloudflare = "cloudflare"

View File

@@ -16,6 +16,7 @@ const (
StopTimeout = 5 * time.Second
FatalStopTimeout = 10 * time.Second
FakeIPMetadataSaveInterval = 10 * time.Second
TLSFragmentFallbackDelay = 500 * time.Millisecond
)
var PortProtocols = map[uint16]string{

563
sing-box/dns/client.go Normal file
View File

@@ -0,0 +1,563 @@
package dns
import (
"context"
"net"
"net/netip"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"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"
"github.com/sagernet/sing/common/task"
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
"github.com/miekg/dns"
)
var (
ErrNoRawSupport = E.New("no raw query support by current transport")
ErrNotCached = E.New("not cached")
ErrResponseRejected = E.New("response rejected")
ErrResponseRejectedCached = E.Extend(ErrResponseRejected, "cached")
)
var _ adapter.DNSClient = (*Client)(nil)
type Client struct {
timeout time.Duration
disableCache bool
disableExpire bool
independentCache bool
rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore
logger logger.ContextLogger
cache freelru.Cache[dns.Question, *dns.Msg]
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
}
type ClientOptions struct {
Timeout time.Duration
DisableCache bool
DisableExpire bool
IndependentCache bool
CacheCapacity uint32
RDRC func() adapter.RDRCStore
Logger logger.ContextLogger
}
func NewClient(options ClientOptions) *Client {
client := &Client{
timeout: options.Timeout,
disableCache: options.DisableCache,
disableExpire: options.DisableExpire,
independentCache: options.IndependentCache,
initRDRCFunc: options.RDRC,
logger: options.Logger,
}
if client.timeout == 0 {
client.timeout = C.DNSTimeout
}
cacheCapacity := options.CacheCapacity
if cacheCapacity < 1024 {
cacheCapacity = 1024
}
if !client.disableCache {
if !client.independentCache {
client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32))
} else {
client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32))
}
}
return client
}
type transportCacheKey struct {
dns.Question
transportTag string
}
func (c *Client) Start() {
if c.initRDRCFunc != nil {
c.rdrc = c.initRDRCFunc()
}
}
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
if len(message.Question) == 0 {
if c.logger != nil {
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
}
responseMessage := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: message.Id,
Response: true,
Rcode: dns.RcodeFormatError,
},
Question: message.Question,
}
return &responseMessage, nil
}
question := message.Question[0]
if options.ClientSubnet.IsValid() {
message = SetClientSubnet(message, options.ClientSubnet, true)
}
isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 &&
len(message.Extra) == 0 &&
!options.ClientSubnet.IsValid()
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
if !disableCache {
response, ttl := c.loadResponse(question, transport)
if response != nil {
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
return response, nil
}
}
if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only {
responseMessage := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: message.Id,
Response: true,
Rcode: dns.RcodeSuccess,
},
Question: []dns.Question{question},
}
if c.logger != nil {
c.logger.DebugContext(ctx, "strategy rejected")
}
return &responseMessage, nil
}
messageId := message.Id
contextTransport, clientSubnetLoaded := transportTagFromContext(ctx)
if clientSubnetLoaded && transport.Tag() == contextTransport {
return nil, E.New("DNS query loopback in transport[", contextTransport, "]")
}
ctx = contextWithTransportTag(ctx, transport.Tag())
if responseChecker != nil && c.rdrc != nil {
rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype)
if rejected {
return nil, ErrResponseRejectedCached
}
}
ctx, cancel := context.WithTimeout(ctx, c.timeout)
response, err := transport.Exchange(ctx, message)
cancel()
if err != nil {
return nil, err
}
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
validResponse := response
loop:
for {
var (
addresses int
queryCNAME string
)
for _, rawRR := range validResponse.Answer {
switch rr := rawRR.(type) {
case *dns.A:
break loop
case *dns.AAAA:
break loop
case *dns.CNAME:
queryCNAME = rr.Target
}
}
if queryCNAME == "" {
break
}
exMessage := *message
exMessage.Question = []dns.Question{{
Name: queryCNAME,
Qtype: question.Qtype,
}}
validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker)
if err != nil {
return nil, err
}
}
if validResponse != response {
response.Answer = append(response.Answer, validResponse.Answer...)
}
}*/
if responseChecker != nil {
addr, addrErr := MessageToAddresses(response)
if addrErr != nil || !responseChecker(addr) {
if c.rdrc != nil {
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
}
logRejectedResponse(c.logger, ctx, response)
return response, ErrResponseRejected
}
}
if question.Qtype == dns.TypeHTTPS {
if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only {
for _, rr := range response.Answer {
https, isHTTPS := rr.(*dns.HTTPS)
if !isHTTPS {
continue
}
content := https.SVCB
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
if options.Strategy == C.DomainStrategyIPv4Only {
return it.Key() != dns.SVCB_IPV6HINT
} else {
return it.Key() != dns.SVCB_IPV4HINT
}
})
https.SVCB = content
}
}
}
var timeToLive uint32
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
}
}
if options.RewriteTTL != nil {
timeToLive = *options.RewriteTTL
}
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
record.Header().Ttl = timeToLive
}
}
response.Id = messageId
if !disableCache {
c.storeCache(transport, question, response, timeToLive)
}
logExchangedResponse(c.logger, ctx, response, timeToLive)
return response, err
}
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
domain = FqdnToDomain(domain)
dnsName := dns.Fqdn(domain)
if options.Strategy == C.DomainStrategyIPv4Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
} else if options.Strategy == C.DomainStrategyIPv6Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
}
var response4 []netip.Addr
var response6 []netip.Addr
var group task.Group
group.Append("exchange4", func(ctx context.Context) error {
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
if err != nil {
return err
}
response4 = response
return nil
})
group.Append("exchange6", func(ctx context.Context) error {
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
if err != nil {
return err
}
response6 = response
return nil
})
err := group.Run(ctx)
if len(response4) == 0 && len(response6) == 0 {
return nil, err
}
return sortAddresses(response4, response6, options.Strategy), nil
}
func (c *Client) ClearCache() {
if c.cache != nil {
c.cache.Purge()
}
if c.transportCache != nil {
c.transportCache.Purge()
}
}
func (c *Client) LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool) {
if c.disableCache || c.independentCache {
return nil, false
}
if dns.IsFqdn(domain) {
domain = domain[:len(domain)-1]
}
dnsName := dns.Fqdn(domain)
if strategy == C.DomainStrategyIPv4Only {
response, err := c.questionCache(dns.Question{
Name: dnsName,
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
}, nil)
if err != ErrNotCached {
return response, true
}
} else if strategy == C.DomainStrategyIPv6Only {
response, err := c.questionCache(dns.Question{
Name: dnsName,
Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET,
}, nil)
if err != ErrNotCached {
return response, true
}
} else {
response4, _ := c.questionCache(dns.Question{
Name: dnsName,
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
}, nil)
response6, _ := c.questionCache(dns.Question{
Name: dnsName,
Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET,
}, nil)
if len(response4) > 0 || len(response6) > 0 {
return sortAddresses(response4, response6, strategy), true
}
}
return nil, false
}
func (c *Client) ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool) {
if c.disableCache || c.independentCache || len(message.Question) != 1 {
return nil, false
}
question := message.Question[0]
response, ttl := c.loadResponse(question, nil)
if response == nil {
return nil, false
}
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
return response, true
}
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
if strategy == C.DomainStrategyPreferIPv6 {
return append(response6, response4...)
} else {
return append(response4, response6...)
}
}
func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, timeToLive uint32) {
if timeToLive == 0 {
return
}
if c.disableExpire {
if !c.independentCache {
c.cache.Add(question, message)
} else {
c.transportCache.Add(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message)
}
return
}
if !c.independentCache {
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
} else {
c.transportCache.AddWithLifetime(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message, time.Second*time.Duration(timeToLive))
}
}
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
question := dns.Question{
Name: name,
Qtype: qType,
Qclass: dns.ClassINET,
}
disableCache := c.disableCache || options.DisableCache
if !disableCache {
cachedAddresses, err := c.questionCache(question, transport)
if err != ErrNotCached {
return cachedAddresses, err
}
}
message := dns.Msg{
MsgHdr: dns.MsgHdr{
RecursionDesired: true,
},
Question: []dns.Question{question},
}
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
if err != nil {
return nil, err
}
return MessageToAddresses(response)
}
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
response, _ := c.loadResponse(question, transport)
if response == nil {
return nil, ErrNotCached
}
return MessageToAddresses(response)
}
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) {
var (
response *dns.Msg
loaded bool
)
if c.disableExpire {
if !c.independentCache {
response, loaded = c.cache.Get(question)
} else {
response, loaded = c.transportCache.Get(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
})
}
if !loaded {
return nil, 0
}
return response.Copy(), 0
} else {
var expireAt time.Time
if !c.independentCache {
response, expireAt, loaded = c.cache.GetWithLifetime(question)
} else {
response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
})
}
if !loaded {
return nil, 0
}
timeNow := time.Now()
if timeNow.After(expireAt) {
if !c.independentCache {
c.cache.Remove(question)
} else {
c.transportCache.Remove(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
})
}
return nil, 0
}
var originTTL int
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
originTTL = int(record.Header().Ttl)
}
}
}
nowTTL := int(expireAt.Sub(timeNow).Seconds())
if nowTTL < 0 {
nowTTL = 0
}
response = response.Copy()
if originTTL > 0 {
duration := uint32(originTTL - nowTTL)
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
record.Header().Ttl = record.Header().Ttl - duration
}
}
} else {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
record.Header().Ttl = uint32(nowTTL)
}
}
}
return response, nowTTL
}
}
func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
return nil, RCodeError(response.Rcode)
}
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawAnswer := range response.Answer {
switch answer := rawAnswer.(type) {
case *dns.A:
addresses = append(addresses, M.AddrFromIP(answer.A))
case *dns.AAAA:
addresses = append(addresses, M.AddrFromIP(answer.AAAA))
case *dns.HTTPS:
for _, value := range answer.SVCB.Value {
if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT {
addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...)
}
}
}
}
return addresses, nil
}
func wrapError(err error) error {
switch dnsErr := err.(type) {
case *net.DNSError:
if dnsErr.IsNotFound {
return RCodeNameError
}
case *net.AddrError:
return RCodeNameError
}
return err
}
type transportKey struct{}
func contextWithTransportTag(ctx context.Context, transportTag string) context.Context {
return context.WithValue(ctx, transportKey{}, transportTag)
}
func transportTagFromContext(ctx context.Context) (string, bool) {
value, loaded := ctx.Value(transportKey{}).(string)
return value, loaded
}
func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg {
response := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: id,
Rcode: dns.RcodeSuccess,
Response: true,
},
Question: []dns.Question{question},
}
for _, address := range addresses {
if address.Is4() {
response.Answer = append(response.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: timeToLive,
},
A: address.AsSlice(),
})
} else {
response.Answer = append(response.Answer, &dns.AAAA{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: timeToLive,
},
AAAA: address.AsSlice(),
})
}
}
return &response
}

View File

@@ -0,0 +1,69 @@
package dns
import (
"context"
"strings"
"github.com/sagernet/sing/common/logger"
"github.com/miekg/dns"
)
func logCachedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl int) {
if logger == nil || len(response.Question) == 0 {
return
}
domain := FqdnToDomain(response.Question[0].Name)
logger.DebugContext(ctx, "cached ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl)
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
logger.InfoContext(ctx, "cached ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
}
}
}
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
if logger == nil || len(response.Question) == 0 {
return
}
domain := FqdnToDomain(response.Question[0].Name)
logger.DebugContext(ctx, "exchanged ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl)
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
logger.InfoContext(ctx, "exchanged ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
}
}
}
func logRejectedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) {
if logger == nil || len(response.Question) == 0 {
return
}
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
logger.InfoContext(ctx, "rejected ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
}
}
}
func FqdnToDomain(fqdn string) string {
if dns.IsFqdn(fqdn) {
return fqdn[:len(fqdn)-1]
}
return fqdn
}
func FormatQuestion(string string) string {
for strings.HasPrefix(string, ";") {
string = string[1:]
}
string = strings.ReplaceAll(string, "\t", " ")
string = strings.ReplaceAll(string, "\n", " ")
string = strings.ReplaceAll(string, ";; ", " ")
string = strings.ReplaceAll(string, "; ", " ")
for strings.Contains(string, " ") {
string = strings.ReplaceAll(string, " ", " ")
}
return strings.TrimSpace(string)
}

View File

@@ -0,0 +1,29 @@
package dns
import (
"github.com/sagernet/sing/common/buf"
"github.com/miekg/dns"
)
func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf.Buffer, error) {
maxLen := 512
if edns0Option := request.IsEdns0(); edns0Option != nil {
if udpSize := int(edns0Option.UDPSize()); udpSize > 512 {
maxLen = udpSize
}
}
responseLen := response.Len()
if responseLen > maxLen {
response.Truncate(maxLen)
}
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
buffer.Resize(headroom, 0)
rawMessage, err := response.PackBuffer(buffer.FreeBytes())
if err != nil {
buffer.Release()
return nil, err
}
buffer.Truncate(len(rawMessage))
return buffer, nil
}

View File

@@ -0,0 +1,56 @@
package dns
import (
"net/netip"
"github.com/miekg/dns"
)
func SetClientSubnet(message *dns.Msg, clientSubnet netip.Prefix, override bool) *dns.Msg {
var (
optRecord *dns.OPT
subnetOption *dns.EDNS0_SUBNET
)
findExists:
for _, record := range message.Extra {
var isOPTRecord bool
if optRecord, isOPTRecord = record.(*dns.OPT); isOPTRecord {
for _, option := range optRecord.Option {
var isEDNS0Subnet bool
subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET)
if isEDNS0Subnet {
if !override {
return message
}
break findExists
}
}
}
}
if optRecord == nil {
exMessage := *message
message = &exMessage
optRecord = &dns.OPT{
Hdr: dns.RR_Header{
Name: ".",
Rrtype: dns.TypeOPT,
},
}
message.Extra = append(message.Extra, optRecord)
} else {
message = message.Copy()
}
if subnetOption == nil {
subnetOption = new(dns.EDNS0_SUBNET)
optRecord.Option = append(optRecord.Option, subnetOption)
}
subnetOption.Code = dns.EDNS0SUBNET
if clientSubnet.Addr().Is4() {
subnetOption.Family = 1
} else {
subnetOption.Family = 2
}
subnetOption.SourceNetmask = uint8(clientSubnet.Bits())
subnetOption.Address = clientSubnet.Addr().AsSlice()
return message
}

33
sing-box/dns/rcode.go Normal file
View File

@@ -0,0 +1,33 @@
package dns
import F "github.com/sagernet/sing/common/format"
const (
RCodeSuccess RCodeError = 0 // NoError
RCodeFormatError RCodeError = 1 // FormErr
RCodeServerFailure RCodeError = 2 // ServFail
RCodeNameError RCodeError = 3 // NXDomain
RCodeNotImplemented RCodeError = 4 // NotImp
RCodeRefused RCodeError = 5 // Refused
)
type RCodeError uint16
func (e RCodeError) Error() string {
switch e {
case RCodeSuccess:
return "success"
case RCodeFormatError:
return "format error"
case RCodeServerFailure:
return "server failure"
case RCodeNameError:
return "name error"
case RCodeNotImplemented:
return "not implemented"
case RCodeRefused:
return "refused"
default:
return F.ToString("unknown error: ", uint16(e))
}
}

436
sing-box/dns/router.go Normal file
View File

@@ -0,0 +1,436 @@
package dns
import (
"context"
"errors"
"net/netip"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSRouter = (*Router)(nil)
type Router struct {
ctx context.Context
logger logger.ContextLogger
transport adapter.DNSTransportManager
outbound adapter.OutboundManager
client adapter.DNSClient
rules []adapter.DNSRule
defaultDomainStrategy C.DomainStrategy
dnsReverseMapping freelru.Cache[netip.Addr, string]
platformInterface platform.Interface
}
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
router := &Router{
ctx: ctx,
logger: logFactory.NewLogger("dns"),
transport: service.FromContext[adapter.DNSTransportManager](ctx),
outbound: service.FromContext[adapter.OutboundManager](ctx),
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
}
router.client = NewClient(ClientOptions{
DisableCache: options.DNSClientOptions.DisableCache,
DisableExpire: options.DNSClientOptions.DisableExpire,
IndependentCache: options.DNSClientOptions.IndependentCache,
CacheCapacity: options.DNSClientOptions.CacheCapacity,
RDRC: func() adapter.RDRCStore {
cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil {
return nil
}
if !cacheFile.StoreRDRC() {
return nil
}
return cacheFile
},
Logger: router.logger,
})
if options.ReverseMapping {
router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32))
}
return router
}
func (r *Router) Initialize(rules []option.DNSRule) error {
for i, ruleOptions := range rules {
dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true)
if err != nil {
return E.Cause(err, "parse dns rule[", i, "]")
}
r.rules = append(r.rules, dnsRule)
}
return nil
}
func (r *Router) Start(stage adapter.StartStage) error {
monitor := taskmonitor.New(r.logger, C.StartTimeout)
switch stage {
case adapter.StartStateStart:
monitor.Start("initialize DNS client")
r.client.Start()
monitor.Finish()
for i, rule := range r.rules {
monitor.Start("initialize DNS rule[", i, "]")
err := rule.Start()
monitor.Finish()
if err != nil {
return E.Cause(err, "initialize DNS rule[", i, "]")
}
}
}
return nil
}
func (r *Router) Close() error {
monitor := taskmonitor.New(r.logger, C.StopTimeout)
var err error
for i, rule := range r.rules {
monitor.Start("close dns rule[", i, "]")
err = E.Append(err, rule.Close(), func(err error) error {
return E.Cause(err, "close dns rule[", i, "]")
})
monitor.Finish()
}
return err
}
func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
metadata := adapter.ContextFrom(ctx)
if metadata == nil {
panic("no context")
}
var currentRuleIndex int
if ruleIndex != -1 {
currentRuleIndex = ruleIndex + 1
}
for ; currentRuleIndex < len(r.rules); currentRuleIndex++ {
currentRule := r.rules[currentRuleIndex]
if currentRule.WithAddressLimit() && !isAddressQuery {
continue
}
metadata.ResetRuleCache()
if currentRule.Match(metadata) {
displayRuleIndex := currentRuleIndex
if displayRuleIndex != -1 {
displayRuleIndex += displayRuleIndex + 1
}
ruleDescription := currentRule.String()
if ruleDescription != "" {
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action())
} else {
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
}
switch action := currentRule.Action().(type) {
case *R.RuleActionDNSRoute:
transport, loaded := r.transport.Transport(action.Server)
if !loaded {
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
continue
}
isFakeIP := transport.Type() == C.DNSTypeFakeIP
if isFakeIP && !allowFakeIP {
continue
}
if action.Strategy != C.DomainStrategyAsIS {
options.Strategy = action.Strategy
}
if isFakeIP || action.DisableCache {
options.DisableCache = true
}
if action.RewriteTTL != nil {
options.RewriteTTL = action.RewriteTTL
}
if action.ClientSubnet.IsValid() {
options.ClientSubnet = action.ClientSubnet
}
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
return transport, currentRule, currentRuleIndex
case *R.RuleActionDNSRouteOptions:
if action.Strategy != C.DomainStrategyAsIS {
options.Strategy = action.Strategy
}
if action.DisableCache {
options.DisableCache = true
}
if action.RewriteTTL != nil {
options.RewriteTTL = action.RewriteTTL
}
if action.ClientSubnet.IsValid() {
options.ClientSubnet = action.ClientSubnet
}
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
case *R.RuleActionReject:
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
return nil, currentRule, currentRuleIndex
}
}
}
return r.transport.Default(), nil, -1
}
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
if len(message.Question) != 1 {
r.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
responseMessage := mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Response: true,
Rcode: mDNS.RcodeFormatError,
},
Question: message.Question,
}
return &responseMessage, nil
}
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
var (
transport adapter.DNSTransport
err error
)
response, cached := r.client.ExchangeCache(ctx, message)
if !cached {
var metadata *adapter.InboundContext
ctx, metadata = adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.QueryType = message.Question[0].Qtype
switch metadata.QueryType {
case mDNS.TypeA:
metadata.IPVersion = 4
case mDNS.TypeAAAA:
metadata.IPVersion = 6
}
metadata.Domain = FqdnToDomain(message.Question[0].Name)
if options.Transport != nil {
transport = options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(ctx, transport, message, options, nil)
} else {
var (
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &options)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return FixedResponse(message.Id, message.Question[0], nil, 0), nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
}
}
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
}
}
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(dnsCtx, transport, message, options, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
}
}
if responseCheck != nil && rejected {
continue
}
break
}
}
}
if err != nil {
return nil, err
}
if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 {
if transport.Type() != C.DNSTypeFakeIP {
for _, answer := range response.Answer {
switch record := answer.(type) {
case *mDNS.A:
r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.A), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second)
case *mDNS.AAAA:
r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.AAAA), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second)
}
}
}
}
return response, nil
}
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
var (
responseAddrs []netip.Addr
cached bool
err error
)
printResult := func() {
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
r.logger.DebugContext(ctx, "response rejected for ", domain)
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
}
} else if len(responseAddrs) == 0 {
r.logger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
err = RCodeNameError
}
}
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
if cached {
if len(responseAddrs) == 0 {
return nil, RCodeNameError
}
return responseAddrs, nil
}
r.logger.DebugContext(ctx, "lookup domain ", domain)
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.Domain = domain
if options.Transport != nil {
transport := options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil)
} else {
var (
transport adapter.DNSTransport
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &options)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return nil, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
}
}
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
}
}
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, options, responseCheck)
if responseCheck == nil || err == nil {
break
}
printResult()
}
}
printResult()
if len(responseAddrs) > 0 {
r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " "))
}
return responseAddrs, err
}
func isAddressQuery(message *mDNS.Msg) bool {
for _, question := range message.Question {
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA || question.Qtype == mDNS.TypeHTTPS {
return true
}
}
return false
}
func (r *Router) ClearCache() {
r.client.ClearCache()
if r.platformInterface != nil {
r.platformInterface.ClearDNSCache()
}
}
func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) {
if r.dnsReverseMapping == nil {
return "", false
}
domain, loaded := r.dnsReverseMapping.Get(ip)
return domain, loaded
}
func (r *Router) ResetNetwork() {
r.ClearCache()
for _, transport := range r.transport.Transports() {
transport.Reset()
}
}

View File

@@ -3,9 +3,6 @@ package dhcp
import (
"context"
"net"
"net/netip"
"net/url"
"os"
"runtime"
"strings"
"sync"
@@ -14,13 +11,18 @@ import (
"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-dns"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
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/common/task"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
@@ -29,76 +31,70 @@ import (
mDNS "github.com/miekg/dns"
)
func init() {
dns.RegisterTransport([]string{"dhcp"}, func(options dns.TransportOptions) (dns.Transport, error) {
return NewTransport(options)
})
func RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.DHCPDNSServerOptions](registry, C.DNSTypeDHCP, NewTransport)
}
var _ adapter.DNSTransport = (*Transport)(nil)
type Transport struct {
options dns.TransportOptions
router adapter.Router
dns.TransportAdapter
ctx context.Context
dialer N.Dialer
logger logger.ContextLogger
networkManager adapter.NetworkManager
interfaceName string
autoInterface bool
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
transports []dns.Transport
transports []adapter.DNSTransport
updateAccess sync.Mutex
updatedAt time.Time
}
func NewTransport(options dns.TransportOptions) (*Transport, error) {
linkURL, err := url.Parse(options.Address)
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewLocalDialer(ctx, options.LocalDNSServerOptions)
if err != nil {
return nil, err
}
if linkURL.Host == "" {
return nil, E.New("missing interface name for DHCP")
}
transport := &Transport{
options: options,
networkManager: service.FromContext[adapter.NetworkManager](options.Context),
interfaceName: linkURL.Host,
autoInterface: linkURL.Host == "auto",
}
return transport, nil
return &Transport{
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeDHCP, tag, options.LocalDNSServerOptions),
ctx: ctx,
dialer: transportDialer,
logger: logger,
networkManager: service.FromContext[adapter.NetworkManager](ctx),
interfaceName: options.Interface,
}, nil
}
func (t *Transport) Name() string {
return t.options.Name
}
func (t *Transport) Start() error {
func (t *Transport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
err := t.fetchServers()
if err != nil {
return err
}
if t.autoInterface {
if t.interfaceName == "" {
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
}
return nil
}
func (t *Transport) Close() error {
for _, transport := range t.transports {
transport.Reset()
}
if t.interfaceCallback != nil {
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
}
return nil
}
func (t *Transport) Reset() {
for _, transport := range t.transports {
transport.Reset()
}
}
func (t *Transport) Close() error {
for _, transport := range t.transports {
transport.Close()
}
if t.interfaceCallback != nil {
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
}
return nil
}
func (t *Transport) Raw() bool {
return true
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
err := t.fetchServers()
if err != nil {
@@ -120,7 +116,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
}
func (t *Transport) fetchInterface() (*control.Interface, error) {
if t.autoInterface {
if t.interfaceName == "" {
if t.networkManager.InterfaceMonitor() == nil {
return nil, E.New("missing monitor for auto DHCP, set route.auto_detect_interface")
}
@@ -152,8 +148,8 @@ func (t *Transport) updateServers() error {
return E.Cause(err, "dhcp: prepare interface")
}
t.options.Logger.Info("dhcp: query DNS servers on ", iface.Name)
fetchCtx, cancel := context.WithTimeout(t.options.Context, C.DHCPTimeout)
t.logger.Info("dhcp: query DNS servers on ", iface.Name)
fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout)
err = t.fetchServers0(fetchCtx, iface)
cancel()
if err != nil {
@@ -169,7 +165,7 @@ func (t *Transport) updateServers() error {
func (t *Transport) interfaceUpdated(defaultInterface *control.Interface, flags int) {
err := t.updateServers()
if err != nil {
t.options.Logger.Error("update servers: ", err)
t.logger.Error("update servers: ", err)
}
}
@@ -181,7 +177,7 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *control.Interface)
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
listenAddr = "255.255.255.255:68"
}
packetConn, err := listener.ListenPacket(t.options.Context, "udp4", listenAddr)
packetConn, err := listener.ListenPacket(t.ctx, "udp4", listenAddr)
if err != nil {
return err
}
@@ -219,17 +215,17 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
dhcpPacket, err := dhcpv4.FromBytes(buffer.Bytes())
if err != nil {
t.options.Logger.Trace("dhcp: parse DHCP response: ", err)
t.logger.Trace("dhcp: parse DHCP response: ", err)
return err
}
if dhcpPacket.MessageType() != dhcpv4.MessageTypeOffer {
t.options.Logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType())
t.logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType())
continue
}
if dhcpPacket.TransactionID != transactionID {
t.options.Logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID)
t.logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID)
continue
}
@@ -237,44 +233,27 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
if len(dns) == 0 {
return nil
}
var addrs []netip.Addr
for _, ip := range dns {
addr, _ := netip.AddrFromSlice(ip)
addrs = append(addrs, addr.Unmap())
}
return t.recreateServers(iface, addrs)
return t.recreateServers(iface, common.Map(dns, func(it net.IP) M.Socksaddr {
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
}))
}
}
func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []netip.Addr) error {
func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []M.Socksaddr) error {
if len(serverAddrs) > 0 {
t.options.Logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, func(it netip.Addr) string {
return it.String()
}), ","), "]")
t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "]")
}
serverDialer := common.Must1(dialer.NewDefault(t.options.Context, option.DialerOptions{
serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{
BindInterface: iface.Name,
UDPFragmentDefault: true,
}))
var transports []dns.Transport
var transports []adapter.DNSTransport
for _, serverAddr := range serverAddrs {
newOptions := t.options
newOptions.Address = serverAddr.String()
newOptions.Dialer = serverDialer
serverTransport, err := dns.NewUDPTransport(newOptions)
if err != nil {
return E.Cause(err, "create UDP transport from DHCP result: ", serverAddr)
}
transports = append(transports, serverTransport)
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, serverAddr))
}
for _, transport := range t.transports {
transport.Close()
transport.Reset()
}
t.transports = transports
return nil
}
func (t *Transport) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) {
return nil, os.ErrInvalid
}

View File

@@ -0,0 +1,56 @@
package fakeip
import (
"context"
"net/netip"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
mDNS "github.com/miekg/dns"
)
func RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.FakeIPDNSServerOptions](registry, C.DNSTypeFakeIP, NewTransport)
}
var _ adapter.FakeIPTransport = (*Transport)(nil)
type Transport struct {
dns.TransportAdapter
logger logger.ContextLogger
store adapter.FakeIPStore
}
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) {
store := NewStore(ctx, logger, options.Inet4Range.Build(netip.Prefix{}), options.Inet6Range.Build(netip.Prefix{}))
return &Transport{
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil),
logger: logger,
store: store,
}, nil
}
func (t *Transport) Reset() {
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA {
return nil, E.New("only IP queries are supported by fakeip")
}
address, err := t.store.Create(question.Name, question.Qtype == mDNS.TypeAAAA)
if err != nil {
return nil, err
}
return dns.FixedResponse(message.Id, question, []netip.Addr{address}, C.DefaultDNSTTL), nil
}
func (t *Transport) Store() adapter.FakeIPStore {
return t.store
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,204 @@
package transport
import (
"bytes"
"context"
"io"
"net"
"net/http"
"net/url"
"strconv"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
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"
aTLS "github.com/sagernet/sing/common/tls"
sHTTP "github.com/sagernet/sing/protocol/http"
mDNS "github.com/miekg/dns"
"golang.org/x/net/http2"
)
const MimeType = "application/dns-message"
var _ adapter.DNSTransport = (*HTTPSTransport)(nil)
func RegisterHTTPS(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTPS, NewHTTPS)
}
type HTTPSTransport struct {
dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
transport *http.Transport
}
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
if err != nil {
return nil, err
}
tlsOptions := common.PtrValueOrDefault(options.TLS)
tlsOptions.Enabled = true
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
if err != nil {
return nil, err
}
if common.Error(tlsConfig.Config()) == nil && !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), http2.NextProtoTLS))
}
if !common.Contains(tlsConfig.NextProtos(), "http/1.1") {
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), "http/1.1"))
}
headers := options.Headers.Build()
host := headers.Get("Host")
if host != "" {
headers.Del("Host")
} else {
if tlsConfig.ServerName() != "" {
host = tlsConfig.ServerName()
} else {
host = options.Server
}
}
destinationURL := url.URL{
Scheme: "https",
Host: host,
}
if destinationURL.Host == "" {
destinationURL.Host = options.Server
}
if options.ServerPort != 0 && options.ServerPort != 443 {
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
}
path := options.Path
if path == "" {
path = "/dns-query"
}
err = sHTTP.URLSetPath(&destinationURL, path)
if err != nil {
return nil, err
}
serverAddr := options.ServerOptions.Build()
if serverAddr.Port == 0 {
serverAddr.Port = 443
}
return NewHTTPSRaw(
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
logger,
transportDialer,
&destinationURL,
headers,
serverAddr,
tlsConfig,
), nil
}
func NewHTTPSRaw(
adapter dns.TransportAdapter,
logger log.ContextLogger,
dialer N.Dialer,
destination *url.URL,
headers http.Header,
serverAddr M.Socksaddr,
tlsConfig tls.Config,
) *HTTPSTransport {
var transport *http.Transport
if tlsConfig != nil {
transport = &http.Transport{
ForceAttemptHTTP2: true,
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
tcpConn, hErr := dialer.DialContext(ctx, network, serverAddr)
if hErr != nil {
return nil, hErr
}
tlsConn, hErr := aTLS.ClientHandshake(ctx, tcpConn, tlsConfig)
if hErr != nil {
tcpConn.Close()
return nil, hErr
}
return tlsConn, nil
},
}
} else {
transport = &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, serverAddr)
},
}
}
return &HTTPSTransport{
TransportAdapter: adapter,
logger: logger,
dialer: dialer,
destination: destination,
headers: headers,
transport: transport,
}
}
func (t *HTTPSTransport) Reset() {
t.transport.CloseIdleConnections()
t.transport = t.transport.Clone()
}
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
exMessage := *message
exMessage.Id = 0
exMessage.Compress = true
requestBuffer := buf.NewSize(1 + message.Len())
rawMessage, err := exMessage.PackBuffer(requestBuffer.FreeBytes())
if err != nil {
requestBuffer.Release()
return nil, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
if err != nil {
requestBuffer.Release()
return nil, err
}
request.Header = t.headers.Clone()
request.Header.Set("Content-Type", MimeType)
request.Header.Set("Accept", MimeType)
response, err := t.transport.RoundTrip(request)
requestBuffer.Release()
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, E.New("unexpected status: ", response.Status)
}
var responseMessage mDNS.Msg
if response.ContentLength > 0 {
responseBuffer := buf.NewSize(int(response.ContentLength))
_, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength))
if err != nil {
return nil, err
}
err = responseMessage.Unpack(responseBuffer.Bytes())
responseBuffer.Release()
} else {
rawMessage, err = io.ReadAll(response.Body)
if err != nil {
return nil, err
}
err = responseMessage.Unpack(rawMessage)
}
if err != nil {
return nil, err
}
return &responseMessage, nil
}

View File

@@ -0,0 +1,194 @@
package local
import (
"context"
"time"
"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/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 RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport)
}
var _ adapter.DNSTransport = (*Transport)(nil)
type Transport struct {
dns.TransportAdapter
hosts *hosts.File
dialer N.Dialer
}
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
}
return &Transport{
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
hosts: hosts.NewFile(hosts.DefaultPath),
dialer: transportDialer,
}, nil
}
func (t *Transport) Reset() {
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(domain)
if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
}
systemConfig := getSystemDNSConfig()
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)
addresses, _ := dns.MessageToAddresses(response)
if len(addresses) == 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) {
var networks []string
if useTCP {
networks = []string{N.NetworkTCP}
} else {
networks = []string{N.NetworkUDP, N.NetworkTCP}
}
request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: uint16(randInt()),
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")
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,82 @@
package transport
import (
"context"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSTransport = (*PredefinedTransport)(nil)
func RegisterPredefined(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.PredefinedDNSServerOptions](registry, C.DNSTypePreDefined, NewPredefined)
}
type PredefinedTransport struct {
dns.TransportAdapter
responses []*predefinedResponse
}
type predefinedResponse struct {
questions []mDNS.Question
answer *mDNS.Msg
}
func NewPredefined(ctx context.Context, logger log.ContextLogger, tag string, options option.PredefinedDNSServerOptions) (adapter.DNSTransport, error) {
var responses []*predefinedResponse
for _, response := range options.Responses {
questions, msg, err := response.Build()
if err != nil {
return nil, err
}
responses = append(responses, &predefinedResponse{
questions: questions,
answer: msg,
})
}
if len(responses) == 0 {
return nil, E.New("empty predefined responses")
}
return &PredefinedTransport{
TransportAdapter: dns.NewTransportAdapter(C.DNSTypePreDefined, tag, nil),
responses: responses,
}, nil
}
func (t *PredefinedTransport) Reset() {
}
func (t *PredefinedTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
for _, response := range t.responses {
for _, question := range response.questions {
if func() bool {
if question.Name == "" && question.Qtype == mDNS.TypeNone {
return true
} else if question.Name == "" {
return common.Any(message.Question, func(it mDNS.Question) bool {
return it.Qtype == question.Qtype
})
} else if question.Qtype == mDNS.TypeNone {
return common.Any(message.Question, func(it mDNS.Question) bool {
return it.Name == question.Name
})
} else {
return common.Contains(message.Question, question)
}
}() {
copyAnswer := *response.answer
copyAnswer.Id = message.Id
return &copyAnswer, nil
}
}
}
return nil, dns.RCodeNameError
}

View File

@@ -0,0 +1,167 @@
package quic
import (
"bytes"
"context"
"io"
"net"
"net/http"
"net/url"
"strconv"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tls"
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/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
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"
sHTTP "github.com/sagernet/sing/protocol/http"
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSTransport = (*HTTP3Transport)(nil)
func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTP3, NewHTTP3)
}
type HTTP3Transport struct {
dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer
destination *url.URL
headers http.Header
transport *http3.Transport
}
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
if err != nil {
return nil, err
}
tlsOptions := common.PtrValueOrDefault(options.TLS)
tlsOptions.Enabled = true
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
if err != nil {
return nil, err
}
stdConfig, err := tlsConfig.Config()
if err != nil {
return nil, err
}
headers := options.Headers.Build()
host := headers.Get("Host")
if host != "" {
headers.Del("Host")
} else {
if tlsConfig.ServerName() != "" {
host = tlsConfig.ServerName()
} else {
host = options.Server
}
}
destinationURL := url.URL{
Scheme: "HTTP3",
Host: host,
}
if destinationURL.Host == "" {
destinationURL.Host = options.Server
}
if options.ServerPort != 0 && options.ServerPort != 443 {
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
}
path := options.Path
if path == "" {
path = "/dns-query"
}
err = sHTTP.URLSetPath(&destinationURL, path)
if err != nil {
return nil, err
}
serverAddr := options.ServerOptions.Build()
if serverAddr.Port == 0 {
serverAddr.Port = 443
}
return &HTTP3Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
logger: logger,
dialer: transportDialer,
destination: &destinationURL,
headers: headers,
transport: &http3.Transport{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (quic.EarlyConnection, error) {
destinationAddr := M.ParseSocksaddr(addr)
conn, dialErr := transportDialer.DialContext(ctx, N.NetworkUDP, destinationAddr)
if dialErr != nil {
return nil, dialErr
}
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsCfg, cfg)
},
TLSClientConfig: stdConfig,
},
}, nil
}
func (t *HTTP3Transport) Reset() {
t.transport.Close()
}
func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
exMessage := *message
exMessage.Id = 0
exMessage.Compress = true
requestBuffer := buf.NewSize(1 + message.Len())
rawMessage, err := exMessage.PackBuffer(requestBuffer.FreeBytes())
if err != nil {
requestBuffer.Release()
return nil, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
if err != nil {
requestBuffer.Release()
return nil, err
}
request.Header = t.headers.Clone()
request.Header.Set("Content-Type", transport.MimeType)
request.Header.Set("Accept", transport.MimeType)
response, err := t.transport.RoundTrip(request)
requestBuffer.Release()
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, E.New("unexpected status: ", response.Status)
}
var responseMessage mDNS.Msg
if response.ContentLength > 0 {
responseBuffer := buf.NewSize(int(response.ContentLength))
_, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength))
if err != nil {
return nil, err
}
err = responseMessage.Unpack(responseBuffer.Bytes())
responseBuffer.Release()
} else {
rawMessage, err = io.ReadAll(response.Body)
if err != nil {
return nil, err
}
err = responseMessage.Unpack(rawMessage)
}
if err != nil {
return nil, err
}
return &responseMessage, nil
}

View File

@@ -0,0 +1,174 @@
package quic
import (
"context"
"errors"
"sync"
"github.com/sagernet/quic-go"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tls"
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"
sQUIC "github.com/sagernet/sing-quic"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSTransport = (*Transport)(nil)
func RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeQUIC, NewQUIC)
}
type Transport struct {
dns.TransportAdapter
ctx context.Context
logger logger.ContextLogger
dialer N.Dialer
serverAddr M.Socksaddr
tlsConfig tls.Config
access sync.Mutex
connection quic.EarlyConnection
}
func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
if err != nil {
return nil, err
}
tlsOptions := common.PtrValueOrDefault(options.TLS)
tlsOptions.Enabled = true
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
if err != nil {
return nil, err
}
if len(tlsConfig.NextProtos()) == 0 {
tlsConfig.SetNextProtos([]string{"doq"})
}
serverAddr := options.ServerOptions.Build()
if serverAddr.Port == 0 {
serverAddr.Port = 853
}
return &Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
ctx: ctx,
logger: logger,
dialer: transportDialer,
serverAddr: serverAddr,
tlsConfig: tlsConfig,
}, nil
}
func (t *Transport) Reset() {
t.access.Lock()
defer t.access.Unlock()
connection := t.connection
if connection != nil {
connection.CloseWithError(0, "")
}
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
var (
conn quic.Connection
err error
response *mDNS.Msg
)
for i := 0; i < 2; i++ {
conn, err = t.openConnection()
if err != nil {
return nil, err
}
response, err = t.exchange(ctx, message, conn)
if err == nil {
return response, nil
} else if !isQUICRetryError(err) {
return nil, err
} else {
conn.CloseWithError(quic.ApplicationErrorCode(0), "")
continue
}
}
return nil, err
}
func (t *Transport) openConnection() (quic.EarlyConnection, error) {
connection := t.connection
if connection != nil && !common.Done(connection.Context()) {
return connection, nil
}
t.access.Lock()
defer t.access.Unlock()
connection = t.connection
if connection != nil && !common.Done(connection.Context()) {
return connection, nil
}
conn, err := t.dialer.DialContext(t.ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, err
}
earlyConnection, err := sQUIC.DialEarly(
t.ctx,
bufio.NewUnbindPacketConn(conn),
t.serverAddr.UDPAddr(),
t.tlsConfig,
nil,
)
if err != nil {
return nil, err
}
t.connection = earlyConnection
return earlyConnection, nil
}
func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn quic.Connection) (*mDNS.Msg, error) {
stream, err := conn.OpenStreamSync(ctx)
if err != nil {
return nil, err
}
defer stream.Close()
defer stream.CancelRead(0)
err = transport.WriteMessage(stream, 0, message)
if err != nil {
return nil, err
}
return transport.ReadMessage(stream)
}
// https://github.com/AdguardTeam/dnsproxy/blob/fd1868577652c639cce3da00e12ca548f421baf1/upstream/upstream_quic.go#L394
func isQUICRetryError(err error) (ok bool) {
var qAppErr *quic.ApplicationError
if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 {
return true
}
var qIdleErr *quic.IdleTimeoutError
if errors.As(err, &qIdleErr) {
return true
}
var resetErr *quic.StatelessResetError
if errors.As(err, &resetErr) {
return true
}
var qTransportError *quic.TransportError
if errors.As(err, &qTransportError) && qTransportError.ErrorCode == quic.NoError {
return true
}
if errors.Is(err, quic.Err0RTTRejected) {
return true
}
return false
}

View File

@@ -0,0 +1,99 @@
package transport
import (
"context"
"encoding/binary"
"io"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSTransport = (*TCPTransport)(nil)
func RegisterTCP(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeTCP, NewTCP)
}
type TCPTransport struct {
dns.TransportAdapter
dialer N.Dialer
serverAddr M.Socksaddr
}
func NewTCP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewRemoteDialer(ctx, options)
if err != nil {
return nil, err
}
serverAddr := options.ServerOptions.Build()
if serverAddr.Port == 0 {
serverAddr.Port = 53
}
return &TCPTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTCP, tag, options),
dialer: transportDialer,
serverAddr: serverAddr,
}, nil
}
func (t *TCPTransport) Reset() {
}
func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
if err != nil {
return nil, err
}
defer conn.Close()
err = WriteMessage(conn, 0, message)
if err != nil {
return nil, err
}
return ReadMessage(conn)
}
func ReadMessage(reader io.Reader) (*mDNS.Msg, error) {
var responseLen uint16
err := binary.Read(reader, binary.BigEndian, &responseLen)
if err != nil {
return nil, err
}
if responseLen < 10 {
return nil, mDNS.ErrShortRead
}
buffer := buf.NewSize(int(responseLen))
defer buffer.Release()
_, err = buffer.ReadFullFrom(reader, int(responseLen))
if err != nil {
return nil, err
}
var message mDNS.Msg
err = message.Unpack(buffer.Bytes())
return &message, err
}
func WriteMessage(writer io.Writer, messageId uint16, message *mDNS.Msg) error {
requestLen := message.Len()
buffer := buf.NewSize(3 + requestLen)
defer buffer.Release()
common.Must(binary.Write(buffer, binary.BigEndian, uint16(requestLen)))
exMessage := *message
exMessage.Id = messageId
exMessage.Compress = true
rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
if err != nil {
return err
}
buffer.Truncate(2 + len(rawMessage))
return common.Error(writer.Write(buffer.Bytes()))
}

View File

@@ -0,0 +1,115 @@
package transport
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"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/common/x/list"
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSTransport = (*TLSTransport)(nil)
func RegisterTLS(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeTLS, NewTLS)
}
type TLSTransport struct {
dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer
serverAddr M.Socksaddr
tlsConfig tls.Config
access sync.Mutex
connections list.List[*tlsDNSConn]
}
type tlsDNSConn struct {
tls.Conn
queryId uint16
}
func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
if err != nil {
return nil, err
}
tlsOptions := common.PtrValueOrDefault(options.TLS)
tlsOptions.Enabled = true
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
if err != nil {
return nil, err
}
serverAddr := options.ServerOptions.Build()
if serverAddr.Port == 0 {
serverAddr.Port = 853
}
return &TLSTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions),
logger: logger,
dialer: transportDialer,
serverAddr: serverAddr,
tlsConfig: tlsConfig,
}, nil
}
func (t *TLSTransport) Reset() {
t.access.Lock()
defer t.access.Unlock()
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
connection.Value.Close()
}
t.connections.Init()
}
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
t.access.Lock()
conn := t.connections.PopFront()
t.access.Unlock()
if conn != nil {
response, err := t.exchange(message, conn)
if err == nil {
return response, nil
}
}
tcpConn, err := t.dialer.DialContext(ctx, N.NetworkTCP, 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})
}
func (t *TLSTransport) exchange(message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) {
conn.queryId++
err := WriteMessage(conn, conn.queryId, message)
if err != nil {
conn.Close()
return nil, E.Cause(err, "write request")
}
response, err := ReadMessage(conn)
if err != nil {
conn.Close()
return nil, E.Cause(err, "read response")
}
t.access.Lock()
t.connections.PushBack(conn)
t.access.Unlock()
return response, nil
}

View File

@@ -0,0 +1,217 @@
package transport
import (
"context"
"net"
"os"
"sync"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSTransport = (*UDPTransport)(nil)
func RegisterUDP(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeUDP, NewUDP)
}
type UDPTransport struct {
dns.TransportAdapter
logger logger.ContextLogger
dialer N.Dialer
serverAddr M.Socksaddr
udpSize int
tcpTransport *TCPTransport
access sync.Mutex
conn *dnsConnection
done chan struct{}
}
func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) {
transportDialer, err := dns.NewRemoteDialer(ctx, options)
if err != nil {
return nil, err
}
serverAddr := options.ServerOptions.Build()
if serverAddr.Port == 0 {
serverAddr.Port = 53
}
return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil
}
func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr) *UDPTransport {
return &UDPTransport{
TransportAdapter: adapter,
logger: logger,
dialer: dialer,
serverAddr: serverAddr,
udpSize: 512,
tcpTransport: &TCPTransport{
dialer: dialer,
serverAddr: serverAddr,
},
done: make(chan struct{}),
}
}
func (t *UDPTransport) Reset() {
t.access.Lock()
defer t.access.Unlock()
close(t.done)
t.done = make(chan struct{})
}
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
response, err := t.exchange(ctx, message)
if err != nil {
return nil, err
}
if response.Truncated {
t.logger.InfoContext(ctx, "response truncated, retrying with TCP")
return t.tcpTransport.Exchange(ctx, message)
}
return response, nil
}
func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.open(ctx)
if err != nil {
return nil, err
}
if edns0Opt := message.IsEdns0(); edns0Opt != nil {
if udpSize := int(edns0Opt.UDPSize()); udpSize > t.udpSize {
t.udpSize = udpSize
}
}
buffer := buf.NewSize(1 + message.Len())
defer buffer.Release()
exMessage := *message
exMessage.Compress = true
messageId := message.Id
callback := &dnsCallback{
done: make(chan struct{}),
}
conn.access.Lock()
conn.queryId++
exMessage.Id = conn.queryId
conn.callbacks[exMessage.Id] = callback
conn.access.Unlock()
defer func() {
conn.access.Lock()
delete(conn.callbacks, messageId)
conn.access.Unlock()
callback.access.Lock()
select {
case <-callback.done:
default:
close(callback.done)
}
callback.access.Unlock()
}()
rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
if err != nil {
return nil, err
}
_, err = conn.Write(rawMessage)
if err != nil {
conn.Close(err)
return nil, err
}
select {
case <-callback.done:
callback.message.Id = messageId
return callback.message, nil
case <-conn.done:
return nil, conn.err
case <-t.done:
return nil, os.ErrClosed
case <-ctx.Done():
conn.Close(ctx.Err())
return nil, ctx.Err()
}
}
func (t *UDPTransport) open(ctx context.Context) (*dnsConnection, error) {
t.access.Lock()
defer t.access.Unlock()
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, err
}
dnsConn := &dnsConnection{
Conn: conn,
done: make(chan struct{}),
callbacks: make(map[uint16]*dnsCallback),
}
go t.recvLoop(dnsConn)
return dnsConn, nil
}
func (t *UDPTransport) recvLoop(conn *dnsConnection) {
for {
buffer := buf.NewSize(t.udpSize)
_, err := buffer.ReadOnceFrom(conn)
if err != nil {
buffer.Release()
conn.Close(err)
return
}
var message mDNS.Msg
err = message.Unpack(buffer.Bytes())
buffer.Release()
if err != nil {
conn.Close(err)
return
}
conn.access.RLock()
callback, loaded := conn.callbacks[message.Id]
conn.access.RUnlock()
if !loaded {
continue
}
callback.access.Lock()
select {
case <-callback.done:
default:
callback.message = &message
close(callback.done)
}
callback.access.Unlock()
}
}
type dnsConnection struct {
net.Conn
access sync.RWMutex
done chan struct{}
closeOnce sync.Once
err error
queryId uint16
callbacks map[uint16]*dnsCallback
}
func (c *dnsConnection) Close(err error) {
c.access.Lock()
defer c.access.Unlock()
c.closeOnce.Do(func() {
close(c.done)
c.err = err
})
c.Conn.Close()
}
type dnsCallback struct {
access sync.Mutex
message *mDNS.Msg
done chan struct{}
}

View File

@@ -0,0 +1,70 @@
package dns
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
)
var _ adapter.LegacyDNSTransport = (*TransportAdapter)(nil)
type TransportAdapter struct {
transportType string
transportTag string
dependencies []string
strategy C.DomainStrategy
clientSubnet netip.Prefix
}
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
return TransportAdapter{
transportType: transportType,
transportTag: transportTag,
dependencies: dependencies,
}
}
func NewTransportAdapterWithLocalOptions(transportType string, transportTag string, localOptions option.LocalDNSServerOptions) TransportAdapter {
return TransportAdapter{
transportType: transportType,
transportTag: transportTag,
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
clientSubnet: localOptions.LegacyClientSubnet,
}
}
func NewTransportAdapterWithRemoteOptions(transportType string, transportTag string, remoteOptions option.RemoteDNSServerOptions) TransportAdapter {
var dependencies []string
if remoteOptions.AddressResolver != "" {
dependencies = []string{remoteOptions.AddressResolver}
}
return TransportAdapter{
transportType: transportType,
transportTag: transportTag,
dependencies: dependencies,
strategy: C.DomainStrategy(remoteOptions.LegacyStrategy),
clientSubnet: remoteOptions.LegacyClientSubnet,
}
}
func (a *TransportAdapter) Type() string {
return a.transportType
}
func (a *TransportAdapter) Tag() string {
return a.transportTag
}
func (a *TransportAdapter) Dependencies() []string {
return a.dependencies
}
func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy {
return a.strategy
}
func (a *TransportAdapter) LegacyClientSubnet() netip.Prefix {
return a.clientSubnet
}

View File

@@ -0,0 +1,101 @@
package dns
import (
"context"
"net"
"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/option"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) {
if options.LegacyDefaultDialer {
return dialer.NewDefaultOutbound(ctx), nil
} else {
return dialer.New(ctx, options.DialerOptions, false)
}
}
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
var (
transportDialer N.Dialer
err error
)
if options.LegacyDefaultDialer {
transportDialer = dialer.NewDefaultOutbound(ctx)
} else {
transportDialer, err = dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
}
if err != nil {
return nil, err
}
if options.AddressResolver != "" {
transport := service.FromContext[adapter.DNSTransportManager](ctx)
resolverTransport, loaded := transport.Transport(options.AddressResolver)
if !loaded {
return nil, E.New("address resolver not found: ", options.AddressResolver)
}
transportDialer = NewTransportDialer(transportDialer, service.FromContext[adapter.DNSRouter](ctx), resolverTransport, C.DomainStrategy(options.AddressStrategy), time.Duration(options.AddressFallbackDelay))
} else if options.ServerIsDomain() {
return nil, E.New("missing address resolver for server: ", options.Server)
}
return transportDialer, nil
}
type TransportDialer struct {
dialer N.Dialer
dnsRouter adapter.DNSRouter
transport adapter.DNSTransport
strategy C.DomainStrategy
fallbackDelay time.Duration
}
func NewTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *TransportDialer {
return &TransportDialer{
dialer,
dnsRouter,
transport,
strategy,
fallbackDelay,
}
}
func (d *TransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if destination.IsIP() {
return d.dialer.DialContext(ctx, network, destination)
}
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
Transport: d.transport,
Strategy: d.strategy,
})
if err != nil {
return nil, err
}
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
}
func (d *TransportDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if destination.IsIP() {
return d.dialer.ListenPacket(ctx, destination)
}
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
Transport: d.transport,
Strategy: d.strategy,
})
if err != nil {
return nil, err
}
conn, _, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
return conn, err
}
func (d *TransportDialer) Upstream() any {
return d.dialer
}

View File

@@ -0,0 +1,288 @@
package dns
import (
"context"
"io"
"os"
"strings"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
var _ adapter.DNSTransportManager = (*TransportManager)(nil)
type TransportManager struct {
logger log.ContextLogger
registry adapter.DNSTransportRegistry
outbound adapter.OutboundManager
defaultTag string
access sync.RWMutex
started bool
stage adapter.StartStage
transports []adapter.DNSTransport
transportByTag map[string]adapter.DNSTransport
dependByTag map[string][]string
defaultTransport adapter.DNSTransport
defaultTransportFallback adapter.DNSTransport
fakeIPTransport adapter.FakeIPTransport
}
func NewTransportManager(logger logger.ContextLogger, registry adapter.DNSTransportRegistry, outbound adapter.OutboundManager, defaultTag string) *TransportManager {
return &TransportManager{
logger: logger,
registry: registry,
outbound: outbound,
defaultTag: defaultTag,
transportByTag: make(map[string]adapter.DNSTransport),
dependByTag: make(map[string][]string),
}
}
func (m *TransportManager) Initialize(defaultTransportFallback adapter.DNSTransport) {
m.defaultTransportFallback = defaultTransportFallback
}
func (m *TransportManager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
outbounds := m.transports
m.access.Unlock()
if stage == adapter.StartStateStart {
return m.startTransports(m.transports)
} else {
for _, outbound := range outbounds {
err := adapter.LegacyStart(outbound, stage)
if err != nil {
return E.Cause(err, stage, " dns/", outbound.Type(), "[", outbound.Tag(), "]")
}
}
}
return nil
}
func (m *TransportManager) startTransports(transports []adapter.DNSTransport) error {
monitor := taskmonitor.New(m.logger, C.StartTimeout)
started := make(map[string]bool)
for {
canContinue := false
startOne:
for _, transportToStart := range transports {
transportTag := transportToStart.Tag()
if started[transportTag] {
continue
}
dependencies := transportToStart.Dependencies()
for _, dependency := range dependencies {
if !started[dependency] {
continue startOne
}
}
started[transportTag] = true
canContinue = true
if starter, isStarter := transportToStart.(adapter.Lifecycle); isStarter {
monitor.Start("start dns/", transportToStart.Type(), "[", transportTag, "]")
err := starter.Start(adapter.StartStateStart)
monitor.Finish()
if err != nil {
return E.Cause(err, "start dns/", transportToStart.Type(), "[", transportTag, "]")
}
}
}
if len(started) == len(transports) {
break
}
if canContinue {
continue
}
currentTransport := common.Find(transports, func(it adapter.DNSTransport) bool {
return !started[it.Tag()]
})
var lintTransport func(oTree []string, oCurrent adapter.DNSTransport) error
lintTransport = func(oTree []string, oCurrent adapter.DNSTransport) error {
problemTransportTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
return !started[it]
})
if common.Contains(oTree, problemTransportTag) {
return E.New("circular server dependency: ", strings.Join(oTree, " -> "), " -> ", problemTransportTag)
}
m.access.Lock()
problemTransport := m.transportByTag[problemTransportTag]
m.access.Unlock()
if problemTransport == nil {
return E.New("dependency[", problemTransportTag, "] not found for server[", oCurrent.Tag(), "]")
}
return lintTransport(append(oTree, problemTransportTag), problemTransport)
}
return lintTransport([]string{currentTransport.Tag()}, currentTransport)
}
return nil
}
func (m *TransportManager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
m.access.Lock()
if !m.started {
m.access.Unlock()
return nil
}
m.started = false
transports := m.transports
m.transports = nil
m.access.Unlock()
var err error
for _, transport := range transports {
if closer, isCloser := transport.(io.Closer); isCloser {
monitor.Start("close server/", transport.Type(), "[", transport.Tag(), "]")
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close server/", transport.Type(), "[", transport.Tag(), "]")
})
monitor.Finish()
}
}
return nil
}
func (m *TransportManager) Transports() []adapter.DNSTransport {
m.access.RLock()
defer m.access.RUnlock()
return m.transports
}
func (m *TransportManager) Transport(tag string) (adapter.DNSTransport, bool) {
m.access.RLock()
outbound, found := m.transportByTag[tag]
m.access.RUnlock()
return outbound, found
}
func (m *TransportManager) Default() adapter.DNSTransport {
m.access.RLock()
defer m.access.RUnlock()
if m.defaultTransport != nil {
return m.defaultTransport
} else {
return m.defaultTransportFallback
}
}
func (m *TransportManager) FakeIP() adapter.FakeIPTransport {
m.access.RLock()
defer m.access.RUnlock()
return m.fakeIPTransport
}
func (m *TransportManager) Remove(tag string) error {
m.access.Lock()
defer m.access.Unlock()
transport, found := m.transportByTag[tag]
if !found {
return os.ErrInvalid
}
delete(m.transportByTag, tag)
index := common.Index(m.transports, func(it adapter.DNSTransport) bool {
return it == transport
})
if index == -1 {
panic("invalid inbound index")
}
m.transports = append(m.transports[:index], m.transports[index+1:]...)
started := m.started
if m.defaultTransport == transport {
if len(m.transports) > 0 {
nextTransport := m.transports[0]
if nextTransport.Type() != C.DNSTypeFakeIP {
return E.New("default server cannot be fakeip")
}
m.defaultTransport = nextTransport
m.logger.Info("updated default server to ", m.defaultTransport.Tag())
} else {
m.defaultTransport = nil
}
}
dependBy := m.dependByTag[tag]
if len(dependBy) > 0 {
return E.New("server[", tag, "] is depended by ", strings.Join(dependBy, ", "))
}
dependencies := transport.Dependencies()
for _, dependency := range dependencies {
if len(m.dependByTag[dependency]) == 1 {
delete(m.dependByTag, dependency)
} else {
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
return it != tag
})
}
}
if started {
transport.Reset()
}
return nil
}
func (m *TransportManager) Create(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) error {
if tag == "" {
return os.ErrInvalid
}
transport, err := m.registry.CreateDNSTransport(ctx, logger, tag, transportType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(transport, stage)
if err != nil {
return E.Cause(err, stage, " dns/", transport.Type(), "[", transport.Tag(), "]")
}
}
}
if existsTransport, loaded := m.transportByTag[tag]; loaded {
if m.started {
err = common.Close(existsTransport)
if err != nil {
return E.Cause(err, "close dns/", existsTransport.Type(), "[", existsTransport.Tag(), "]")
}
}
existsIndex := common.Index(m.transports, func(it adapter.DNSTransport) bool {
return it == existsTransport
})
if existsIndex == -1 {
panic("invalid inbound index")
}
m.transports = append(m.transports[:existsIndex], m.transports[existsIndex+1:]...)
}
m.transports = append(m.transports, transport)
m.transportByTag[tag] = transport
dependencies := transport.Dependencies()
for _, dependency := range dependencies {
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
}
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultTransport == nil) {
if transport.Type() == C.DNSTypeFakeIP {
return E.New("default server cannot be fakeip")
}
m.defaultTransport = transport
if m.started {
m.logger.Info("updated default server to ", transport.Tag())
}
}
if transport.Type() == C.DNSTypeFakeIP {
if m.fakeIPTransport != nil {
return E.New("multiple fakeip server are not supported")
}
m.fakeIPTransport = transport.(adapter.FakeIPTransport)
}
return nil
}

View File

@@ -0,0 +1,72 @@
package dns
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type TransportConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.DNSTransport, error)
func RegisterTransport[Options any](registry *TransportRegistry, transportType string, constructor TransportConstructorFunc[Options]) {
registry.register(transportType, func() any {
return new(Options)
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.DNSTransport, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.DNSTransportRegistry = (*TransportRegistry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.DNSTransport, error)
)
type TransportRegistry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructors map[string]constructorFunc
}
func NewTransportRegistry() *TransportRegistry {
return &TransportRegistry{
optionsType: make(map[string]optionsConstructorFunc),
constructors: make(map[string]constructorFunc),
}
}
func (r *TransportRegistry) CreateOptions(transportType string) (any, bool) {
r.access.Lock()
defer r.access.Unlock()
optionsConstructor, loaded := r.optionsType[transportType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (r *TransportRegistry) CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (adapter.DNSTransport, error) {
r.access.Lock()
defer r.access.Unlock()
constructor, loaded := r.constructors[transportType]
if !loaded {
return nil, E.New("transport type not found: " + transportType)
}
return constructor(ctx, logger, tag, options)
}
func (r *TransportRegistry) register(transportType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
r.access.Lock()
defer r.access.Unlock()
r.optionsType[transportType] = optionsConstructor
r.constructors[transportType] = constructor
}

View File

@@ -2,9 +2,125 @@
icon: material/alert-decagram
---
#### 1.11.0-beta.24
### 1.11.0
* Fixes and improvements
Important changes since 1.10:
* Introducing rule actions **1**
* Improve tun compatibility **3**
* Merge route options to route actions **4**
* Add `network_type`, `network_is_expensive` and `network_is_constrainted` rule items **5**
* Add multi network dialing **6**
* Add `cache_capacity` DNS option **7**
* Add `override_address` and `override_port` route options **8**
* Upgrade WireGuard outbound to endpoint **9**
* Add UDP GSO support for WireGuard
* Make GSO adaptive **10**
* Add UDP timeout route option **11**
* Add more masquerade options for hysteria2 **12**
* Add `rule-set merge` command
* Add port hopping support for Hysteria2 **13**
* Hysteria2 `ignore_client_bandwidth` behavior update **14**
**1**:
New rule actions replace legacy inbound fields and special outbound fields,
and can be used for pre-matching **2**.
See [Rule](/configuration/route/rule/),
[Rule Action](/configuration/route/rule_action/),
[DNS Rule](/configuration/dns/rule/) and
[DNS Rule Action](/configuration/dns/rule_action/).
For migration, see
[Migrate legacy special outbounds to rule actions](/migration/#migrate-legacy-special-outbounds-to-rule-actions),
[Migrate legacy inbound fields to rule actions](/migration/#migrate-legacy-inbound-fields-to-rule-actions)
and [Migrate legacy DNS route options to rule actions](/migration/#migrate-legacy-dns-route-options-to-rule-actions).
**2**:
Similar to Surge's pre-matching.
Specifically, new rule actions allow you to reject connections with
TCP RST (for TCP connections) and ICMP port unreachable (for UDP packets)
before connection established to improve tun's compatibility.
See [Rule Action](/configuration/route/rule_action/).
**3**:
When `gvisor` tun stack is enabled, even if the request passes routing,
if the outbound connection establishment fails,
the connection still does not need to be established and a TCP RST is replied.
**4**:
Route options in DNS route actions will no longer be considered deprecated,
see [DNS Route Action](/configuration/dns/rule_action/).
Also, now `udp_disable_domain_unmapping` and `udp_connect` can also be configured in route action,
see [Route Action](/configuration/route/rule_action/).
**5**:
When using in graphical clients, new routing rule items allow you to match on
network type (WIFI, cellular, etc.), whether the network is expensive, and whether Low Data Mode is enabled.
See [Route Rule](/configuration/route/rule/), [DNS Route Rule](/configuration/dns/rule/)
and [Headless Rule](/configuration/rule-set/headless-rule/).
**6**:
Similar to Surge's strategy.
New options allow you to connect using multiple network interfaces,
prefer or only use one type of interface,
and configure a timeout to fallback to other interfaces.
See [Dial Fields](/configuration/shared/dial/#network_strategy),
[Rule Action](/configuration/route/rule_action/#network_strategy)
and [Route](/configuration/route/#default_network_strategy).
**7**:
See [DNS](/configuration/dns/#cache_capacity).
**8**:
See [Rule Action](/configuration/route/#override_address) and
[Migrate destination override fields to route options](/migration/#migrate-destination-override-fields-to-route-options).
**9**:
The new WireGuard endpoint combines inbound and outbound capabilities,
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).
**10**:
For WireGuard outbound and endpoint, GSO will be automatically enabled when available,
see [WireGuard Outbound](/configuration/outbound/wireguard/#gso).
For TUN, GSO has been removed,
see [Deprecated](/deprecated/#gso-option-in-tun).
**11**:
See [Rule Action](/configuration/route/rule_action/#udp_timeout).
**12**:
See [Hysteria2](/configuration/inbound/hysteria2/#masquerade).
**13**:
See [Hysteria2](/configuration/outbound/hysteria2/).
**14**:
When `up_mbps` and `down_mbps` are set, `ignore_client_bandwidth` instead denies clients from using BBR CC.
### 1.10.7

View File

@@ -1,3 +1,11 @@
---
icon: material/delete-clock
---
!!! failure "Deprecated in sing-box 1.12.0"
Legacy fake-ip configuration is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
### Structure
```json

View File

@@ -1,3 +1,11 @@
---
icon: material/delete-clock
---
!!! failure "已在 sing-box 1.12.0 废弃"
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
### 结构
```json

View File

@@ -49,8 +49,6 @@ Default domain strategy for resolving the domain names.
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
Take no effect if `server.strategy` is set.
#### disable_cache
Disable dns cache.

View File

@@ -48,8 +48,6 @@ icon: material/new-box
可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`
如果设置了 `server.strategy`,则不生效。
#### disable_cache
禁用 DNS 缓存。

View File

@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [strategy](#strategy)
!!! question "Since sing-box 1.11.0"
### route
@@ -10,6 +14,7 @@ icon: material/new-box
{
"action": "route", // default
"server": "",
"strategy": "",
"disable_cache": false,
"rewrite_ttl": 0,
"client_subnet": null
@@ -24,6 +29,12 @@ icon: material/new-box
Tag of target server.
#### strategy
Set domain strategy for this query.
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
#### disable_cache
Disable cache and save cache in this query.

View File

@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [strategy](#strategy)
!!! question "自 sing-box 1.11.0 起"
### route
@@ -11,7 +15,7 @@ icon: material/new-box
"action": "route", // 默认
"server": "",
// 兼容性
"strategy": "",
"disable_cache": false,
"rewrite_ttl": 0,
"client_subnet": null
@@ -26,6 +30,12 @@ icon: material/new-box
目标 DNS 服务器的标签。
#### strategy
为此查询设置域名策略。
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`
#### disable_cache
在此查询中禁用缓存。

View File

@@ -0,0 +1,38 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# DHCP
### Structure
```json
{
"dns": {
"servers": [
{
"type": "dhcp",
"tag": "",
"interface": "",
// Dial Fields
}
]
}
}
```
### Fields
#### interface
Interface name to listen on.
Tge default interface will be used by default.
### Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -0,0 +1,33 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# Fake IP
### Structure
```json
{
"dns": {
"servers": {
"type": "fakeip",
"tag": "",
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
}
}
}
```
### Fields
#### inet4_range
IPv4 address range for FakeIP.
#### inet6_address
IPv6 address range for FakeIP.

View File

@@ -0,0 +1,69 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# DNS over HTTP3 (DoH3)
### Structure
```json
{
"dns": {
"servers": {
"type": "h3",
"tag": "",
"server": "",
"server_port": 443,
"path": "",
"headers": {},
"tls": {},
// Dial Fields
}
}
}
```
!!! info "Difference from legacy H3 server"
* The old 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.
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
### Fields
#### server
==Required==
The address of the DNS server.
If domain name is used, `domain_resolver` must also be set to resolve IP address.
#### server_port
The port of the DNS server.
`853` will be used by default.
#### path
The path of the DNS server.
`/dns-query` will be used by default.
#### headers
Additional headers to be sent to the DNS server.
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -0,0 +1,69 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# DNS over HTTPS (DoH)
### Structure
```json
{
"dns": {
"servers": {
"type": "https",
"tag": "",
"server": "",
"server_port": 443,
"path": "",
"headers": {},
"tls": {},
// Dial Fields
}
}
}
```
!!! info "Difference from legacy HTTPS server"
* The old 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.
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
### Fields
#### server
==Required==
The address of the DNS server.
If domain name is used, `domain_resolver` must also be set to resolve IP address.
#### server_port
The port of the DNS server.
`853` will be used by default.
#### path
The path of the DNS server.
`/dns-query` will be used by default.
#### headers
Additional headers to be sent to the DNS server.
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -0,0 +1,46 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [type](#type)
# DNS Server
### Structure
```json
{
"dns": {
"servers": [
{
"type": "",
"tag": ""
}
]
}
}
```
#### type
The type of the DNS server.
| Type | Format |
|-----------------|-----------------------------------------------------|
| empty (default) | [Legacy](/configuration/dns/server/legacy/) |
| `tcp` | [TCP](/configuration/dns/server/tcp/) |
| `udp` | [UDP](/configuration/dns/server/udp/) |
| `tls` | [TLS](/configuration/dns/server/tls/) |
| `https` | [HTTPS](/configuration/dns/server/https/) |
| `quic` | [QUIC](/configuration/dns/server/quic/) |
| `h3` | [HTTP/3](/configuration/dns/server/http3/) |
| `predefined` | [Predefined](/configuration/dns/server/predefined/) |
| `dhcp` | [DHCP](/configuration/dns/server/dhcp/) |
| `fakeip` | [Fake IP](/configuration/dns/server/fakeip/) |
#### tag
The tag of the DNS server.

View File

@@ -1,3 +1,11 @@
---
icon: material/delete-clock
---
!!! failure "Deprecated in sing-box 1.12.0"
Legacy DNS servers is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [client_subnet](#client_subnet)

View File

@@ -1,3 +1,11 @@
---
icon: material/delete-clock
---
!!! failure "Deprecated in sing-box 1.12.0"
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [client_subnet](#client_subnet)

Some files were not shown because too many files have changed in this diff Show More