Compare commits

..

131 Commits

Author SHA1 Message Date
世界
c3346a14ce documentation: Bump version 2025-02-22 20:51:56 +08:00
世界
f3dbf579b6 documentation: Fix ECH migration 2025-02-22 20:51:56 +08:00
世界
ce0eba8d47 Fix TLS client crash 2025-02-22 20:47:49 +08:00
世界
60167bc049 documentation: Bump version 2025-02-22 17:46:28 +08:00
世界
fc39ddb173 documentation: Fix AnyTLS doc 2025-02-22 17:46:28 +08:00
anytls
0bbe99852a Add AnyTLS protocol 2025-02-22 17:46:28 +08:00
世界
417726e3e0 Migrate to stdlib ECH support 2025-02-22 17:46:28 +08:00
世界
277dfc7c44 documentation: Bump version 2025-02-21 08:30:40 +08:00
世界
7a090e1219 Fix predefined DNS server 2025-02-21 08:30:40 +08:00
世界
8fc7725e3d Fix missing links for new deprecated warnings 2025-02-20 22:28:54 +08:00
世界
af813c0d1a Omit reject.method=default 2025-02-20 20:05:10 +08:00
Gavin Luo
918bef80d3 release: Fix systemd permissions 2025-02-20 20:05:10 +08:00
ReleTor
10cd6c9ae0 documentation: Minor fixes 2025-02-20 20:05:10 +08:00
世界
b00370a6fc Add fallback local DNS server for iOS 2025-02-20 20:05:10 +08:00
世界
cbc3844650 Get darwin local DNS server from libresolv 2025-02-20 20:05:10 +08:00
世界
32cc9d9c90 Improve resolve action 2025-02-20 20:05:10 +08:00
世界
f752cd2784 platform: Add update WIFI state func 2025-02-20 15:16:29 +08:00
世界
63a0b5e2ce documentation: Bump version 2025-02-18 21:57:28 +08:00
世界
7c6a81ed13 Fix http.FileServer short write 2025-02-18 21:57:28 +08:00
世界
28f4fff423 Fix toolchain version 2025-02-18 21:57:28 +08:00
世界
8f81011f5b Fix save domain in fake-ip 2025-02-18 21:57:23 +08:00
世界
12ac163fbf Fix parsing A records 2025-02-18 21:57:23 +08:00
世界
de6ee34f12 Fix http3 DNS server 2025-02-18 18:19:26 +08:00
世界
3b12d00320 documentation: Make it clear that auth key is not required for Tailscale 2025-02-18 18:19:25 +08:00
世界
44ee71c540 Fix linter 2025-02-18 18:19:23 +08:00
世界
4cc3472bed Add back port hopping to hysteria 1 2025-02-18 18:19:21 +08:00
世界
1926bacbb6 documentation: Bump version 2025-02-18 18:19:19 +08:00
世界
f2e2d9cecd Remove unused debug messages 2025-02-18 18:19:17 +08:00
世界
fe96959649 release: Fix update android version 2025-02-18 18:19:15 +08:00
世界
187e78968c Update dependencies 2025-02-18 18:19:13 +08:00
世界
ed5de40065 Fix crash on route address set update 2025-02-18 18:19:08 +08:00
世界
b223e8d721 Fix tailscale dialer 2025-02-18 18:19:06 +08:00
世界
d884e07147 documentation: Bump version 2025-02-18 18:19:05 +08:00
世界
44177b9e58 Fix Android certificate initialize 2025-02-18 18:19:03 +08:00
世界
c57bbcb68a documentation: Add Tailscale DNS 2025-02-18 18:19:02 +08:00
世界
d573465ae6 documentation: Fix missing advertise_exit_node 2025-02-18 18:19:00 +08:00
世界
2dd3c00719 documentation: Bump version 2025-02-18 18:18:59 +08:00
xchacha20-poly1305
039f306d50 Remove single quotes of raw Moziila certs 2025-02-18 18:18:56 +08:00
世界
a3a0362552 Add Tailscale endpoint 2025-02-18 18:18:55 +08:00
世界
edd9bed002 Bump Go to go1.24 2025-02-18 18:18:53 +08:00
世界
782d51929e Build legacy binaries with latest Go 2025-02-18 18:18:50 +08:00
世界
86b162e0f0 Fix parsing legacy DNS servers 2025-02-18 18:18:49 +08:00
世界
801f9fb4ef documentation: Bump version 2025-02-18 18:18:47 +08:00
ReleTor
9c5af6dfe4 documentation: Fixes 2025-02-18 18:18:44 +08:00
世界
3053c9a852 Fix crash in exchangeParallel 2025-02-18 18:18:42 +08:00
世界
4d70a07017 Fix match DNS rule for fqdn 2025-02-18 18:18:41 +08:00
世界
3002aba29e Fix WireGuard panic 2025-02-18 18:18:39 +08:00
世界
22acbd36c5 Fix domain resolver for DNS server 2025-02-18 18:18:36 +08:00
世界
1ddd084f41 documentation: Fix fakeip example 2025-02-18 18:18:35 +08:00
世界
90068238ce documentation: Remove outdated icons 2025-02-18 18:18:34 +08:00
世界
41a697374c documentation: Certificate store 2025-02-18 18:18:27 +08:00
世界
4239f003a6 documentation: TLS fragment 2025-02-18 18:18:26 +08:00
世界
b74568d601 documentation: Outbound domain resolver 2025-02-18 18:18:23 +08:00
世界
f539766361 documentation: Refactor DNS 2025-02-18 18:18:21 +08:00
世界
8f75ab8bb9 Add certificate store 2025-02-18 18:17:59 +08:00
世界
5ac6dd25ef Add TLS fragment support 2025-02-18 18:17:59 +08:00
世界
b0355c27fd refactor: Outbound domain resolver 2025-02-18 18:17:58 +08:00
世界
f83f1122af refactor: DNS 2025-02-18 18:17:58 +08:00
世界
093013687c Fix sniff QUIC hidden in three or more packets 2025-02-18 18:14:59 +08:00
世界
ff31c469a0 Override version 2025-02-11 15:55:15 +08:00
世界
fbe390268c Bump version 2025-02-11 01:32:14 +08:00
世界
07ac01dcb7 platform: Update NDK to r28 2025-02-11 01:32:14 +08:00
ReleTor
badfdb62cd documentation: Fixes 2025-02-11 01:32:14 +08:00
printfer
986a410b30 documentation: Fix migration links 2025-02-11 01:32:14 +08:00
世界
9db2d58545 Fix override address 2025-02-11 01:32:14 +08:00
世界
4eed46ac59 Fix respond ICMP echo 2025-02-10 15:12:10 +08:00
世界
abc38d1dab Fix udpnat2 crash 2025-02-10 15:11:26 +08:00
世界
8d6c4f1289 release: Skip testflight when another build in review 2025-02-06 12:02:47 +08:00
世界
a2d40eb8b8 Fix override UDP destination 2025-02-06 11:20:35 +08:00
世界
17b502bb4b Update dependencies 2025-02-06 09:08:52 +08:00
世界
a0d4421085 Update quic-go to v0.49.0 2025-02-06 08:50:21 +08:00
世界
0d443072d1 Fix panic in auto-redirect initialize 2025-02-06 08:49:25 +08:00
世界
c9fb99b799 Fix missing ENOTCONN in IsClosed check 2025-02-06 08:48:49 +08:00
世界
92d245ad04 Bump version 2025-02-05 09:59:52 +08:00
世界
0908627297 Fix crash on remote rule-set stop 2025-02-05 08:58:10 +08:00
世界
7f79458b4f Minor updates 2025-02-01 19:49:33 +08:00
世界
9b4c11ba95 Fix rule-set not closed 2025-02-01 19:49:33 +08:00
世界
27c31eac5d Fix local rule-set not updated 2025-02-01 19:42:21 +08:00
世界
bab8dc0b82 Fix missing handshake for early conn 2025-01-31 12:57:35 +08:00
世界
d09d2fb665 Bump version 2025-01-30 15:09:54 +08:00
世界
e64cf3b7df Do not set address sets to routes on Apple platforms
Network Extension was observed to stop for unknown reasons
2025-01-27 13:40:39 +08:00
世界
9b73222314 documentation: Bump version 2025-01-27 10:53:14 +08:00
世界
3923b57abf release: Update NDK to r28-beta3 2025-01-27 10:53:14 +08:00
世界
4807e64609 documentation: Fix typo 2025-01-27 10:04:07 +08:00
世界
eeb37d89f1 Fix rule-set upgrade command 2025-01-27 09:50:27 +08:00
世界
08c1ec4b7e Fix legacy routes 2025-01-27 09:50:27 +08:00
HystericalDragon
6b4cf67add Fix endpoints not close 2025-01-27 09:50:27 +08:00
世界
e65926fd08 Fix tests 2025-01-13 15:14:30 +08:00
世界
f2ec319fe1 Fix system time 2025-01-13 15:14:30 +08:00
世界
32377a61b7 Add port hopping for hysteria2 2025-01-13 15:14:30 +08:00
世界
7aac801ccd tun: Set address sets to routes 2025-01-13 15:14:30 +08:00
世界
96fdf59ee4 Fix default dialer on legacy xiaomi systems 2025-01-13 15:14:30 +08:00
世界
50b8f3ab94 Add rule-set merge command 2025-01-13 15:14:30 +08:00
世界
ff7aaf977b Fix DNS match 2025-01-13 15:14:30 +08:00
世界
9a1efbe54d Fix domain strategy 2025-01-13 15:14:30 +08:00
世界
906c21f458 Fix time service 2025-01-13 15:14:30 +08:00
世界
d5e7af7a7e Fix socks5 UDP implementation 2025-01-13 15:14:30 +08:00
世界
4d41f03bd5 clash-api: Fix missing endpoints 2025-01-13 15:14:30 +08:00
世界
30704a15a7 hysteria2: Add more masquerade options 2025-01-13 15:14:30 +08:00
世界
83889178ed Improve timeouts 2025-01-13 15:14:30 +08:00
世界
1d2720bf5e Add UDP timeout route option 2025-01-13 15:14:30 +08:00
世界
c4b6d0eadb Make GSO adaptive 2025-01-13 15:14:30 +08:00
世界
0c66888691 Fix lint 2025-01-13 15:14:30 +08:00
世界
68781387fe refactor: WireGuard endpoint 2025-01-13 15:14:30 +08:00
世界
fd299a0961 refactor: connection manager 2025-01-13 15:14:30 +08:00
世界
285a82050c documentation: Fix typo 2025-01-13 15:14:30 +08:00
世界
2dbb8c55c9 Add override destination to route options 2025-01-13 15:14:30 +08:00
世界
effcf39469 Add dns.cache_capacity 2025-01-13 15:14:30 +08:00
世界
9db9484863 Refactor multi networks strategy 2025-01-13 15:14:30 +08:00
世界
ca813f461b documentation: Remove unused titles 2025-01-13 15:14:30 +08:00
世界
bb46cdb2b3 Add multi network dialing 2025-01-13 15:14:30 +08:00
世界
dcb10c21a1 documentation: Merge route options to route actions 2025-01-13 15:14:29 +08:00
世界
05ea0ca00e Add network_[type/is_expensive/is_constrained] rule items 2025-01-13 15:14:29 +08:00
世界
c098f282b1 Merge route options to route actions 2025-01-13 15:14:29 +08:00
世界
ecf82d197c refactor: Platform Interfaces 2025-01-13 15:14:29 +08:00
世界
9afe75586a refactor: Extract services form router 2025-01-13 15:14:29 +08:00
世界
a1be455202 refactor: Modular network manager 2025-01-13 15:14:29 +08:00
世界
19fb214226 refactor: Modular inbound/outbound manager 2025-01-13 15:14:29 +08:00
世界
28ec898a8c documentation: Add rule action 2025-01-13 15:14:29 +08:00
世界
467b1bbeeb documentation: Update the scheduled removal time of deprecated features 2025-01-13 15:14:29 +08:00
世界
02ab8ce806 documentation: Remove outdated icons 2025-01-13 15:14:29 +08:00
世界
ce69e620e9 Migrate bad options to library 2025-01-13 15:14:29 +08:00
世界
1133cf3ef5 Implement udp connect 2025-01-13 15:14:29 +08:00
世界
59a607e303 Implement new deprecated warnings 2025-01-13 15:14:29 +08:00
世界
313be3d7a4 Improve rule actions 2025-01-13 15:14:29 +08:00
世界
4fe40fcee0 Remove unused reject methods 2025-01-13 15:14:29 +08:00
世界
e233fd4fe5 refactor: Modular inbounds/outbounds 2025-01-13 15:14:29 +08:00
世界
9f7683818f Implement dns-hijack 2025-01-13 15:14:29 +08:00
世界
179e3cb2f5 Implement resolve(server) 2025-01-13 15:14:29 +08:00
世界
41b960552d Implement TCP and ICMP rejects 2025-01-13 15:14:29 +08:00
世界
8304295c48 Crazy sekai overturns the small pond 2025-01-13 15:14:29 +08:00
229 changed files with 11501 additions and 2277 deletions

22
.github/setup_legacy_go.sh vendored Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
VERSION="1.23.6"
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go $HOME/go/go_legacy
cd $HOME/go/go_legacy
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.23.x
# that means after golang1.24 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
# revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1

View File

@@ -46,7 +46,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -112,7 +112,6 @@ jobs:
- name: darwin_amd64
goos: darwin
goarch: amd64
require_legacy_go: true
- name: android_arm64
goos: android
goarch: arm64
@@ -134,32 +133,29 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Cache legacy Go
if: matrix.require_legacy_go
id: cache-legacy-go
uses: actions/cache@v4
with:
path: |
~/go/go1.20.14
key: go120
~/go/go_legacy
key: go_legacy_1236
- name: Setup legacy Go
if: matrix.require_legacy_go == 'true' && 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
mv go $HOME/go/go1.20.14
if: matrix.require_legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
run: bash .github/setup_legacy_go.sh
- name: Setup Android NDK
if: matrix.goos == 'android'
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
ndk-version: r28
local-cache: true
- name: Setup Goreleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
version: 2.5.1
install-only: true
- name: Extract signing key
run: |-
@@ -219,12 +215,12 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
ndk-version: r28
- name: Setup OpenJDK
run: |-
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
@@ -256,9 +252,16 @@ jobs:
with:
path: ~/.gradle
key: gradle-${{ hashFiles('**/*.gradle') }}
- name: Build
- name: Update version
if: github.event_name == 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/update_android_version --ci
- name: Update nightly version
if: github.event_name != 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/update_android_version --ci --nightly
- name: Build
run: |-
mkdir clients/android/app/libs
cp libbox.aar clients/android/app/libs
cd clients/android
@@ -294,12 +297,12 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
ndk-version: r28
- name: Setup OpenJDK
run: |-
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
@@ -392,7 +395,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
@@ -548,7 +551,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

@@ -28,7 +28,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:

View File

@@ -16,7 +16,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
go-version: ^1.24
- name: Extract signing key
run: |-
mkdir -p $HOME/.gnupg

View File

@@ -21,18 +21,16 @@ linters-settings:
- -SA1003
run:
go: "1.23"
go: "1.24"
build-tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
- badlinkname
issues:
exclude-dirs:

View File

@@ -9,17 +9,16 @@ builds:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
- -checklinkname=0
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
targets:

View File

@@ -11,19 +11,19 @@ builds:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
- -checklinkname=0
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOTOOLCHAIN=local
targets:
- linux_386
- linux_amd64_v1
@@ -50,18 +50,19 @@ builds:
- with_reality_server
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOROOT={{ .Env.GOPATH }}/go1.20.14
gobinary: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
- GOROOT={{ .Env.GOPATH }}/go_legacy
tool: "{{ .Env.GOPATH }}/go_legacy/bin/go"
targets:
- windows_amd64_v1
- windows_386
- darwin_amd64_v1
- id: android
<<: *template
env:
- CGO_ENABLED=1
- GOTOOLCHAIN=local
overrides:
- goos: android
goarch: arm
@@ -123,8 +124,8 @@ nfpms:
- deb
- rpm
- archlinux
# - apk
# - ipk
# - apk
# - ipk
priority: extra
contents:
- src: release/config/config.json

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box
@@ -13,9 +13,9 @@ RUN set -ex \
&& export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api" \
-o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
./cmd/sing-box
FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"

View File

@@ -1,16 +1,15 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
TAGS_GO121 = with_ech
TAGS_GO123 = with_tailscale,badlinkname
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121),$(TAGS_GO123)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
TAGS_GO123 = with_tailscale
TAGS ?= $(TAGS_GO120),$(TAGS_GO123)
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls,with_reality_server
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid= -checklinkname=0"
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH)
@@ -18,14 +17,17 @@ PREFIX ?= $(shell go env GOPATH)
.PHONY: test release docs build
build:
export GOTOOLCHAIN=local && \
go build $(MAIN_PARAMS) $(MAIN)
ci_build_go120:
go build $(PARAMS) $(MAIN)
export GOTOOLCHAIN=local && \
go build $(PARAMS) $(MAIN) && \
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
ci_build:
go build $(PARAMS) $(MAIN)
export GOTOOLCHAIN=local && \
go build $(PARAMS) $(MAIN) && \
go build $(MAIN_PARAMS) $(MAIN)
generate_completions:
@@ -62,6 +64,9 @@ 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_certificates:
go run ./cmd/internal/update_certificates
release:
go run ./cmd/internal/build goreleaser release --clean --skip publish
mkdir dist/release

21
adapter/certificate.go Normal file
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()
}

View File

@@ -6,7 +6,6 @@ import (
"encoding/binary"
"time"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing/common/varbin"
)
@@ -15,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 {
@@ -38,17 +50,17 @@ type CacheFile interface {
StoreSelected(group string, selected string) error
LoadGroupExpand(group string) (isExpand bool, loaded bool)
StoreGroupExpand(group string, expand bool) error
LoadRuleSet(tag string) *SavedRuleSet
SaveRuleSet(tag string, set *SavedRuleSet) error
LoadRuleSet(tag string) *SavedBinary
SaveRuleSet(tag string, set *SavedBinary) error
}
type SavedRuleSet struct {
type SavedBinary struct {
Content []byte
LastUpdated time.Time
LastEtag string
}
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
var buffer bytes.Buffer
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
if err != nil {
@@ -69,7 +81,7 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
return buffer.Bytes(), nil
}
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
func (s *SavedBinary) UnmarshalBinary(data []byte) error {
reader := bytes.NewReader(data)
var version uint8
err := binary.Read(reader, binary.BigEndian, &version)

View File

@@ -71,6 +71,8 @@ type InboundContext struct {
UDPDisableDomainUnmapping bool
UDPConnect bool
UDPTimeout time.Duration
TLSFragment bool
TLSFragmentFallbackDelay time.Duration
NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType

View File

@@ -25,15 +25,18 @@ type NetworkManager interface {
PackageManager() tun.PackageManager
WIFIState() WIFIState
ResetNetwork()
UpdateWIFIState()
}
type NetworkOptions struct {
NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
BindInterface string
RoutingMark uint32
BindInterface string
RoutingMark uint32
DomainResolver string
DomainResolveOptions DNSQueryOptions
NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
}
type InterfaceUpdateListener interface {

View File

@@ -2,6 +2,7 @@ package adapter
import (
"context"
"crypto/tls"
"net"
"net/http"
"sync"
@@ -9,6 +10,7 @@ import (
C "github.com/sagernet/sing-box/constant"
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"
"go4.org/netipx"
@@ -66,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),
}
}
@@ -89,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

22
box.go
View File

@@ -12,6 +12,7 @@ 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"
@@ -141,6 +142,20 @@ 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)
@@ -187,7 +202,7 @@ func New(options Options) (*Box, error) {
transportOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize inbound[", i, "]")
return nil, E.Cause(err, "initialize DNS server[", i, "]")
}
}
err = dnsRouter.Initialize(dnsOptions.Rules)
@@ -210,7 +225,7 @@ func New(options Options) (*Box, error) {
endpointOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize inbound[", i, "]")
return nil, E.Cause(err, "initialize endpoint[", i, "]")
}
}
for i, inboundOptions := range options.Inbounds {
@@ -280,7 +295,6 @@ func New(options Options) (*Box, error) {
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)
@@ -446,7 +460,7 @@ func (s *Box) Close() error {
close(s.done)
}
err := common.Close(
s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, 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

@@ -5,6 +5,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/sagernet/asc-go/asc"
@@ -194,6 +195,10 @@ func publishTestflight(ctx context.Context) error {
log.Info(string(platform), " ", tag, " create submission")
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
if err != nil {
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
log.Error(err)
break
}
return err
}
}

View File

@@ -55,10 +55,10 @@ func init() {
if err != nil {
currentTag = "unknown"
}
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid= -checklinkname=0")
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api", "with_tailscale")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_tailscale")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
debugTags = append(debugTags, "debug")
}

View File

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

View File

@@ -13,10 +13,14 @@ import (
"github.com/sagernet/sing/common"
)
var flagRunInCI bool
var (
flagRunInCI bool
flagRunNightly bool
)
func init() {
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
}
func main() {
@@ -46,21 +50,23 @@ func main() {
switch propPair[0] {
case "VERSION_NAME":
if propPair[1] != newVersion {
log.Info("updated version from ", propPair[1], " to ", newVersion)
versionUpdated = true
propPair[1] = newVersion
log.Info("updated version to ", newVersion)
}
case "GO_VERSION":
if propPair[1] != runtime.Version() {
log.Info("updated Go version from ", propPair[1], " to ", runtime.Version())
goVersionUpdated = true
propPair[1] = runtime.Version()
log.Info("updated Go version to ", runtime.Version())
}
}
}
if !(versionUpdated || goVersionUpdated) {
log.Info("version not changed")
return
} else if flagRunInCI && !flagRunNightly {
log.Fatal("version changed, commit changes first.")
}
for _, propPair := range propsList {
switch propPair[0] {

View File

@@ -0,0 +1,71 @@
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(`")
cert := record[certIndex]
// Remove single quotes
cert = cert[1 : len(cert)-1]
generated.WriteString(cert)
generated.WriteString("`))\n")
}
generated.WriteString("}\n")
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
}

View File

@@ -9,8 +9,6 @@ import (
"github.com/spf13/cobra"
)
var pqSignatureSchemesEnabled bool
var commandGenerateECHKeyPair = &cobra.Command{
Use: "ech-keypair <plain_server_name>",
Short: "Generate TLS ECH key pair",
@@ -24,12 +22,11 @@ var commandGenerateECHKeyPair = &cobra.Command{
}
func init() {
commandGenerateECHKeyPair.Flags().BoolVar(&pqSignatureSchemesEnabled, "pq-signature-schemes-enabled", false, "Enable PQ signature schemes")
commandGenerate.AddCommand(commandGenerateECHKeyPair)
}
func generateECHKeyPair(serverName string) error {
configPem, keyPem, err := tls.ECHKeygenDefault(serverName, pqSignatureSchemesEnabled)
configPem, keyPem, err := tls.ECHKeygenDefault(serverName)
if err != nil {
return err
}

View File

@@ -30,7 +30,7 @@ func init() {
}
func generateTLSKeyPair(serverName string) error {
privateKeyPem, publicKeyPem, err := tls.GenerateKeyPair(time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
if err != nil {
return err
}

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")
}

View File

@@ -1,31 +0,0 @@
//go:build go1.21 && !without_badtls && with_ech
package badtls
import (
"net"
_ "unsafe"
"github.com/sagernet/cloudflare-tls"
"github.com/sagernet/sing/common"
)
func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := common.Cast[*tls.Conn](conn)
if !loaded {
return
}
return true, func() error {
return echReadRecord(tlsConn)
}, func() error {
return echHandlePostHandshakeMessage(tlsConn)
}
})
}
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
func echReadRecord(c *tls.Conn) error
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
func echHandlePostHandshakeMessage(c *tls.Conn) error

File diff suppressed because it is too large Load Diff

182
common/certificate/store.go Normal file
View File

@@ -0,0 +1,182 @@
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, "":
systemPool = x509.NewCertPool()
var systemValid bool
for _, cert := range service.FromContext[platform.Interface](ctx).SystemCertificates() {
if systemPool.AppendCertsFromPEM([]byte(cert)) {
systemValid = true
}
}
if !systemValid {
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

@@ -35,7 +35,6 @@ type DefaultDialer struct {
udpListener net.ListenConfig
udpAddr4 string
udpAddr6 string
isWireGuardListener bool
networkManager adapter.NetworkManager
networkStrategy *C.NetworkStrategy
defaultNetworkStrategy bool
@@ -183,11 +182,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
}
setMultiPathTCP(&dialer4)
}
if options.IsWireGuardListener {
for _, controlFn := range WgControlFns {
listener.Control = control.Append(listener.Control, controlFn)
}
}
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
if err != nil {
return nil, err
@@ -204,7 +198,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
udpListener: listener,
udpAddr4: udpAddr4,
udpAddr6: udpAddr6,
isWireGuardListener: options.IsWireGuardListener,
networkManager: networkManager,
networkStrategy: networkStrategy,
defaultNetworkStrategy: defaultNetworkStrategy,

View File

@@ -29,16 +29,18 @@ func (d *DetourDialer) Start() error {
}
func (d *DetourDialer) Dialer() (N.Dialer, error) {
d.initOnce.Do(func() {
var loaded bool
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
if !loaded {
d.initErr = E.New("outbound detour not found: ", d.detour)
}
})
d.initOnce.Do(d.init)
return d.dialer, d.initErr
}
func (d *DetourDialer) init() {
var loaded bool
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
if !loaded {
d.initErr = E.New("outbound detour not found: ", d.detour)
}
}
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
dialer, err := d.Dialer()
if err != nil {

View File

@@ -16,81 +16,108 @@ import (
"github.com/sagernet/sing/service"
)
type Options struct {
Context context.Context
Options option.DialerOptions
RemoteIsDomain bool
DirectResolver bool
ResolverOnDetour bool
NewDialer bool
}
// TODO: merge with NewWithOptions
func New(ctx context.Context, options option.DialerOptions, remoteIsDomain bool) (N.Dialer, error) {
if options.IsWireGuardListener {
return NewDefault(ctx, options)
}
return NewWithOptions(Options{
Context: ctx,
Options: options,
RemoteIsDomain: remoteIsDomain,
})
}
func NewWithOptions(options Options) (N.Dialer, error) {
dialOptions := options.Options
var (
dialer N.Dialer
err error
)
if options.Detour == "" {
dialer, err = NewDefault(ctx, options)
if err != nil {
return nil, err
}
} else {
outboundManager := service.FromContext[adapter.OutboundManager](ctx)
if dialOptions.Detour != "" {
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
if outboundManager == nil {
return nil, E.New("missing outbound manager")
}
dialer = NewDetour(outboundManager, options.Detour)
}
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,
resolveTransport,
C.DomainStrategy(options.DomainStrategy),
time.Duration(options.FallbackDelay))
dialer = NewDetour(outboundManager, dialOptions.Detour)
} else {
dialer, err = NewDefault(options.Context, dialOptions)
if err != nil {
return nil, err
}
}
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour) {
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
var defaultOptions adapter.NetworkOptions
if networkManager != nil {
defaultOptions = networkManager.DefaultOptions()
}
var (
server string
dnsQueryOptions adapter.DNSQueryOptions
resolveFallbackDelay time.Duration
)
if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" {
var transport adapter.DNSTransport
if !options.DirectResolver {
var loaded bool
transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server)
if !loaded {
return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server)
}
}
var strategy C.DomainStrategy
if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) {
strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy)
} else if
//nolint:staticcheck
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
//nolint:staticcheck
strategy = C.DomainStrategy(dialOptions.DomainStrategy)
}
server = dialOptions.DomainResolver.Server
dnsQueryOptions = adapter.DNSQueryOptions{
Transport: transport,
Strategy: strategy,
DisableCache: dialOptions.DomainResolver.DisableCache,
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
}
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.DirectResolver {
return nil, E.New("missing domain resolver for domain server address")
} else if defaultOptions.DomainResolver != "" {
dnsQueryOptions = defaultOptions.DomainResolveOptions
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
if !loaded {
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
}
dnsQueryOptions.Transport = transport
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address")
} else {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
}
dialer = NewResolveDialer(
options.Context,
dialer,
dialOptions.Detour == "" && !dialOptions.TCPFastOpen,
server,
dnsQueryOptions,
resolveFallbackDelay,
)
}
return dialer, nil
}
func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInterfaceDialer, error) {
if options.Detour != "" {
return nil, E.New("`detour` is not supported in direct context")
}
if options.IsWireGuardListener {
return NewDefault(ctx, options)
}
dialer, err := NewDefault(ctx, options)
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.DNSRouter](ctx),
dialer,
true,
resolveTransport,
C.DomainStrategy(options.DomainStrategy),
time.Duration(options.FallbackDelay),
), nil
}
type ParallelInterfaceDialer interface {
N.Dialer
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)

View File

@@ -3,14 +3,17 @@ package dialer
import (
"context"
"net"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/bufio"
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"
)
var (
@@ -18,23 +21,37 @@ var (
_ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil)
)
type ResolveDialer interface {
N.Dialer
QueryOptions() adapter.DNSQueryOptions
}
type ParallelInterfaceResolveDialer interface {
ParallelInterfaceDialer
QueryOptions() adapter.DNSQueryOptions
}
type resolveDialer struct {
transport adapter.DNSTransportManager
router adapter.DNSRouter
dialer N.Dialer
parallel bool
router adapter.DNSRouter
transport adapter.DNSTransport
strategy C.DomainStrategy
server string
initOnce sync.Once
initErr error
queryOptions adapter.DNSQueryOptions
fallbackDelay time.Duration
}
func NewResolveDialer(router adapter.DNSRouter, dialer N.Dialer, parallel bool, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) N.Dialer {
func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer {
return &resolveDialer{
dialer,
parallel,
router,
transport,
strategy,
fallbackDelay,
transport: service.FromContext[adapter.DNSTransportManager](ctx),
router: service.FromContext[adapter.DNSRouter](ctx),
dialer: dialer,
parallel: parallel,
server: server,
queryOptions: queryOptions,
fallbackDelay: fallbackDelay,
}
}
@@ -43,42 +60,68 @@ type resolveParallelNetworkDialer struct {
dialer ParallelInterfaceDialer
}
func NewResolveParallelInterfaceDialer(router adapter.DNSRouter, dialer ParallelInterfaceDialer, parallel bool, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) ParallelInterfaceDialer {
func NewResolveParallelInterfaceDialer(ctx context.Context, dialer ParallelInterfaceDialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ParallelInterfaceResolveDialer {
return &resolveParallelNetworkDialer{
resolveDialer{
dialer,
parallel,
router,
transport,
strategy,
fallbackDelay,
transport: service.FromContext[adapter.DNSTransportManager](ctx),
router: service.FromContext[adapter.DNSRouter](ctx),
dialer: dialer,
parallel: parallel,
server: server,
queryOptions: queryOptions,
fallbackDelay: fallbackDelay,
},
dialer,
}
}
func (d *resolveDialer) initialize() error {
d.initOnce.Do(d.initServer)
return d.initErr
}
func (d *resolveDialer) initServer() {
if d.server == "" {
return
}
transport, loaded := d.transport.Transport(d.server)
if !loaded {
d.initErr = E.New("domain resolver not found: " + d.server)
return
}
d.queryOptions.Transport = transport
}
func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
err := d.initialize()
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
if err != nil {
return nil, err
}
if d.parallel {
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
} else {
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
}
}
func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
err := d.initialize()
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
if err != nil {
return nil, err
}
@@ -89,12 +132,24 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
}
func (d *resolveDialer) QueryOptions() adapter.DNSQueryOptions {
return d.queryOptions
}
func (d *resolveDialer) Upstream() any {
return d.dialer
}
func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
err := d.initialize()
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
return d.dialer.DialContext(ctx, network, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
if err != nil {
return nil, err
}
@@ -102,21 +157,28 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
fallbackDelay = d.fallbackDelay
}
if d.parallel {
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
} else {
return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
}
}
func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
err := d.initialize()
if err != nil {
return nil, err
}
if !destination.IsFqdn() {
return d.dialer.ListenPacket(ctx, destination)
}
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
addresses, err := d.router.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{Transport: d.transport, Strategy: d.strategy})
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
if err != nil {
return nil, err
}
if fallbackDelay == 0 {
fallbackDelay = d.fallbackDelay
}
conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
if err != nil {
return nil, err
@@ -124,6 +186,10 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
}
func (d *resolveDialer) Upstream() any {
func (d *resolveParallelNetworkDialer) QueryOptions() adapter.DNSQueryOptions {
return d.queryOptions
}
func (d *resolveParallelNetworkDialer) Upstream() any {
return d.dialer
}

View File

@@ -23,6 +23,7 @@ type Config struct {
}
type Info struct {
ProcessID uint32
ProcessPath string
PackageName string
User string

View File

@@ -2,14 +2,11 @@ package process
import (
"context"
"fmt"
"net/netip"
"os"
"syscall"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/winiphlpapi"
"golang.org/x/sys/windows"
)
@@ -26,209 +23,39 @@ func NewSearcher(_ Config) (Searcher, error) {
return &windowsSearcher{}, nil
}
var (
modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
procGetExtendedTcpTable = modiphlpapi.NewProc("GetExtendedTcpTable")
procGetExtendedUdpTable = modiphlpapi.NewProc("GetExtendedUdpTable")
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW")
)
func initWin32API() error {
err := modiphlpapi.Load()
if err != nil {
return E.Cause(err, "load iphlpapi.dll")
}
err = procGetExtendedTcpTable.Find()
if err != nil {
return E.Cause(err, "load iphlpapi::GetExtendedTcpTable")
}
err = procGetExtendedUdpTable.Find()
if err != nil {
return E.Cause(err, "load iphlpapi::GetExtendedUdpTable")
}
err = modkernel32.Load()
if err != nil {
return E.Cause(err, "load kernel32.dll")
}
err = procQueryFullProcessImageNameW.Find()
if err != nil {
return E.Cause(err, "load kernel32::QueryFullProcessImageNameW")
}
return nil
return winiphlpapi.LoadExtendedTable()
}
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
pid, err := winiphlpapi.FindPid(network, source)
if err != nil {
return nil, err
}
return &Info{ProcessPath: processName, UserId: -1}, nil
}
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
family := windows.AF_INET
if ip.Is6() {
family = windows.AF_INET6
}
const (
tcpTablePidConn = 4
udpTablePid = 1
)
var class int
var fn uintptr
switch network {
case N.NetworkTCP:
fn = procGetExtendedTcpTable.Addr()
class = tcpTablePidConn
case N.NetworkUDP:
fn = procGetExtendedUdpTable.Addr()
class = udpTablePid
default:
return "", os.ErrInvalid
}
buf, err := getTransportTable(fn, family, class)
path, err := getProcessPath(pid)
if err != nil {
return "", err
return &Info{ProcessID: pid, UserId: -1}, err
}
s := newSearcher(family == windows.AF_INET, network == N.NetworkTCP)
pid, err := s.Search(buf, ip, uint16(srcPort))
if err != nil {
return "", err
}
return getExecPathFromPID(pid)
return &Info{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
}
type searcher struct {
itemSize int
port int
ip int
ipSize int
pid int
tcpState int
}
func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) {
n := int(readNativeUint32(b[:4]))
itemSize := s.itemSize
for i := 0; i < n; i++ {
row := b[4+itemSize*i : 4+itemSize*(i+1)]
if s.tcpState >= 0 {
tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4])
// MIB_TCP_STATE_ESTAB, only check established connections for TCP
if tcpState != 5 {
continue
}
}
// according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian.
// this field can be illustrated as follows depends on different machine endianess:
// little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB)
// big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB)
// so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32
srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4])))
if srcPort != port {
continue
}
srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize])
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) {
continue
}
pid := readNativeUint32(row[s.pid : s.pid+4])
return pid, nil
}
return 0, ErrNotFound
}
func newSearcher(isV4, isTCP bool) *searcher {
var itemSize, port, ip, ipSize, pid int
tcpState := -1
switch {
case isV4 && isTCP:
// struct MIB_TCPROW_OWNER_PID
itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0
case isV4 && !isTCP:
// struct MIB_UDPROW_OWNER_PID
itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8
case !isV4 && isTCP:
// struct MIB_TCP6ROW_OWNER_PID
itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48
case !isV4 && !isTCP:
// struct MIB_UDP6ROW_OWNER_PID
itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24
}
return &searcher{
itemSize: itemSize,
port: port,
ip: ip,
ipSize: ipSize,
pid: pid,
tcpState: tcpState,
}
}
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
for size, buf := uint32(8), make([]byte, 8); ; {
ptr := unsafe.Pointer(&buf[0])
err, _, _ := syscall.SyscallN(fn, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
switch err {
case 0:
return buf, nil
case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER):
buf = make([]byte, size)
default:
return nil, fmt.Errorf("syscall error: %d", err)
}
}
}
func readNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}
func getExecPathFromPID(pid uint32) (string, error) {
// kernel process starts with a colon in order to distinguish with normal processes
func getProcessPath(pid uint32) (string, error) {
switch pid {
case 0:
// reserved pid for system idle process
return ":System Idle Process", nil
case 4:
// reserved pid for windows kernel image
return ":System", nil
}
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return "", err
}
defer windows.CloseHandle(h)
defer windows.CloseHandle(handle)
size := uint32(syscall.MAX_LONG_PATH)
buf := make([]uint16, syscall.MAX_LONG_PATH)
size := uint32(len(buf))
r1, _, err := syscall.SyscallN(
procQueryFullProcessImageNameW.Addr(),
uintptr(h),
uintptr(0),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
)
if r1 == 0 {
err = windows.QueryFullProcessImageName(handle, 0, &buf[0], &size)
if err != nil {
return "", err
}
return syscall.UTF16ToString(buf[:size]), nil
return windows.UTF16ToString(buf[:size]), nil
}

View File

@@ -12,6 +12,26 @@ import (
"github.com/stretchr/testify/require"
)
func TestSniffQUICChromeNew(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("ca0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489ad89c322f75f9a383c90d126a0b21104cb519c2bb32e6a134e86896452e942b26c519b8c7ac9e4c99fae5e1f65cf08fb98443b30e4567932e8fb0789820d8f33037b59ac8113530258c9467dfb52489396dae01f099d28b234efa107fa411f2a1ffa2abe74988e03d662d4296024e95ce0fe1671724937157f77b84990478a2d4060676cf0827b4e8c600654111750414dafa0cccb332f3020c2922a015f445df5edc9c7d2d1ceea9fddcc9ff821c9183aa39a70da20fcc057579e1051c1c899148d6cf9d08b4919822082d040d1ce03ca4f216be6cb7ef03db6df0993ef1ccce5c8c648980554f41704526e1809d2545739f5872e75ec797db1c99f5682e2eda9363cb32aa367b7b363c782ddbacf874183cc15c8a2db068dd4093eebdd096ad33832a7939deb0a872279744f5a56dc001ba62fac973bf680f3b362bdd336add4dd102f462b773bf70bfce1921070a802a92025273a177186d1a643081b42175eb789ccddadb71033ef4feacbf6fd282ab622cf61669d73cda559e411c6ccdd8f003443b6933b7729b7a357aa4aa2fba0f365f829a4d497afb5dc2648a53bc9f3e786d955069d0a4781088a5463747dfe9958ea19ea444eae947ec6a67640955f710f93640084f3fbb8ad259b68dbc0ee0b7fab2d81bffd83ed8a6d33522dbfef43bec0a0fb4bdf1cb712dc4ced0680c0687fa240fd157baa232b1c84e14adce6421cf9270f9b3972f98fc67b344b8a4f1fb551e26f7f76d484ed9f8197f231dc5d9a44cc0ddce73d7f810a620851f4e97eb5037ab5135d7c3be5b80cc32d19910b8387aca64c93c02dc3e35238b78e6aff470722078982e58802844932b6041446bfdcc97ba640cbb86721bcd0f40f27b77aa6287ce5674ec1720134b9302875482c3269787e004b9edb483d44f326eef38c0e83cb46af96488c2e696bc2524567fb29c1e8edcd5a73615496d172d46a9d29e0505c0018b7bbb00165eca0389e09c4b1d73b6cc4a2f735a720650134a2e98e8105e20695cf231b92586237dfe0f99c897414e51c21627496276535f07abb53fb2b554376fe520fa45a3e944fd91dfe7a72aead08842b6b63d8edf861fb911954c83bd9a896eb9da4af5eff646455069d747facd4e77c254096843bff7c3e9031dbdf8dc37ea45f1122922fcbc322ec1378f3c7c1af0da62e1052e6210f1b23073f93a82d90e14cb20bc4501d487a1c848674d57a7c269b13590b3a99d8b8b4f6d0dfbd1d2cbbe7a32c0d5c84ae7ec438b0b19f3862d8fabaa828d06c7e3c6967405cd56a1ae90f38633e2ee0e3ecfca3df399fe12f029e0860a1a30da010300d0c94f0bf56091d00011488c1429928b21c739ebf50ba8be91116315d3173f6d2c56735722478c4d74392ba84d1727036b3d64e8c2263b0f33cb8086be587ca6b3940259c06afa2683868856529303ae12e91d7ca874568be7f2bfaa0656dfab0ed31ed90eaea10fb7f3433ec59a334abe6211d547fa0c825ac45d3691e749d15432008de83e9f6d98f368359137ae803d9189b3386f800c7c0cf4b615d1983cf82d9981a8105b60a80fe66c9b0d439b5ba153dd19e9e7483a01cf3b02b4597540b38e658d4eb8455e030b2bf2690bdd78c23f16fe5")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f")
require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err)
require.Equal(t, "www.google.com", metadata.Domain)
}
func TestSniffQUICChromium(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620")

View File

@@ -16,7 +16,7 @@ import (
"github.com/caddyserver/certmagic"
"github.com/libdns/alidns"
"github.com/libdns/cloudflare"
"github.com/mholt/acmez/acme"
"github.com/mholt/acmez/v3/acme"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

View File

@@ -29,15 +29,12 @@ func NewClient(ctx context.Context, serverAddress string, options option.Outboun
if !options.Enabled {
return nil, nil
}
if options.ECH != nil && options.ECH.Enabled {
return NewECHClient(ctx, serverAddress, options)
} else if options.Reality != nil && options.Reality.Enabled {
if options.Reality != nil && options.Reality.Enabled {
return NewRealityClient(ctx, serverAddress, options)
} else if options.UTLS != nil && options.UTLS.Enabled {
return NewUTLSClient(ctx, serverAddress, options)
} else {
return NewSTDClient(ctx, serverAddress, options)
}
return NewSTDClient(ctx, serverAddress, options)
}
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {

174
common/tls/ech.go Normal file
View File

@@ -0,0 +1,174 @@
//go:build go1.24
package tls
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/pem"
"net"
"os"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
aTLS "github.com/sagernet/sing/common/tls"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
"golang.org/x/crypto/cryptobyte"
)
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
var echConfig []byte
if len(options.ECH.Config) > 0 {
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
} else if options.ECH.ConfigPath != "" {
content, err := os.ReadFile(options.ECH.ConfigPath)
if err != nil {
return nil, E.Cause(err, "read ECH config")
}
echConfig = content
}
//nolint:staticcheck
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
}
if len(echConfig) > 0 {
block, rest := pem.Decode(echConfig)
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
return nil, E.New("invalid ECH configs pem")
}
tlsConfig.EncryptedClientHelloConfigList = block.Bytes
return &STDClientConfig{tlsConfig}, nil
} else {
return &STDECHClientConfig{STDClientConfig{tlsConfig}, service.FromContext[adapter.DNSRouter](ctx)}, nil
}
}
func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, tlsConfig *tls.Config, echKeyPath *string) error {
var echKey []byte
if len(options.ECH.Key) > 0 {
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
} else if options.ECH.KeyPath != "" {
content, err := os.ReadFile(options.ECH.KeyPath)
if err != nil {
return E.Cause(err, "read ECH keys")
}
echKey = content
*echKeyPath = options.ECH.KeyPath
} else {
return E.New("missing ECH keys")
}
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
tlsConfig.EncryptedClientHelloKeys = echKeys
//nolint:staticcheck
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
}
return nil
}
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
echKey, err := os.ReadFile(echKeyPath)
if err != nil {
return E.Cause(err, "reload ECH keys from ", echKeyPath)
}
block, _ := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" {
return E.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
tlsConfig.EncryptedClientHelloKeys = echKeys
return nil
}
type STDECHClientConfig struct {
STDClientConfig
dnsRouter adapter.DNSRouter
}
func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
if len(s.config.EncryptedClientHelloConfigList) == 0 {
message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{
{
Name: mDNS.Fqdn(s.config.ServerName),
Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET,
},
},
}
response, err := s.dnsRouter.Exchange(ctx, message, adapter.DNSQueryOptions{})
if err != nil {
return nil, E.Cause(err, "fetch ECH config list")
}
if response.Rcode != mDNS.RcodeSuccess {
return nil, E.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list")
}
for _, rr := range response.Answer {
switch resource := rr.(type) {
case *mDNS.HTTPS:
for _, value := range resource.Value {
if value.Key().String() == "ech" {
echConfigList, err := base64.StdEncoding.DecodeString(value.String())
if err != nil {
return nil, E.Cause(err, "decode ECH config")
}
s.config.EncryptedClientHelloConfigList = echConfigList
}
}
}
}
return nil, E.New("no ECH config found in DNS records")
}
tlsConn, err := s.Client(conn)
if err != nil {
return nil, err
}
err = tlsConn.HandshakeContext(ctx)
if err != nil {
return nil, err
}
return tlsConn, nil
}
func (s *STDECHClientConfig) Clone() Config {
return &STDECHClientConfig{STDClientConfig{s.config.Clone()}, s.dnsRouter}
}
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {
var keys []tls.EncryptedClientHelloKey
rawString := cryptobyte.String(raw)
for !rawString.Empty() {
var key tls.EncryptedClientHelloKey
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
return nil, E.New("error parsing private key")
}
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
return nil, E.New("error parsing config")
}
keys = append(keys, key)
}
if len(keys) == 0 {
return nil, E.New("empty ECH keys")
}
return keys, nil
}

View File

@@ -1,243 +0,0 @@
//go:build with_ech
package tls
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"net"
"net/netip"
"os"
"strings"
cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
)
type echClientConfig struct {
config *cftls.Config
}
func (c *echClientConfig) ServerName() string {
return c.config.ServerName
}
func (c *echClientConfig) SetServerName(serverName string) {
c.config.ServerName = serverName
}
func (c *echClientConfig) NextProtos() []string {
return c.config.NextProtos
}
func (c *echClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (c *echClientConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for ECH")
}
func (c *echClientConfig) Client(conn net.Conn) (Conn, error) {
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
}
func (c *echClientConfig) Clone() Config {
return &echClientConfig{
config: c.config.Clone(),
}
}
type echConnWrapper struct {
*cftls.Conn
}
func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
state := c.Conn.ConnectionState()
//nolint:staticcheck
return tls.ConnectionState{
Version: state.Version,
HandshakeComplete: state.HandshakeComplete,
DidResume: state.DidResume,
CipherSuite: state.CipherSuite,
NegotiatedProtocol: state.NegotiatedProtocol,
NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual,
ServerName: state.ServerName,
PeerCertificates: state.PeerCertificates,
VerifiedChains: state.VerifiedChains,
SignedCertificateTimestamps: state.SignedCertificateTimestamps,
OCSPResponse: state.OCSPResponse,
TLSUnique: state.TLSUnique,
}
}
func (c *echConnWrapper) Upstream() any {
return c.Conn
}
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
var serverName string
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 cftls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
tlsConfig.ServerName = serverName
}
if options.Insecure {
tlsConfig.InsecureSkipVerify = options.Insecure
} else if options.DisableSNI {
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyConnection = func(state cftls.ConnectionState) error {
verifyOptions := x509.VerifyOptions{
DNSName: serverName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert)
}
_, err := state.PeerCertificates[0].Verify(verifyOptions)
return err
}
}
if len(options.ALPN) > 0 {
tlsConfig.NextProtos = options.ALPN
}
if options.MinVersion != "" {
minVersion, err := ParseTLSVersion(options.MinVersion)
if err != nil {
return nil, E.Cause(err, "parse min_version")
}
tlsConfig.MinVersion = minVersion
}
if options.MaxVersion != "" {
maxVersion, err := ParseTLSVersion(options.MaxVersion)
if err != nil {
return nil, E.Cause(err, "parse max_version")
}
tlsConfig.MaxVersion = maxVersion
}
if options.CipherSuites != nil {
find:
for _, cipherSuite := range options.CipherSuites {
for _, tlsCipherSuite := range cftls.CipherSuites() {
if cipherSuite == tlsCipherSuite.Name {
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
continue find
}
}
return nil, E.New("unknown cipher_suite: ", cipherSuite)
}
}
var certificate []byte
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
} else if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {
return nil, E.Cause(err, "read certificate")
}
certificate = content
}
if len(certificate) > 0 {
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(certificate) {
return nil, E.New("failed to parse certificate:\n\n", certificate)
}
tlsConfig.RootCAs = certPool
}
// ECH Config
tlsConfig.ECHEnabled = true
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
var echConfig []byte
if len(options.ECH.Config) > 0 {
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
} else if options.ECH.ConfigPath != "" {
content, err := os.ReadFile(options.ECH.ConfigPath)
if err != nil {
return nil, E.Cause(err, "read ECH config")
}
echConfig = content
}
if len(echConfig) > 0 {
block, rest := pem.Decode(echConfig)
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
return nil, E.New("invalid ECH configs pem")
}
echConfigs, err := cftls.UnmarshalECHConfigs(block.Bytes)
if err != nil {
return nil, E.Cause(err, "parse ECH configs")
}
tlsConfig.ClientECHConfigs = echConfigs
} else {
tlsConfig.GetClientECHConfigs = fetchECHClientConfig(ctx)
}
return &echClientConfig{&tlsConfig}, nil
}
func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
return func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{
{
Name: serverName + ".",
Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET,
},
},
}
response, err := service.FromContext[adapter.DNSRouter](ctx).Exchange(ctx, message, adapter.DNSQueryOptions{})
if err != nil {
return nil, err
}
if response.Rcode != mDNS.RcodeSuccess {
return nil, dns.RCodeError(response.Rcode)
}
for _, rr := range response.Answer {
switch resource := rr.(type) {
case *mDNS.HTTPS:
for _, value := range resource.Value {
if value.Key().String() == "ech" {
echConfig, err := base64.StdEncoding.DecodeString(value.String())
if err != nil {
return nil, E.Cause(err, "decode ECH config")
}
return cftls.UnmarshalECHConfigs(echConfig)
}
}
default:
return nil, E.New("unknown resource record type: ", resource.Header().Rrtype)
}
}
return nil, E.New("no ECH config found")
}
}

View File

@@ -1,5 +1,3 @@
//go:build with_ech
package tls
import (
@@ -7,14 +5,13 @@ import (
"encoding/binary"
"encoding/pem"
cftls "github.com/sagernet/cloudflare-tls"
E "github.com/sagernet/sing/common/exceptions"
"github.com/cloudflare/circl/hpke"
"github.com/cloudflare/circl/kem"
)
func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) {
func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) {
cipherSuites := []echCipherSuite{
{
kdf: hpke.KDF_HKDF_SHA256,
@@ -24,13 +21,9 @@ func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (config
aead: hpke.AEAD_ChaCha20Poly1305,
},
}
keyConfig := []myECHKeyConfig{
{id: 0, kem: hpke.KEM_X25519_HKDF_SHA256},
}
if pqSignatureSchemesEnabled {
keyConfig = append(keyConfig, myECHKeyConfig{id: 1, kem: hpke.KEM_X25519_KYBER768_DRAFT00})
}
keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites)
if err != nil {
@@ -59,7 +52,6 @@ func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (config
type echKeyConfigPair struct {
id uint8
key cftls.EXP_ECHKey
rawKey []byte
conf myECHKeyConfig
rawConf []byte
@@ -155,15 +147,6 @@ func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite [
sk = append(sk, secBuf...)
sk = be.AppendUint16(sk, uint16(len(b)))
sk = append(sk, b...)
cfECHKeys, err := cftls.EXP_UnmarshalECHKeys(sk)
if err != nil {
return nil, E.Cause(err, "bug: can't parse generated ECH server key")
}
if len(cfECHKeys) != 1 {
return nil, E.New("bug: unexpected server key count")
}
pair.key = cfECHKeys[0]
pair.rawKey = sk
pairs = append(pairs, pair)

View File

@@ -1,55 +0,0 @@
//go:build with_quic && with_ech
package tls
import (
"context"
"net"
"net/http"
"github.com/sagernet/cloudflare-tls"
"github.com/sagernet/quic-go/ech"
"github.com/sagernet/quic-go/http3_ech"
"github.com/sagernet/sing-quic"
M "github.com/sagernet/sing/common/metadata"
)
var (
_ qtls.Config = (*echClientConfig)(nil)
_ qtls.ServerConfig = (*echServerConfig)(nil)
)
func (c *echClientConfig) Dial(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.Connection, error) {
return quic.Dial(ctx, conn, addr, c.config, config)
}
func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.EarlyConnection, error) {
return quic.DialEarly(ctx, conn, addr, c.config, config)
}
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper {
return &http3.Transport{
TLSClientConfig: c.config,
QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
if err != nil {
return nil, err
}
*quicConnPtr = quicConn
return quicConn, nil
},
}
}
func (c *echServerConfig) Listen(conn net.PacketConn, config *quic.Config) (qtls.Listener, error) {
return quic.Listen(conn, c.config, config)
}
func (c *echServerConfig) ListenEarly(conn net.PacketConn, config *quic.Config) (qtls.EarlyListener, error) {
return quic.ListenEarly(conn, c.config, config)
}
func (c *echServerConfig) ConfigureHTTP3() {
http3.ConfigureTLSConfig(c.config)
}

View File

@@ -1,278 +0,0 @@
//go:build with_ech
package tls
import (
"context"
"crypto/tls"
"encoding/pem"
"net"
"os"
"strings"
cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
)
type echServerConfig struct {
config *cftls.Config
logger log.Logger
certificate []byte
key []byte
certificatePath string
keyPath string
echKeyPath string
watcher *fswatch.Watcher
}
func (c *echServerConfig) ServerName() string {
return c.config.ServerName
}
func (c *echServerConfig) SetServerName(serverName string) {
c.config.ServerName = serverName
}
func (c *echServerConfig) NextProtos() []string {
return c.config.NextProtos
}
func (c *echServerConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func (c *echServerConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for ECH")
}
func (c *echServerConfig) Client(conn net.Conn) (Conn, error) {
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
}
func (c *echServerConfig) Server(conn net.Conn) (Conn, error) {
return &echConnWrapper{cftls.Server(conn, c.config)}, nil
}
func (c *echServerConfig) Clone() Config {
return &echServerConfig{
config: c.config.Clone(),
}
}
func (c *echServerConfig) Start() error {
err := c.startWatcher()
if err != nil {
c.logger.Warn("create credentials watcher: ", err)
}
return nil
}
func (c *echServerConfig) startWatcher() error {
var watchPath []string
if c.certificatePath != "" {
watchPath = append(watchPath, c.certificatePath)
}
if c.keyPath != "" {
watchPath = append(watchPath, c.keyPath)
}
if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath)
}
if len(watchPath) == 0 {
return nil
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
err := c.credentialsUpdated(path)
if err != nil {
c.logger.Error(E.Cause(err, "reload credentials from ", path))
}
},
})
if err != nil {
return err
}
err = watcher.Start()
if err != nil {
return err
}
c.watcher = watcher
return nil
}
func (c *echServerConfig) credentialsUpdated(path string) error {
if path == c.certificatePath || path == c.keyPath {
if path == c.certificatePath {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
return err
}
c.certificate = certificate
} else {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return err
}
c.key = key
}
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "parse key pair")
}
c.config.Certificates = []cftls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
} else {
echKeyContent, err := os.ReadFile(c.echKeyPath)
if err != nil {
return err
}
block, rest := pem.Decode(echKeyContent)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return E.Cause(err, "create ECH key set")
}
c.config.ServerECHProvider = echKeySet
c.logger.Info("reloaded ECH keys")
}
return nil
}
func (c *echServerConfig) Close() error {
var err error
if c.watcher != nil {
err = E.Append(err, c.watcher.Close(), func(err error) error {
return E.Cause(err, "close credentials watcher")
})
}
return err
}
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
if !options.Enabled {
return nil, nil
}
var tlsConfig cftls.Config
if options.ACME != nil && len(options.ACME.Domain) > 0 {
return nil, E.New("acme is unavailable in ech")
}
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
if options.ServerName != "" {
tlsConfig.ServerName = options.ServerName
}
if len(options.ALPN) > 0 {
tlsConfig.NextProtos = append(options.ALPN, tlsConfig.NextProtos...)
}
if options.MinVersion != "" {
minVersion, err := ParseTLSVersion(options.MinVersion)
if err != nil {
return nil, E.Cause(err, "parse min_version")
}
tlsConfig.MinVersion = minVersion
}
if options.MaxVersion != "" {
maxVersion, err := ParseTLSVersion(options.MaxVersion)
if err != nil {
return nil, E.Cause(err, "parse max_version")
}
tlsConfig.MaxVersion = maxVersion
}
if options.CipherSuites != nil {
find:
for _, cipherSuite := range options.CipherSuites {
for _, tlsCipherSuite := range tls.CipherSuites() {
if cipherSuite == tlsCipherSuite.Name {
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
continue find
}
}
return nil, E.New("unknown cipher_suite: ", cipherSuite)
}
}
var certificate []byte
var key []byte
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
} else if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {
return nil, E.Cause(err, "read certificate")
}
certificate = content
}
if len(options.Key) > 0 {
key = []byte(strings.Join(options.Key, "\n"))
} else if options.KeyPath != "" {
content, err := os.ReadFile(options.KeyPath)
if err != nil {
return nil, E.Cause(err, "read key")
}
key = content
}
if certificate == nil {
return nil, E.New("missing certificate")
} else if key == nil {
return nil, E.New("missing key")
}
keyPair, err := cftls.X509KeyPair(certificate, key)
if err != nil {
return nil, E.Cause(err, "parse x509 key pair")
}
tlsConfig.Certificates = []cftls.Certificate{keyPair}
var echKey []byte
if len(options.ECH.Key) > 0 {
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
} else if options.ECH.KeyPath != "" {
content, err := os.ReadFile(options.ECH.KeyPath)
if err != nil {
return nil, E.Cause(err, "read ECH key")
}
echKey = content
} else {
return nil, E.New("missing ECH key")
}
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return nil, E.New("invalid ECH keys pem")
}
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
return nil, E.Cause(err, "parse ECH keys")
}
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
if err != nil {
return nil, E.Cause(err, "create ECH key set")
}
tlsConfig.ECHEnabled = true
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
tlsConfig.ServerECHProvider = echKeySet
return &echServerConfig{
config: &tlsConfig,
logger: logger,
certificate: certificate,
key: key,
certificatePath: options.CertificatePath,
keyPath: options.KeyPath,
echKeyPath: options.ECH.KeyPath,
}, nil
}

View File

@@ -1,25 +1,23 @@
//go:build !with_ech
//go:build !go1.24
package tls
import (
"context"
"crypto/tls"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
var errECHNotIncluded = E.New(`ECH is not included in this build, rebuild with -tags with_ech`)
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
return nil, errECHNotIncluded
func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions, tlsConfig *tls.Config) (Config, error) {
return nil, E.New("ECH requires go1.24, please recompile your binary.")
}
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, errECHNotIncluded
func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, tlsConfig *tls.Config, echKeyPath *string) error {
return E.New("ECH requires go1.24, please recompile your binary.")
}
func ECHKeygenDefault(host string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) {
return "", "", errECHNotIncluded
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
return E.New("ECH requires go1.24, please recompile your binary.")
}

View File

@@ -11,8 +11,11 @@ import (
"time"
)
func GenerateCertificate(timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
privateKeyPem, publicKeyPem, err := GenerateKeyPair(timeFunc, serverName, timeFunc().Add(time.Hour))
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
if timeFunc == nil {
timeFunc = time.Now
}
privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour))
if err != nil {
return nil, err
}
@@ -23,10 +26,7 @@ func GenerateCertificate(timeFunc func() time.Time, serverName string) (*tls.Cer
return &certificate, err
}
func GenerateKeyPair(timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
if timeFunc == nil {
timeFunc = time.Now
}
func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return
@@ -47,7 +47,11 @@ func GenerateKeyPair(timeFunc func() time.Time, serverName string, expire time.T
},
DNSNames: []string{serverName},
}
publicDer, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
if parent == nil {
parent = template
parentKey = key
}
publicDer, err := x509.CreateCertificate(rand.Reader, template, parent, key.Public(), parentKey)
if err != nil {
return
}

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

@@ -16,13 +16,10 @@ func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLS
if !options.Enabled {
return nil, nil
}
if options.ECH != nil && options.ECH.Enabled {
return NewECHServer(ctx, logger, options)
} else if options.Reality != nil && options.Reality.Enabled {
if options.Reality != nil && options.Reality.Enabled {
return NewRealityServer(ctx, logger, options)
} else {
return NewSTDServer(ctx, logger, options)
}
return NewSTDServer(ctx, logger, options)
}
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {

View File

@@ -8,6 +8,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"
@@ -58,6 +59,7 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
var tlsConfig tls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if options.DisableSNI {
tlsConfig.ServerName = "127.0.0.1"
} else {
@@ -125,5 +127,8 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
}
tlsConfig.RootCAs = certPool
}
if options.ECH != nil && options.ECH.Enabled {
return parseECHClientConfig(ctx, options, &tlsConfig)
}
return &STDClientConfig{&tlsConfig}, nil
}

View File

@@ -26,6 +26,7 @@ type STDServerConfig struct {
key []byte
certificatePath string
keyPath string
echKeyPath string
watcher *fswatch.Watcher
}
@@ -94,12 +95,15 @@ func (c *STDServerConfig) startWatcher() error {
if c.keyPath != "" {
watchPath = append(watchPath, c.keyPath)
}
if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath)
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
err := c.certificateUpdated(path)
if err != nil {
c.logger.Error(err)
c.logger.Error(E.Cause(err, "reload certificate"))
}
},
})
@@ -115,25 +119,33 @@ func (c *STDServerConfig) startWatcher() error {
}
func (c *STDServerConfig) certificateUpdated(path string) error {
if path == c.certificatePath {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath)
if path == c.certificatePath || path == c.keyPath {
if path == c.certificatePath {
certificate, err := os.ReadFile(c.certificatePath)
if err != nil {
return E.Cause(err, "reload certificate from ", c.certificatePath)
}
c.certificate = certificate
} else if path == c.keyPath {
key, err := os.ReadFile(c.keyPath)
if err != nil {
return E.Cause(err, "reload key from ", c.keyPath)
}
c.key = key
}
c.certificate = certificate
} else if path == c.keyPath {
key, err := os.ReadFile(c.keyPath)
keyPair, err := tls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "reload key from ", c.keyPath)
return E.Cause(err, "reload key pair")
}
c.key = key
c.config.Certificates = []tls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
} else if path == c.echKeyPath {
err := reloadECHKeys(c.echKeyPath, c.config)
if err != nil {
return err
}
c.logger.Info("reloaded ECH keys")
}
keyPair, err := tls.X509KeyPair(c.certificate, c.key)
if err != nil {
return E.Cause(err, "reload key pair")
}
c.config.Certificates = []tls.Certificate{keyPair}
c.logger.Info("reloaded TLS certificate")
return nil
}
@@ -222,7 +234,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
}
if certificate == nil && key == nil && options.Insecure {
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return GenerateCertificate(ntp.TimeFuncFromContext(ctx), info.ServerName)
return GenerateKeyPair(nil, nil, ntp.TimeFuncFromContext(ctx), info.ServerName)
}
} else {
if certificate == nil {
@@ -238,6 +250,13 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
tlsConfig.Certificates = []tls.Certificate{keyPair}
}
}
var echKeyPath string
if options.ECH != nil && options.ECH.Enabled {
err = parseECHServerConfig(ctx, options, tlsConfig, &echKeyPath)
if err != nil {
return nil, err
}
}
return &STDServerConfig{
config: tlsConfig,
logger: logger,
@@ -246,5 +265,6 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
key: key,
certificatePath: options.CertificatePath,
keyPath: options.KeyPath,
echKeyPath: echKeyPath,
}, nil
}

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 {

107
common/tlsfragment/conn.go Normal file
View File

@@ -0,0 +1,107 @@
package tf
import (
"context"
"math/rand"
"net"
"strings"
"time"
N "github.com/sagernet/sing/common/network"
"golang.org/x/net/publicsuffix"
)
type Conn struct {
net.Conn
tcpConn *net.TCPConn
ctx context.Context
firstPacketWritten bool
fallbackDelay time.Duration
}
func NewConn(conn net.Conn, ctx context.Context, fallbackDelay time.Duration) (*Conn, error) {
tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
return &Conn{
Conn: conn,
tcpConn: tcpConn,
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 {
if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(true)
if err != nil {
return
}
}
splits := strings.Split(serverName.ServerName, ".")
currentIndex := serverName.Index
if publicSuffix := publicsuffix.List.PublicSuffix(serverName.ServerName); publicSuffix != "" {
splits = splits[:len(splits)-strings.Count(serverName.ServerName, ".")]
}
if len(splits) > 1 && 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++ {
var payload []byte
if i == 0 {
payload = b[:splitIndexes[i]]
} else if i == len(splitIndexes) {
payload = b[splitIndexes[i-1]:]
} else {
payload = b[splitIndexes[i-1]:splitIndexes[i]]
}
if c.tcpConn != nil && i != len(splitIndexes) {
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
if err != nil {
return
}
} else {
_, err = c.Conn.Write(payload)
if err != nil {
return
}
}
}
if c.tcpConn != nil {
err = c.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
}

131
common/tlsfragment/index.go Normal file
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
ServerName string
}
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),
ServerName: string(sex),
}
}
exs = exs[4+exLen:]
currentIndex += 4 + int(exLen)
}
return nil
}

View File

@@ -0,0 +1,93 @@
package tf
import (
"context"
"net"
"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 writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
_, err := conn.Write(payload)
if err != nil {
return err
}
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,40 @@
package tf
import (
"context"
"net"
"time"
"github.com/sagernet/sing/common/control"
"golang.org/x/sys/unix"
)
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
_, err := conn.Write(payload)
if err != nil {
return err
}
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 || windows)
package tf
import (
"context"
"net"
"time"
)
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
time.Sleep(fallbackDelay)
return nil
}

View File

@@ -0,0 +1,28 @@
package tf
import (
"context"
"errors"
"net"
"time"
"github.com/sagernet/sing/common/winiphlpapi"
"golang.org/x/sys/windows"
)
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
start := time.Now()
err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload)
if err != nil {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
time.Sleep(fallbackDelay)
return nil
}
return err
}
if time.Since(start) <= 20*time.Millisecond {
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

7
constant/certificate.go Normal file
View File

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

View File

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

View File

@@ -19,6 +19,7 @@ const (
TypeTor = "tor"
TypeSSH = "ssh"
TypeShadowTLS = "shadowtls"
TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"

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{

View File

@@ -17,7 +17,7 @@ import (
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
"github.com/miekg/dns"
dns "github.com/miekg/dns"
)
var (
@@ -484,7 +484,7 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
return nil, RCodeError(response.Rcode)
return nil, RcodeError(response.Rcode)
}
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawAnswer := range response.Answer {
@@ -508,10 +508,10 @@ func wrapError(err error) error {
switch dnsErr := err.(type) {
case *net.DNSError:
if dnsErr.IsNotFound {
return RCodeNameError
return RcodeNameError
}
case *net.AddrError:
return RCodeNameError
return RcodeNameError
}
return err
}
@@ -561,3 +561,73 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim
}
return &response
}
func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg {
response := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: id,
Rcode: dns.RcodeSuccess,
Response: true,
},
Question: []dns.Question{question},
Answer: []dns.RR{
&dns.CNAME{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
Ttl: timeToLive,
},
Target: record,
},
},
}
return &response
}
func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg {
response := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: id,
Rcode: dns.RcodeSuccess,
Response: true,
},
Question: []dns.Question{question},
Answer: []dns.RR{
&dns.TXT{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: timeToLive,
},
Txt: records,
},
},
}
return &response
}
func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg {
response := dns.Msg{
MsgHdr: dns.MsgHdr{
Id: id,
Rcode: dns.RcodeSuccess,
Response: true,
},
Question: []dns.Question{question},
}
for _, record := range records {
response.Answer = append(response.Answer, &dns.MX{
Hdr: dns.RR_Header{
Name: question.Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: timeToLive,
},
Preference: record.Pref,
Mx: record.Host,
})
}
return &response
}

View File

@@ -1,33 +1,17 @@
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
import (
mDNS "github.com/miekg/dns"
)
type RCodeError uint16
const (
RcodeFormatError RcodeError = mDNS.RcodeFormatError
RcodeNameError RcodeError = mDNS.RcodeNameError
RcodeRefused RcodeError = mDNS.RcodeRefused
)
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))
}
type RcodeError int
func (e RcodeError) Error() string {
return mDNS.RcodeToString[int(e)]
}

View File

@@ -154,6 +154,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
if isFakeIP && !allowFakeIP {
continue
}
if action.Strategy != C.DomainStrategyAsIS {
options.Strategy = action.Strategy
}
if isFakeIP || action.DisableCache {
options.DisableCache = true
}
@@ -171,9 +174,11 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
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
}
@@ -183,9 +188,7 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
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
}
}
@@ -246,7 +249,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &options)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
@@ -265,10 +269,10 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
return rule.MatchAddressLimit(metadata)
}
}
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
if dnsOptions.Strategy == C.DomainStrategyAsIS {
dnsOptions.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(dnsCtx, transport, message, options, responseCheck)
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
@@ -294,7 +298,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
return nil, err
}
if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 {
if transport.Type() != C.DNSTypeFakeIP {
if transport == nil || transport.Type() != C.DNSTypeFakeIP {
for _, answer := range response.Answer {
switch record := answer.(type) {
case *mDNS.A:
@@ -325,20 +329,20 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
}
} else if len(responseAddrs) == 0 {
r.logger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
err = RCodeNameError
err = RcodeNameError
}
}
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
if cached {
if len(responseAddrs) == 0 {
return nil, RCodeNameError
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
metadata.Domain = FqdnToDomain(domain)
if options.Transport != nil {
transport := options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {

View File

@@ -36,6 +36,17 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
}, nil
}
func (t *Transport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return t.store.Start()
}
func (t *Transport) Close() error {
return t.store.Close()
}
func (t *Transport) Reset() {
}
@@ -44,7 +55,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
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)
address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA)
if err != nil {
return nil, err
}

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,17 @@
package hosts
import (
"path/filepath"
"golang.org/x/sys/windows"
)
var DefaultPath string
func init() {
systemDirectory, err := windows.GetSystemDirectory()
if err != nil {
systemDirectory = "C:\\Windows\\System32"
}
DefaultPath = filepath.Join(systemDirectory, "Drivers/etc/hosts")
}

2
dns/transport/hosts/testdata/hosts vendored Normal file
View File

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

View File

@@ -62,9 +62,20 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
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: options.Host,
Host: host,
}
if destinationURL.Host == "" {
destinationURL.Host = options.Server
@@ -89,7 +100,7 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
logger,
transportDialer,
&destinationURL,
options.Headers.Build(),
headers,
serverAddr,
tlsConfig,
), nil

View File

@@ -2,14 +2,15 @@ package local
import (
"context"
"math/rand"
"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"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@@ -18,14 +19,12 @@ import (
mDNS "github.com/miekg/dns"
)
func RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport)
}
var _ adapter.DNSTransport = (*Transport)(nil)
type Transport struct {
dns.TransportAdapter
ctx context.Context
hosts *hosts.File
dialer N.Dialer
}
@@ -35,7 +34,8 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
return nil, err
}
return &Transport{
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeTCP, tag, options),
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
hosts: hosts.NewFile(hosts.DefaultPath),
dialer: transportDialer,
}, nil
}
@@ -47,9 +47,9 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addressStrings, _ := lookupStaticHost(domain)
if len(addressStrings) > 0 {
return dns.FixedResponse(message.Id, question, common.Map(addressStrings, M.ParseAddr), C.DefaultDNSTTL), nil
addresses := t.hosts.Lookup(domain)
if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
}
systemConfig := getSystemDNSConfig()
@@ -62,7 +62,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
var lastErr error
for _, fqdn := range nameList(systemConfig, domain) {
for _, fqdn := range systemConfig.nameList(domain) {
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
if err != nil {
lastErr = err
@@ -83,6 +83,12 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
results := make(chan queryResult)
startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
if err == nil {
addresses, _ := dns.MessageToAddresses(response)
if len(addresses) == 0 {
err = E.New(fqdn, ": empty result")
}
}
select {
case results <- queryResult{response, err}:
case <-returned:
@@ -90,14 +96,25 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
}
queryCtx, queryCancel := context.WithCancel(ctx)
defer queryCancel()
for _, fqdn := range nameList(systemConfig, domain) {
var nameCount int
for _, fqdn := range systemConfig.nameList(domain) {
nameCount++
go startRacer(queryCtx, fqdn)
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case result := <-results:
return result.response, result.err
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...)
}
}
}
}
@@ -118,7 +135,7 @@ func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn stri
return response, nil
}
}
return nil, lastErr
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) {
@@ -130,7 +147,7 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
}
request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: uint16(randInt()),
Id: uint16(rand.Uint32()),
RecursionDesired: true,
AuthenticatedData: ad,
},

View File

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

View File

@@ -0,0 +1,201 @@
package local
import (
"context"
"errors"
"net"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
)
func RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewFallbackTransport)
}
type FallbackTransport struct {
adapter.DNSTransport
ctx context.Context
fallback bool
resolver net.Resolver
}
func NewFallbackTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
transport, err := NewTransport(ctx, logger, tag, options)
if err != nil {
return nil, err
}
return &FallbackTransport{
DNSTransport: transport,
ctx: ctx,
}, nil
}
func (f *FallbackTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
platformInterface := service.FromContext[platform.Interface](f.ctx)
if platformInterface == nil {
return nil
}
inboundManager := service.FromContext[adapter.InboundManager](f.ctx)
for _, inbound := range inboundManager.Inbounds() {
if inbound.Type() == C.TypeTun {
// platform tun hijacks DNS, so we can only use cgo resolver here
f.fallback = true
break
}
}
return nil
}
func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if f.fallback {
return f.DNSTransport.Exchange(ctx, message)
}
question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
var network string
if question.Qtype == mDNS.TypeA {
network = "ip4"
} else {
network = "ip6"
}
addresses, err := f.resolver.LookupNetIP(ctx, network, domain)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
} else if question.Qtype == mDNS.TypeNS {
records, err := f.resolver.LookupNS(ctx, domain)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
response := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeSuccess,
Response: true,
},
Question: []mDNS.Question{question},
}
for _, record := range records {
response.Answer = append(response.Answer, &mDNS.NS{
Hdr: mDNS.RR_Header{
Name: question.Name,
Rrtype: mDNS.TypeNS,
Class: mDNS.ClassINET,
Ttl: C.DefaultDNSTTL,
},
Ns: record.Host,
})
}
return response, nil
} else if question.Qtype == mDNS.TypeCNAME {
cname, err := f.resolver.LookupCNAME(ctx, domain)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeSuccess,
Response: true,
},
Question: []mDNS.Question{question},
Answer: []mDNS.RR{
&mDNS.CNAME{
Hdr: mDNS.RR_Header{
Name: question.Name,
Rrtype: mDNS.TypeCNAME,
Class: mDNS.ClassINET,
Ttl: C.DefaultDNSTTL,
},
Target: cname,
},
},
}, nil
} else if question.Qtype == mDNS.TypeTXT {
records, err := f.resolver.LookupTXT(ctx, domain)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeSuccess,
Response: true,
},
Question: []mDNS.Question{question},
Answer: []mDNS.RR{
&mDNS.TXT{
Hdr: mDNS.RR_Header{
Name: question.Name,
Rrtype: mDNS.TypeCNAME,
Class: mDNS.ClassINET,
Ttl: C.DefaultDNSTTL,
},
Txt: records,
},
},
}, nil
} else if question.Qtype == mDNS.TypeMX {
records, err := f.resolver.LookupMX(ctx, domain)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
response := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeSuccess,
Response: true,
},
Question: []mDNS.Question{question},
}
for _, record := range records {
response.Answer = append(response.Answer, &mDNS.MX{
Hdr: mDNS.RR_Header{
Name: question.Name,
Rrtype: mDNS.TypeA,
Class: mDNS.ClassINET,
Ttl: C.DefaultDNSTTL,
},
Preference: record.Pref,
Mx: record.Host,
})
}
return response, nil
} else {
return nil, E.New("only A, AAAA, NS, CNAME, TXT, MX queries are supported on current platform when using TUN, please switch to a fixed DNS server.")
}
}

View File

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

View File

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

View File

@@ -0,0 +1,146 @@
package local
import (
"os"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
)
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
}
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,53 @@
//go:build darwin && cgo
package local
/*
#include <stdlib.h>
#include <stdio.h>
#include <resolv.h>
#include <arpa/inet.h>
*/
import "C"
import (
"time"
E "github.com/sagernet/sing/common/exceptions"
"github.com/miekg/dns"
)
func dnsReadConfig(_ string) *dnsConfig {
if C.res_init() != 0 {
return &dnsConfig{
servers: defaultNS,
search: dnsDefaultSearch(),
ndots: 1,
timeout: 5 * time.Second,
attempts: 2,
err: E.New("libresolv initialization failed"),
}
}
conf := &dnsConfig{
ndots: 1,
timeout: 5 * time.Second,
attempts: int(C._res.retry),
}
for i := 0; i < int(C._res.nscount); i++ {
ns := C._res.nsaddr_list[i]
addr := C.inet_ntoa(ns.sin_addr)
if addr == nil {
continue
}
conf.servers = append(conf.servers, C.GoString(addr))
}
for i := 0; ; i++ {
search := C._res.dnsrch[i]
if search == nil {
break
}
conf.search = append(conf.search, dns.Fqdn(C.GoString(search)))
}
return conf
}

View File

@@ -0,0 +1,23 @@
package local
import (
"os"
"strings"
_ "unsafe"
"github.com/miekg/dns"
)
//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{dns.Fqdn(hn[i+1:])}
}
return nil
}

View File

@@ -0,0 +1,155 @@
//go:build !windows && !(darwin && cgo)
package local
import (
"bufio"
"net"
"net/netip"
"os"
"strings"
"time"
"github.com/miekg/dns"
)
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{dns.Fqdn(f[1])}
}
case "search":
conf.search = make([]string, 0, len(f)-1)
for i := 1; i < len(f); i++ {
name := dns.Fqdn(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
}
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,97 @@
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
}
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

@@ -74,9 +74,10 @@ func (t *PredefinedTransport) Exchange(ctx context.Context, message *mDNS.Msg) (
}() {
copyAnswer := *response.answer
copyAnswer.Id = message.Id
copyAnswer.Question = message.Question
return &copyAnswer, nil
}
}
}
return nil, dns.RCodeNameError
return nil, dns.RcodeNameError
}

View File

@@ -60,9 +60,20 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
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: options.Host,
Scheme: "https",
Host: host,
}
if destinationURL.Host == "" {
destinationURL.Host = options.Server
@@ -87,7 +98,7 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
logger: logger,
dialer: transportDialer,
destination: &destinationURL,
headers: options.Headers.Build(),
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)

View File

@@ -144,6 +144,13 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
func (t *UDPTransport) open(ctx context.Context) (*dnsConnection, error) {
t.access.Lock()
defer t.access.Unlock()
if t.conn != nil {
select {
case <-t.conn.done:
default:
return t.conn, nil
}
}
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, err
@@ -154,6 +161,7 @@ func (t *UDPTransport) open(ctx context.Context) (*dnsConnection, error) {
callbacks: make(map[uint16]*dnsCallback),
}
go t.recvLoop(dnsConn)
t.conn = dnsConn
return dnsConn, nil
}
@@ -201,8 +209,6 @@ type dnsConnection struct {
}
func (c *dnsConnection) Close(err error) {
c.access.Lock()
defer c.access.Unlock()
c.closeOnce.Do(func() {
close(c.done)
c.err = err

View File

@@ -27,9 +27,14 @@ func NewTransportAdapter(transportType string, transportTag string, dependencies
}
func NewTransportAdapterWithLocalOptions(transportType string, transportTag string, localOptions option.LocalDNSServerOptions) TransportAdapter {
var dependencies []string
if localOptions.DomainResolver != nil && localOptions.DomainResolver.Server != "" {
dependencies = append(dependencies, localOptions.DomainResolver.Server)
}
return TransportAdapter{
transportType: transportType,
transportTag: transportTag,
dependencies: dependencies,
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
clientSubnet: localOptions.LegacyClientSubnet,
}
@@ -37,8 +42,11 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri
func NewTransportAdapterWithRemoteOptions(transportType string, transportTag string, remoteOptions option.RemoteDNSServerOptions) TransportAdapter {
var dependencies []string
if remoteOptions.AddressResolver != "" {
dependencies = []string{remoteOptions.AddressResolver}
if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" {
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
}
if remoteOptions.LegacyAddressResolver != "" {
dependencies = append(dependencies, remoteOptions.LegacyAddressResolver)
}
return TransportAdapter{
transportType: transportType,

View File

@@ -19,37 +19,39 @@ func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (
if options.LegacyDefaultDialer {
return dialer.NewDefaultOutbound(ctx), nil
} else {
return dialer.New(ctx, options.DialerOptions, false)
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
DirectResolver: true,
})
}
}
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 := dialer.NewDefaultOutbound(ctx)
if options.LegacyAddressResolver != "" {
transport := service.FromContext[adapter.DNSTransportManager](ctx)
resolverTransport, loaded := transport.Transport(options.LegacyAddressResolver)
if !loaded {
return nil, E.New("address resolver not found: ", options.LegacyAddressResolver)
}
transportDialer = newTransportDialer(transportDialer, service.FromContext[adapter.DNSRouter](ctx), resolverTransport, C.DomainStrategy(options.LegacyAddressStrategy), time.Duration(options.LegacyAddressFallbackDelay))
} else if options.ServerIsDomain() {
return nil, E.New("missing address resolver for server: ", options.Server)
}
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
} else {
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: options.ServerIsDomain(),
DirectResolver: true,
})
}
return transportDialer, nil
}
type TransportDialer struct {
type legacyTransportDialer struct {
dialer N.Dialer
dnsRouter adapter.DNSRouter
transport adapter.DNSTransport
@@ -57,8 +59,8 @@ type TransportDialer struct {
fallbackDelay time.Duration
}
func NewTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *TransportDialer {
return &TransportDialer{
func newTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *legacyTransportDialer {
return &legacyTransportDialer{
dialer,
dnsRouter,
transport,
@@ -67,7 +69,7 @@ func NewTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport
}
}
func (d *TransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
func (d *legacyTransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if destination.IsIP() {
return d.dialer.DialContext(ctx, network, destination)
}
@@ -81,7 +83,7 @@ func (d *TransportDialer) DialContext(ctx context.Context, network string, desti
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) {
func (d *legacyTransportDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if destination.IsIP() {
return d.dialer.ListenPacket(ctx, destination)
}
@@ -96,6 +98,6 @@ func (d *TransportDialer) ListenPacket(ctx context.Context, destination M.Socksa
return conn, err
}
func (d *TransportDialer) Upstream() any {
func (d *legacyTransportDialer) Upstream() any {
return d.dialer
}

View File

@@ -56,12 +56,12 @@ func (m *TransportManager) Start(stage adapter.StartStage) error {
}
m.started = true
m.stage = stage
outbounds := m.transports
transports := m.transports
m.access.Unlock()
if stage == adapter.StartStateStart {
return m.startTransports(m.transports)
} else {
for _, outbound := range outbounds {
for _, outbound := range transports {
err := adapter.LegacyStart(outbound, stage)
if err != nil {
return E.Cause(err, stage, " dns/", outbound.Type(), "[", outbound.Tag(), "]")

View File

@@ -2,10 +2,247 @@
icon: material/alert-decagram
---
#### 1.11.0-beta.23
#### 1.12.0-alpha.11
* Fixes and improvements
#### 1.12.0-alpha.10
* Add AnyTLS protocol **1**
* Improve `resolve` route action **2**
* Migrate to stdlib ECH implementation **3**
* Fixes and improvements
**1**:
The new AnyTLS protocol claims to mitigate TLS proxy traffic characteristics and comes with a new multiplexing scheme.
See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/configuration/outbound/anytls/).
**2**:
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions, see [Route Action](/configuration/route/rule_action).
**3**:
See [TLS](/configuration/shared/tls).
The build tag `with_ech` is no longer needed and has been removed.
#### 1.12.0-alpha.7
* Add Tailscale DNS server **1**
* Fixes and improvements
**1**:
See [Tailscale](/configuration/dns/server/tailscale/).
#### 1.12.0-alpha.6
* Add Tailscale endpoint **1**
* Drop support for go1.22 **2**
* Fixes and improvements
**1**:
See [Tailscale](/configuration/endpoint/tailscale/).
**2**:
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
### 1.11.3
* Fixes and improvements
_This version overwrites 1.11.2, as incorrect binaries were released due to a bug in the continuous integration process._
#### 1.12.0-alpha.5
* Fixes and improvements
### 1.11.1
* Fixes and improvements
#### 1.12.0-alpha.2
* Update quic-go to v0.49.0
* Fixes and improvements
#### 1.12.0-alpha.1
* Refactor DNS servers **1**
* Add domain resolver options**2**
* Add TLS fragment route options **3**
* Add certificate options **4**
**1**:
DNS servers are refactored for better performance and scalability.
See [DNS server](/configuration/dns/server/).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
Compatibility for old formats will be removed in sing-box 1.14.0.
**2**:
Legacy `outbound` DNS rules are deprecated
and can be replaced by the new `domain_resolver` option.
See [Dial Fields](/configuration/shared/dial/#domain_resolver) and
[Route](/configuration/route/#default_domain_resolver).
For migration,
see [Migrate outbound DNS rule items to domain resolver](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver).
**3**:
The new TLS fragment route options allow you to fragment TLS handshakes to bypass firewalls.
This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, and should not be used
to circumvent real censorship.
Since it is not designed for performance, it should not be applied to all connections, but only to server names that are
known to be blocked.
See [Route Action](/configuration/route/rule_action/#tls_fragment).
**4**:
New certificate options allow you to manage the default list of trusted X509 CA certificates.
For the system certificate list, fixed Go not reading Android trusted certificates correctly.
You can also use the Mozilla Included List instead, or add trusted certificates yourself.
See [Certificate](/configuration/certificate/).
### 1.11.0
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
* Fixes and improvements

View File

@@ -0,0 +1,54 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# Certificate
### Structure
```json
{
"store": "",
"certificate": [],
"certificate_path": [],
"certificate_directory_path": []
}
```
!!! note ""
You can ignore the JSON Array [] tag when the content is only one item
### Fields
#### store
The default X509 trusted CA certificate list.
| Type | Description |
|--------------------|---------------------------------------------------------------------------------------------------------------|
| `system` (default) | System trusted CA certificates |
| `mozilla` | [Mozilla Included List](https://wiki.mozilla.org/CA/Included_Certificates) with China CA certificates removed |
| `none` | Empty list |
#### certificate
The certificate line array to trust, in PEM format.
#### certificate_path
!!! note ""
Will be automatically reloaded if file modified.
The paths to certificates to trust, in PEM format.
#### certificate_directory_path
!!! note ""
Will be automatically reloaded if file modified.
The directory path to search for certificates to trust,in PEM format.

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

@@ -1,7 +1,11 @@
---
icon: material/new-box
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.12.0"
:material-delete-clock: [outbound](#outbound)
!!! quote "Changes in sing-box 1.11.0"
:material-plus: [action](#action)
@@ -395,6 +399,10 @@ Invert match result.
#### outbound
!!! failure "Deprecated in sing-box 1.12.0"
`outbound` rule items are deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver).
Match outbound.
`any` can be used as a value to match any outbound.

View File

@@ -1,7 +1,11 @@
---
icon: material/new-box
icon: material/alert-decagram
---
!!! quote "sing-box 1.12.0 中的更改"
:material-delete-clock: [outbound](#outbound)
!!! quote "sing-box 1.11.0 中的更改"
:material-plus: [action](#action)
@@ -395,6 +399,10 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
#### outbound
!!! failure "已在 sing-box 1.12.0 废弃"
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver)。
匹配出站。
`any` 可作为值用于匹配任意出站。

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,8 +14,9 @@ icon: material/new-box
{
"action": "route", // default
"server": "",
"strategy": "",
"disable_cache": false,
"rewrite_ttl": 0,
"rewrite_ttl": null,
"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.
@@ -38,7 +49,7 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
Will overrides `dns.client_subnet`.
### route-options

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
@@ -10,10 +14,10 @@ icon: material/new-box
{
"action": "route", // 默认
"server": "",
// 兼容性
"strategy": "",
"disable_cache": false,
"rewrite_ttl": 0,
"rewrite_ttl": null,
"client_subnet": null
}
```
@@ -26,6 +30,12 @@ icon: material/new-box
目标 DNS 服务器的标签。
#### strategy
为此查询设置域名策略。
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`
#### disable_cache
在此查询中禁用缓存。
@@ -40,7 +50,7 @@ icon: material/new-box
如果值是 IP 地址而不是前缀,则会自动附加 `/32``/128`
将覆盖 `dns.client_subnet``servers.[].client_subnet`
将覆盖 `dns.client_subnet`.
### route-options

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,35 @@
---
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,71 @@
---
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.
`443` 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.

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