Compare commits

..

121 Commits

Author SHA1 Message Date
世界
1974c767c9 documentation: Bump version 2025-07-07 18:56:29 +08:00
世界
2c286b7adc Add firewalld compatibility for auto redirect 2025-07-07 18:56:29 +08:00
世界
e8b70162de Fix DNS reject check 2025-07-07 14:12:43 +08:00
世界
30cfd620c2 Improve copy 2025-07-07 14:08:49 +08:00
世界
e8bd70341a Increase default mtu under network extension to 4064 2025-07-07 14:08:48 +08:00
世界
7acf52d650 release: Fix publish testflight 2025-07-07 14:08:48 +08:00
世界
1ba0827995 Improve darwin tun performance 2025-07-07 14:08:48 +08:00
世界
eb3baf56e7 Improve nftables rules for openwrt 2025-07-07 14:08:48 +08:00
世界
97a44e654a Fixed DoH server recover from conn freezes 2025-07-07 14:08:47 +08:00
世界
5dec974d09 Update libresolv usage 2025-07-07 14:08:47 +08:00
yu
60dd283043 documentation: Update client configuration manual 2025-07-07 14:08:47 +08:00
yanwo
fcefb52ce4 documentation: Fix typo
Signed-off-by: yanwo <ogilvy@gmail.com>
2025-07-07 14:08:47 +08:00
anytinz
c2aaa9903b documentation: Fix wrong SideStore loopback ip 2025-07-07 14:08:47 +08:00
世界
df2e80d126 Revert "release: Add IPA build"
After testing, it seems that since extensions are not handled correctly, it cannot be installed by SideStore.
2025-07-07 14:08:47 +08:00
世界
7552ea4fef release: Add IPA build 2025-07-07 14:08:46 +08:00
世界
28683d277f Add API to dump AdGuard rules 2025-07-07 14:08:46 +08:00
Sukka
2476f43123 Improve AdGuard rule-set parser 2025-07-07 14:08:46 +08:00
Restia-Ashbell
c20c41eccc Add ECH support for uTLS 2025-07-07 14:08:45 +08:00
世界
046ee4a5ba Improve TLS fragments 2025-07-07 14:08:45 +08:00
世界
52a13e6927 Add cache support for ssm-api 2025-07-07 14:08:45 +08:00
世界
aacbab0874 Fix service will not be closed 2025-07-07 14:08:44 +08:00
世界
388d489246 Add loopback address support for tun 2025-07-07 14:08:44 +08:00
世界
4d66403bfa Fix tproxy listener 2025-07-07 14:08:44 +08:00
世界
0c3b4ef9ca Fix systemd package 2025-07-07 14:08:44 +08:00
世界
d0b6ca7099 Fix missing home for derp service 2025-07-07 14:08:43 +08:00
Zero Clover
2be22948f0 documentation: Fix services 2025-07-07 14:08:43 +08:00
世界
fb33008243 Fix dns.client_subnet ignored 2025-07-07 14:08:43 +08:00
世界
8455326d9a documentation: Minor fixes 2025-07-07 14:08:43 +08:00
世界
f51d8df6b0 Fix tailscale forward 2025-07-07 14:08:43 +08:00
世界
e0d14675ad Minor fixes 2025-07-07 14:08:42 +08:00
世界
a668aaaa02 Add SSM API service 2025-07-07 14:08:42 +08:00
世界
e785431242 Add resolved service and DNS server 2025-07-07 14:08:41 +08:00
世界
6b5de379c6 Add DERP service 2025-07-07 14:08:41 +08:00
世界
c448b2d1ad Add service component type 2025-07-07 14:08:41 +08:00
世界
e9fc4c3779 Fix tproxy tcp control 2025-07-07 14:08:41 +08:00
愚者
1ba70c8f07 release: Fix build tags for android
Signed-off-by: 愚者 <11926619+FansChou@users.noreply.github.com>
2025-07-07 14:08:40 +08:00
世界
9f4bb555f7 prevent creation of bind and mark controls on unsupported platforms 2025-07-07 14:08:40 +08:00
PuerNya
46afe058e6 documentation: Fix description of reject DNS action behavior 2025-07-07 14:08:40 +08:00
Restia-Ashbell
9c3a985b79 Fix TLS record fragment 2025-07-07 14:08:40 +08:00
世界
4ed65e0fa9 Add missing accept_routes option for Tailscale 2025-07-07 14:08:40 +08:00
世界
6cbc771bfb Add TLS record fragment support 2025-07-07 14:08:39 +08:00
世界
ddee7ecb6f Fix set edns0 client subnet 2025-07-07 14:08:39 +08:00
世界
219d8658be Update minor dependencies 2025-07-07 14:08:39 +08:00
世界
0f51a47ffe Update certmagic and providers 2025-07-07 14:08:39 +08:00
世界
a0699e09f8 Update protobuf and grpc 2025-07-07 14:08:38 +08:00
世界
1a55024c95 Add control options for listeners 2025-07-07 14:08:38 +08:00
世界
a57ed46111 Update quic-go to v0.52.0 2025-07-07 14:08:37 +08:00
世界
e3381dea33 Update utls to v1.7.2 2025-07-07 14:08:37 +08:00
世界
ad6fc71e42 Handle EDNS version downgrade 2025-07-07 14:08:37 +08:00
世界
577fc63722 documentation: Fix anytls padding scheme description 2025-07-07 14:08:37 +08:00
安容
f62b579430 Report invalid DNS address early 2025-07-07 14:08:36 +08:00
世界
99496599c2 Fix wireguard listen_port 2025-07-07 14:08:36 +08:00
世界
b1a02fd528 clash-api: Add more meta api 2025-07-07 14:08:36 +08:00
世界
bfbdd05e9d Fix DNS lookup 2025-07-07 14:08:36 +08:00
世界
59cf61cac4 Fix fetch ECH configs 2025-07-07 14:08:35 +08:00
reletor
97b447d399 documentation: Minor fixes 2025-07-07 14:08:35 +08:00
caelansar
ba2009d8a4 Fix callback deletion in UDP transport 2025-07-07 14:08:35 +08:00
世界
a56b9fa0d0 documentation: Try to make the play review happy 2025-07-07 14:08:34 +08:00
世界
cdfec40038 Fix missing handling of legacy domain_strategy options 2025-07-07 14:08:34 +08:00
世界
1df6852ad8 Improve local DNS server 2025-07-07 14:08:34 +08:00
anytls
f4a318cac0 Update anytls
Co-authored-by: anytls <anytls>
2025-07-07 14:08:34 +08:00
世界
8dc620fb97 Fix DNS dialer 2025-07-07 14:08:33 +08:00
世界
acdfb8affa release: Skip override version for iOS 2025-07-07 14:08:33 +08:00
iikira
f982d3dcdb Fix UDP DNS server crash
Signed-off-by: iikira <i2@mail.iikira.com>
2025-07-07 14:08:33 +08:00
ReleTor
5f1c5ecc21 Fix fetch ECH configs 2025-07-07 14:08:32 +08:00
世界
e881d5cb2c Allow direct outbounds without domain_resolver 2025-07-07 14:08:32 +08:00
世界
f340a4445c Fix Tailscale dialer 2025-07-07 14:08:32 +08:00
dyhkwong
4452ae83e8 Fix DNS over QUIC stream close 2025-07-07 14:08:31 +08:00
anytls
7bc92d219a Update anytls
Co-authored-by: anytls <anytls>
2025-07-07 14:08:31 +08:00
Rambling2076
5b5babb7d3 Fix missing with_tailscale in Dockerfile
Signed-off-by: Rambling2076 <Rambling2076@proton.me>
2025-07-07 14:08:31 +08:00
世界
6c6a215038 Fail when default DNS server not found 2025-07-07 14:08:31 +08:00
世界
34f03a4151 Update gVisor to 20250319.0 2025-07-07 14:08:30 +08:00
世界
338d67bbd5 Explicitly reject detour to empty direct outbounds 2025-07-07 14:08:30 +08:00
世界
d3b3320f51 Add netns support 2025-07-07 14:08:29 +08:00
世界
dda405d580 Add wildcard name support for predefined records 2025-07-07 14:08:29 +08:00
世界
28946f65f7 Remove map usage in options 2025-07-07 14:08:29 +08:00
世界
1119c06dc2 Fix unhandled DNS loop 2025-07-07 14:08:29 +08:00
世界
a577734efb Add wildcard-sni support for shadow-tls inbound 2025-07-07 14:08:28 +08:00
k9982874
706f4adca4 Add ntp protocol sniffing 2025-07-07 14:08:28 +08:00
世界
ab13ffe20c option: Fix marshal legacy DNS options 2025-07-07 14:08:28 +08:00
世界
a550e8a563 Make domain_resolver optional when only one DNS server is configured 2025-07-07 14:08:28 +08:00
世界
ff00aeb580 Fix DNS lookup context pollution 2025-07-07 14:08:27 +08:00
世界
a810e3213e Fix http3 DNS server connecting to wrong address 2025-07-07 14:08:27 +08:00
Restia-Ashbell
eecd3d7376 documentation: Fix typo 2025-07-07 14:08:27 +08:00
anytls
d4ed82ebaa Update sing-anytls
Co-authored-by: anytls <anytls>
2025-07-07 14:08:26 +08:00
k9982874
8184d9ee9f Fix hosts DNS server 2025-07-07 14:08:26 +08:00
世界
f1edfe581e Fix UDP DNS server crash 2025-07-07 14:08:26 +08:00
世界
7571ff6450 documentation: Fix missing ip_accept_any DNS rule option 2025-07-07 14:08:26 +08:00
世界
60f21507ad Fix anytls dialer usage 2025-07-07 14:08:26 +08:00
世界
b97ca94600 Move predefined DNS server to rule action 2025-07-07 14:08:25 +08:00
世界
7e05192370 Fix domain resolver on direct outbound 2025-07-07 14:08:25 +08:00
Zephyruso
e8c7e278f0 Fix missing AnyTLS display name 2025-07-07 14:08:24 +08:00
anytls
b5d199d8a7 Update sing-anytls
Co-authored-by: anytls <anytls>
2025-07-07 14:08:24 +08:00
Estel
c408916b47 documentation: Fix typo
Signed-off-by: Estel <callmebedrockdigger@gmail.com>
2025-07-07 14:08:24 +08:00
TargetLocked
fa1dfa3970 Fix parsing legacy DNS options 2025-07-07 14:08:24 +08:00
世界
a8da3bf4b7 Fix DNS fallback 2025-07-07 14:08:23 +08:00
世界
a1a00d36b8 documentation: Fix missing hosts DNS server 2025-07-07 14:08:23 +08:00
anytls
b6a8a84a99 Add MinIdleSession option to AnyTLS outbound
Co-authored-by: anytls <anytls>
2025-07-07 14:08:23 +08:00
ReleTor
99b9e7c14c documentation: Minor fixes 2025-07-07 14:08:23 +08:00
libtry486
fc3d43fc7f documentation: Fix typo
fix typo

Signed-off-by: libtry486 <89328481+libtry486@users.noreply.github.com>
2025-07-07 14:08:22 +08:00
Alireza Ahmadi
69c0f424d9 Fix Outbound deadlock 2025-07-07 14:08:22 +08:00
世界
6e87e49690 documentation: Fix AnyTLS doc 2025-07-07 14:08:22 +08:00
anytls
638e5ad2be Add AnyTLS protocol 2025-07-07 14:08:21 +08:00
世界
366406a558 Migrate to stdlib ECH support 2025-07-07 14:08:21 +08:00
世界
4cf1e58eed Add fallback local DNS server for iOS 2025-07-07 14:08:21 +08:00
世界
5927d7affe Get darwin local DNS server from libresolv 2025-07-07 14:08:21 +08:00
世界
6f5dd46b32 Improve resolve action 2025-07-07 14:08:20 +08:00
世界
7d048a014f Add back port hopping to hysteria 1 2025-07-07 14:08:20 +08:00
xchacha20-poly1305
d51593e8a5 Remove single quotes of raw Moziila certs 2025-07-07 14:08:19 +08:00
世界
fc88d34edc Add Tailscale endpoint 2025-07-07 14:08:19 +08:00
世界
12a3fd3b60 Build legacy binaries with latest Go 2025-07-07 14:08:19 +08:00
世界
b69a381aa8 documentation: Remove outdated icons 2025-07-07 14:08:18 +08:00
世界
7f0419d34c documentation: Certificate store 2025-07-07 14:08:18 +08:00
世界
e758cbcd8c documentation: TLS fragment 2025-07-07 14:08:18 +08:00
世界
8b494f1c4b documentation: Outbound domain resolver 2025-07-07 14:08:17 +08:00
世界
0eaeef81fe documentation: Refactor DNS 2025-07-07 14:08:17 +08:00
世界
25ebabeb63 Add certificate store 2025-07-07 14:08:17 +08:00
世界
d2cb0ee4ae Add TLS fragment support 2025-07-07 14:08:17 +08:00
世界
c81a0ec4d7 refactor: Outbound domain resolver 2025-07-07 14:08:16 +08:00
世界
59fbf08651 refactor: DNS 2025-07-07 14:08:16 +08:00
世界
f792420a78 Bump version 2025-07-07 14:03:10 +08:00
307 changed files with 4495 additions and 16574 deletions

View File

@@ -16,7 +16,7 @@ release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf release/config/sing-box-dbus.xml=/usr/share/dbus-1/system.d/sing-box-dbus.conf
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish

View File

@@ -1,27 +1,25 @@
#!/usr/bin/env bash #!/usr/bin/env bash
VERSION="1.25.1" VERSION="1.23.6"
mkdir -p $HOME/go mkdir -p $HOME/go
cd $HOME/go cd $HOME/go
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz" wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz" tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go go_win7 mv go go_legacy
cd go_win7 cd go_legacy
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.25.x # this patch file only works on golang1.23.x
# that means after golang1.26 release it must be changed # that means after golang1.24 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/ # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
# revert: # revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"' 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/8cb5472d94c34b88733a81091bd328e70ee565a4.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/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1 curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1

View File

@@ -40,13 +40,13 @@ jobs:
version: ${{ steps.outputs.outputs.version }} version: ${{ steps.outputs.outputs.version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.24
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -88,14 +88,13 @@ jobs:
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" } - { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: windows, arch: amd64 } - { os: windows, arch: amd64 }
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" } - { os: windows, arch: amd64, legacy_go: true }
- { os: windows, arch: "386" } - { os: windows, arch: "386" }
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" } - { os: windows, arch: "386", legacy_go: true }
- { os: windows, arch: arm64 } - { os: windows, arch: arm64 }
- { os: darwin, arch: amd64 } - { os: darwin, arch: amd64 }
- { os: darwin, arch: arm64 } - { os: darwin, arch: arm64 }
- { os: darwin, arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" } - { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" } - { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
@@ -103,36 +102,31 @@ jobs:
- { os: android, arch: "386", ndk: "i686-linux-android21" } - { os: android, arch: "386", ndk: "i686-linux-android21" }
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }} if: ${{ ! matrix.legacy_go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.24
- name: Setup Go 1.24 - name: Cache Legacy Go
if: matrix.legacy_go124 if: matrix.require_legacy_go
uses: actions/setup-go@v5 id: cache-legacy-go
with:
go-version: ~1.24.6
- name: Cache Go for Windows 7
if: matrix.legacy_win7
id: cache-go-for-windows7
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: | path: |
~/go/go_win7 ~/go/go_legacy
key: go_win7_1251 key: go_legacy_1236
- name: Setup Go for Windows 7 - name: Setup Legacy Go
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true' if: matrix.legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
run: |- run: |-
.github/setup_go_for_windows7.sh .github/setup_legacy_go.sh
- name: Setup Go for Windows 7 - name: Setup Legacy Go 2
if: matrix.legacy_win7 if: matrix.legacy_go
run: |- run: |-
echo "PATH=$HOME/go/go_win7/bin:$PATH" >> $GITHUB_ENV echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_win7" >> $GITHUB_ENV echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
- name: Setup Android NDK - name: Setup Android NDK
if: matrix.os == 'android' if: matrix.os == 'android'
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -146,7 +140,7 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0' TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build - name: Build
if: matrix.os != 'android' if: matrix.os != 'android'
@@ -154,7 +148,7 @@ jobs:
set -xeuo pipefail set -xeuo pipefail
mkdir -p dist mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \ -ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "0" CGO_ENABLED: "0"
@@ -174,7 +168,7 @@ jobs:
export CXX="${CC}++" export CXX="${CC}++"
mkdir -p dist mkdir -p dist
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \ -ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "1" CGO_ENABLED: "1"
@@ -190,8 +184,8 @@ jobs:
DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}" DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}"
elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then
DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}" DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}"
elif [[ -n "${{ matrix.legacy_name }}" ]]; then elif [[ "${{ matrix.legacy_go }}" == 'true' ]]; then
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}" DIR_NAME="${DIR_NAME}-legacy"
fi fi
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}" echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}" PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
@@ -283,7 +277,7 @@ jobs:
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_name && format('-legacy-{0}', matrix.legacy_name) }} name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_go && '-legacy' || '' }}
path: "dist" path: "dist"
build_android: build_android:
name: Build Android name: Build Android
@@ -293,14 +287,14 @@ jobs:
- calculate_version - calculate_version
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.24
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -373,14 +367,14 @@ jobs:
- calculate_version - calculate_version
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.24
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -432,8 +426,7 @@ jobs:
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }} SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
build_apple: build_apple:
name: Build Apple clients name: Build Apple clients
runs-on: macos-26 runs-on: macos-15
if: false
needs: needs:
- calculate_version - calculate_version
strategy: strategy:
@@ -471,7 +464,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
if: matrix.if if: matrix.if
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: 'recursive' submodules: 'recursive'
@@ -479,7 +472,15 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.24
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Setup Xcode beta
if: matrix.if && github.ref == 'refs/heads/dev-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Set tag - name: Set tag
if: matrix.if if: matrix.if
run: |- run: |-
@@ -623,7 +624,7 @@ jobs:
- build_apple - build_apple
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Cache ghr - name: Cache ghr
@@ -646,7 +647,7 @@ jobs:
git tag v${{ needs.calculate_version.outputs.version }} -f git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV" echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Download builds - name: Download builds
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
path: dist path: dist
merge-multiple: true merge-multiple: true

View File

@@ -39,7 +39,7 @@ jobs:
echo "ref=$ref" echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
ref: ${{ steps.ref.outputs.ref }} ref: ${{ steps.ref.outputs.ref }}
fetch-depth: 0 fetch-depth: 0
@@ -107,7 +107,7 @@ jobs:
echo "latest=$latest" echo "latest=$latest"
echo "latest=$latest" >> $GITHUB_OUTPUT echo "latest=$latest" >> $GITHUB_OUTPUT
- name: Download digests - name: Download digests
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
path: /tmp/digests path: /tmp/digests
pattern: digests-* pattern: digests-*

View File

@@ -22,17 +22,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25 go-version: ^1.24
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v8 uses: golangci/golangci-lint-action@v6
with: with:
version: v2.4.0 version: latest
args: --timeout=30m args: --timeout=30m
install-mode: binary install-mode: binary
verify: false verify: false

View File

@@ -7,11 +7,6 @@ on:
description: "Version name" description: "Version name"
required: true required: true
type: string type: string
forceBeta:
description: "Force beta"
required: false
type: boolean
default: false
release: release:
types: types:
- published - published
@@ -24,13 +19,13 @@ jobs:
version: ${{ steps.outputs.outputs.version }} version: ${{ steps.outputs.outputs.version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.24
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -65,13 +60,13 @@ jobs:
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 } - { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.24
- name: Setup Android NDK - name: Setup Android NDK
if: matrix.os == 'android' if: matrix.os == 'android'
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -85,14 +80,14 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0' TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build - name: Build
run: | run: |
set -xeuo pipefail set -xeuo pipefail
mkdir -p dist mkdir -p dist
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \ go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \ -ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
./cmd/sing-box ./cmd/sing-box
env: env:
CGO_ENABLED: "0" CGO_ENABLED: "0"
@@ -104,11 +99,11 @@ jobs:
run: |- run: |-
TZ=UTC touch -t '197001010000' dist/sing-box TZ=UTC touch -t '197001010000' dist/sing-box
- name: Set name - name: Set name
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta if: ${{ ! contains(needs.calculate_version.outputs.version, '-') }}
run: |- run: |-
echo "NAME=sing-box" >> "$GITHUB_ENV" echo "NAME=sing-box" >> "$GITHUB_ENV"
- name: Set beta name - name: Set beta name
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta if: contains(needs.calculate_version.outputs.version, '-')
run: |- run: |-
echo "NAME=sing-box-beta" >> "$GITHUB_ENV" echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
- name: Set version - name: Set version
@@ -171,7 +166,7 @@ jobs:
- build - build
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set tag - name: Set tag
@@ -180,7 +175,7 @@ jobs:
git tag v${{ needs.calculate_version.outputs.version }} -f git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV" echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Download builds - name: Download builds
uses: actions/download-artifact@v5 uses: actions/download-artifact@v4
with: with:
path: dist path: dist
merge-multiple: true merge-multiple: true

4
.gitignore vendored
View File

@@ -15,6 +15,4 @@
.DS_Store .DS_Store
/config.d/ /config.d/
/venv/ /venv/
CLAUDE.md
AGENTS.md
/.claude/

View File

@@ -1,6 +1,27 @@
version: "2" linters:
disable-all: true
enable:
- gofumpt
- govet
- gci
- staticcheck
- paralleltest
- ineffassign
linters-settings:
gci:
custom-order: true
sections:
- standard
- prefix(github.com/sagernet/)
- default
staticcheck:
checks:
- all
- -SA1003
run: run:
go: "1.25" go: "1.23"
build-tags: build-tags:
- with_gvisor - with_gvisor
- with_quic - with_quic
@@ -9,51 +30,7 @@ run:
- with_utls - with_utls
- with_acme - with_acme
- with_clash_api - with_clash_api
linters:
default: none issues:
enable: exclude-dirs:
- govet - transport/simple-obfs
- ineffassign
- paralleltest
- staticcheck
settings:
staticcheck:
checks:
- all
- -S1000
- -S1008
- -S1017
- -ST1003
- -QF1001
- -QF1003
- -QF1008
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- transport/simple-obfs
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofumpt
settings:
gci:
sections:
- standard
- prefix(github.com/sagernet/)
- default
custom-order: true
exclusions:
generated: lax
paths:
- transport/simple-obfs
- third_party$
- builtin$
- examples$

View File

@@ -59,8 +59,8 @@ nfpms:
dst: /usr/lib/sysusers.d/sing-box.conf dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules - src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml - src: release/config/sing-box-dbus.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf dst: /usr/share/dbus-1/system.d/sing-box-dbus.conf
- src: release/completions/sing-box.bash - src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@@ -140,8 +140,8 @@ nfpms:
dst: /usr/lib/sysusers.d/sing-box.conf dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules - src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml - src: release/config/sing-box-dbus.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf dst: /usr/share/dbus-1/system.d/sing-box-dbus.conf
- src: release/completions/sing-box.bash - src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
LABEL maintainer="nekohasekai <contact-git@sekai.icu>" LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
COPY . /go/src/github.com/sagernet/sing-box COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /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 COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \ && export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \ && go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0" \ "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale" \
-o /go/bin/sing-box \ -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 ./cmd/sing-box
FROM --platform=$TARGETPLATFORM alpine AS dist FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>" LABEL maintainer="nekohasekai <contact-git@sekai.icu>"

View File

@@ -1,12 +1,12 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0 TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH) GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest) VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid= -checklinkname=0" PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)" MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
MAIN = ./cmd/sing-box MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH) PREFIX ?= $(shell go env GOPATH)
@@ -17,10 +17,6 @@ build:
export GOTOOLCHAIN=local && \ export GOTOOLCHAIN=local && \
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
race:
export GOTOOLCHAIN=local && \
go build -race $(MAIN_PARAMS) $(MAIN)
ci_build: ci_build:
export GOTOOLCHAIN=local && \ export GOTOOLCHAIN=local && \
go build $(PARAMS) $(MAIN) && \ go build $(PARAMS) $(MAIN) && \
@@ -38,7 +34,7 @@ fmt:
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" . @gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
fmt_install: fmt_install:
go install -v mvdan.cc/gofumpt@v0.8.0 go install -v mvdan.cc/gofumpt@latest
go install -v github.com/daixiang0/gci@latest go install -v github.com/daixiang0/gci@latest
lint: lint:
@@ -49,7 +45,7 @@ lint:
GOOS=freebsd golangci-lint run ./... GOOS=freebsd golangci-lint run ./...
lint_install: lint_install:
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0 go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
proto: proto:
@go run ./cmd/internal/protogen @go run ./cmd/internal/protogen
@@ -249,8 +245,8 @@ lib:
go run ./cmd/internal/build_libbox -target ios go run ./cmd/internal/build_libbox -target ios
lib_install: lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.8 go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.7
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.8 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.7
docs: docs:
venv/bin/mkdocs serve venv/bin/mkdocs serve

View File

@@ -5,6 +5,7 @@ import (
"net/netip" "net/netip"
"time" "time"
"github.com/sagernet/sing-box/common/process"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
@@ -56,7 +57,6 @@ type InboundContext struct {
Domain string Domain string
Client string Client string
SniffContext any SniffContext any
SnifferNames []string
SniffError error SniffError error
// cache // cache
@@ -84,7 +84,7 @@ type InboundContext struct {
DestinationAddresses []netip.Addr DestinationAddresses []netip.Addr
SourceGeoIPCode string SourceGeoIPCode string
GeoIPCode string GeoIPCode string
ProcessInfo *ConnectionOwner ProcessInfo *process.Info
QueryType uint16 QueryType uint16
FakeIP bool FakeIP bool
@@ -135,7 +135,8 @@ func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
func OverrideContext(ctx context.Context) context.Context { func OverrideContext(ctx context.Context) context.Context {
if metadata := ContextFrom(ctx); metadata != nil { if metadata := ContextFrom(ctx); metadata != nil {
newMetadata := *metadata var newMetadata InboundContext
newMetadata = *metadata
return WithContext(ctx, &newMetadata) return WithContext(ctx, &newMetadata)
} }
return ctx return ctx

View File

@@ -10,7 +10,6 @@ import (
type NetworkManager interface { type NetworkManager interface {
Lifecycle Lifecycle
Initialize(ruleSets []RuleSet)
InterfaceFinder() control.InterfaceFinder InterfaceFinder() control.InterfaceFinder
UpdateInterfaces() error UpdateInterfaces() error
DefaultNetworkInterface() *NetworkInterface DefaultNetworkInterface() *NetworkInterface
@@ -21,14 +20,12 @@ type NetworkManager interface {
DefaultOptions() NetworkOptions DefaultOptions() NetworkOptions
RegisterAutoRedirectOutputMark(mark uint32) error RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32 AutoRedirectOutputMark() uint32
AutoRedirectOutputMarkFunc() control.Func
NetworkMonitor() tun.NetworkUpdateMonitor NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager PackageManager() tun.PackageManager
NeedWIFIState() bool
WIFIState() WIFIState WIFIState() WIFIState
UpdateWIFIState()
ResetNetwork() ResetNetwork()
UpdateWIFIState()
} }
type NetworkOptions struct { type NetworkOptions struct {

View File

@@ -2,12 +2,9 @@ package adapter
import ( import (
"context" "context"
"net/netip"
"time"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
@@ -21,17 +18,6 @@ type Outbound interface {
N.Dialer N.Dialer
} }
type OutboundWithPreferredRoutes interface {
Outbound
PreferredDomain(domain string) bool
PreferredAddress(address netip.Addr) bool
}
type DirectRouteOutbound interface {
Outbound
NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
}
type OutboundRegistry interface { type OutboundRegistry interface {
option.OutboundOptionsRegistry option.OutboundOptionsRegistry
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error) CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)

View File

@@ -30,7 +30,7 @@ type Manager struct {
outboundByTag map[string]adapter.Outbound outboundByTag map[string]adapter.Outbound
dependByTag map[string][]string dependByTag map[string][]string
defaultOutbound adapter.Outbound defaultOutbound adapter.Outbound
defaultOutboundFallback func() (adapter.Outbound, error) defaultOutboundFallback adapter.Outbound
} }
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager { func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
@@ -44,7 +44,7 @@ func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry,
} }
} }
func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, error)) { func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) {
m.defaultOutboundFallback = defaultOutboundFallback m.defaultOutboundFallback = defaultOutboundFallback
} }
@@ -55,31 +55,18 @@ func (m *Manager) Start(stage adapter.StartStage) error {
} }
m.started = true m.started = true
m.stage = stage m.stage = stage
outbounds := m.outbounds
m.access.Unlock()
if stage == adapter.StartStateStart { if stage == adapter.StartStateStart {
if m.defaultTag != "" && m.defaultOutbound == nil { if m.defaultTag != "" && m.defaultOutbound == nil {
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag) defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
if !loaded { if !loaded {
m.access.Unlock()
return E.New("default outbound not found: ", m.defaultTag) return E.New("default outbound not found: ", m.defaultTag)
} }
m.defaultOutbound = defaultEndpoint m.defaultOutbound = defaultEndpoint
} }
if m.defaultOutbound == nil {
directOutbound, err := m.defaultOutboundFallback()
if err != nil {
m.access.Unlock()
return E.Cause(err, "create direct outbound for fallback")
}
m.outbounds = append(m.outbounds, directOutbound)
m.outboundByTag[directOutbound.Tag()] = directOutbound
m.defaultOutbound = directOutbound
}
outbounds := m.outbounds
m.access.Unlock()
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...)) return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
} else { } else {
outbounds := m.outbounds
m.access.Unlock()
for _, outbound := range outbounds { for _, outbound := range outbounds {
err := adapter.LegacyStart(outbound, stage) err := adapter.LegacyStart(outbound, stage)
if err != nil { if err != nil {
@@ -200,7 +187,11 @@ func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
func (m *Manager) Default() adapter.Outbound { func (m *Manager) Default() adapter.Outbound {
m.access.RLock() m.access.RLock()
defer m.access.RUnlock() defer m.access.RUnlock()
return m.defaultOutbound if m.defaultOutbound != nil {
return m.defaultOutbound
} else {
return m.defaultOutboundFallback
}
} }
func (m *Manager) Remove(tag string) error { func (m *Manager) Remove(tag string) error {

View File

@@ -1,70 +0,0 @@
package adapter
import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/logger"
)
type PlatformInterface interface {
Initialize(networkManager NetworkManager) error
UsePlatformAutoDetectInterfaceControl() bool
AutoDetectInterfaceControl(fd int) error
UsePlatformInterface() bool
OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
UsePlatformDefaultInterfaceMonitor() bool
CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
UsePlatformNetworkInterfaces() bool
NetworkInterfaces() ([]NetworkInterface, error)
UnderNetworkExtension() bool
NetworkExtensionIncludeAllNetworks() bool
ClearDNSCache()
RequestPermissionForWIFIState() error
ReadWIFIState() WIFIState
SystemCertificates() []string
UsePlatformConnectionOwnerFinder() bool
FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error)
UsePlatformWIFIMonitor() bool
UsePlatformNotification() bool
SendNotification(notification *Notification) error
}
type FindConnectionOwnerRequest struct {
IpProtocol int32
SourceAddress string
SourcePort int32
DestinationAddress string
DestinationPort int32
}
type ConnectionOwner struct {
ProcessID uint32
UserId int32
UserName string
ProcessPath string
AndroidPackageName string
}
type Notification struct {
Identifier string
TypeName string
TypeID int32
Title string
Subtitle string
Body string
OpenURL string
}
type SystemProxyStatus struct {
Available bool
Enabled bool
}

View File

@@ -6,10 +6,8 @@ import (
"net" "net"
"net/http" "net/http"
"sync" "sync"
"time"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-tun"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
@@ -21,9 +19,10 @@ import (
type Router interface { type Router interface {
Lifecycle Lifecycle
ConnectionRouter ConnectionRouter
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) PreMatch(metadata InboundContext) error
ConnectionRouterEx ConnectionRouterEx
RuleSet(tag string) (RuleSet, bool) RuleSet(tag string) (RuleSet, bool)
NeedWIFIState() bool
Rules() []Rule Rules() []Rule
AppendTracker(tracker ConnectionTracker) AppendTracker(tracker ConnectionTracker)
ResetNetwork() ResetNetwork()

View File

@@ -78,8 +78,8 @@ func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
// Deprecated: removed // Deprecated: removed
func UpstreamMetadata(metadata InboundContext) M.Metadata { func UpstreamMetadata(metadata InboundContext) M.Metadata {
return M.Metadata{ return M.Metadata{
Source: metadata.Source.Unwrap(), Source: metadata.Source,
Destination: metadata.Destination.Unwrap(), Destination: metadata.Destination,
} }
} }

24
box.go
View File

@@ -22,6 +22,7 @@ import (
"github.com/sagernet/sing-box/dns/transport/local" "github.com/sagernet/sing-box/dns/transport/local"
"github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/direct" "github.com/sagernet/sing-box/protocol/direct"
@@ -138,7 +139,7 @@ func New(options Options) (*Box, error) {
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
needV2RayAPI = true needV2RayAPI = true
} }
platformInterface := service.FromContext[adapter.PlatformInterface](ctx) platformInterface := service.FromContext[platform.Interface](ctx)
var defaultLogWriter io.Writer var defaultLogWriter io.Writer
if platformInterface != nil { if platformInterface != nil {
defaultLogWriter = io.Discard defaultLogWriter = io.Discard
@@ -183,7 +184,7 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.ServiceManager](ctx, serviceManager) service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions) networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
if err != nil { if err != nil {
return nil, E.Cause(err, "initialize network manager") return nil, E.Cause(err, "initialize network manager")
} }
@@ -313,23 +314,22 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize service[", i, "]") return nil, E.Cause(err, "initialize service[", i, "]")
} }
} }
outboundManager.Initialize(func() (adapter.Outbound, error) { outboundManager.Initialize(common.Must1(
return direct.NewOutbound( direct.NewOutbound(
ctx, ctx,
router, router,
logFactory.NewLogger("outbound/direct"), logFactory.NewLogger("outbound/direct"),
"direct", "direct",
option.DirectOutboundOptions{}, option.DirectOutboundOptions{},
) ),
}) ))
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) { dnsTransportManager.Initialize(common.Must1(
return local.NewTransport( local.NewTransport(
ctx, ctx,
logFactory.NewLogger("dns/local"), logFactory.NewLogger("dns/local"),
"local", "local",
option.LocalDNSServerOptions{}, option.LocalDNSServerOptions{},
) )))
})
if platformInterface != nil { if platformInterface != nil {
err = platformInterface.Initialize(networkManager) err = platformInterface.Initialize(networkManager)
if err != nil { if err != nil {
@@ -526,7 +526,3 @@ func (s *Box) Inbound() adapter.InboundManager {
func (s *Box) Outbound() adapter.OutboundManager { func (s *Box) Outbound() adapter.OutboundManager {
return s.outbound return s.outbound
} }
func (s *Box) LogFactory() log.Factory {
return s.logFactory
}

View File

@@ -134,7 +134,6 @@ func publishTestflight(ctx context.Context) error {
asc.PlatformTVOS, asc.PlatformTVOS,
} }
} }
waitingForProcess := false
for _, platform := range platforms { for _, platform := range platforms {
log.Info(string(platform), " list builds") log.Info(string(platform), " list builds")
for { for {
@@ -146,13 +145,12 @@ func publishTestflight(ctx context.Context) error {
return err return err
} }
build := builds.Data[0] build := builds.Data[0]
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) { if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
log.Info(string(platform), " ", tag, " waiting for process") log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second) time.Sleep(15 * time.Second)
continue continue
} }
if *build.Attributes.ProcessingState != "VALID" { if *build.Attributes.ProcessingState != "VALID" {
waitingForProcess = true
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState) log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second) time.Sleep(15 * time.Second)
continue continue

View File

@@ -46,7 +46,7 @@ var (
sharedFlags []string sharedFlags []string
debugFlags []string debugFlags []string
sharedTags []string sharedTags []string
macOSTags []string darwinTags []string
memcTags []string memcTags []string
notMemcTags []string notMemcTags []string
debugTags []string debugTags []string
@@ -59,11 +59,11 @@ func init() {
if err != nil { if err != nil {
currentTag = "unknown" 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+" -checklinkname=0") debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0") sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
macOSTags = append(macOSTags, "with_dhcp") darwinTags = append(darwinTags, "with_dhcp")
memcTags = append(memcTags, "with_tailscale") memcTags = append(memcTags, "with_tailscale")
notMemcTags = append(notMemcTags, "with_low_memory") notMemcTags = append(notMemcTags, "with_low_memory")
debugTags = append(debugTags, "debug") debugTags = append(debugTags, "debug")
@@ -158,9 +158,7 @@ func buildApple() {
"-tags-not-macos=with_low_memory", "-tags-not-macos=with_low_memory",
} }
if !withTailscale { if !withTailscale {
args = append(args, "-tags-macos="+strings.Join(append(macOSTags, memcTags...), ",")) args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
} else {
args = append(args, "-tags-macos="+strings.Join(macOSTags, ","))
} }
if !debugEnabled { if !debugEnabled {
@@ -169,7 +167,7 @@ func buildApple() {
args = append(args, debugFlags...) args = append(args, debugFlags...)
} }
tags := sharedTags tags := append(sharedTags, darwinTags...)
if withTailscale { if withTailscale {
tags = append(tags, memcTags...) tags = append(tags, memcTags...)
} }

View File

@@ -1,284 +0,0 @@
package main
import (
"context"
"fmt"
"io"
"net/netip"
"os"
"os/exec"
"strings"
"syscall"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/shell"
)
var iperf3Path string
func main() {
err := main0()
if err != nil {
log.Fatal(err)
}
}
func main0() error {
err := shell.Exec("sudo", "ls").Run()
if err != nil {
return err
}
results, err := runTests()
if err != nil {
return err
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(results)
}
func runTests() ([]TestResult, error) {
boxPaths := []string{
os.ExpandEnv("$HOME/Downloads/sing-box-1.11.15-darwin-arm64/sing-box"),
//"/Users/sekai/Downloads/sing-box-1.11.15-linux-arm64/sing-box",
"./sing-box",
}
stacks := []string{
"gvisor",
"system",
}
mtus := []int{
1500,
4064,
// 16384,
// 32768,
// 49152,
65535,
}
flagList := [][]string{
{},
}
var results []TestResult
for _, boxPath := range boxPaths {
for _, stack := range stacks {
for _, mtu := range mtus {
if strings.HasPrefix(boxPath, ".") {
for _, flags := range flagList {
result, err := testOnce(boxPath, stack, mtu, false, flags)
if err != nil {
return nil, err
}
results = append(results, *result)
}
} else {
result, err := testOnce(boxPath, stack, mtu, false, nil)
if err != nil {
return nil, err
}
results = append(results, *result)
}
}
}
}
return results, nil
}
type TestResult struct {
BoxPath string `json:"box_path"`
Stack string `json:"stack"`
MTU int `json:"mtu"`
Flags []string `json:"flags"`
MultiThread bool `json:"multi_thread"`
UploadSpeed string `json:"upload_speed"`
DownloadSpeed string `json:"download_speed"`
}
func testOnce(boxPath string, stackName string, mtu int, multiThread bool, flags []string) (result *TestResult, err error) {
testAddress := netip.MustParseAddr("1.1.1.1")
testConfig := option.Options{
Inbounds: []option.Inbound{
{
Type: C.TypeTun,
Options: &option.TunInboundOptions{
Address: []netip.Prefix{netip.MustParsePrefix("172.18.0.1/30")},
AutoRoute: true,
MTU: uint32(mtu),
Stack: stackName,
RouteAddress: []netip.Prefix{netip.PrefixFrom(testAddress, testAddress.BitLen())},
},
},
},
Route: &option.RouteOptions{
Rules: []option.Rule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
RawDefaultRule: option.RawDefaultRule{
IPCIDR: []string{testAddress.String()},
},
RuleAction: option.RuleAction{
Action: C.RuleActionTypeRouteOptions,
RouteOptionsOptions: option.RouteOptionsActionOptions{
OverrideAddress: "127.0.0.1",
},
},
},
},
},
AutoDetectInterface: true,
},
}
ctx := include.Context(context.Background())
tempConfig, err := os.CreateTemp("", "tun-bench-*.json")
if err != nil {
return
}
defer os.Remove(tempConfig.Name())
encoder := json.NewEncoderContext(ctx, tempConfig)
encoder.SetIndent("", " ")
err = encoder.Encode(testConfig)
if err != nil {
return nil, E.Cause(err, "encode test config")
}
tempConfig.Close()
var sudoArgs []string
if len(flags) > 0 {
sudoArgs = append(sudoArgs, "env")
sudoArgs = append(sudoArgs, flags...)
}
sudoArgs = append(sudoArgs, boxPath, "run", "-c", tempConfig.Name())
boxProcess := shell.Exec("sudo", sudoArgs...)
boxProcess.Stdout = &stderrWriter{}
boxProcess.Stderr = io.Discard
err = boxProcess.Start()
if err != nil {
return
}
if C.IsDarwin {
iperf3Path, err = exec.LookPath("iperf3-darwin")
} else {
iperf3Path, err = exec.LookPath("iperf3")
}
if err != nil {
return
}
serverProcess := shell.Exec(iperf3Path, "-s")
serverProcess.Stdout = io.Discard
serverProcess.Stderr = io.Discard
err = serverProcess.Start()
if err != nil {
return nil, E.Cause(err, "start iperf3 server")
}
time.Sleep(time.Second)
args := []string{"-c", testAddress.String()}
if multiThread {
args = append(args, "-P", "10")
}
uploadProcess := shell.Exec(iperf3Path, args...)
output, err := uploadProcess.Read()
if err != nil {
boxProcess.Process.Signal(syscall.SIGKILL)
serverProcess.Process.Signal(syscall.SIGKILL)
println(output)
return
}
uploadResult := common.SubstringBeforeLast(output, "iperf Done.")
uploadResult = common.SubstringBeforeLast(uploadResult, "sender")
uploadResult = common.SubstringBeforeLast(uploadResult, "bits/sec")
uploadResult = common.SubstringAfterLast(uploadResult, "Bytes")
uploadResult = strings.ReplaceAll(uploadResult, " ", "")
result = &TestResult{
BoxPath: boxPath,
Stack: stackName,
MTU: mtu,
Flags: flags,
MultiThread: multiThread,
UploadSpeed: uploadResult,
}
downloadProcess := shell.Exec(iperf3Path, append(args, "-R")...)
output, err = downloadProcess.Read()
if err != nil {
boxProcess.Process.Signal(syscall.SIGKILL)
serverProcess.Process.Signal(syscall.SIGKILL)
println(output)
return
}
downloadResult := common.SubstringBeforeLast(output, "iperf Done.")
downloadResult = common.SubstringBeforeLast(downloadResult, "receiver")
downloadResult = common.SubstringBeforeLast(downloadResult, "bits/sec")
downloadResult = common.SubstringAfterLast(downloadResult, "Bytes")
downloadResult = strings.ReplaceAll(downloadResult, " ", "")
result.DownloadSpeed = downloadResult
printArgs := []any{boxPath, stackName, mtu, "upload", uploadResult, "download", downloadResult}
if len(flags) > 0 {
printArgs = append(printArgs, "flags", strings.Join(flags, " "))
}
if multiThread {
printArgs = append(printArgs, "(-P 10)")
}
fmt.Println(printArgs...)
err = boxProcess.Process.Signal(syscall.SIGTERM)
if err != nil {
return
}
err = serverProcess.Process.Signal(syscall.SIGTERM)
if err != nil {
return
}
boxDone := make(chan struct{})
go func() {
boxProcess.Cmd.Wait()
close(boxDone)
}()
serverDone := make(chan struct{})
go func() {
serverProcess.Process.Wait()
close(serverDone)
}()
select {
case <-boxDone:
case <-time.After(2 * time.Second):
boxProcess.Process.Kill()
case <-time.After(4 * time.Second):
println("box process did not close!")
os.Exit(1)
}
select {
case <-serverDone:
case <-time.After(2 * time.Second):
serverProcess.Process.Kill()
case <-time.After(4 * time.Second):
println("server process did not close!")
os.Exit(1)
}
return
}
type stderrWriter struct{}
func (w *stderrWriter) Write(p []byte) (n int, err error) {
return os.Stderr.Write(p)
}

View File

@@ -6,10 +6,8 @@ import (
"strings" "strings"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -71,7 +69,7 @@ func compileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
err = srs.Write(outputFile, plainRuleSet.Options, downgradeRuleSetVersion(plainRuleSet.Version, plainRuleSet.Options)) err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version)
if err != nil { if err != nil {
outputFile.Close() outputFile.Close()
os.Remove(outputPath) os.Remove(outputPath)
@@ -80,18 +78,3 @@ func compileRuleSet(sourcePath string) error {
outputFile.Close() outputFile.Close()
return nil return nil
} }
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
len(rule.DefaultInterfaceAddress) > 0
}) {
version = C.RuleSetVersion3
}
if version == C.RuleSetVersion3 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
return len(rule.NetworkType) > 0 || rule.NetworkIsExpensive || rule.NetworkIsConstrained
}) {
version = C.RuleSetVersion2
}
return version
}

View File

@@ -22,7 +22,7 @@ func initializeHTTP3Client(instance *box.Box) error {
} }
http3Client = &http.Client{ http3Client = &http.Client{
Transport: &http3.Transport{ Transport: &http3.Transport{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) { Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
destination := M.ParseSocksaddr(addr) destination := M.ParseSocksaddr(addr)
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination) udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
if dErr != nil { if dErr != nil {

View File

@@ -1,176 +0,0 @@
//go:build go1.25 && badlinkname
package badtls
import (
"bytes"
"os"
"reflect"
"sync/atomic"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/tls"
)
type RawConn struct {
pointer unsafe.Pointer
methods *Methods
IsClient *bool
IsHandshakeComplete *atomic.Bool
Vers *uint16
CipherSuite *uint16
RawInput *bytes.Buffer
Input *bytes.Reader
Hand *bytes.Buffer
CloseNotifySent *bool
CloseNotifyErr *error
In *RawHalfConn
Out *RawHalfConn
BytesSent *int64
PacketsSent *int64
ActiveCall *atomic.Int32
Tmp *[16]byte
}
func NewRawConn(rawTLSConn tls.Conn) (*RawConn, error) {
var (
pointer unsafe.Pointer
methods *Methods
loaded bool
)
for _, tlsCreator := range methodRegistry {
pointer, methods, loaded = tlsCreator(rawTLSConn)
if loaded {
break
}
}
if !loaded {
return nil, os.ErrInvalid
}
conn := &RawConn{
pointer: pointer,
methods: methods,
}
rawConn := reflect.Indirect(reflect.ValueOf(rawTLSConn))
rawIsClient := rawConn.FieldByName("isClient")
if !rawIsClient.IsValid() || rawIsClient.Kind() != reflect.Bool {
return nil, E.New("invalid Conn.isClient")
}
conn.IsClient = (*bool)(unsafe.Pointer(rawIsClient.UnsafeAddr()))
rawIsHandshakeComplete := rawConn.FieldByName("isHandshakeComplete")
if !rawIsHandshakeComplete.IsValid() || rawIsHandshakeComplete.Kind() != reflect.Struct {
return nil, E.New("invalid Conn.isHandshakeComplete")
}
conn.IsHandshakeComplete = (*atomic.Bool)(unsafe.Pointer(rawIsHandshakeComplete.UnsafeAddr()))
rawVers := rawConn.FieldByName("vers")
if !rawVers.IsValid() || rawVers.Kind() != reflect.Uint16 {
return nil, E.New("invalid Conn.vers")
}
conn.Vers = (*uint16)(unsafe.Pointer(rawVers.UnsafeAddr()))
rawCipherSuite := rawConn.FieldByName("cipherSuite")
if !rawCipherSuite.IsValid() || rawCipherSuite.Kind() != reflect.Uint16 {
return nil, E.New("invalid Conn.cipherSuite")
}
conn.CipherSuite = (*uint16)(unsafe.Pointer(rawCipherSuite.UnsafeAddr()))
rawRawInput := rawConn.FieldByName("rawInput")
if !rawRawInput.IsValid() || rawRawInput.Kind() != reflect.Struct {
return nil, E.New("invalid Conn.rawInput")
}
conn.RawInput = (*bytes.Buffer)(unsafe.Pointer(rawRawInput.UnsafeAddr()))
rawInput := rawConn.FieldByName("input")
if !rawInput.IsValid() || rawInput.Kind() != reflect.Struct {
return nil, E.New("invalid Conn.input")
}
conn.Input = (*bytes.Reader)(unsafe.Pointer(rawInput.UnsafeAddr()))
rawHand := rawConn.FieldByName("hand")
if !rawHand.IsValid() || rawHand.Kind() != reflect.Struct {
return nil, E.New("invalid Conn.hand")
}
conn.Hand = (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
rawCloseNotifySent := rawConn.FieldByName("closeNotifySent")
if !rawCloseNotifySent.IsValid() || rawCloseNotifySent.Kind() != reflect.Bool {
return nil, E.New("invalid Conn.closeNotifySent")
}
conn.CloseNotifySent = (*bool)(unsafe.Pointer(rawCloseNotifySent.UnsafeAddr()))
rawCloseNotifyErr := rawConn.FieldByName("closeNotifyErr")
if !rawCloseNotifyErr.IsValid() || rawCloseNotifyErr.Kind() != reflect.Interface {
return nil, E.New("invalid Conn.closeNotifyErr")
}
conn.CloseNotifyErr = (*error)(unsafe.Pointer(rawCloseNotifyErr.UnsafeAddr()))
rawIn := rawConn.FieldByName("in")
if !rawIn.IsValid() || rawIn.Kind() != reflect.Struct {
return nil, E.New("invalid Conn.in")
}
halfIn, err := NewRawHalfConn(rawIn, methods)
if err != nil {
return nil, E.Cause(err, "invalid Conn.in")
}
conn.In = halfIn
rawOut := rawConn.FieldByName("out")
if !rawOut.IsValid() || rawOut.Kind() != reflect.Struct {
return nil, E.New("invalid Conn.out")
}
halfOut, err := NewRawHalfConn(rawOut, methods)
if err != nil {
return nil, E.Cause(err, "invalid Conn.out")
}
conn.Out = halfOut
rawBytesSent := rawConn.FieldByName("bytesSent")
if !rawBytesSent.IsValid() || rawBytesSent.Kind() != reflect.Int64 {
return nil, E.New("invalid Conn.bytesSent")
}
conn.BytesSent = (*int64)(unsafe.Pointer(rawBytesSent.UnsafeAddr()))
rawPacketsSent := rawConn.FieldByName("packetsSent")
if !rawPacketsSent.IsValid() || rawPacketsSent.Kind() != reflect.Int64 {
return nil, E.New("invalid Conn.packetsSent")
}
conn.PacketsSent = (*int64)(unsafe.Pointer(rawPacketsSent.UnsafeAddr()))
rawActiveCall := rawConn.FieldByName("activeCall")
if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Struct {
return nil, E.New("invalid Conn.activeCall")
}
conn.ActiveCall = (*atomic.Int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr()))
rawTmp := rawConn.FieldByName("tmp")
if !rawTmp.IsValid() || rawTmp.Kind() != reflect.Array || rawTmp.Len() != 16 || rawTmp.Type().Elem().Kind() != reflect.Uint8 {
return nil, E.New("invalid Conn.tmp")
}
conn.Tmp = (*[16]byte)(unsafe.Pointer(rawTmp.UnsafeAddr()))
return conn, nil
}
func (c *RawConn) ReadRecord() error {
return c.methods.readRecord(c.pointer)
}
func (c *RawConn) HandlePostHandshakeMessage() error {
return c.methods.handlePostHandshakeMessage(c.pointer)
}
func (c *RawConn) WriteRecordLocked(typ uint16, data []byte) (int, error) {
return c.methods.writeRecordLocked(c.pointer, typ, data)
}

View File

@@ -1,121 +0,0 @@
//go:build go1.25 && badlinkname
package badtls
import (
"hash"
"reflect"
"sync"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
)
type RawHalfConn struct {
pointer unsafe.Pointer
methods *Methods
*sync.Mutex
Err *error
Version *uint16
Cipher *any
Seq *[8]byte
ScratchBuf *[13]byte
TrafficSecret *[]byte
Mac *hash.Hash
RawKey *[]byte
RawIV *[]byte
RawMac *[]byte
}
func NewRawHalfConn(rawHalfConn reflect.Value, methods *Methods) (*RawHalfConn, error) {
halfConn := &RawHalfConn{
pointer: (unsafe.Pointer)(rawHalfConn.UnsafeAddr()),
methods: methods,
}
rawMutex := rawHalfConn.FieldByName("Mutex")
if !rawMutex.IsValid() || rawMutex.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid halfConn.Mutex")
}
halfConn.Mutex = (*sync.Mutex)(unsafe.Pointer(rawMutex.UnsafeAddr()))
rawErr := rawHalfConn.FieldByName("err")
if !rawErr.IsValid() || rawErr.Kind() != reflect.Interface {
return nil, E.New("badtls: invalid halfConn.err")
}
halfConn.Err = (*error)(unsafe.Pointer(rawErr.UnsafeAddr()))
rawVersion := rawHalfConn.FieldByName("version")
if !rawVersion.IsValid() || rawVersion.Kind() != reflect.Uint16 {
return nil, E.New("badtls: invalid halfConn.version")
}
halfConn.Version = (*uint16)(unsafe.Pointer(rawVersion.UnsafeAddr()))
rawCipher := rawHalfConn.FieldByName("cipher")
if !rawCipher.IsValid() || rawCipher.Kind() != reflect.Interface {
return nil, E.New("badtls: invalid halfConn.cipher")
}
halfConn.Cipher = (*any)(unsafe.Pointer(rawCipher.UnsafeAddr()))
rawSeq := rawHalfConn.FieldByName("seq")
if !rawSeq.IsValid() || rawSeq.Kind() != reflect.Array || rawSeq.Len() != 8 || rawSeq.Type().Elem().Kind() != reflect.Uint8 {
return nil, E.New("badtls: invalid halfConn.seq")
}
halfConn.Seq = (*[8]byte)(unsafe.Pointer(rawSeq.UnsafeAddr()))
rawScratchBuf := rawHalfConn.FieldByName("scratchBuf")
if !rawScratchBuf.IsValid() || rawScratchBuf.Kind() != reflect.Array || rawScratchBuf.Len() != 13 || rawScratchBuf.Type().Elem().Kind() != reflect.Uint8 {
return nil, E.New("badtls: invalid halfConn.scratchBuf")
}
halfConn.ScratchBuf = (*[13]byte)(unsafe.Pointer(rawScratchBuf.UnsafeAddr()))
rawTrafficSecret := rawHalfConn.FieldByName("trafficSecret")
if !rawTrafficSecret.IsValid() || rawTrafficSecret.Kind() != reflect.Slice || rawTrafficSecret.Type().Elem().Kind() != reflect.Uint8 {
return nil, E.New("badtls: invalid halfConn.trafficSecret")
}
halfConn.TrafficSecret = (*[]byte)(unsafe.Pointer(rawTrafficSecret.UnsafeAddr()))
rawMac := rawHalfConn.FieldByName("mac")
if !rawMac.IsValid() || rawMac.Kind() != reflect.Interface {
return nil, E.New("badtls: invalid halfConn.mac")
}
halfConn.Mac = (*hash.Hash)(unsafe.Pointer(rawMac.UnsafeAddr()))
rawKey := rawHalfConn.FieldByName("rawKey")
if rawKey.IsValid() {
if /*!rawKey.IsValid() || */ rawKey.Kind() != reflect.Slice || rawKey.Type().Elem().Kind() != reflect.Uint8 {
return nil, E.New("badtls: invalid halfConn.rawKey")
}
halfConn.RawKey = (*[]byte)(unsafe.Pointer(rawKey.UnsafeAddr()))
rawIV := rawHalfConn.FieldByName("rawIV")
if !rawIV.IsValid() || rawIV.Kind() != reflect.Slice || rawIV.Type().Elem().Kind() != reflect.Uint8 {
return nil, E.New("badtls: invalid halfConn.rawIV")
}
halfConn.RawIV = (*[]byte)(unsafe.Pointer(rawIV.UnsafeAddr()))
rawMAC := rawHalfConn.FieldByName("rawMac")
if !rawMAC.IsValid() || rawMAC.Kind() != reflect.Slice || rawMAC.Type().Elem().Kind() != reflect.Uint8 {
return nil, E.New("badtls: invalid halfConn.rawMac")
}
halfConn.RawMac = (*[]byte)(unsafe.Pointer(rawMAC.UnsafeAddr()))
}
return halfConn, nil
}
func (hc *RawHalfConn) Decrypt(record []byte) ([]byte, uint8, error) {
return hc.methods.decrypt(hc.pointer, record)
}
func (hc *RawHalfConn) SetErrorLocked(err error) error {
return hc.methods.setErrorLocked(hc.pointer, err)
}
func (hc *RawHalfConn) SetTrafficSecret(suite unsafe.Pointer, level int, secret []byte) {
hc.methods.setTrafficSecret(hc.pointer, suite, level, secret)
}
func (hc *RawHalfConn) ExplicitNonceLen() int {
return hc.methods.explicitNonceLen(hc.pointer)
}

View File

@@ -1,9 +1,18 @@
//go:build go1.25 && badlinkname //go:build go1.21 && !without_badtls
package badtls package badtls
import ( import (
"bytes"
"context"
"net"
"os"
"reflect"
"sync"
"unsafe"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/tls" "github.com/sagernet/sing/common/tls"
) )
@@ -12,21 +21,63 @@ var _ N.ReadWaiter = (*ReadWaitConn)(nil)
type ReadWaitConn struct { type ReadWaitConn struct {
tls.Conn tls.Conn
rawConn *RawConn halfAccess *sync.Mutex
readWaitOptions N.ReadWaitOptions rawInput *bytes.Buffer
input *bytes.Reader
hand *bytes.Buffer
readWaitOptions N.ReadWaitOptions
tlsReadRecord func() error
tlsHandlePostHandshakeMessage func() error
} }
func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) { func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
if _, isReadWaitConn := conn.(N.ReadWaiter); isReadWaitConn { var (
return conn, nil loaded bool
tlsReadRecord func() error
tlsHandlePostHandshakeMessage func() error
)
for _, tlsCreator := range tlsRegistry {
loaded, tlsReadRecord, tlsHandlePostHandshakeMessage = tlsCreator(conn)
if loaded {
break
}
} }
rawConn, err := NewRawConn(conn) if !loaded {
if err != nil { return nil, os.ErrInvalid
return nil, err
} }
rawConn := reflect.Indirect(reflect.ValueOf(conn))
rawHalfConn := rawConn.FieldByName("in")
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid half conn")
}
rawHalfMutex := rawHalfConn.FieldByName("Mutex")
if !rawHalfMutex.IsValid() || rawHalfMutex.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid half mutex")
}
halfAccess := (*sync.Mutex)(unsafe.Pointer(rawHalfMutex.UnsafeAddr()))
rawRawInput := rawConn.FieldByName("rawInput")
if !rawRawInput.IsValid() || rawRawInput.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid raw input")
}
rawInput := (*bytes.Buffer)(unsafe.Pointer(rawRawInput.UnsafeAddr()))
rawInput0 := rawConn.FieldByName("input")
if !rawInput0.IsValid() || rawInput0.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid input")
}
input := (*bytes.Reader)(unsafe.Pointer(rawInput0.UnsafeAddr()))
rawHand := rawConn.FieldByName("hand")
if !rawHand.IsValid() || rawHand.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid hand")
}
hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
return &ReadWaitConn{ return &ReadWaitConn{
Conn: conn, Conn: conn,
rawConn: rawConn, halfAccess: halfAccess,
rawInput: rawInput,
input: input,
hand: hand,
tlsReadRecord: tlsReadRecord,
tlsHandlePostHandshakeMessage: tlsHandlePostHandshakeMessage,
}, nil }, nil
} }
@@ -36,36 +87,36 @@ func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy
} }
func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) { func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
//err = c.HandshakeContext(context.Background()) err = c.HandshakeContext(context.Background())
//if err != nil { if err != nil {
// return return
//} }
c.rawConn.In.Lock() c.halfAccess.Lock()
defer c.rawConn.In.Unlock() defer c.halfAccess.Unlock()
for c.rawConn.Input.Len() == 0 { for c.input.Len() == 0 {
err = c.rawConn.ReadRecord() err = c.tlsReadRecord()
if err != nil { if err != nil {
return return
} }
for c.rawConn.Hand.Len() > 0 { for c.hand.Len() > 0 {
err = c.rawConn.HandlePostHandshakeMessage() err = c.tlsHandlePostHandshakeMessage()
if err != nil { if err != nil {
return return
} }
} }
} }
buffer = c.readWaitOptions.NewBuffer() buffer = c.readWaitOptions.NewBuffer()
n, err := c.rawConn.Input.Read(buffer.FreeBytes()) n, err := c.input.Read(buffer.FreeBytes())
if err != nil { if err != nil {
buffer.Release() buffer.Release()
return return
} }
buffer.Truncate(n) buffer.Truncate(n)
if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.Input.Len() > 0 && if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 &&
// recordType(c.RawInput.Bytes()[0]) == recordTypeAlert { // recordType(c.rawInput.Bytes()[0]) == recordTypeAlert {
c.rawConn.RawInput.Bytes()[0] == 21 { c.rawInput.Bytes()[0] == 21 {
_ = c.rawConn.ReadRecord() _ = c.tlsReadRecord()
// return n, err // will be io.EOF on closeNotify // return n, err // will be io.EOF on closeNotify
} }
@@ -77,6 +128,24 @@ func (c *ReadWaitConn) Upstream() any {
return c.Conn return c.Conn
} }
func (c *ReadWaitConn) ReaderReplaceable() bool { var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
return true
func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := conn.(*tls.STDConn)
if !loaded {
return
}
return true, func() error {
return stdTLSReadRecord(tlsConn)
}, func() error {
return stdTLSHandlePostHandshakeMessage(tlsConn)
}
})
} }
//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord
func stdTLSReadRecord(c *tls.STDConn) error
//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
func stdTLSHandlePostHandshakeMessage(c *tls.STDConn) error

View File

@@ -1,4 +1,4 @@
//go:build !go1.25 || !badlinkname //go:build !go1.21 || without_badtls
package badtls package badtls

View File

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

View File

@@ -1,62 +0,0 @@
//go:build go1.25 && badlinkname
package badtls
import (
"crypto/tls"
"net"
"unsafe"
)
type Methods struct {
readRecord func(c unsafe.Pointer) error
handlePostHandshakeMessage func(c unsafe.Pointer) error
writeRecordLocked func(c unsafe.Pointer, typ uint16, data []byte) (int, error)
setErrorLocked func(hc unsafe.Pointer, err error) error
decrypt func(hc unsafe.Pointer, record []byte) ([]byte, uint8, error)
setTrafficSecret func(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte)
explicitNonceLen func(hc unsafe.Pointer) int
}
var methodRegistry []func(conn net.Conn) (unsafe.Pointer, *Methods, bool)
func init() {
methodRegistry = append(methodRegistry, func(conn net.Conn) (unsafe.Pointer, *Methods, bool) {
tlsConn, loaded := conn.(*tls.Conn)
if !loaded {
return nil, nil, false
}
return unsafe.Pointer(tlsConn), &Methods{
readRecord: stdTLSReadRecord,
handlePostHandshakeMessage: stdTLSHandlePostHandshakeMessage,
writeRecordLocked: stdWriteRecordLocked,
setErrorLocked: stdSetErrorLocked,
decrypt: stdDecrypt,
setTrafficSecret: stdSetTrafficSecret,
explicitNonceLen: stdExplicitNonceLen,
}, true
})
}
//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord
func stdTLSReadRecord(c unsafe.Pointer) error
//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
func stdTLSHandlePostHandshakeMessage(c unsafe.Pointer) error
//go:linkname stdWriteRecordLocked crypto/tls.(*Conn).writeRecordLocked
func stdWriteRecordLocked(c unsafe.Pointer, typ uint16, data []byte) (int, error)
//go:linkname stdSetErrorLocked crypto/tls.(*halfConn).setErrorLocked
func stdSetErrorLocked(hc unsafe.Pointer, err error) error
//go:linkname stdDecrypt crypto/tls.(*halfConn).decrypt
func stdDecrypt(hc unsafe.Pointer, record []byte) ([]byte, uint8, error)
//go:linkname stdSetTrafficSecret crypto/tls.(*halfConn).setTrafficSecret
func stdSetTrafficSecret(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte)
//go:linkname stdExplicitNonceLen crypto/tls.(*halfConn).explicitNonceLen
func stdExplicitNonceLen(hc unsafe.Pointer) int

View File

@@ -1,56 +0,0 @@
//go:build go1.25 && badlinkname
package badtls
import (
"net"
"unsafe"
N "github.com/sagernet/sing/common/network"
"github.com/metacubex/utls"
)
func init() {
methodRegistry = append(methodRegistry, func(conn net.Conn) (unsafe.Pointer, *Methods, bool) {
var pointer unsafe.Pointer
if uConn, loaded := N.CastReader[*tls.Conn](conn); loaded {
pointer = unsafe.Pointer(uConn)
} else if uConn, loaded := N.CastReader[*tls.UConn](conn); loaded {
pointer = unsafe.Pointer(uConn.Conn)
} else {
return nil, nil, false
}
return pointer, &Methods{
readRecord: utlsReadRecord,
handlePostHandshakeMessage: utlsHandlePostHandshakeMessage,
writeRecordLocked: utlsWriteRecordLocked,
setErrorLocked: utlsSetErrorLocked,
decrypt: utlsDecrypt,
setTrafficSecret: utlsSetTrafficSecret,
explicitNonceLen: utlsExplicitNonceLen,
}, true
})
}
//go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord
func utlsReadRecord(c unsafe.Pointer) error
//go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage
func utlsHandlePostHandshakeMessage(c unsafe.Pointer) error
//go:linkname utlsWriteRecordLocked github.com/metacubex/utls.(*Conn).writeRecordLocked
func utlsWriteRecordLocked(hc unsafe.Pointer, typ uint16, data []byte) (int, error)
//go:linkname utlsSetErrorLocked github.com/metacubex/utls.(*halfConn).setErrorLocked
func utlsSetErrorLocked(hc unsafe.Pointer, err error) error
//go:linkname utlsDecrypt github.com/metacubex/utls.(*halfConn).decrypt
func utlsDecrypt(hc unsafe.Pointer, record []byte) ([]byte, uint8, error)
//go:linkname utlsSetTrafficSecret github.com/metacubex/utls.(*halfConn).setTrafficSecret
func utlsSetTrafficSecret(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte)
//go:linkname utlsExplicitNonceLen github.com/metacubex/utls.(*halfConn).explicitNonceLen
func utlsExplicitNonceLen(hc unsafe.Pointer) int

View File

@@ -5,8 +5,6 @@ import (
"strings" "strings"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"golang.org/x/mod/semver"
) )
type Version struct { type Version struct {
@@ -18,19 +16,7 @@ type Version struct {
PreReleaseVersion int PreReleaseVersion int
} }
func (v Version) LessThan(anotherVersion Version) bool { func (v Version) After(anotherVersion Version) bool {
return !v.GreaterThanOrEqual(anotherVersion)
}
func (v Version) LessThanOrEqual(anotherVersion Version) bool {
return v == anotherVersion || anotherVersion.GreaterThan(v)
}
func (v Version) GreaterThanOrEqual(anotherVersion Version) bool {
return v == anotherVersion || v.GreaterThan(anotherVersion)
}
func (v Version) GreaterThan(anotherVersion Version) bool {
if v.Major > anotherVersion.Major { if v.Major > anotherVersion.Major {
return true return true
} else if v.Major < anotherVersion.Major { } else if v.Major < anotherVersion.Major {
@@ -58,29 +44,19 @@ func (v Version) GreaterThan(anotherVersion Version) bool {
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion { } else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
return false return false
} }
} } else if v.PreReleaseIdentifier == "rc" && anotherVersion.PreReleaseIdentifier == "beta" {
preReleaseIdentifier := parsePreReleaseIdentifier(v.PreReleaseIdentifier)
anotherPreReleaseIdentifier := parsePreReleaseIdentifier(anotherVersion.PreReleaseIdentifier)
if preReleaseIdentifier < anotherPreReleaseIdentifier {
return true return true
} else if preReleaseIdentifier > anotherPreReleaseIdentifier { } else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "rc" {
return false
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
return true
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
return false return false
} }
} }
return false return false
} }
func parsePreReleaseIdentifier(identifier string) int {
if strings.HasPrefix(identifier, "rc") {
return 1
} else if strings.HasPrefix(identifier, "beta") {
return 2
} else if strings.HasPrefix(identifier, "alpha") {
return 3
}
return 0
}
func (v Version) VersionString() string { func (v Version) VersionString() string {
return F.ToString(v.Major, ".", v.Minor, ".", v.Patch) return F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
} }
@@ -107,10 +83,6 @@ func (v Version) BadString() string {
return version return version
} }
func IsValid(versionName string) bool {
return semver.IsValid("v" + versionName)
}
func Parse(versionName string) (version Version) { func Parse(versionName string) (version Version) {
if strings.HasPrefix(versionName, "v") { if strings.HasPrefix(versionName, "v") {
versionName = versionName[1:] versionName = versionName[1:]

View File

@@ -10,9 +10,9 @@ func TestCompareVersion(t *testing.T) {
t.Parallel() t.Parallel()
require.Equal(t, "1.3.0-beta.1", Parse("v1.3.0-beta1").String()) require.Equal(t, "1.3.0-beta.1", Parse("v1.3.0-beta1").String())
require.Equal(t, "1.3-beta1", Parse("v1.3.0-beta.1").BadString()) require.Equal(t, "1.3-beta1", Parse("v1.3.0-beta.1").BadString())
require.True(t, Parse("1.3.0").GreaterThan(Parse("1.3-beta1"))) require.True(t, Parse("1.3.0").After(Parse("1.3-beta1")))
require.True(t, Parse("1.3.0").GreaterThan(Parse("1.3.0-beta1"))) require.True(t, Parse("1.3.0").After(Parse("1.3.0-beta1")))
require.True(t, Parse("1.3.0-beta1").GreaterThan(Parse("1.3.0-alpha1"))) require.True(t, Parse("1.3.0-beta1").After(Parse("1.3.0-alpha1")))
require.True(t, Parse("1.3.1").GreaterThan(Parse("1.3.0"))) require.True(t, Parse("1.3.1").After(Parse("1.3.0")))
require.True(t, Parse("1.4").GreaterThan(Parse("1.3"))) require.True(t, Parse("1.4").After(Parse("1.3")))
} }

View File

@@ -7,11 +7,11 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
@@ -21,7 +21,6 @@ import (
var _ adapter.CertificateStore = (*Store)(nil) var _ adapter.CertificateStore = (*Store)(nil)
type Store struct { type Store struct {
access sync.RWMutex
systemPool *x509.CertPool systemPool *x509.CertPool
currentPool *x509.CertPool currentPool *x509.CertPool
certificate string certificate string
@@ -35,7 +34,7 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
switch options.Store { switch options.Store {
case C.CertificateStoreSystem, "": case C.CertificateStoreSystem, "":
systemPool = x509.NewCertPool() systemPool = x509.NewCertPool()
platformInterface := service.FromContext[adapter.PlatformInterface](ctx) platformInterface := service.FromContext[platform.Interface](ctx)
var systemValid bool var systemValid bool
if platformInterface != nil { if platformInterface != nil {
for _, cert := range platformInterface.SystemCertificates() { for _, cert := range platformInterface.SystemCertificates() {
@@ -116,14 +115,10 @@ func (s *Store) Close() error {
} }
func (s *Store) Pool() *x509.CertPool { func (s *Store) Pool() *x509.CertPool {
s.access.RLock()
defer s.access.RUnlock()
return s.currentPool return s.currentPool
} }
func (s *Store) update() error { func (s *Store) update() error {
s.access.Lock()
defer s.access.Unlock()
var currentPool *x509.CertPool var currentPool *x509.CertPool
if s.systemPool == nil { if s.systemPool == nil {
currentPool = x509.NewCertPool() currentPool = x509.NewCertPool()

View File

@@ -454,5 +454,5 @@ func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) {
for len(ruleParts) < 4 { for len(ruleParts) < 4 {
ruleParts = append(ruleParts, 0) ruleParts = append(ruleParts, 0)
} }
return netip.PrefixFrom(netip.AddrFrom4([4]byte(ruleParts)), bitLen), nil return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ruleParts)), bitLen), nil
} }

View File

@@ -12,15 +12,15 @@ import (
"github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/listener"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/database64128/tfo-go/v2"
) )
var ( var (
@@ -29,8 +29,8 @@ var (
) )
type DefaultDialer struct { type DefaultDialer struct {
dialer4 tfo.Dialer dialer4 tcpDialer
dialer6 tfo.Dialer dialer6 tcpDialer
udpDialer4 net.Dialer udpDialer4 net.Dialer
udpDialer6 net.Dialer udpDialer6 net.Dialer
udpListener net.ListenConfig udpListener net.ListenConfig
@@ -43,12 +43,12 @@ type DefaultDialer struct {
networkType []C.InterfaceType networkType []C.InterfaceType
fallbackNetworkType []C.InterfaceType fallbackNetworkType []C.InterfaceType
networkFallbackDelay time.Duration networkFallbackDelay time.Duration
networkLastFallback common.TypedValue[time.Time] networkLastFallback atomic.TypedValue[time.Time]
} }
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) { func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
networkManager := service.FromContext[adapter.NetworkManager](ctx) networkManager := service.FromContext[adapter.NetworkManager](ctx)
platformInterface := service.FromContext[adapter.PlatformInterface](ctx) platformInterface := service.FromContext[platform.Interface](ctx)
var ( var (
dialer net.Dialer dialer net.Dialer
@@ -89,35 +89,37 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if networkManager != nil { if networkManager != nil {
defaultOptions := networkManager.DefaultOptions() defaultOptions := networkManager.DefaultOptions()
if defaultOptions.BindInterface != "" { if !disableDefaultBind {
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1) if defaultOptions.BindInterface != "" {
dialer.Control = control.Append(dialer.Control, bindFunc) bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
listener.Control = control.Append(listener.Control, bindFunc)
} else if networkManager.AutoDetectInterface() && !disableDefaultBind {
if platformInterface != nil {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
networkStrategy = defaultOptions.NetworkStrategy
networkType = defaultOptions.NetworkType
fallbackNetworkType = defaultOptions.FallbackNetworkType
}
networkFallbackDelay = time.Duration(options.FallbackDelay)
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
networkFallbackDelay = defaultOptions.FallbackDelay
}
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
bindFunc := networkManager.ProtectFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else {
bindFunc := networkManager.AutoDetectInterfaceFunc()
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} else if networkManager.AutoDetectInterface() {
if platformInterface != nil {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
networkStrategy = defaultOptions.NetworkStrategy
networkType = defaultOptions.NetworkType
fallbackNetworkType = defaultOptions.FallbackNetworkType
}
networkFallbackDelay = time.Duration(options.FallbackDelay)
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
networkFallbackDelay = defaultOptions.FallbackDelay
}
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
bindFunc := networkManager.ProtectFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else {
bindFunc := networkManager.AutoDetectInterfaceFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
}
} }
} }
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 { if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
@@ -125,11 +127,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true)) listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
} }
} }
if networkManager != nil {
markFunc := networkManager.AutoRedirectOutputMarkFunc()
dialer.Control = control.Append(dialer.Control, markFunc)
listener.Control = control.Append(listener.Control, markFunc)
}
if options.ReuseAddr { if options.ReuseAddr {
listener.Control = control.Append(listener.Control, control.ReuseAddr()) listener.Control = control.Append(listener.Control, control.ReuseAddr())
} }
@@ -178,10 +175,19 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String() udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
} }
if options.TCPMultiPath { if options.TCPMultiPath {
dialer4.SetMultipathTCP(true) if !go121Available {
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
}
setMultiPathTCP(&dialer4)
}
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
if err != nil {
return nil, err
}
tcpDialer6, err := newTCPDialer(dialer6, options.TCPFastOpen)
if err != nil {
return nil, err
} }
tcpDialer4 := tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen}
tcpDialer6 := tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen}
return &DefaultDialer{ return &DefaultDialer{
dialer4: tcpDialer4, dialer4: tcpDialer4,
dialer6: tcpDialer6, dialer6: tcpDialer6,
@@ -261,11 +267,11 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
} }
var dialer net.Dialer var dialer net.Dialer
if N.NetworkName(network) == N.NetworkTCP { if N.NetworkName(network) == N.NetworkTCP {
dialer = d.dialer4.Dialer dialer = dialerFromTCPDialer(d.dialer4)
} else { } else {
dialer = d.udpDialer4 dialer = d.udpDialer4
} }
fastFallback := time.Since(d.networkLastFallback.Load()) < C.TCPTimeout fastFallback := time.Now().Sub(d.networkLastFallback.Load()) < C.TCPTimeout
var ( var (
conn net.Conn conn net.Conn
isPrimary bool isPrimary bool
@@ -307,14 +313,6 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
} }
} }
func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
if !destination.Is6() {
return d.dialer6.Dialer
} else {
return d.dialer4.Dialer
}
}
func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
if strategy == nil { if strategy == nil {
strategy = d.networkStrategy strategy = d.networkStrategy
@@ -348,8 +346,18 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
return trackPacketConn(packetConn, nil) return trackPacketConn(packetConn, nil)
} }
func (d *DefaultDialer) WireGuardControl() control.Func { func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
return d.udpListener.Control udpListener := d.udpListener
udpListener.Control = control.Append(udpListener.Control, func(network, address string, conn syscall.RawConn) error {
for _, wgControlFn := range WgControlFns {
err := wgControlFn(network, address, conn)
if err != nil {
return err
}
}
return nil
})
return udpListener.ListenPacket(context.Background(), network, address)
} }
func trackConn(conn net.Conn, err error) (net.Conn, error) { func trackConn(conn net.Conn, err error) (net.Conn, error) {

View File

@@ -0,0 +1,19 @@
//go:build go1.20
package dialer
import (
"net"
"github.com/metacubex/tfo-go"
)
type tcpDialer = tfo.Dialer
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
return tfo.Dialer{Dialer: dialer, DisableTFO: !tfoEnabled}, nil
}
func dialerFromTCPDialer(dialer tcpDialer) net.Dialer {
return dialer.Dialer
}

View File

@@ -0,0 +1,11 @@
//go:build go1.21
package dialer
import "net"
const go121Available = true
func setMultiPathTCP(dialer *net.Dialer) {
dialer.SetMultipathTCP(true)
}

View File

@@ -0,0 +1,22 @@
//go:build !go1.20
package dialer
import (
"net"
E "github.com/sagernet/sing/common/exceptions"
)
type tcpDialer = net.Dialer
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
if tfoEnabled {
return dialer, E.New("TCP Fast Open requires go1.20, please recompile your binary.")
}
return dialer, nil
}
func dialerFromTCPDialer(dialer tcpDialer) net.Dialer {
return dialer
}

View File

@@ -0,0 +1,12 @@
//go:build !go1.21
package dialer
import (
"net"
)
const go121Available = false
func setMultiPathTCP(dialer *net.Dialer) {
}

View File

@@ -111,7 +111,7 @@ func NewWithOptions(options Options) (N.Dialer, error) {
dnsQueryOptions.Transport = dnsTransport.Default() dnsQueryOptions.Transport = dnsTransport.Default()
} else if options.NewDialer { } else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address") return nil, E.New("missing domain resolver for domain server address")
} else { } else if !options.DirectOutbound {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver) deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
} }
} }

View File

@@ -1,3 +1,5 @@
//go:build go1.20
package dialer package dialer
import ( import (
@@ -6,15 +8,13 @@ import (
"net" "net"
"os" "os"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/database64128/tfo-go/v2" "github.com/metacubex/tfo-go"
) )
type slowOpenConn struct { type slowOpenConn struct {
@@ -22,7 +22,7 @@ type slowOpenConn struct {
ctx context.Context ctx context.Context
network string network string
destination M.Socksaddr destination M.Socksaddr
conn atomic.Pointer[net.TCPConn] conn net.Conn
create chan struct{} create chan struct{}
done chan struct{} done chan struct{}
access sync.Mutex access sync.Mutex
@@ -30,7 +30,7 @@ type slowOpenConn struct {
err error err error
} }
func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP { if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
switch N.NetworkName(network) { switch N.NetworkName(network) {
case N.NetworkTCP, N.NetworkUDP: case N.NetworkTCP, N.NetworkUDP:
@@ -50,25 +50,22 @@ func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, de
} }
func (c *slowOpenConn) Read(b []byte) (n int, err error) { func (c *slowOpenConn) Read(b []byte) (n int, err error) {
conn := c.conn.Load() if c.conn == nil {
if conn != nil { select {
return conn.Read(b) case <-c.create:
} if c.err != nil {
select { return 0, c.err
case <-c.create: }
if c.err != nil { case <-c.done:
return 0, c.err return 0, os.ErrClosed
} }
return c.conn.Load().Read(b)
case <-c.done:
return 0, os.ErrClosed
} }
return c.conn.Read(b)
} }
func (c *slowOpenConn) Write(b []byte) (n int, err error) { func (c *slowOpenConn) Write(b []byte) (n int, err error) {
tcpConn := c.conn.Load() if c.conn != nil {
if tcpConn != nil { return c.conn.Write(b)
return tcpConn.Write(b)
} }
c.access.Lock() c.access.Lock()
defer c.access.Unlock() defer c.access.Unlock()
@@ -77,7 +74,7 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if c.err != nil { if c.err != nil {
return 0, c.err return 0, c.err
} }
return c.conn.Load().Write(b) return c.conn.Write(b)
case <-c.done: case <-c.done:
return 0, os.ErrClosed return 0, os.ErrClosed
default: default:
@@ -86,7 +83,7 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if err != nil { if err != nil {
c.err = err c.err = err
} else { } else {
c.conn.Store(conn.(*net.TCPConn)) c.conn = conn
} }
n = len(b) n = len(b)
close(c.create) close(c.create)
@@ -96,77 +93,70 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
func (c *slowOpenConn) Close() error { func (c *slowOpenConn) Close() error {
c.closeOnce.Do(func() { c.closeOnce.Do(func() {
close(c.done) close(c.done)
conn := c.conn.Load() if c.conn != nil {
if conn != nil { c.conn.Close()
conn.Close()
} }
}) })
return nil return nil
} }
func (c *slowOpenConn) LocalAddr() net.Addr { func (c *slowOpenConn) LocalAddr() net.Addr {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
return M.Socksaddr{} return M.Socksaddr{}
} }
return conn.LocalAddr() return c.conn.LocalAddr()
} }
func (c *slowOpenConn) RemoteAddr() net.Addr { func (c *slowOpenConn) RemoteAddr() net.Addr {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
return M.Socksaddr{} return M.Socksaddr{}
} }
return conn.RemoteAddr() return c.conn.RemoteAddr()
} }
func (c *slowOpenConn) SetDeadline(t time.Time) error { func (c *slowOpenConn) SetDeadline(t time.Time) error {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
return os.ErrInvalid return os.ErrInvalid
} }
return conn.SetDeadline(t) return c.conn.SetDeadline(t)
} }
func (c *slowOpenConn) SetReadDeadline(t time.Time) error { func (c *slowOpenConn) SetReadDeadline(t time.Time) error {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
return os.ErrInvalid return os.ErrInvalid
} }
return conn.SetReadDeadline(t) return c.conn.SetReadDeadline(t)
} }
func (c *slowOpenConn) SetWriteDeadline(t time.Time) error { func (c *slowOpenConn) SetWriteDeadline(t time.Time) error {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
return os.ErrInvalid return os.ErrInvalid
} }
return conn.SetWriteDeadline(t) return c.conn.SetWriteDeadline(t)
} }
func (c *slowOpenConn) Upstream() any { func (c *slowOpenConn) Upstream() any {
return common.PtrOrNil(c.conn.Load()) return c.conn
} }
func (c *slowOpenConn) ReaderReplaceable() bool { func (c *slowOpenConn) ReaderReplaceable() bool {
return c.conn.Load() != nil return c.conn != nil
} }
func (c *slowOpenConn) WriterReplaceable() bool { func (c *slowOpenConn) WriterReplaceable() bool {
return c.conn.Load() != nil return c.conn != nil
} }
func (c *slowOpenConn) LazyHeadroom() bool { func (c *slowOpenConn) LazyHeadroom() bool {
return c.conn.Load() == nil return c.conn == nil
} }
func (c *slowOpenConn) NeedHandshake() bool { func (c *slowOpenConn) NeedHandshake() bool {
return c.conn.Load() == nil return c.conn == nil
} }
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) { func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
conn := c.conn.Load() if c.conn == nil {
if conn == nil {
select { select {
case <-c.create: case <-c.create:
if c.err != nil { if c.err != nil {
@@ -176,5 +166,5 @@ func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
return 0, c.err return 0, c.err
} }
} }
return bufio.Copy(w, c.conn.Load()) return bufio.Copy(w, c.conn)
} }

20
common/dialer/tfo_stub.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build !go1.20
package dialer
import (
"context"
"net"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch N.NetworkName(network) {
case N.NetworkTCP, N.NetworkUDP:
return dialer.DialContext(ctx, network, destination.String())
default:
return dialer.DialContext(ctx, network, destination.AddrString())
}
}

View File

@@ -1,9 +1,13 @@
package dialer package dialer
import ( import (
"net"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
) )
type WireGuardListener interface { type WireGuardListener interface {
WireGuardControl() control.Func ListenPacketCompat(network, address string) (net.PacketConn, error)
} }
var WgControlFns []control.Func

View File

@@ -1,133 +0,0 @@
//go:build linux && go1.25 && badlinkname
package ktls
import (
"bytes"
"context"
"crypto/tls"
"errors"
"io"
"net"
"os"
"syscall"
"github.com/sagernet/sing-box/common/badtls"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
"golang.org/x/sys/unix"
)
type Conn struct {
aTLS.Conn
ctx context.Context
logger logger.ContextLogger
conn net.Conn
rawConn *badtls.RawConn
syscallConn syscall.Conn
rawSyscallConn syscall.RawConn
readWaitOptions N.ReadWaitOptions
kernelTx bool
kernelRx bool
pendingRxSplice bool
}
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
err := Load()
if err != nil {
return nil, err
}
syscallConn, isSyscallConn := N.CastReader[interface {
io.Reader
syscall.Conn
}](conn.NetConn())
if !isSyscallConn {
return nil, os.ErrInvalid
}
rawSyscallConn, err := syscallConn.SyscallConn()
if err != nil {
return nil, err
}
rawConn, err := badtls.NewRawConn(conn)
if err != nil {
return nil, err
}
if *rawConn.Vers != tls.VersionTLS13 {
return nil, os.ErrInvalid
}
for rawConn.RawInput.Len() > 0 {
err = rawConn.ReadRecord()
if err != nil {
return nil, err
}
for rawConn.Hand.Len() > 0 {
err = rawConn.HandlePostHandshakeMessage()
if err != nil {
return nil, E.Cause(err, "handle post-handshake messages")
}
}
}
kConn := &Conn{
Conn: conn,
ctx: ctx,
logger: logger,
conn: conn.NetConn(),
rawConn: rawConn,
syscallConn: syscallConn,
rawSyscallConn: rawSyscallConn,
}
err = kConn.setupKernel(txOffload, rxOffload)
if err != nil {
return nil, err
}
return kConn, nil
}
func (c *Conn) Upstream() any {
return c.Conn
}
func (c *Conn) SyscallConnForRead() syscall.RawConn {
if !c.kernelRx {
return nil
}
if !*c.rawConn.IsClient {
c.logger.WarnContext(c.ctx, "ktls: RX splice is unavailable on the server size, since it will cause an unknown failure")
return nil
}
c.logger.DebugContext(c.ctx, "ktls: RX splice requested")
return c.rawSyscallConn
}
func (c *Conn) HandleSyscallReadError(inputErr error) ([]byte, error) {
if errors.Is(inputErr, unix.EINVAL) {
c.pendingRxSplice = true
err := c.readRecord()
if err != nil {
return nil, E.Cause(err, "ktls: handle non-application-data record")
}
var input bytes.Buffer
if c.rawConn.Input.Len() > 0 {
_, err = c.rawConn.Input.WriteTo(&input)
if err != nil {
return nil, err
}
}
return input.Bytes(), nil
} else if errors.Is(inputErr, unix.EBADMSG) {
return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertBadRecordMAC))
} else {
return nil, E.Cause(inputErr, "ktls: unexpected errno")
}
}
func (c *Conn) SyscallConnForWrite() syscall.RawConn {
if !c.kernelTx {
return nil
}
c.logger.DebugContext(c.ctx, "ktls: TX splice requested")
return c.rawSyscallConn
}

View File

@@ -1,80 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && go1.25 && badlinkname
package ktls
import (
"crypto/tls"
"net"
)
const (
// alert level
alertLevelWarning = 1
alertLevelError = 2
)
const (
alertCloseNotify = 0
alertUnexpectedMessage = 10
alertBadRecordMAC = 20
alertDecryptionFailed = 21
alertRecordOverflow = 22
alertDecompressionFailure = 30
alertHandshakeFailure = 40
alertBadCertificate = 42
alertUnsupportedCertificate = 43
alertCertificateRevoked = 44
alertCertificateExpired = 45
alertCertificateUnknown = 46
alertIllegalParameter = 47
alertUnknownCA = 48
alertAccessDenied = 49
alertDecodeError = 50
alertDecryptError = 51
alertExportRestriction = 60
alertProtocolVersion = 70
alertInsufficientSecurity = 71
alertInternalError = 80
alertInappropriateFallback = 86
alertUserCanceled = 90
alertNoRenegotiation = 100
alertMissingExtension = 109
alertUnsupportedExtension = 110
alertCertificateUnobtainable = 111
alertUnrecognizedName = 112
alertBadCertificateStatusResponse = 113
alertBadCertificateHashValue = 114
alertUnknownPSKIdentity = 115
alertCertificateRequired = 116
alertNoApplicationProtocol = 120
alertECHRequired = 121
)
func (c *Conn) sendAlertLocked(err uint8) error {
switch err {
case alertNoRenegotiation, alertCloseNotify:
c.rawConn.Tmp[0] = alertLevelWarning
default:
c.rawConn.Tmp[0] = alertLevelError
}
c.rawConn.Tmp[1] = byte(err)
_, writeErr := c.writeRecordLocked(recordTypeAlert, c.rawConn.Tmp[0:2])
if err == alertCloseNotify {
// closeNotify is a special case in that it isn't an error.
return writeErr
}
return c.rawConn.Out.SetErrorLocked(&net.OpError{Op: "local error", Err: tls.AlertError(err)})
}
// sendAlert sends a TLS alert message.
func (c *Conn) sendAlert(err uint8) error {
c.rawConn.Out.Lock()
defer c.rawConn.Out.Unlock()
return c.sendAlertLocked(err)
}

View File

@@ -1,326 +0,0 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && go1.25 && badlinkname
package ktls
import (
"crypto/tls"
"unsafe"
"github.com/sagernet/sing-box/common/badtls"
)
type kernelCryptoCipherType uint16
const (
TLS_CIPHER_AES_GCM_128 kernelCryptoCipherType = 51
TLS_CIPHER_AES_GCM_128_IV_SIZE kernelCryptoCipherType = 8
TLS_CIPHER_AES_GCM_128_KEY_SIZE kernelCryptoCipherType = 16
TLS_CIPHER_AES_GCM_128_SALT_SIZE kernelCryptoCipherType = 4
TLS_CIPHER_AES_GCM_128_TAG_SIZE kernelCryptoCipherType = 16
TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8
TLS_CIPHER_AES_GCM_256 kernelCryptoCipherType = 52
TLS_CIPHER_AES_GCM_256_IV_SIZE kernelCryptoCipherType = 8
TLS_CIPHER_AES_GCM_256_KEY_SIZE kernelCryptoCipherType = 32
TLS_CIPHER_AES_GCM_256_SALT_SIZE kernelCryptoCipherType = 4
TLS_CIPHER_AES_GCM_256_TAG_SIZE kernelCryptoCipherType = 16
TLS_CIPHER_AES_GCM_256_REC_SEQ_SIZE kernelCryptoCipherType = 8
TLS_CIPHER_AES_CCM_128 kernelCryptoCipherType = 53
TLS_CIPHER_AES_CCM_128_IV_SIZE kernelCryptoCipherType = 8
TLS_CIPHER_AES_CCM_128_KEY_SIZE kernelCryptoCipherType = 16
TLS_CIPHER_AES_CCM_128_SALT_SIZE kernelCryptoCipherType = 4
TLS_CIPHER_AES_CCM_128_TAG_SIZE kernelCryptoCipherType = 16
TLS_CIPHER_AES_CCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8
TLS_CIPHER_CHACHA20_POLY1305 kernelCryptoCipherType = 54
TLS_CIPHER_CHACHA20_POLY1305_IV_SIZE kernelCryptoCipherType = 12
TLS_CIPHER_CHACHA20_POLY1305_KEY_SIZE kernelCryptoCipherType = 32
TLS_CIPHER_CHACHA20_POLY1305_SALT_SIZE kernelCryptoCipherType = 0
TLS_CIPHER_CHACHA20_POLY1305_TAG_SIZE kernelCryptoCipherType = 16
TLS_CIPHER_CHACHA20_POLY1305_REC_SEQ_SIZE kernelCryptoCipherType = 8
// TLS_CIPHER_SM4_GCM kernelCryptoCipherType = 55
// TLS_CIPHER_SM4_GCM_IV_SIZE kernelCryptoCipherType = 8
// TLS_CIPHER_SM4_GCM_KEY_SIZE kernelCryptoCipherType = 16
// TLS_CIPHER_SM4_GCM_SALT_SIZE kernelCryptoCipherType = 4
// TLS_CIPHER_SM4_GCM_TAG_SIZE kernelCryptoCipherType = 16
// TLS_CIPHER_SM4_GCM_REC_SEQ_SIZE kernelCryptoCipherType = 8
// TLS_CIPHER_SM4_CCM kernelCryptoCipherType = 56
// TLS_CIPHER_SM4_CCM_IV_SIZE kernelCryptoCipherType = 8
// TLS_CIPHER_SM4_CCM_KEY_SIZE kernelCryptoCipherType = 16
// TLS_CIPHER_SM4_CCM_SALT_SIZE kernelCryptoCipherType = 4
// TLS_CIPHER_SM4_CCM_TAG_SIZE kernelCryptoCipherType = 16
// TLS_CIPHER_SM4_CCM_REC_SEQ_SIZE kernelCryptoCipherType = 8
TLS_CIPHER_ARIA_GCM_128 kernelCryptoCipherType = 57
TLS_CIPHER_ARIA_GCM_128_IV_SIZE kernelCryptoCipherType = 8
TLS_CIPHER_ARIA_GCM_128_KEY_SIZE kernelCryptoCipherType = 16
TLS_CIPHER_ARIA_GCM_128_SALT_SIZE kernelCryptoCipherType = 4
TLS_CIPHER_ARIA_GCM_128_TAG_SIZE kernelCryptoCipherType = 16
TLS_CIPHER_ARIA_GCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8
TLS_CIPHER_ARIA_GCM_256 kernelCryptoCipherType = 58
TLS_CIPHER_ARIA_GCM_256_IV_SIZE kernelCryptoCipherType = 8
TLS_CIPHER_ARIA_GCM_256_KEY_SIZE kernelCryptoCipherType = 32
TLS_CIPHER_ARIA_GCM_256_SALT_SIZE kernelCryptoCipherType = 4
TLS_CIPHER_ARIA_GCM_256_TAG_SIZE kernelCryptoCipherType = 16
TLS_CIPHER_ARIA_GCM_256_REC_SEQ_SIZE kernelCryptoCipherType = 8
)
type kernelCrypto interface {
String() string
}
type kernelCryptoInfo struct {
version uint16
cipher_type kernelCryptoCipherType
}
var _ kernelCrypto = &kernelCryptoAES128GCM{}
type kernelCryptoAES128GCM struct {
kernelCryptoInfo
iv [TLS_CIPHER_AES_GCM_128_IV_SIZE]byte
key [TLS_CIPHER_AES_GCM_128_KEY_SIZE]byte
salt [TLS_CIPHER_AES_GCM_128_SALT_SIZE]byte
rec_seq [TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE]byte
}
func (crypto *kernelCryptoAES128GCM) String() string {
crypto.cipher_type = TLS_CIPHER_AES_GCM_128
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
}
var _ kernelCrypto = &kernelCryptoAES256GCM{}
type kernelCryptoAES256GCM struct {
kernelCryptoInfo
iv [TLS_CIPHER_AES_GCM_256_IV_SIZE]byte
key [TLS_CIPHER_AES_GCM_256_KEY_SIZE]byte
salt [TLS_CIPHER_AES_GCM_256_SALT_SIZE]byte
rec_seq [TLS_CIPHER_AES_GCM_256_REC_SEQ_SIZE]byte
}
func (crypto *kernelCryptoAES256GCM) String() string {
crypto.cipher_type = TLS_CIPHER_AES_GCM_256
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
}
var _ kernelCrypto = &kernelCryptoAES128CCM{}
type kernelCryptoAES128CCM struct {
kernelCryptoInfo
iv [TLS_CIPHER_AES_CCM_128_IV_SIZE]byte
key [TLS_CIPHER_AES_CCM_128_KEY_SIZE]byte
salt [TLS_CIPHER_AES_CCM_128_SALT_SIZE]byte
rec_seq [TLS_CIPHER_AES_CCM_128_REC_SEQ_SIZE]byte
}
func (crypto *kernelCryptoAES128CCM) String() string {
crypto.cipher_type = TLS_CIPHER_AES_CCM_128
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
}
var _ kernelCrypto = &kernelCryptoChacha20Poly1035{}
type kernelCryptoChacha20Poly1035 struct {
kernelCryptoInfo
iv [TLS_CIPHER_CHACHA20_POLY1305_IV_SIZE]byte
key [TLS_CIPHER_CHACHA20_POLY1305_KEY_SIZE]byte
salt [TLS_CIPHER_CHACHA20_POLY1305_SALT_SIZE]byte
rec_seq [TLS_CIPHER_CHACHA20_POLY1305_REC_SEQ_SIZE]byte
}
func (crypto *kernelCryptoChacha20Poly1035) String() string {
crypto.cipher_type = TLS_CIPHER_CHACHA20_POLY1305
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
}
// var _ kernelCrypto = &kernelCryptoSM4GCM{}
// type kernelCryptoSM4GCM struct {
// kernelCryptoInfo
// iv [TLS_CIPHER_SM4_GCM_IV_SIZE]byte
// key [TLS_CIPHER_SM4_GCM_KEY_SIZE]byte
// salt [TLS_CIPHER_SM4_GCM_SALT_SIZE]byte
// rec_seq [TLS_CIPHER_SM4_GCM_REC_SEQ_SIZE]byte
// }
// func (crypto *kernelCryptoSM4GCM) String() string {
// crypto.cipher_type = TLS_CIPHER_SM4_GCM
// return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
// }
// var _ kernelCrypto = &kernelCryptoSM4CCM{}
// type kernelCryptoSM4CCM struct {
// kernelCryptoInfo
// iv [TLS_CIPHER_SM4_CCM_IV_SIZE]byte
// key [TLS_CIPHER_SM4_CCM_KEY_SIZE]byte
// salt [TLS_CIPHER_SM4_CCM_SALT_SIZE]byte
// rec_seq [TLS_CIPHER_SM4_CCM_REC_SEQ_SIZE]byte
// }
// func (crypto *kernelCryptoSM4CCM) String() string {
// crypto.cipher_type = TLS_CIPHER_SM4_CCM
// return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
// }
var _ kernelCrypto = &kernelCryptoARIA128GCM{}
type kernelCryptoARIA128GCM struct {
kernelCryptoInfo
iv [TLS_CIPHER_ARIA_GCM_128_IV_SIZE]byte
key [TLS_CIPHER_ARIA_GCM_128_KEY_SIZE]byte
salt [TLS_CIPHER_ARIA_GCM_128_SALT_SIZE]byte
rec_seq [TLS_CIPHER_ARIA_GCM_128_REC_SEQ_SIZE]byte
}
func (crypto *kernelCryptoARIA128GCM) String() string {
crypto.cipher_type = TLS_CIPHER_ARIA_GCM_128
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
}
var _ kernelCrypto = &kernelCryptoARIA256GCM{}
type kernelCryptoARIA256GCM struct {
kernelCryptoInfo
iv [TLS_CIPHER_ARIA_GCM_256_IV_SIZE]byte
key [TLS_CIPHER_ARIA_GCM_256_KEY_SIZE]byte
salt [TLS_CIPHER_ARIA_GCM_256_SALT_SIZE]byte
rec_seq [TLS_CIPHER_ARIA_GCM_256_REC_SEQ_SIZE]byte
}
func (crypto *kernelCryptoARIA256GCM) String() string {
crypto.cipher_type = TLS_CIPHER_ARIA_GCM_256
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
}
func kernelCipher(kernel *Support, hc *badtls.RawHalfConn, cipherSuite uint16, isRX bool) kernelCrypto {
if !kernel.TLS {
return nil
}
switch *hc.Version {
case tls.VersionTLS12:
if isRX && !kernel.TLS_Version13_RX {
return nil
}
case tls.VersionTLS13:
if !kernel.TLS_Version13 {
return nil
}
if isRX && !kernel.TLS_Version13_RX {
return nil
}
default:
return nil
}
var key, iv []byte
if *hc.Version == tls.VersionTLS13 {
key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), *hc.TrafficSecret)
/*if isRX {
key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), keyLog.RemoteTrafficSecret)
} else {
key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), keyLog.TrafficSecret)
}*/
} else {
// csPtr := cipherSuiteByID(cipherSuite)
// keysFromMasterSecret(*hc.Version, csPtr, keyLog.Secret, keyLog.Random)
return nil
}
switch cipherSuite {
case tls.TLS_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
crypto := new(kernelCryptoAES128GCM)
crypto.version = *hc.Version
copy(crypto.key[:], key)
copy(crypto.iv[:], iv[4:])
copy(crypto.salt[:], iv[:4])
crypto.rec_seq = *hc.Seq
return crypto
case tls.TLS_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
if !kernel.TLS_AES_256_GCM {
return nil
}
crypto := new(kernelCryptoAES256GCM)
crypto.version = *hc.Version
copy(crypto.key[:], key)
copy(crypto.iv[:], iv[4:])
copy(crypto.salt[:], iv[:4])
crypto.rec_seq = *hc.Seq
return crypto
//case tls.TLS_AES_128_CCM_SHA256, tls.TLS_RSA_WITH_AES_128_CCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_SHA256:
// if !kernel.TLS_AES_128_CCM {
// return nil
// }
//
// crypto := new(kernelCryptoAES128CCM)
//
// crypto.version = *hc.Version
// copy(crypto.key[:], key)
// copy(crypto.iv[:], iv[4:])
// copy(crypto.salt[:], iv[:4])
// crypto.rec_seq = *hc.Seq
//
// return crypto
case tls.TLS_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
if !kernel.TLS_CHACHA20_POLY1305 {
return nil
}
crypto := new(kernelCryptoChacha20Poly1035)
crypto.version = *hc.Version
copy(crypto.key[:], key)
copy(crypto.iv[:], iv)
crypto.rec_seq = *hc.Seq
return crypto
//case tls.TLS_RSA_WITH_ARIA_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256:
// if !kernel.TLS_ARIA_GCM {
// return nil
// }
//
// crypto := new(kernelCryptoARIA128GCM)
//
// crypto.version = *hc.Version
// copy(crypto.key[:], key)
// copy(crypto.iv[:], iv[4:])
// copy(crypto.salt[:], iv[:4])
// crypto.rec_seq = *hc.Seq
//
// return crypto
//case tls.TLS_RSA_WITH_ARIA_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384:
// if !kernel.TLS_ARIA_GCM {
// return nil
// }
//
// crypto := new(kernelCryptoARIA256GCM)
//
// crypto.version = *hc.Version
// copy(crypto.key[:], key)
// copy(crypto.iv[:], iv[4:])
// copy(crypto.salt[:], iv[:4])
// crypto.rec_seq = *hc.Seq
//
// return crypto
default:
return nil
}
}

View File

@@ -1,67 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && go1.25 && badlinkname
package ktls
import (
"fmt"
"net"
"time"
)
func (c *Conn) Close() error {
if !c.kernelTx {
return c.Conn.Close()
}
// Interlock with Conn.Write above.
var x int32
for {
x = c.rawConn.ActiveCall.Load()
if x&1 != 0 {
return net.ErrClosed
}
if c.rawConn.ActiveCall.CompareAndSwap(x, x|1) {
break
}
}
if x != 0 {
// io.Writer and io.Closer should not be used concurrently.
// If Close is called while a Write is currently in-flight,
// interpret that as a sign that this Close is really just
// being used to break the Write and/or clean up resources and
// avoid sending the alertCloseNotify, which may block
// waiting on handshakeMutex or the c.out mutex.
return c.conn.Close()
}
var alertErr error
if c.rawConn.IsHandshakeComplete.Load() {
if err := c.closeNotify(); err != nil {
alertErr = fmt.Errorf("tls: failed to send closeNotify alert (but connection was closed anyway): %w", err)
}
}
if err := c.conn.Close(); err != nil {
return err
}
return alertErr
}
func (c *Conn) closeNotify() error {
c.rawConn.Out.Lock()
defer c.rawConn.Out.Unlock()
if !*c.rawConn.CloseNotifySent {
// Set a Write Deadline to prevent possibly blocking forever.
c.SetWriteDeadline(time.Now().Add(time.Second * 5))
*c.rawConn.CloseNotifyErr = c.sendAlertLocked(alertCloseNotify)
*c.rawConn.CloseNotifySent = true
// Any subsequent writes will fail.
c.SetWriteDeadline(time.Now())
}
return *c.rawConn.CloseNotifyErr
}

View File

@@ -1,24 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && go1.25 && badlinkname
package ktls
const (
maxPlaintext = 16384 // maximum plaintext payload length
maxCiphertext = 16384 + 2048 // maximum ciphertext payload length
maxCiphertextTLS13 = 16384 + 256 // maximum ciphertext length in TLS 1.3
recordHeaderLen = 5 // record header length
maxHandshake = 65536 // maximum handshake we support (protocol max is 16 MB)
maxHandshakeCertificateMsg = 262144 // maximum certificate message size (256 KiB)
maxUselessRecords = 16 // maximum number of consecutive non-advancing records
)
const (
recordTypeChangeCipherSpec = 20
recordTypeAlert = 21
recordTypeHandshake = 22
recordTypeApplicationData = 23
)

View File

@@ -1,238 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && go1.25 && badlinkname
package ktls
import (
"fmt"
"golang.org/x/crypto/cryptobyte"
)
// The marshalingFunction type is an adapter to allow the use of ordinary
// functions as cryptobyte.MarshalingValue.
type marshalingFunction func(b *cryptobyte.Builder) error
func (f marshalingFunction) Marshal(b *cryptobyte.Builder) error {
return f(b)
}
// addBytesWithLength appends a sequence of bytes to the cryptobyte.Builder. If
// the length of the sequence is not the value specified, it produces an error.
func addBytesWithLength(b *cryptobyte.Builder, v []byte, n int) {
b.AddValue(marshalingFunction(func(b *cryptobyte.Builder) error {
if len(v) != n {
return fmt.Errorf("invalid value length: expected %d, got %d", n, len(v))
}
b.AddBytes(v)
return nil
}))
}
// addUint64 appends a big-endian, 64-bit value to the cryptobyte.Builder.
func addUint64(b *cryptobyte.Builder, v uint64) {
b.AddUint32(uint32(v >> 32))
b.AddUint32(uint32(v))
}
// readUint64 decodes a big-endian, 64-bit value into out and advances over it.
// It reports whether the read was successful.
func readUint64(s *cryptobyte.String, out *uint64) bool {
var hi, lo uint32
if !s.ReadUint32(&hi) || !s.ReadUint32(&lo) {
return false
}
*out = uint64(hi)<<32 | uint64(lo)
return true
}
// readUint8LengthPrefixed acts like s.ReadUint8LengthPrefixed, but targets a
// []byte instead of a cryptobyte.String.
func readUint8LengthPrefixed(s *cryptobyte.String, out *[]byte) bool {
return s.ReadUint8LengthPrefixed((*cryptobyte.String)(out))
}
// readUint16LengthPrefixed acts like s.ReadUint16LengthPrefixed, but targets a
// []byte instead of a cryptobyte.String.
func readUint16LengthPrefixed(s *cryptobyte.String, out *[]byte) bool {
return s.ReadUint16LengthPrefixed((*cryptobyte.String)(out))
}
// readUint24LengthPrefixed acts like s.ReadUint24LengthPrefixed, but targets a
// []byte instead of a cryptobyte.String.
func readUint24LengthPrefixed(s *cryptobyte.String, out *[]byte) bool {
return s.ReadUint24LengthPrefixed((*cryptobyte.String)(out))
}
type keyUpdateMsg struct {
updateRequested bool
}
func (m *keyUpdateMsg) marshal() ([]byte, error) {
var b cryptobyte.Builder
b.AddUint8(typeKeyUpdate)
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
if m.updateRequested {
b.AddUint8(1)
} else {
b.AddUint8(0)
}
})
return b.Bytes()
}
func (m *keyUpdateMsg) unmarshal(data []byte) bool {
s := cryptobyte.String(data)
var updateRequested uint8
if !s.Skip(4) || // message type and uint24 length field
!s.ReadUint8(&updateRequested) || !s.Empty() {
return false
}
switch updateRequested {
case 0:
m.updateRequested = false
case 1:
m.updateRequested = true
default:
return false
}
return true
}
// TLS handshake message types.
const (
typeHelloRequest uint8 = 0
typeClientHello uint8 = 1
typeServerHello uint8 = 2
typeNewSessionTicket uint8 = 4
typeEndOfEarlyData uint8 = 5
typeEncryptedExtensions uint8 = 8
typeCertificate uint8 = 11
typeServerKeyExchange uint8 = 12
typeCertificateRequest uint8 = 13
typeServerHelloDone uint8 = 14
typeCertificateVerify uint8 = 15
typeClientKeyExchange uint8 = 16
typeFinished uint8 = 20
typeCertificateStatus uint8 = 22
typeKeyUpdate uint8 = 24
typeCompressedCertificate uint8 = 25
typeMessageHash uint8 = 254 // synthetic message
)
// TLS compression types.
const (
compressionNone uint8 = 0
)
// TLS extension numbers
const (
extensionServerName uint16 = 0
extensionStatusRequest uint16 = 5
extensionSupportedCurves uint16 = 10 // supported_groups in TLS 1.3, see RFC 8446, Section 4.2.7
extensionSupportedPoints uint16 = 11
extensionSignatureAlgorithms uint16 = 13
extensionALPN uint16 = 16
extensionSCT uint16 = 18
extensionPadding uint16 = 21
extensionExtendedMasterSecret uint16 = 23
extensionCompressCertificate uint16 = 27 // compress_certificate in TLS 1.3
extensionSessionTicket uint16 = 35
extensionPreSharedKey uint16 = 41
extensionEarlyData uint16 = 42
extensionSupportedVersions uint16 = 43
extensionCookie uint16 = 44
extensionPSKModes uint16 = 45
extensionCertificateAuthorities uint16 = 47
extensionSignatureAlgorithmsCert uint16 = 50
extensionKeyShare uint16 = 51
extensionQUICTransportParameters uint16 = 57
extensionALPS uint16 = 17513
extensionRenegotiationInfo uint16 = 0xff01
extensionECHOuterExtensions uint16 = 0xfd00
extensionEncryptedClientHello uint16 = 0xfe0d
)
type handshakeMessage interface {
marshal() ([]byte, error)
unmarshal([]byte) bool
}
type newSessionTicketMsgTLS13 struct {
lifetime uint32
ageAdd uint32
nonce []byte
label []byte
maxEarlyData uint32
}
func (m *newSessionTicketMsgTLS13) marshal() ([]byte, error) {
var b cryptobyte.Builder
b.AddUint8(typeNewSessionTicket)
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddUint32(m.lifetime)
b.AddUint32(m.ageAdd)
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(m.nonce)
})
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(m.label)
})
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
if m.maxEarlyData > 0 {
b.AddUint16(extensionEarlyData)
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddUint32(m.maxEarlyData)
})
}
})
})
return b.Bytes()
}
func (m *newSessionTicketMsgTLS13) unmarshal(data []byte) bool {
*m = newSessionTicketMsgTLS13{}
s := cryptobyte.String(data)
var extensions cryptobyte.String
if !s.Skip(4) || // message type and uint24 length field
!s.ReadUint32(&m.lifetime) ||
!s.ReadUint32(&m.ageAdd) ||
!readUint8LengthPrefixed(&s, &m.nonce) ||
!readUint16LengthPrefixed(&s, &m.label) ||
!s.ReadUint16LengthPrefixed(&extensions) ||
!s.Empty() {
return false
}
for !extensions.Empty() {
var extension uint16
var extData cryptobyte.String
if !extensions.ReadUint16(&extension) ||
!extensions.ReadUint16LengthPrefixed(&extData) {
return false
}
switch extension {
case extensionEarlyData:
if !extData.ReadUint32(&m.maxEarlyData) {
return false
}
default:
// Ignore unknown extensions.
continue
}
if !extData.Empty() {
return false
}
}
return true
}

View File

@@ -1,173 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && go1.25 && badlinkname
package ktls
import (
"crypto/tls"
"errors"
"fmt"
"io"
"os"
)
// handlePostHandshakeMessage processes a handshake message arrived after the
// handshake is complete. Up to TLS 1.2, it indicates the start of a renegotiation.
func (c *Conn) handlePostHandshakeMessage() error {
if *c.rawConn.Vers != tls.VersionTLS13 {
return errors.New("ktls: kernel does not support TLS 1.2 renegotiation")
}
msg, err := c.readHandshake(nil)
if err != nil {
return err
}
//c.retryCount++
//if c.retryCount > maxUselessRecords {
// c.sendAlert(alertUnexpectedMessage)
// return c.in.setErrorLocked(errors.New("tls: too many non-advancing records"))
//}
switch msg := msg.(type) {
case *newSessionTicketMsgTLS13:
// return errors.New("ktls: received new session ticket")
return nil
case *keyUpdateMsg:
return c.handleKeyUpdate(msg)
}
// The QUIC layer is supposed to treat an unexpected post-handshake CertificateRequest
// as a QUIC-level PROTOCOL_VIOLATION error (RFC 9001, Section 4.4). Returning an
// unexpected_message alert here doesn't provide it with enough information to distinguish
// this condition from other unexpected messages. This is probably fine.
c.sendAlert(alertUnexpectedMessage)
return fmt.Errorf("tls: received unexpected handshake message of type %T", msg)
}
func (c *Conn) handleKeyUpdate(keyUpdate *keyUpdateMsg) error {
//if c.quic != nil {
// c.sendAlert(alertUnexpectedMessage)
// return c.in.setErrorLocked(errors.New("tls: received unexpected key update message"))
//}
cipherSuite := cipherSuiteTLS13ByID(*c.rawConn.CipherSuite)
if cipherSuite == nil {
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertInternalError))
}
newSecret := nextTrafficSecret(cipherSuite, *c.rawConn.In.TrafficSecret)
c.rawConn.In.SetTrafficSecret(cipherSuite, 0 /*tls.QUICEncryptionLevelInitial*/, newSecret)
err := c.resetupRX()
if err != nil {
c.sendAlert(alertInternalError)
return c.rawConn.In.SetErrorLocked(fmt.Errorf("ktls: resetupRX failed: %w", err))
}
if keyUpdate.updateRequested {
c.rawConn.Out.Lock()
defer c.rawConn.Out.Unlock()
resetup, err := c.resetupTX()
if err != nil {
c.sendAlertLocked(alertInternalError)
return c.rawConn.Out.SetErrorLocked(fmt.Errorf("ktls: resetupTX failed: %w", err))
}
msg := &keyUpdateMsg{}
msgBytes, err := msg.marshal()
if err != nil {
return err
}
_, err = c.writeRecordLocked(recordTypeHandshake, msgBytes)
if err != nil {
// Surface the error at the next write.
c.rawConn.Out.SetErrorLocked(err)
return nil
}
newSecret := nextTrafficSecret(cipherSuite, *c.rawConn.Out.TrafficSecret)
c.rawConn.Out.SetTrafficSecret(cipherSuite, 0 /*QUICEncryptionLevelInitial*/, newSecret)
err = resetup()
if err != nil {
return c.rawConn.Out.SetErrorLocked(fmt.Errorf("ktls: resetupTX failed: %w", err))
}
}
return nil
}
func (c *Conn) readHandshakeBytes(n int) error {
//if c.quic != nil {
// return c.quicReadHandshakeBytes(n)
//}
for c.rawConn.Hand.Len() < n {
if err := c.readRecord(); err != nil {
return err
}
}
return nil
}
func (c *Conn) readHandshake(transcript io.Writer) (any, error) {
if err := c.readHandshakeBytes(4); err != nil {
return nil, err
}
data := c.rawConn.Hand.Bytes()
maxHandshakeSize := maxHandshake
// hasVers indicates we're past the first message, forcing someone trying to
// make us just allocate a large buffer to at least do the initial part of
// the handshake first.
//if c.haveVers && data[0] == typeCertificate {
// Since certificate messages are likely to be the only messages that
// can be larger than maxHandshake, we use a special limit for just
// those messages.
//maxHandshakeSize = maxHandshakeCertificateMsg
//}
n := int(data[1])<<16 | int(data[2])<<8 | int(data[3])
if n > maxHandshakeSize {
c.sendAlertLocked(alertInternalError)
return nil, c.rawConn.In.SetErrorLocked(fmt.Errorf("tls: handshake message of length %d bytes exceeds maximum of %d bytes", n, maxHandshakeSize))
}
if err := c.readHandshakeBytes(4 + n); err != nil {
return nil, err
}
data = c.rawConn.Hand.Next(4 + n)
return c.unmarshalHandshakeMessage(data, transcript)
}
func (c *Conn) unmarshalHandshakeMessage(data []byte, transcript io.Writer) (any, error) {
var m handshakeMessage
switch data[0] {
case typeNewSessionTicket:
if *c.rawConn.Vers == tls.VersionTLS13 {
m = new(newSessionTicketMsgTLS13)
} else {
return nil, os.ErrInvalid
}
case typeKeyUpdate:
m = new(keyUpdateMsg)
default:
return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
}
// The handshake message unmarshalers
// expect to be able to keep references to data,
// so pass in a fresh copy that won't be overwritten.
data = append([]byte(nil), data...)
if !m.unmarshal(data) {
return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecodeError))
}
if transcript != nil {
transcript.Write(data)
}
return m, nil
}

View File

@@ -1,329 +0,0 @@
//go:build linux && go1.25 && badlinkname
package ktls
import (
"crypto/tls"
"errors"
"io"
"os"
"strings"
"sync"
"syscall"
"unsafe"
"github.com/sagernet/sing-box/common/badversion"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/shell"
"golang.org/x/sys/unix"
)
// mod from https://gitlab.com/go-extension/tls
const (
TLS_TX = 1
TLS_RX = 2
TLS_TX_ZEROCOPY_RO = 3 // TX zerocopy (only sendfile now)
TLS_RX_EXPECT_NO_PAD = 4 // Attempt opportunistic zero-copy, TLS 1.3 only
TLS_SET_RECORD_TYPE = 1
TLS_GET_RECORD_TYPE = 2
)
type Support struct {
TLS, TLS_RX bool
TLS_Version13, TLS_Version13_RX bool
TLS_TX_ZEROCOPY bool
TLS_RX_NOPADDING bool
TLS_AES_256_GCM bool
TLS_AES_128_CCM bool
TLS_CHACHA20_POLY1305 bool
TLS_SM4 bool
TLS_ARIA_GCM bool
TLS_Version13_KeyUpdate bool
}
var KernelSupport = sync.OnceValues(func() (*Support, error) {
var uname unix.Utsname
err := unix.Uname(&uname)
if err != nil {
return nil, err
}
kernelVersion := badversion.Parse(strings.Trim(string(uname.Release[:]), "\x00"))
if err != nil {
return nil, err
}
var support Support
switch {
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6, Minor: 14}):
support.TLS_Version13_KeyUpdate = true
fallthrough
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6, Minor: 1}):
support.TLS_ARIA_GCM = true
fallthrough
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6}):
support.TLS_Version13_RX = true
support.TLS_RX_NOPADDING = true
fallthrough
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 19}):
support.TLS_TX_ZEROCOPY = true
fallthrough
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 16}):
support.TLS_SM4 = true
fallthrough
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 11}):
support.TLS_CHACHA20_POLY1305 = true
fallthrough
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 2}):
support.TLS_AES_128_CCM = true
fallthrough
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 1}):
support.TLS_AES_256_GCM = true
support.TLS_Version13 = true
fallthrough
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 4, Minor: 17}):
support.TLS_RX = true
fallthrough
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 4, Minor: 13}):
support.TLS = true
}
if support.TLS && support.TLS_Version13 {
_, err := os.Stat("/sys/module/tls")
if err != nil {
if os.Getuid() == 0 {
output, err := shell.Exec("modprobe", "tls").Read()
if err != nil {
return nil, E.Extend(E.Cause(err, "modprobe tls"), output)
}
} else {
return nil, E.New("ktls: kernel TLS module not loaded")
}
}
}
return &support, nil
})
func Load() error {
support, err := KernelSupport()
if err != nil {
return E.Cause(err, "ktls: check availability")
}
if !support.TLS || !support.TLS_Version13 {
return E.New("ktls: kernel does not support TLS 1.3")
}
return nil
}
func (c *Conn) setupKernel(txOffload, rxOffload bool) error {
if !txOffload && !rxOffload {
return os.ErrInvalid
}
support, err := KernelSupport()
if err != nil {
return E.Cause(err, "check availability")
}
if !support.TLS || !support.TLS_Version13 {
return E.New("kernel does not support TLS 1.3")
}
c.rawConn.Out.Lock()
defer c.rawConn.Out.Unlock()
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
return syscall.SetsockoptString(int(fd), unix.SOL_TCP, unix.TCP_ULP, "tls")
})
if err != nil {
return os.NewSyscallError("setsockopt", err)
}
if txOffload {
txCrypto := kernelCipher(support, c.rawConn.Out, *c.rawConn.CipherSuite, false)
if txCrypto == nil {
return E.New("unsupported cipher suite")
}
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_TX, txCrypto.String())
})
if err != nil {
return err
}
if support.TLS_TX_ZEROCOPY {
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
return syscall.SetsockoptInt(int(fd), unix.SOL_TLS, TLS_TX_ZEROCOPY_RO, 1)
})
if err != nil {
return err
}
}
c.kernelTx = true
c.logger.DebugContext(c.ctx, "ktls: kernel TLS TX enabled")
}
if rxOffload {
rxCrypto := kernelCipher(support, c.rawConn.In, *c.rawConn.CipherSuite, true)
if rxCrypto == nil {
return E.New("unsupported cipher suite")
}
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_RX, rxCrypto.String())
})
if err != nil {
return err
}
if *c.rawConn.Vers >= tls.VersionTLS13 && support.TLS_RX_NOPADDING {
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
return syscall.SetsockoptInt(int(fd), unix.SOL_TLS, TLS_RX_EXPECT_NO_PAD, 1)
})
if err != nil {
return err
}
}
c.kernelRx = true
c.logger.DebugContext(c.ctx, "ktls: kernel TLS RX enabled")
}
return nil
}
func (c *Conn) resetupTX() (func() error, error) {
if !c.kernelTx {
return nil, nil
}
support, err := KernelSupport()
if err != nil {
return nil, err
}
if !support.TLS_Version13_KeyUpdate {
return nil, errors.New("ktls: kernel does not support rekey")
}
txCrypto := kernelCipher(support, c.rawConn.Out, *c.rawConn.CipherSuite, false)
if txCrypto == nil {
return nil, errors.New("ktls: set kernelCipher on unsupported tls session")
}
return func() error {
return control.Raw(c.rawSyscallConn, func(fd uintptr) error {
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_TX, txCrypto.String())
})
}, nil
}
func (c *Conn) resetupRX() error {
if !c.kernelRx {
return nil
}
support, err := KernelSupport()
if err != nil {
return err
}
if !support.TLS_Version13_KeyUpdate {
return errors.New("ktls: kernel does not support rekey")
}
rxCrypto := kernelCipher(support, c.rawConn.In, *c.rawConn.CipherSuite, true)
if rxCrypto == nil {
return errors.New("ktls: set kernelCipher on unsupported tls session")
}
return control.Raw(c.rawSyscallConn, func(fd uintptr) error {
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_RX, rxCrypto.String())
})
}
func (c *Conn) readKernelRecord() (uint8, []byte, error) {
if c.rawConn.RawInput.Len() < maxPlaintext {
c.rawConn.RawInput.Grow(maxPlaintext - c.rawConn.RawInput.Len())
}
data := c.rawConn.RawInput.Bytes()[:maxPlaintext]
// cmsg for record type
buffer := make([]byte, unix.CmsgSpace(1))
cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&buffer[0]))
cmsg.SetLen(unix.CmsgLen(1))
var iov unix.Iovec
iov.Base = &data[0]
iov.SetLen(len(data))
var msg unix.Msghdr
msg.Control = &buffer[0]
msg.Controllen = cmsg.Len
msg.Iov = &iov
msg.Iovlen = 1
var n int
var err error
er := c.rawSyscallConn.Read(func(fd uintptr) bool {
n, err = recvmsg(int(fd), &msg, 0)
return err != unix.EAGAIN || c.pendingRxSplice
})
if er != nil {
return 0, nil, er
}
switch err {
case nil:
case syscall.EINVAL, syscall.EAGAIN:
return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertProtocolVersion))
case syscall.EMSGSIZE:
return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertRecordOverflow))
case syscall.EBADMSG:
return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecryptError))
default:
return 0, nil, err
}
if n <= 0 {
return 0, nil, c.rawConn.In.SetErrorLocked(io.EOF)
}
if cmsg.Level == unix.SOL_TLS && cmsg.Type == TLS_GET_RECORD_TYPE {
typ := buffer[unix.CmsgLen(0)]
return typ, data[:n], nil
}
return recordTypeApplicationData, data[:n], nil
}
func (c *Conn) writeKernelRecord(typ uint16, data []byte) (int, error) {
if typ == recordTypeApplicationData {
return c.conn.Write(data)
}
// cmsg for record type
buffer := make([]byte, unix.CmsgSpace(1))
cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&buffer[0]))
cmsg.SetLen(unix.CmsgLen(1))
buffer[unix.CmsgLen(0)] = byte(typ)
cmsg.Level = unix.SOL_TLS
cmsg.Type = TLS_SET_RECORD_TYPE
var iov unix.Iovec
iov.Base = &data[0]
iov.SetLen(len(data))
var msg unix.Msghdr
msg.Control = &buffer[0]
msg.Controllen = cmsg.Len
msg.Iov = &iov
msg.Iovlen = 1
var n int
var err error
ew := c.rawSyscallConn.Write(func(fd uintptr) bool {
n, err = sendmsg(int(fd), &msg, 0)
return err != unix.EAGAIN
})
if ew != nil {
return 0, ew
}
return n, err
}
//go:linkname recvmsg golang.org/x/sys/unix.recvmsg
func recvmsg(fd int, msg *unix.Msghdr, flags int) (n int, err error)
//go:linkname sendmsg golang.org/x/sys/unix.sendmsg
func sendmsg(fd int, msg *unix.Msghdr, flags int) (n int, err error)

View File

@@ -1,24 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && go1.25 && badlinkname
package ktls
import "unsafe"
//go:linkname cipherSuiteByID github.com/metacubex/utls.cipherSuiteByID
func cipherSuiteByID(id uint16) unsafe.Pointer
//go:linkname keysFromMasterSecret github.com/metacubex/utls.keysFromMasterSecret
func keysFromMasterSecret(version uint16, suite unsafe.Pointer, masterSecret, clientRandom, serverRandom []byte, macLen, keyLen, ivLen int) (clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV []byte)
//go:linkname cipherSuiteTLS13ByID github.com/metacubex/utls.cipherSuiteTLS13ByID
func cipherSuiteTLS13ByID(id uint16) unsafe.Pointer
//go:linkname nextTrafficSecret github.com/metacubex/utls.(*cipherSuiteTLS13).nextTrafficSecret
func nextTrafficSecret(cs unsafe.Pointer, trafficSecret []byte) []byte
//go:linkname trafficKey github.com/metacubex/utls.(*cipherSuiteTLS13).trafficKey
func trafficKey(cs unsafe.Pointer, trafficSecret []byte) (key, iv []byte)

View File

@@ -1,292 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && go1.25 && badlinkname
package ktls
import (
"bytes"
"crypto/tls"
"fmt"
"io"
"net"
)
func (c *Conn) Read(b []byte) (int, error) {
if !c.kernelRx {
return c.Conn.Read(b)
}
if len(b) == 0 {
// Put this after Handshake, in case people were calling
// Read(nil) for the side effect of the Handshake.
return 0, nil
}
c.rawConn.In.Lock()
defer c.rawConn.In.Unlock()
for c.rawConn.Input.Len() == 0 {
if err := c.readRecord(); err != nil {
return 0, err
}
for c.rawConn.Hand.Len() > 0 {
if err := c.handlePostHandshakeMessage(); err != nil {
return 0, err
}
}
}
n, _ := c.rawConn.Input.Read(b)
// If a close-notify alert is waiting, read it so that we can return (n,
// EOF) instead of (n, nil), to signal to the HTTP response reading
// goroutine that the connection is now closed. This eliminates a race
// where the HTTP response reading goroutine would otherwise not observe
// the EOF until its next read, by which time a client goroutine might
// have already tried to reuse the HTTP connection for a new request.
// See https://golang.org/cl/76400046 and https://golang.org/issue/3514
if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.RawInput.Len() > 0 &&
c.rawConn.RawInput.Bytes()[0] == recordTypeAlert {
if err := c.readRecord(); err != nil {
return n, err // will be io.EOF on closeNotify
}
}
return n, nil
}
func (c *Conn) readRecord() error {
if *c.rawConn.In.Err != nil {
return *c.rawConn.In.Err
}
typ, data, err := c.readRawRecord()
if err != nil {
return err
}
if len(data) > maxPlaintext {
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertRecordOverflow))
}
// Application Data messages are always protected.
if c.rawConn.In.Cipher == nil && typ == recordTypeApplicationData {
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
}
//if typ != recordTypeAlert && typ != recordTypeChangeCipherSpec && len(data) > 0 {
// This is a state-advancing message: reset the retry count.
// c.retryCount = 0
//}
// Handshake messages MUST NOT be interleaved with other record types in TLS 1.3.
if *c.rawConn.Vers == tls.VersionTLS13 && typ != recordTypeHandshake && c.rawConn.Hand.Len() > 0 {
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
}
switch typ {
default:
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
case recordTypeAlert:
//if c.quic != nil {
// return c.rawConn.In.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
//}
if len(data) != 2 {
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
}
if data[1] == alertCloseNotify {
return c.rawConn.In.SetErrorLocked(io.EOF)
}
if *c.rawConn.Vers == tls.VersionTLS13 {
// TLS 1.3 removed warning-level alerts except for alertUserCanceled
// (RFC 8446, § 6.1). Since at least one major implementation
// (https://bugs.openjdk.org/browse/JDK-8323517) misuses this alert,
// many TLS stacks now ignore it outright when seen in a TLS 1.3
// handshake (e.g. BoringSSL, NSS, Rustls).
if data[1] == alertUserCanceled {
// Like TLS 1.2 alertLevelWarning alerts, we drop the record and retry.
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
}
return c.rawConn.In.SetErrorLocked(&net.OpError{Op: "remote error", Err: tls.AlertError(data[1])})
}
switch data[0] {
case alertLevelWarning:
// Drop the record on the floor and retry.
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
case alertLevelError:
return c.rawConn.In.SetErrorLocked(&net.OpError{Op: "remote error", Err: tls.AlertError(data[1])})
default:
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
}
case recordTypeChangeCipherSpec:
if len(data) != 1 || data[0] != 1 {
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecodeError))
}
// Handshake messages are not allowed to fragment across the CCS.
if c.rawConn.Hand.Len() > 0 {
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
}
// In TLS 1.3, change_cipher_spec records are ignored until the
// Finished. See RFC 8446, Appendix D.4. Note that according to Section
// 5, a server can send a ChangeCipherSpec before its ServerHello, when
// c.vers is still unset. That's not useful though and suspicious if the
// server then selects a lower protocol version, so don't allow that.
if *c.rawConn.Vers == tls.VersionTLS13 {
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
}
// if !expectChangeCipherSpec {
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
//}
//if err := c.rawConn.In.changeCipherSpec(); err != nil {
// return c.rawConn.In.setErrorLocked(c.sendAlert(err.(alert)))
//}
case recordTypeApplicationData:
// Some OpenSSL servers send empty records in order to randomize the
// CBC RawIV. Ignore a limited number of empty records.
if len(data) == 0 {
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
}
// Note that data is owned by c.rawInput, following the Next call above,
// to avoid copying the plaintext. This is safe because c.rawInput is
// not read from or written to until c.input is drained.
c.rawConn.Input.Reset(data)
case recordTypeHandshake:
if len(data) == 0 {
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
}
c.rawConn.Hand.Write(data)
}
return nil
}
//nolint:staticcheck
func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) {
// Read from kernel.
if c.kernelRx {
return c.readKernelRecord()
}
// Read header, payload.
if err = c.readFromUntil(c.conn, recordHeaderLen); err != nil {
// RFC 8446, Section 6.1 suggests that EOF without an alertCloseNotify
// is an error, but popular web sites seem to do this, so we accept it
// if and only if at the record boundary.
if err == io.ErrUnexpectedEOF && c.rawConn.RawInput.Len() == 0 {
err = io.EOF
}
if e, ok := err.(net.Error); !ok || !e.Temporary() {
c.rawConn.In.SetErrorLocked(err)
}
return
}
hdr := c.rawConn.RawInput.Bytes()[:recordHeaderLen]
typ = hdr[0]
vers := uint16(hdr[1])<<8 | uint16(hdr[2])
expectedVers := *c.rawConn.Vers
if expectedVers == tls.VersionTLS13 {
// All TLS 1.3 records are expected to have 0x0303 (1.2) after
// the initial hello (RFC 8446 Section 5.1).
expectedVers = tls.VersionTLS12
}
n := int(hdr[3])<<8 | int(hdr[4])
if /*c.haveVers && */ vers != expectedVers {
c.sendAlert(alertProtocolVersion)
msg := fmt.Sprintf("received record with version %x when expecting version %x", vers, expectedVers)
err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(nil, msg))
return
}
//if !c.haveVers {
// // First message, be extra suspicious: this might not be a TLS
// // client. Bail out before reading a full 'body', if possible.
// // The current max version is 3.3 so if the version is >= 16.0,
// // it's probably not real.
// if (typ != recordTypeAlert && typ != recordTypeHandshake) || vers >= 0x1000 {
// err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(c.conn, "first record does not look like a TLS handshake"))
// return
// }
//}
if *c.rawConn.Vers == tls.VersionTLS13 && n > maxCiphertextTLS13 || n > maxCiphertext {
c.sendAlert(alertRecordOverflow)
msg := fmt.Sprintf("oversized record received with length %d", n)
err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(nil, msg))
return
}
if err = c.readFromUntil(c.conn, recordHeaderLen+n); err != nil {
if e, ok := err.(net.Error); !ok || !e.Temporary() {
c.rawConn.In.SetErrorLocked(err)
}
return
}
// Process message.
record := c.rawConn.RawInput.Next(recordHeaderLen + n)
data, typ, err = c.rawConn.In.Decrypt(record)
if err != nil {
err = c.rawConn.In.SetErrorLocked(c.sendAlert(uint8(err.(tls.AlertError))))
return
}
return
}
// retryReadRecord recurs into readRecordOrCCS to drop a non-advancing record, like
// a warning alert, empty application_data, or a change_cipher_spec in TLS 1.3.
func (c *Conn) retryReadRecord( /*expectChangeCipherSpec bool*/ ) error {
//c.retryCount++
//if c.retryCount > maxUselessRecords {
// c.sendAlert(alertUnexpectedMessage)
// return c.in.setErrorLocked(errors.New("tls: too many ignored records"))
//}
return c.readRecord( /*expectChangeCipherSpec*/ )
}
// atLeastReader reads from R, stopping with EOF once at least N bytes have been
// read. It is different from an io.LimitedReader in that it doesn't cut short
// the last Read call, and in that it considers an early EOF an error.
type atLeastReader struct {
R io.Reader
N int64
}
func (r *atLeastReader) Read(p []byte) (int, error) {
if r.N <= 0 {
return 0, io.EOF
}
n, err := r.R.Read(p)
r.N -= int64(n) // won't underflow unless len(p) >= n > 9223372036854775809
if r.N > 0 && err == io.EOF {
return n, io.ErrUnexpectedEOF
}
if r.N <= 0 && err == nil {
return n, io.EOF
}
return n, err
}
// readFromUntil reads from r into c.rawConn.RawInput until c.rawConn.RawInput contains
// at least n bytes or else returns an error.
func (c *Conn) readFromUntil(r io.Reader, n int) error {
if c.rawConn.RawInput.Len() >= n {
return nil
}
needs := n - c.rawConn.RawInput.Len()
// There might be extra input waiting on the wire. Make a best effort
// attempt to fetch it so that it can be used in (*Conn).Read to
// "predict" closeNotify alerts.
c.rawConn.RawInput.Grow(needs + bytes.MinRead)
_, err := c.rawConn.RawInput.ReadFrom(&atLeastReader{r, int64(needs)})
return err
}
func (c *Conn) newRecordHeaderError(conn net.Conn, msg string) (err tls.RecordHeaderError) {
err.Msg = msg
err.Conn = conn
copy(err.RecordHeader[:], c.rawConn.RawInput.Bytes())
return err
}

View File

@@ -1,41 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && go1.25 && badlinkname
package ktls
import (
"github.com/sagernet/sing/common/buf"
N "github.com/sagernet/sing/common/network"
)
func (c *Conn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) {
c.readWaitOptions = options
return false
}
func (c *Conn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
c.rawConn.In.Lock()
defer c.rawConn.In.Unlock()
for c.rawConn.Input.Len() == 0 {
err = c.readRecord()
if err != nil {
return
}
}
buffer = c.readWaitOptions.NewBuffer()
n, err := c.rawConn.Input.Read(buffer.FreeBytes())
if err != nil {
buffer.Release()
return
}
buffer.Truncate(n)
if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.Input.Len() > 0 &&
c.rawConn.RawInput.Bytes()[0] == recordTypeAlert {
_ = c.rawConn.ReadRecord()
}
c.readWaitOptions.PostReturn(buffer)
return
}

View File

@@ -1,15 +0,0 @@
//go:build linux && go1.25 && !badlinkname
package ktls
import (
"context"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
aTLS "github.com/sagernet/sing/common/tls"
)
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
return nil, E.New("kTLS requires build flags `badlinkname` and `-ldflags=-checklinkname=0`, please recompile your binary")
}

View File

@@ -1,15 +0,0 @@
//go:build !linux
package ktls
import (
"context"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
aTLS "github.com/sagernet/sing/common/tls"
)
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
return nil, E.New("kTLS is only supported on Linux")
}

View File

@@ -1,15 +0,0 @@
//go:build linux && !go1.25
package ktls
import (
"context"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
aTLS "github.com/sagernet/sing/common/tls"
)
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
return nil, E.New("kTLS requires Go 1.25 or later, please recompile your binary")
}

View File

@@ -1,154 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && go1.25 && badlinkname
package ktls
import (
"crypto/cipher"
"crypto/tls"
"errors"
"net"
)
func (c *Conn) Write(b []byte) (int, error) {
if !c.kernelTx {
return c.Conn.Write(b)
}
// interlock with Close below
for {
x := c.rawConn.ActiveCall.Load()
if x&1 != 0 {
return 0, net.ErrClosed
}
if c.rawConn.ActiveCall.CompareAndSwap(x, x+2) {
break
}
}
defer c.rawConn.ActiveCall.Add(-2)
//if err := c.Conn.HandshakeContext(context.Background()); err != nil {
// return 0, err
//}
c.rawConn.Out.Lock()
defer c.rawConn.Out.Unlock()
if err := *c.rawConn.Out.Err; err != nil {
return 0, err
}
if !c.rawConn.IsHandshakeComplete.Load() {
return 0, tls.AlertError(alertInternalError)
}
if *c.rawConn.CloseNotifySent {
// return 0, errShutdown
return 0, errors.New("tls: protocol is shutdown")
}
// TLS 1.0 is susceptible to a chosen-plaintext
// attack when using block mode ciphers due to predictable IVs.
// This can be prevented by splitting each Application Data
// record into two records, effectively randomizing the RawIV.
//
// https://www.openssl.org/~bodo/tls-cbc.txt
// https://bugzilla.mozilla.org/show_bug.cgi?id=665814
// https://www.imperialviolet.org/2012/01/15/beastfollowup.html
var m int
if len(b) > 1 && *c.rawConn.Vers == tls.VersionTLS10 {
if _, ok := (*c.rawConn.Out.Cipher).(cipher.BlockMode); ok {
n, err := c.writeRecordLocked(recordTypeApplicationData, b[:1])
if err != nil {
return n, c.rawConn.Out.SetErrorLocked(err)
}
m, b = 1, b[1:]
}
}
n, err := c.writeRecordLocked(recordTypeApplicationData, b)
return n + m, c.rawConn.Out.SetErrorLocked(err)
}
func (c *Conn) writeRecordLocked(typ uint16, data []byte) (n int, err error) {
if !c.kernelTx {
return c.rawConn.WriteRecordLocked(typ, data)
}
/*for len(data) > 0 {
m := len(data)
if maxPayload := c.maxPayloadSizeForWrite(typ); m > maxPayload {
m = maxPayload
}
_, err = c.writeKernelRecord(typ, data[:m])
if err != nil {
return
}
n += m
data = data[m:]
}*/
return c.writeKernelRecord(typ, data)
}
const (
// tcpMSSEstimate is a conservative estimate of the TCP maximum segment
// size (MSS). A constant is used, rather than querying the kernel for
// the actual MSS, to avoid complexity. The value here is the IPv6
// minimum MTU (1280 bytes) minus the overhead of an IPv6 header (40
// bytes) and a TCP header with timestamps (32 bytes).
tcpMSSEstimate = 1208
// recordSizeBoostThreshold is the number of bytes of application data
// sent after which the TLS record size will be increased to the
// maximum.
recordSizeBoostThreshold = 128 * 1024
)
func (c *Conn) maxPayloadSizeForWrite(typ uint16) int {
if /*c.config.DynamicRecordSizingDisabled ||*/ typ != recordTypeApplicationData {
return maxPlaintext
}
if *c.rawConn.PacketsSent >= recordSizeBoostThreshold {
return maxPlaintext
}
// Subtract TLS overheads to get the maximum payload size.
payloadBytes := tcpMSSEstimate - recordHeaderLen - c.rawConn.Out.ExplicitNonceLen()
if rawCipher := *c.rawConn.Out.Cipher; rawCipher != nil {
switch ciph := rawCipher.(type) {
case cipher.Stream:
payloadBytes -= (*c.rawConn.Out.Mac).Size()
case cipher.AEAD:
payloadBytes -= ciph.Overhead()
/*case cbcMode:
blockSize := ciph.BlockSize()
// The payload must fit in a multiple of blockSize, with
// room for at least one padding byte.
payloadBytes = (payloadBytes & ^(blockSize - 1)) - 1
// The RawMac is appended before padding so affects the
// payload size directly.
payloadBytes -= c.out.mac.Size()*/
default:
panic("unknown cipher type")
}
}
if *c.rawConn.Vers == tls.VersionTLS13 {
payloadBytes-- // encrypted ContentType
}
// Allow packet growth in arithmetic progression up to max.
pkt := *c.rawConn.PacketsSent
*c.rawConn.PacketsSent++
if pkt > 1000 {
return maxPlaintext // avoid overflow in multiply below
}
n := payloadBytes * int(pkt+1)
if n > maxPlaintext {
n = maxPlaintext
}
return n
}

View File

@@ -0,0 +1,11 @@
//go:build go1.21
package listener
import "net"
const go121Available = true
func setMultiPathTCP(listenConfig *net.ListenConfig) {
listenConfig.SetMultipathTCP(true)
}

View File

@@ -0,0 +1,16 @@
//go:build go1.23
package listener
import (
"net"
"time"
)
func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
listener.KeepAliveConfig = net.KeepAliveConfig{
Enable: true,
Idle: idle,
Interval: interval,
}
}

View File

@@ -0,0 +1,10 @@
//go:build !go1.21
package listener
import "net"
const go121Available = false
func setMultiPathTCP(listenConfig *net.ListenConfig) {
}

View File

@@ -0,0 +1,15 @@
//go:build !go1.23
package listener
import (
"net"
"time"
"github.com/sagernet/sing/common/control"
)
func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
listener.KeepAlive = idle
listener.Control = control.Append(listener.Control, control.SetKeepAlivePeriod(idle, interval))
}

View File

@@ -3,7 +3,6 @@ package listener
import ( import (
"net" "net"
"net/netip" "net/netip"
"strings"
"syscall" "syscall"
"time" "time"
@@ -17,7 +16,7 @@ import (
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/database64128/tfo-go/v2" "github.com/metacubex/tfo-go"
) )
func (l *Listener) ListenTCP() (net.Listener, error) { func (l *Listener) ListenTCP() (net.Listener, error) {
@@ -46,19 +45,18 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
if keepInterval == 0 { if keepInterval == 0 {
keepInterval = C.TCPKeepAliveInterval keepInterval = C.TCPKeepAliveInterval
} }
listenConfig.KeepAliveConfig = net.KeepAliveConfig{ setKeepAliveConfig(&listenConfig, keepIdle, keepInterval)
Enable: true,
Idle: keepIdle,
Interval: keepInterval,
}
} }
if l.listenOptions.TCPMultiPath { if l.listenOptions.TCPMultiPath {
listenConfig.SetMultipathTCP(true) if !go121Available {
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
}
setMultiPathTCP(&listenConfig)
} }
if l.tproxy { if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error { return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, !strings.HasSuffix(network, "4"), false) return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), false)
}) })
}) })
} }

View File

@@ -5,7 +5,6 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"strings"
"syscall" "syscall"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -42,7 +41,7 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
if l.tproxy { if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error { listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error { return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, !strings.HasSuffix(network, "4"), true) return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), true)
}) })
}) })
} }
@@ -165,8 +164,9 @@ func (l *Listener) loopUDPOut() {
if l.shutdown.Load() && E.IsClosed(err) { if l.shutdown.Load() && E.IsClosed(err) {
return return
} }
l.udpConn.Close()
l.logger.Error("udp listener write back: ", destination, ": ", err) l.logger.Error("udp listener write back: ", destination, ": ", err)
continue return
} }
continue continue
case <-l.packetOutboundClosed: case <-l.packetOutboundClosed:

View File

@@ -5,7 +5,6 @@ import (
"net/netip" "net/netip"
"os/user" "os/user"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -13,7 +12,7 @@ import (
) )
type Searcher interface { type Searcher interface {
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error)
} }
var ErrNotFound = E.New("process not found") var ErrNotFound = E.New("process not found")
@@ -23,7 +22,15 @@ type Config struct {
PackageManager tun.PackageManager PackageManager tun.PackageManager
} }
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { type Info struct {
ProcessID uint32
ProcessPath string
PackageName string
User string
UserId int32
}
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
info, err := searcher.FindProcessInfo(ctx, network, source, destination) info, err := searcher.FindProcessInfo(ctx, network, source, destination)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -31,7 +38,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
if info.UserId != -1 { if info.UserId != -1 {
osUser, _ := user.LookupId(F.ToString(info.UserId)) osUser, _ := user.LookupId(F.ToString(info.UserId))
if osUser != nil { if osUser != nil {
info.UserName = osUser.Username info.User = osUser.Username
} }
} }
return info, nil return info, nil

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"net/netip" "net/netip"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
) )
@@ -18,22 +17,22 @@ func NewSearcher(config Config) (Searcher, error) {
return &androidSearcher{config.PackageManager}, nil return &androidSearcher{config.PackageManager}, nil
} }
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
_, uid, err := resolveSocketByNetlink(network, source, destination) _, uid, err := resolveSocketByNetlink(network, source, destination)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded { if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
return &adapter.ConnectionOwner{ return &Info{
UserId: int32(uid), UserId: int32(uid),
AndroidPackageName: sharedPackage, PackageName: sharedPackage,
}, nil }, nil
} }
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded { if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
return &adapter.ConnectionOwner{ return &Info{
UserId: int32(uid), UserId: int32(uid),
AndroidPackageName: packageName, PackageName: packageName,
}, nil }, nil
} }
return &adapter.ConnectionOwner{UserId: int32(uid)}, nil return &Info{UserId: int32(uid)}, nil
} }

View File

@@ -10,7 +10,6 @@ import (
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/sagernet/sing-box/adapter"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
@@ -24,12 +23,12 @@ func NewSearcher(_ Config) (Searcher, error) {
return &darwinSearcher{}, nil return &darwinSearcher{}, nil
} }
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
processName, err := findProcessName(network, source.Addr(), int(source.Port())) processName, err := findProcessName(network, source.Addr(), int(source.Port()))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil return &Info{ProcessPath: processName, UserId: -1}, nil
} }
var structSize = func() int { var structSize = func() int {
@@ -97,11 +96,11 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
switch { switch {
case flag&0x1 > 0 && isIPv4: case flag&0x1 > 0 && isIPv4:
// ipv4 // ipv4
srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80])) srcIP = netip.AddrFrom4(*(*[4]byte)(buf[inp+76 : inp+80]))
srcIsIPv4 = true srcIsIPv4 = true
case flag&0x2 > 0 && !isIPv4: case flag&0x2 > 0 && !isIPv4:
// ipv6 // ipv6
srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80])) srcIP = netip.AddrFrom16(*(*[16]byte)(buf[inp+64 : inp+80]))
default: default:
continue continue
} }

View File

@@ -6,7 +6,6 @@ import (
"context" "context"
"net/netip" "net/netip"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
) )
@@ -20,7 +19,7 @@ func NewSearcher(config Config) (Searcher, error) {
return &linuxSearcher{config.Logger}, nil return &linuxSearcher{config.Logger}, nil
} }
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
inode, uid, err := resolveSocketByNetlink(network, source, destination) inode, uid, err := resolveSocketByNetlink(network, source, destination)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -29,7 +28,7 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou
if err != nil { if err != nil {
s.logger.DebugContext(ctx, "find process path: ", err) s.logger.DebugContext(ctx, "find process path: ", err)
} }
return &adapter.ConnectionOwner{ return &Info{
UserId: int32(uid), UserId: int32(uid),
ProcessPath: processPath, ProcessPath: processPath,
}, nil }, nil

View File

@@ -5,7 +5,6 @@ import (
"net/netip" "net/netip"
"syscall" "syscall"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/winiphlpapi" "github.com/sagernet/sing/common/winiphlpapi"
@@ -28,16 +27,16 @@ func initWin32API() error {
return winiphlpapi.LoadExtendedTable() return winiphlpapi.LoadExtendedTable()
} }
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
pid, err := winiphlpapi.FindPid(network, source) pid, err := winiphlpapi.FindPid(network, source)
if err != nil { if err != nil {
return nil, err return nil, err
} }
path, err := getProcessPath(pid) path, err := getProcessPath(pid)
if err != nil { if err != nil {
return &adapter.ConnectionOwner{ProcessID: pid, UserId: -1}, err return &Info{ProcessID: pid, UserId: -1}, err
} }
return &adapter.ConnectionOwner{ProcessID: pid, ProcessPath: path, UserId: -1}, nil return &Info{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
} }
func getProcessPath(pid uint32) (string, error) { func getProcessPath(pid uint32) (string, error) {

View File

@@ -1,9 +0,0 @@
package settings
import "github.com/sagernet/sing-box/adapter"
type WIFIMonitor interface {
ReadWIFIState() adapter.WIFIState
Start() error
Close() error
}

View File

@@ -1,46 +0,0 @@
package settings
import (
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
)
type LinuxWIFIMonitor struct {
monitor WIFIMonitor
}
func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
monitors := []func(func(adapter.WIFIState)) (WIFIMonitor, error){
newNetworkManagerMonitor,
newIWDMonitor,
newWpaSupplicantMonitor,
newConnManMonitor,
}
var errors []error
for _, factory := range monitors {
monitor, err := factory(callback)
if err == nil {
return &LinuxWIFIMonitor{monitor: monitor}, nil
}
errors = append(errors, err)
}
return nil, E.Cause(E.Errors(errors...), "no supported WIFI manager found")
}
func (m *LinuxWIFIMonitor) ReadWIFIState() adapter.WIFIState {
return m.monitor.ReadWIFIState()
}
func (m *LinuxWIFIMonitor) Start() error {
if m.monitor != nil {
return m.monitor.Start()
}
return nil
}
func (m *LinuxWIFIMonitor) Close() error {
if m.monitor != nil {
return m.monitor.Close()
}
return nil
}

View File

@@ -1,160 +0,0 @@
package settings
import (
"context"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/godbus/dbus/v5"
)
type connmanMonitor struct {
conn *dbus.Conn
callback func(adapter.WIFIState)
cancel context.CancelFunc
signalChan chan *dbus.Signal
}
func newConnManMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return nil, err
}
cmObj := conn.Object("net.connman", "/")
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
call := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0)
if call.Err != nil {
conn.Close()
return nil, call.Err
}
return &connmanMonitor{conn: conn, callback: callback}, nil
}
func (m *connmanMonitor) ReadWIFIState() adapter.WIFIState {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
cmObj := m.conn.Object("net.connman", "/")
var services []interface{}
err := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0).Store(&services)
if err != nil {
return adapter.WIFIState{}
}
for _, service := range services {
servicePair, ok := service.([]interface{})
if !ok || len(servicePair) != 2 {
continue
}
serviceProps, ok := servicePair[1].(map[string]dbus.Variant)
if !ok {
continue
}
typeVariant, hasType := serviceProps["Type"]
if !hasType {
continue
}
serviceType, ok := typeVariant.Value().(string)
if !ok || serviceType != "wifi" {
continue
}
stateVariant, hasState := serviceProps["State"]
if !hasState {
continue
}
state, ok := stateVariant.Value().(string)
if !ok || (state != "online" && state != "ready") {
continue
}
nameVariant, hasName := serviceProps["Name"]
if !hasName {
continue
}
ssid, ok := nameVariant.Value().(string)
if !ok || ssid == "" {
continue
}
bssidVariant, hasBSSID := serviceProps["BSSID"]
if !hasBSSID {
return adapter.WIFIState{SSID: ssid}
}
bssid, ok := bssidVariant.Value().(string)
if !ok {
return adapter.WIFIState{SSID: ssid}
}
return adapter.WIFIState{
SSID: ssid,
BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")),
}
}
return adapter.WIFIState{}
}
func (m *connmanMonitor) Start() error {
if m.callback == nil {
return nil
}
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
m.signalChan = make(chan *dbus.Signal, 10)
m.conn.Signal(m.signalChan)
err := m.conn.AddMatchSignal(
dbus.WithMatchInterface("net.connman.Service"),
dbus.WithMatchSender("net.connman"),
)
if err != nil {
return err
}
state := m.ReadWIFIState()
go m.monitorSignals(ctx, m.signalChan, state)
m.callback(state)
return nil
}
func (m *connmanMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) {
for {
select {
case <-ctx.Done():
return
case signal, ok := <-signalChan:
if !ok {
return
}
if signal.Name == "PropertyChanged" {
state := m.ReadWIFIState()
if state != lastState {
lastState = state
m.callback(state)
}
}
}
}
}
func (m *connmanMonitor) Close() error {
if m.cancel != nil {
m.cancel()
}
if m.signalChan != nil {
m.conn.RemoveSignal(m.signalChan)
close(m.signalChan)
}
if m.conn != nil {
return m.conn.Close()
}
return nil
}

View File

@@ -1,184 +0,0 @@
package settings
import (
"context"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/godbus/dbus/v5"
)
type iwdMonitor struct {
conn *dbus.Conn
callback func(adapter.WIFIState)
cancel context.CancelFunc
signalChan chan *dbus.Signal
}
func newIWDMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return nil, err
}
iwdObj := conn.Object("net.connman.iwd", "/")
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
call := iwdObj.CallWithContext(ctx, "org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0)
if call.Err != nil {
conn.Close()
return nil, call.Err
}
return &iwdMonitor{conn: conn, callback: callback}, nil
}
func (m *iwdMonitor) ReadWIFIState() adapter.WIFIState {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
iwdObj := m.conn.Object("net.connman.iwd", "/")
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err := iwdObj.CallWithContext(ctx, "org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&objects)
if err != nil {
return adapter.WIFIState{}
}
for _, interfaces := range objects {
stationProps, hasStation := interfaces["net.connman.iwd.Station"]
if !hasStation {
continue
}
stateVariant, hasState := stationProps["State"]
if !hasState {
continue
}
state, ok := stateVariant.Value().(string)
if !ok || state != "connected" {
continue
}
connectedNetworkVariant, hasNetwork := stationProps["ConnectedNetwork"]
if !hasNetwork {
continue
}
networkPath, ok := connectedNetworkVariant.Value().(dbus.ObjectPath)
if !ok || networkPath == "/" {
continue
}
networkInterfaces, hasNetworkPath := objects[networkPath]
if !hasNetworkPath {
continue
}
networkProps, hasNetworkInterface := networkInterfaces["net.connman.iwd.Network"]
if !hasNetworkInterface {
continue
}
nameVariant, hasName := networkProps["Name"]
if !hasName {
continue
}
ssid, ok := nameVariant.Value().(string)
if !ok {
continue
}
connectedBSSVariant, hasBSS := stationProps["ConnectedAccessPoint"]
if !hasBSS {
return adapter.WIFIState{SSID: ssid}
}
bssPath, ok := connectedBSSVariant.Value().(dbus.ObjectPath)
if !ok || bssPath == "/" {
return adapter.WIFIState{SSID: ssid}
}
bssInterfaces, hasBSSPath := objects[bssPath]
if !hasBSSPath {
return adapter.WIFIState{SSID: ssid}
}
bssProps, hasBSSInterface := bssInterfaces["net.connman.iwd.BasicServiceSet"]
if !hasBSSInterface {
return adapter.WIFIState{SSID: ssid}
}
addressVariant, hasAddress := bssProps["Address"]
if !hasAddress {
return adapter.WIFIState{SSID: ssid}
}
bssid, ok := addressVariant.Value().(string)
if !ok {
return adapter.WIFIState{SSID: ssid}
}
return adapter.WIFIState{
SSID: ssid,
BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")),
}
}
return adapter.WIFIState{}
}
func (m *iwdMonitor) Start() error {
if m.callback == nil {
return nil
}
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
m.signalChan = make(chan *dbus.Signal, 10)
m.conn.Signal(m.signalChan)
err := m.conn.AddMatchSignal(
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
dbus.WithMatchSender("net.connman.iwd"),
)
if err != nil {
return err
}
state := m.ReadWIFIState()
go m.monitorSignals(ctx, m.signalChan, state)
m.callback(state)
return nil
}
func (m *iwdMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) {
for {
select {
case <-ctx.Done():
return
case signal, ok := <-signalChan:
if !ok {
return
}
if signal.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" {
state := m.ReadWIFIState()
if state != lastState {
lastState = state
m.callback(state)
}
}
}
}
}
func (m *iwdMonitor) Close() error {
if m.cancel != nil {
m.cancel()
}
if m.signalChan != nil {
m.conn.RemoveSignal(m.signalChan)
close(m.signalChan)
}
if m.conn != nil {
return m.conn.Close()
}
return nil
}

View File

@@ -1,157 +0,0 @@
package settings
import (
"context"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/godbus/dbus/v5"
)
type networkManagerMonitor struct {
conn *dbus.Conn
callback func(adapter.WIFIState)
cancel context.CancelFunc
signalChan chan *dbus.Signal
}
func newNetworkManagerMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return nil, err
}
nmObj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
var state uint32
err = nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "State").Store(&state)
if err != nil {
conn.Close()
return nil, err
}
return &networkManagerMonitor{conn: conn, callback: callback}, nil
}
func (m *networkManagerMonitor) ReadWIFIState() adapter.WIFIState {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
nmObj := m.conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
var primaryConnectionPath dbus.ObjectPath
err := nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "PrimaryConnection").Store(&primaryConnectionPath)
if err != nil || primaryConnectionPath == "/" {
return adapter.WIFIState{}
}
connObj := m.conn.Object("org.freedesktop.NetworkManager", primaryConnectionPath)
var devicePaths []dbus.ObjectPath
err = connObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Connection.Active", "Devices").Store(&devicePaths)
if err != nil || len(devicePaths) == 0 {
return adapter.WIFIState{}
}
for _, devicePath := range devicePaths {
deviceObj := m.conn.Object("org.freedesktop.NetworkManager", devicePath)
var deviceType uint32
err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device", "DeviceType").Store(&deviceType)
if err != nil || deviceType != 2 {
continue
}
var accessPointPath dbus.ObjectPath
err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint").Store(&accessPointPath)
if err != nil || accessPointPath == "/" {
continue
}
apObj := m.conn.Object("org.freedesktop.NetworkManager", accessPointPath)
var ssidBytes []byte
err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "Ssid").Store(&ssidBytes)
if err != nil {
continue
}
var hwAddress string
err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "HwAddress").Store(&hwAddress)
if err != nil {
continue
}
ssid := strings.TrimSpace(string(ssidBytes))
if ssid == "" {
continue
}
return adapter.WIFIState{
SSID: ssid,
BSSID: strings.ToUpper(strings.ReplaceAll(hwAddress, ":", "")),
}
}
return adapter.WIFIState{}
}
func (m *networkManagerMonitor) Start() error {
if m.callback == nil {
return nil
}
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
m.signalChan = make(chan *dbus.Signal, 10)
m.conn.Signal(m.signalChan)
err := m.conn.AddMatchSignal(
dbus.WithMatchSender("org.freedesktop.NetworkManager"),
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
)
if err != nil {
return err
}
state := m.ReadWIFIState()
go m.monitorSignals(ctx, m.signalChan, state)
m.callback(state)
return nil
}
func (m *networkManagerMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) {
for {
select {
case <-ctx.Done():
return
case signal, ok := <-signalChan:
if !ok {
return
}
if signal.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" {
state := m.ReadWIFIState()
if state != lastState {
lastState = state
m.callback(state)
}
}
}
}
}
func (m *networkManagerMonitor) Close() error {
if m.cancel != nil {
m.cancel()
}
if m.signalChan != nil {
m.conn.RemoveSignal(m.signalChan)
close(m.signalChan)
}
if m.conn != nil {
return m.conn.Close()
}
return nil
}

View File

@@ -1,179 +0,0 @@
package settings
import (
"bufio"
"context"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
)
type wpaSupplicantMonitor struct {
socketPath string
callback func(adapter.WIFIState)
cancel context.CancelFunc
}
func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
socketDirs := []string{"/var/run/wpa_supplicant", "/run/wpa_supplicant"}
for _, socketDir := range socketDirs {
entries, err := os.ReadDir(socketDir)
if err != nil {
continue
}
for _, entry := range entries {
if entry.IsDir() || entry.Name() == "." || entry.Name() == ".." {
continue
}
socketPath := filepath.Join(socketDir, entry.Name())
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"}
remoteAddr := &net.UnixAddr{Name: socketPath, Net: "unixgram"}
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
if err != nil {
continue
}
conn.Close()
return &wpaSupplicantMonitor{socketPath: socketPath, callback: callback}, nil
}
}
return nil, os.ErrNotExist
}
func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState {
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"}
remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
if err != nil {
return adapter.WIFIState{}
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(3 * time.Second))
status, err := m.sendCommand(conn, "STATUS")
if err != nil {
return adapter.WIFIState{}
}
var ssid, bssid string
var connected bool
scanner := bufio.NewScanner(strings.NewReader(status))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "wpa_state=") {
state := strings.TrimPrefix(line, "wpa_state=")
connected = state == "COMPLETED"
} else if strings.HasPrefix(line, "ssid=") {
ssid = strings.TrimPrefix(line, "ssid=")
} else if strings.HasPrefix(line, "bssid=") {
bssid = strings.TrimPrefix(line, "bssid=")
}
}
if !connected || ssid == "" {
return adapter.WIFIState{}
}
return adapter.WIFIState{
SSID: ssid,
BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")),
}
}
func (m *wpaSupplicantMonitor) sendCommand(conn *net.UnixConn, command string) (string, error) {
_, err := conn.Write([]byte(command + "\n"))
if err != nil {
return "", err
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
return "", err
}
response := string(buf[:n])
if strings.HasPrefix(response, "FAIL") {
return "", os.ErrInvalid
}
return strings.TrimSpace(response), nil
}
func (m *wpaSupplicantMonitor) Start() error {
if m.callback == nil {
return nil
}
ctx, cancel := context.WithCancel(context.Background())
m.cancel = cancel
state := m.ReadWIFIState()
go m.monitorEvents(ctx, state)
m.callback(state)
return nil
}
func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adapter.WIFIState) {
var consecutiveErrors int
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-mon-%d", os.Getpid()), Net: "unixgram"}
remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
if err != nil {
return
}
defer conn.Close()
_, err = conn.Write([]byte("ATTACH\n"))
if err != nil {
return
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil || !strings.HasPrefix(string(buf[:n]), "OK") {
return
}
for {
select {
case <-ctx.Done():
return
default:
}
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
n, err := conn.Read(buf)
if err != nil {
consecutiveErrors++
if consecutiveErrors > 10 {
return
}
time.Sleep(time.Second)
continue
}
consecutiveErrors = 0
msg := string(buf[:n])
if strings.Contains(msg, "CTRL-EVENT-CONNECTED") || strings.Contains(msg, "CTRL-EVENT-DISCONNECTED") {
state := m.ReadWIFIState()
if state != lastState {
lastState = state
m.callback(state)
}
}
}
}
func (m *wpaSupplicantMonitor) Close() error {
if m.cancel != nil {
m.cancel()
}
return nil
}

View File

@@ -1,27 +0,0 @@
//go:build !linux
package settings
import (
"os"
"github.com/sagernet/sing-box/adapter"
)
type stubWIFIMonitor struct{}
func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
return nil, os.ErrInvalid
}
func (m *stubWIFIMonitor) ReadWIFIState() adapter.WIFIState {
return adapter.WIFIState{}
}
func (m *stubWIFIMonitor) Start() error {
return nil
}
func (m *stubWIFIMonitor) Close() error {
return nil
}

View File

@@ -17,5 +17,8 @@ var uQUICChrome115 = &ja3.ClientHello{
} }
func maybeUQUIC(fingerprint *ja3.ClientHello) bool { func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
return !uQUICChrome115.Equals(fingerprint, true) if uQUICChrome115.Equals(fingerprint, true) {
return true
}
return false
} }

View File

@@ -56,7 +56,7 @@ func TestSniffUQUICChrome115(t *testing.T) {
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, metadata.Protocol, C.ProtocolQUIC) require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
require.Equal(t, metadata.Client, C.ClientChromium) require.Equal(t, metadata.Client, C.ClientQUICGo)
require.Equal(t, metadata.Domain, "www.google.com") require.Equal(t, metadata.Domain, "www.google.com")
} }

View File

@@ -12,8 +12,6 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/domain" "github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
"github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/common/varbin"
"go4.org/netipx" "go4.org/netipx"
@@ -43,8 +41,6 @@ const (
ruleItemNetworkType ruleItemNetworkType
ruleItemNetworkIsExpensive ruleItemNetworkIsExpensive
ruleItemNetworkIsConstrained ruleItemNetworkIsConstrained
ruleItemNetworkInterfaceAddress
ruleItemDefaultInterfaceAddress
ruleItemFinal uint8 = 0xFF ruleItemFinal uint8 = 0xFF
) )
@@ -234,51 +230,6 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
rule.NetworkIsExpensive = true rule.NetworkIsExpensive = true
case ruleItemNetworkIsConstrained: case ruleItemNetworkIsConstrained:
rule.NetworkIsConstrained = true rule.NetworkIsConstrained = true
case ruleItemNetworkInterfaceAddress:
rule.NetworkInterfaceAddress = new(badjson.TypedMap[option.InterfaceType, badoption.Listable[*badoption.Prefixable]])
var size uint64
size, err = binary.ReadUvarint(reader)
if err != nil {
return
}
for i := uint64(0); i < size; i++ {
var key uint8
err = binary.Read(reader, binary.BigEndian, &key)
if err != nil {
return
}
var value []*badoption.Prefixable
var prefixCount uint64
prefixCount, err = binary.ReadUvarint(reader)
if err != nil {
return
}
for j := uint64(0); j < prefixCount; j++ {
var prefix netip.Prefix
prefix, err = readPrefix(reader)
if err != nil {
return
}
value = append(value, common.Ptr(badoption.Prefixable(prefix)))
}
rule.NetworkInterfaceAddress.Put(option.InterfaceType(key), value)
}
case ruleItemDefaultInterfaceAddress:
var value []*badoption.Prefixable
var prefixCount uint64
prefixCount, err = binary.ReadUvarint(reader)
if err != nil {
return
}
for j := uint64(0); j < prefixCount; j++ {
var prefix netip.Prefix
prefix, err = readPrefix(reader)
if err != nil {
return
}
value = append(value, common.Ptr(badoption.Prefixable(prefix)))
}
rule.DefaultInterfaceAddress = value
case ruleItemFinal: case ruleItemFinal:
err = binary.Read(reader, binary.BigEndian, &rule.Invert) err = binary.Read(reader, binary.BigEndian, &rule.Invert)
return return
@@ -395,7 +346,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
} }
if len(rule.NetworkType) > 0 { if len(rule.NetworkType) > 0 {
if generateVersion < C.RuleSetVersion3 { if generateVersion < C.RuleSetVersion3 {
return E.New("`network_type` rule item is only supported in version 3 or later") return E.New("network_type rule item is only supported in version 3 or later")
} }
err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType) err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType)
if err != nil { if err != nil {
@@ -403,71 +354,17 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
} }
} }
if rule.NetworkIsExpensive { if rule.NetworkIsExpensive {
if generateVersion < C.RuleSetVersion3 {
return E.New("`network_is_expensive` rule item is only supported in version 3 or later")
}
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive) err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive)
if err != nil { if err != nil {
return err return err
} }
} }
if rule.NetworkIsConstrained { if rule.NetworkIsConstrained {
if generateVersion < C.RuleSetVersion3 {
return E.New("`network_is_constrained` rule item is only supported in version 3 or later")
}
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained) err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained)
if err != nil { if err != nil {
return err return err
} }
} }
if rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 {
if generateVersion < C.RuleSetVersion4 {
return E.New("`network_interface_address` rule item is only supported in version 4 or later")
}
err = writer.WriteByte(ruleItemNetworkInterfaceAddress)
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(rule.NetworkInterfaceAddress.Size()))
if err != nil {
return err
}
for _, entry := range rule.NetworkInterfaceAddress.Entries() {
err = binary.Write(writer, binary.BigEndian, uint8(entry.Key.Build()))
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(len(entry.Value)))
if err != nil {
return err
}
for _, rawPrefix := range entry.Value {
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
if err != nil {
return err
}
}
}
}
if len(rule.DefaultInterfaceAddress) > 0 {
if generateVersion < C.RuleSetVersion4 {
return E.New("`default_interface_address` rule item is only supported in version 4 or later")
}
err = writer.WriteByte(ruleItemDefaultInterfaceAddress)
if err != nil {
return err
}
_, err = varbin.WriteUvarint(writer, uint64(len(rule.DefaultInterfaceAddress)))
if err != nil {
return err
}
for _, rawPrefix := range rule.DefaultInterfaceAddress {
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
if err != nil {
return err
}
}
}
if len(rule.WIFISSID) > 0 { if len(rule.WIFISSID) > 0 {
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
if err != nil { if err != nil {

View File

@@ -1,33 +0,0 @@
package srs
import (
"encoding/binary"
"net/netip"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
)
func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian)
if err != nil {
return netip.Prefix{}, err
}
prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian)
if err != nil {
return netip.Prefix{}, err
}
return netip.PrefixFrom(M.AddrFromIP(addrSlice), int(prefixBits)), nil
}
func writePrefix(writer varbin.Writer, prefix netip.Prefix) error {
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
if err != nil {
return err
}
return nil
}

View File

@@ -2,71 +2,39 @@ package tls
import ( import (
"context" "context"
"crypto/tls"
"errors"
"net" "net"
"os" "os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/badtls" "github.com/sagernet/sing-box/common/badtls"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls" aTLS "github.com/sagernet/sing/common/tls"
) )
func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) { func NewDialerFromOptions(ctx context.Context, router adapter.Router, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
if !options.Enabled { if !options.Enabled {
return dialer, nil return dialer, nil
} }
config, err := NewClientWithOptions(ClientOptions{ config, err := NewClient(ctx, serverAddress, options)
Context: ctx,
Logger: logger,
ServerAddress: serverAddress,
Options: options,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewDialer(dialer, config), nil return NewDialer(dialer, config), nil
} }
func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return NewClientWithOptions(ClientOptions{ if !options.Enabled {
Context: ctx,
Logger: logger,
ServerAddress: serverAddress,
Options: options,
})
}
type ClientOptions struct {
Context context.Context
Logger logger.ContextLogger
ServerAddress string
Options option.OutboundTLSOptions
KTLSCompatible bool
}
func NewClientWithOptions(options ClientOptions) (Config, error) {
if !options.Options.Enabled {
return nil, nil return nil, nil
} }
if !options.KTLSCompatible { if options.Reality != nil && options.Reality.Enabled {
if options.Options.KernelTx { return NewRealityClient(ctx, serverAddress, options)
options.Logger.Warn("enabling kTLS TX in current scenarios will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_tx") } else if options.UTLS != nil && options.UTLS.Enabled {
} return NewUTLSClient(ctx, serverAddress, options)
} }
if options.Options.KernelRx { return NewSTDClient(ctx, serverAddress, options)
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
}
if options.Options.Reality != nil && options.Options.Reality.Enabled {
return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options)
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options)
}
return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options)
} }
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) { func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
@@ -85,55 +53,26 @@ func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, e
return tlsConn, nil return tlsConn, nil
} }
type Dialer interface { type Dialer struct {
N.Dialer
DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error)
}
type defaultDialer struct {
dialer N.Dialer dialer N.Dialer
config Config config Config
} }
func NewDialer(dialer N.Dialer, config Config) Dialer { func NewDialer(dialer N.Dialer, config Config) N.Dialer {
return &defaultDialer{dialer, config} return &Dialer{dialer, config}
} }
func (d *defaultDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (d *Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if N.NetworkName(network) != N.NetworkTCP { if network != N.NetworkTCP {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
return d.DialTLSContext(ctx, destination) conn, err := d.dialer.DialContext(ctx, network, destination)
if err != nil {
return nil, err
}
return ClientHandshake(ctx, conn, d.config)
} }
func (d *defaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (d *Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
return d.dialContext(ctx, destination, true)
}
func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr, echRetry bool) (Conn, error) {
conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination)
if err != nil {
return nil, err
}
tlsConn, err := aTLS.ClientHandshake(ctx, conn, d.config)
if err != nil {
conn.Close()
var echErr *tls.ECHRejectionError
if echRetry && errors.As(err, &echErr) && len(echErr.RetryConfigList) > 0 {
if echConfig, isECH := d.config.(ECHCapableConfig); isECH {
echConfig.SetECHConfigList(echErr.RetryConfigList)
return d.dialContext(ctx, destination, false)
}
}
return nil, err
}
return tlsConn, nil
}
func (d *defaultDialer) Upstream() any {
return d.dialer
}

View File

@@ -69,7 +69,11 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
} else { } else {
return E.New("missing ECH keys") return E.New("missing ECH keys")
} }
echKeys, err := parseECHKeys(echKey) 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 { if err != nil {
return E.Cause(err, "parse ECH keys") return E.Cause(err, "parse ECH keys")
} }
@@ -81,29 +85,21 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
return nil return nil
} }
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error { func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
echKeys, err := parseECHKeys(echKey) echKey, err := os.ReadFile(echKeyPath)
if err != nil { if err != nil {
return err return E.Cause(err, "reload ECH keys from ", echKeyPath)
} }
c.access.Lock()
config := c.config.Clone()
config.EncryptedClientHelloKeys = echKeys
c.config = config
c.access.Unlock()
return nil
}
func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) {
block, _ := pem.Decode(echKey) block, _ := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" { if block == nil || block.Type != "ECH KEYS" {
return nil, E.New("invalid ECH keys pem") return E.New("invalid ECH keys pem")
} }
echKeys, err := UnmarshalECHKeys(block.Bytes) echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse ECH keys") return E.Cause(err, "parse ECH keys")
} }
return echKeys, nil tlsConfig.EncryptedClientHelloKeys = echKeys
return nil
} }
type ECHClientConfig struct { type ECHClientConfig struct {
@@ -129,7 +125,7 @@ func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (a
func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
s.access.Lock() s.access.Lock()
defer s.access.Unlock() defer s.access.Unlock()
if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Since(s.lastUpdate) > s.lastTTL { if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
message := &mDNS.Msg{ message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{ MsgHdr: mDNS.MsgHdr{
RecursionDesired: true, RecursionDesired: true,

View File

@@ -1,11 +1,14 @@
package tls package tls
import ( import (
"crypto/ecdh" "bytes"
"crypto/rand" "encoding/binary"
"encoding/pem" "encoding/pem"
"golang.org/x/crypto/cryptobyte" E "github.com/sagernet/sing/common/exceptions"
"github.com/cloudflare/circl/hpke"
"github.com/cloudflare/circl/kem"
) )
type ECHCapableConfig interface { type ECHCapableConfig interface {
@@ -14,68 +17,145 @@ type ECHCapableConfig interface {
SetECHConfigList([]byte) SetECHConfigList([]byte)
} }
func ECHKeygenDefault(publicName string) (configPem string, keyPem string, err error) { func ECHKeygenDefault(serverName string) (configPem string, keyPem string, err error) {
echKey, err := ecdh.X25519().GenerateKey(rand.Reader) cipherSuites := []echCipherSuite{
{
kdf: hpke.KDF_HKDF_SHA256,
aead: hpke.AEAD_AES128GCM,
}, {
kdf: hpke.KDF_HKDF_SHA256,
aead: hpke.AEAD_ChaCha20Poly1305,
},
}
keyConfig := []myECHKeyConfig{
{id: 0, kem: hpke.KEM_X25519_HKDF_SHA256},
}
keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites)
if err != nil { if err != nil {
return return
} }
echConfig, err := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0)
if err != nil { var configBuffer bytes.Buffer
return var totalLen uint16
for _, keyPair := range keyPairs {
totalLen += uint16(len(keyPair.rawConf))
} }
configBuilder := cryptobyte.NewBuilder(nil) binary.Write(&configBuffer, binary.BigEndian, totalLen)
configBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { for _, keyPair := range keyPairs {
builder.AddBytes(echConfig) configBuffer.Write(keyPair.rawConf)
})
configBytes, err := configBuilder.Bytes()
if err != nil {
return
} }
keyBuilder := cryptobyte.NewBuilder(nil)
keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { var keyBuffer bytes.Buffer
builder.AddBytes(echKey.Bytes()) for _, keyPair := range keyPairs {
}) keyBuffer.Write(keyPair.rawKey)
keyBuilder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
keyBytes, err := keyBuilder.Bytes()
if err != nil {
return
} }
configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBytes}))
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBytes})) configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer.Bytes()}))
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer.Bytes()}))
return return
} }
func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) ([]byte, error) { type echKeyConfigPair struct {
const extensionEncryptedClientHello = 0xfe0d id uint8
const DHKEM_X25519_HKDF_SHA256 = 0x0020 rawKey []byte
const KDF_HKDF_SHA256 = 0x0001 conf myECHKeyConfig
builder := cryptobyte.NewBuilder(nil) rawConf []byte
builder.AddUint16(extensionEncryptedClientHello) }
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddUint8(id) type echCipherSuite struct {
kdf hpke.KDF
builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support aead hpke.AEAD
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { }
builder.AddBytes(pubKey)
}) type myECHKeyConfig struct {
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { id uint8
const ( kem hpke.KEM
AEAD_AES_128_GCM = 0x0001 seed []byte
AEAD_AES_256_GCM = 0x0002 }
AEAD_ChaCha20Poly1305 = 0x0003
) func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite []echCipherSuite) ([]echKeyConfigPair, error) {
for _, aeadID := range []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305} { be := binary.BigEndian
builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support // prepare for future update
builder.AddUint16(aeadID) if version != 0xfe0d {
} return nil, E.New("unsupported ECH version", version)
}) }
builder.AddUint8(maxNameLen)
builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) { suiteBuf := make([]byte, 0, len(suite)*4+2)
builder.AddBytes([]byte(publicName)) suiteBuf = be.AppendUint16(suiteBuf, uint16(len(suite))*4)
}) for _, s := range suite {
builder.AddUint16(0) // extensions if !s.kdf.IsValid() || !s.aead.IsValid() {
}) return nil, E.New("invalid HPKE cipher suite")
return builder.Bytes() }
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.kdf))
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.aead))
}
pairs := []echKeyConfigPair{}
for _, c := range conf {
pair := echKeyConfigPair{}
pair.id = c.id
pair.conf = c
if !c.kem.IsValid() {
return nil, E.New("invalid HPKE KEM")
}
kpGenerator := c.kem.Scheme().GenerateKeyPair
if len(c.seed) > 0 {
kpGenerator = func() (kem.PublicKey, kem.PrivateKey, error) {
pub, sec := c.kem.Scheme().DeriveKeyPair(c.seed)
return pub, sec, nil
}
if len(c.seed) < c.kem.Scheme().PrivateKeySize() {
return nil, E.New("HPKE KEM seed too short")
}
}
pub, sec, err := kpGenerator()
if err != nil {
return nil, E.Cause(err, "generate ECH config key pair")
}
b := []byte{}
b = be.AppendUint16(b, version)
b = be.AppendUint16(b, 0) // length field
// contents
// key config
b = append(b, c.id)
b = be.AppendUint16(b, uint16(c.kem))
pubBuf, err := pub.MarshalBinary()
if err != nil {
return nil, E.Cause(err, "serialize ECH public key")
}
b = be.AppendUint16(b, uint16(len(pubBuf)))
b = append(b, pubBuf...)
b = append(b, suiteBuf...)
// end key config
// max name len, not supported
b = append(b, 0)
// server name
b = append(b, byte(len(serverName)))
b = append(b, []byte(serverName)...)
// extensions, not supported
b = be.AppendUint16(b, 0)
be.PutUint16(b[2:], uint16(len(b)-4))
pair.rawConf = b
secBuf, err := sec.MarshalBinary()
if err != nil {
return nil, E.Cause(err, "serialize ECH private key")
}
sk := []byte{}
sk = be.AppendUint16(sk, uint16(len(secBuf)))
sk = append(sk, secBuf...)
sk = be.AppendUint16(sk, uint16(len(b)))
sk = append(sk, b...)
pair.rawKey = sk
pairs = append(pairs, pair)
}
return pairs, nil
} }

23
common/tls/ech_stub.go Normal file
View File

@@ -0,0 +1,23 @@
//go:build !go1.24
package tls
import (
"context"
"crypto/tls"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New("ECH requires go1.24, please recompile your binary.")
}
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 reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
return E.New("ECH requires go1.24, please recompile your binary.")
}

View File

@@ -1,67 +0,0 @@
package tls
import (
"context"
"net"
"github.com/sagernet/sing-box/common/ktls"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
aTLS "github.com/sagernet/sing/common/tls"
)
type KTLSClientConfig struct {
Config
logger logger.ContextLogger
kernelTx, kernelRx bool
}
func (w *KTLSClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
tlsConn, err := aTLS.ClientHandshake(ctx, conn, w.Config)
if err != nil {
return nil, err
}
kConn, err := ktls.NewConn(ctx, w.logger, tlsConn, w.kernelTx, w.kernelRx)
if err != nil {
tlsConn.Close()
return nil, E.Cause(err, "initialize kernel TLS")
}
return kConn, nil
}
func (w *KTLSClientConfig) Clone() Config {
return &KTLSClientConfig{
w.Config.Clone(),
w.logger,
w.kernelTx,
w.kernelRx,
}
}
type KTlSServerConfig struct {
ServerConfig
logger logger.ContextLogger
kernelTx, kernelRx bool
}
func (w *KTlSServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
tlsConn, err := aTLS.ServerHandshake(ctx, conn, w.ServerConfig)
if err != nil {
return nil, err
}
kConn, err := ktls.NewConn(ctx, w.logger, tlsConn, w.kernelTx, w.kernelRx)
if err != nil {
tlsConn.Close()
return nil, E.Cause(err, "initialize kernel TLS")
}
return kConn, nil
}
func (w *KTlSServerConfig) Clone() Config {
return &KTlSServerConfig{
w.ServerConfig.Clone().(ServerConfig),
w.logger,
w.kernelTx,
w.kernelRx,
}
}

View File

@@ -28,12 +28,10 @@ import (
"unsafe" "unsafe"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/debug" "github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
aTLS "github.com/sagernet/sing/common/tls" aTLS "github.com/sagernet/sing/common/tls"
@@ -51,12 +49,12 @@ type RealityClientConfig struct {
shortID [8]byte shortID [8]byte
} }
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*RealityClientConfig, error) {
if options.UTLS == nil || !options.UTLS.Enabled { if options.UTLS == nil || !options.UTLS.Enabled {
return nil, E.New("uTLS is required by reality client") return nil, E.New("uTLS is required by reality client")
} }
uClient, err := NewUTLSClient(ctx, logger, serverAddress, options) uClient, err := NewUTLSClient(ctx, serverAddress, options)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -76,20 +74,7 @@ func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAd
if decodedLen > 8 { if decodedLen > 8 {
return nil, E.New("invalid short_id") return nil, E.New("invalid short_id")
} }
return &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}, nil
var config Config = &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}
if options.KernelRx || options.KernelTx {
if !C.IsLinux {
return nil, E.New("kTLS is only supported on Linux")
}
config = &KTLSClientConfig{
Config: config,
logger: logger,
kernelTx: options.KernelTx,
kernelRx: options.KernelRx,
}
}
return config, nil
} }
func (e *RealityClientConfig) ServerName() string { func (e *RealityClientConfig) ServerName() string {
@@ -108,7 +93,7 @@ func (e *RealityClientConfig) SetNextProtos(nextProto []string) {
e.uClient.SetNextProtos(nextProto) e.uClient.SetNextProtos(nextProto)
} }
func (e *RealityClientConfig) STDConfig() (*STDConfig, error) { func (e *RealityClientConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for reality") return nil, E.New("unsupported usage for reality")
} }
@@ -322,11 +307,3 @@ func (c *realityClientConnWrapper) Upstream() any {
func (c *realityClientConnWrapper) CloseWrite() error { func (c *realityClientConnWrapper) CloseWrite() error {
return c.Close() return c.Close()
} }
func (c *realityClientConnWrapper) ReaderReplaceable() bool {
return true
}
func (c *realityClientConnWrapper) WriterReplaceable() bool {
return true
}

View File

@@ -12,7 +12,6 @@ import (
"time" "time"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -29,7 +28,7 @@ type RealityServerConfig struct {
config *utls.RealityConfig config *utls.RealityConfig
} }
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) {
var tlsConfig utls.RealityConfig var tlsConfig utls.RealityConfig
if options.ACME != nil && len(options.ACME.Domain) > 0 { if options.ACME != nil && len(options.ACME.Domain) > 0 {
@@ -68,10 +67,7 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt
return nil, E.New("unknown cipher_suite: ", cipherSuite) return nil, E.New("unknown cipher_suite: ", cipherSuite)
} }
} }
if len(options.CurvePreferences) > 0 { if len(options.Certificate) > 0 || options.CertificatePath != "" {
return nil, E.New("curve preferences is unavailable in reality")
}
if len(options.Certificate) > 0 || options.CertificatePath != "" || len(options.ClientCertificatePublicKeySHA256) > 0 {
return nil, E.New("certificate is unavailable in reality") return nil, E.New("certificate is unavailable in reality")
} }
if len(options.Key) > 0 || options.KeyPath != "" { if len(options.Key) > 0 || options.KeyPath != "" {
@@ -123,22 +119,7 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt
return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
} }
if options.ECH != nil && options.ECH.Enabled { return &RealityServerConfig{&tlsConfig}, nil
return nil, E.New("Reality is conflict with ECH")
}
var config ServerConfig = &RealityServerConfig{&tlsConfig}
if options.KernelTx || options.KernelRx {
if !C.IsLinux {
return nil, E.New("kTLS is only supported on Linux")
}
config = &KTlSServerConfig{
ServerConfig: config,
logger: logger,
kernelTx: options.KernelTx,
kernelRx: options.KernelRx,
}
}
return config, nil
} }
func (c *RealityServerConfig) ServerName() string { func (c *RealityServerConfig) ServerName() string {
@@ -157,7 +138,7 @@ func (c *RealityServerConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto c.config.NextProtos = nextProto
} }
func (c *RealityServerConfig) STDConfig() (*tls.Config, error) { func (c *RealityServerConfig) Config() (*tls.Config, error) {
return nil, E.New("unsupported usage for reality") return nil, E.New("unsupported usage for reality")
} }
@@ -225,11 +206,3 @@ func (c *realityConnWrapper) Upstream() any {
func (c *realityConnWrapper) CloseWrite() error { func (c *realityConnWrapper) CloseWrite() error {
return c.Close() return c.Close()
} }
func (c *realityConnWrapper) ReaderReplaceable() bool {
return true
}
func (c *realityConnWrapper) WriterReplaceable() bool {
return true
}

View File

@@ -12,37 +12,14 @@ import (
aTLS "github.com/sagernet/sing/common/tls" aTLS "github.com/sagernet/sing/common/tls"
) )
type ServerOptions struct { func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
Context context.Context if !options.Enabled {
Logger log.ContextLogger
Options option.InboundTLSOptions
KTLSCompatible bool
}
func NewServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
return NewServerWithOptions(ServerOptions{
Context: ctx,
Logger: logger,
Options: options,
})
}
func NewServerWithOptions(options ServerOptions) (ServerConfig, error) {
if !options.Options.Enabled {
return nil, nil return nil, nil
} }
if !options.KTLSCompatible { if options.Reality != nil && options.Reality.Enabled {
if options.Options.KernelTx { return NewRealityServer(ctx, logger, options)
options.Logger.Warn("enabling kTLS TX in current scenarios will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_tx")
}
} }
if options.Options.KernelRx { return NewSTDServer(ctx, logger, options)
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
}
if options.Options.Reality != nil && options.Options.Reality.Enabled {
return NewRealityServer(options.Context, options.Logger, options.Options)
}
return NewSTDServer(options.Context, options.Logger, options.Options)
} }
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) { func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {

View File

@@ -1,12 +1,9 @@
package tls package tls
import ( import (
"bytes"
"context" "context"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/base64"
"net" "net"
"os" "os"
"strings" "strings"
@@ -14,10 +11,8 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tlsfragment" "github.com/sagernet/sing-box/common/tlsfragment"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
) )
@@ -45,7 +40,7 @@ func (c *STDClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto c.config.NextProtos = nextProto
} }
func (c *STDClientConfig) STDConfig() (*STDConfig, error) { func (c *STDClientConfig) Config() (*STDConfig, error) {
return c.config, nil return c.config, nil
} }
@@ -57,13 +52,7 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
} }
func (c *STDClientConfig) Clone() Config { func (c *STDClientConfig) Clone() Config {
return &STDClientConfig{ return &STDClientConfig{c.ctx, c.config.Clone(), c.fragment, c.fragmentFallbackDelay, c.recordFragment}
ctx: c.ctx,
config: c.config.Clone(),
fragment: c.fragment,
fragmentFallbackDelay: c.fragmentFallbackDelay,
recordFragment: c.recordFragment,
}
} }
func (c *STDClientConfig) ECHConfigList() []byte { func (c *STDClientConfig) ECHConfigList() []byte {
@@ -74,7 +63,7 @@ func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte
c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList
} }
func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
var serverName string var serverName string
if options.ServerName != "" { if options.ServerName != "" {
serverName = options.ServerName serverName = options.ServerName
@@ -97,29 +86,16 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
tlsConfig.InsecureSkipVerify = true tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error { tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
verifyOptions := x509.VerifyOptions{ verifyOptions := x509.VerifyOptions{
Roots: tlsConfig.RootCAs,
DNSName: serverName, DNSName: serverName,
Intermediates: x509.NewCertPool(), Intermediates: x509.NewCertPool(),
} }
for _, cert := range state.PeerCertificates[1:] { for _, cert := range state.PeerCertificates[1:] {
verifyOptions.Intermediates.AddCert(cert) verifyOptions.Intermediates.AddCert(cert)
} }
if tlsConfig.Time != nil {
verifyOptions.CurrentTime = tlsConfig.Time()
}
_, err := state.PeerCertificates[0].Verify(verifyOptions) _, err := state.PeerCertificates[0].Verify(verifyOptions)
return err return err
} }
} }
if len(options.CertificatePublicKeySHA256) > 0 {
if len(options.Certificate) > 0 || options.CertificatePath != "" {
return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path")
}
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
}
}
if len(options.ALPN) > 0 { if len(options.ALPN) > 0 {
tlsConfig.NextProtos = options.ALPN tlsConfig.NextProtos = options.ALPN
} }
@@ -149,9 +125,6 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
return nil, E.New("unknown cipher_suite: ", cipherSuite) return nil, E.New("unknown cipher_suite: ", cipherSuite)
} }
} }
for _, curve := range options.CurvePreferences {
tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curve))
}
var certificate []byte var certificate []byte
if len(options.Certificate) > 0 { if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n")) certificate = []byte(strings.Join(options.Certificate, "\n"))
@@ -169,43 +142,10 @@ func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
} }
tlsConfig.RootCAs = certPool tlsConfig.RootCAs = certPool
} }
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} stdConfig := &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
if options.ECH != nil && options.ECH.Enabled { if options.ECH != nil && options.ECH.Enabled {
var err error return parseECHClientConfig(ctx, stdConfig, options)
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options) } else {
if err != nil { return stdConfig, nil
return nil, err
}
} }
if options.KernelRx || options.KernelTx {
if !C.IsLinux {
return nil, E.New("kTLS is only supported on Linux")
}
config = &KTLSClientConfig{
Config: config,
logger: logger,
kernelTx: options.KernelTx,
kernelRx: options.KernelRx,
}
}
return config, nil
}
func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error {
leafCertificate, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return E.Cause(err, "failed to parse leaf certificate")
}
pubKeyBytes, err := x509.MarshalPKIXPublicKey(leafCertificate.PublicKey)
if err != nil {
return E.Cause(err, "failed to marshal public key")
}
hashValue := sha256.Sum256(pubKeyBytes)
for _, value := range knownHashValues {
if bytes.Equal(value, hashValue[:]) {
return nil
}
}
return E.New("unrecognized remote public key: ", base64.StdEncoding.EncodeToString(hashValue[:]))
} }

View File

@@ -3,16 +3,13 @@ package tls
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"crypto/x509"
"net" "net"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
@@ -23,36 +20,26 @@ import (
var errInsecureUnused = E.New("tls: insecure unused") var errInsecureUnused = E.New("tls: insecure unused")
type STDServerConfig struct { type STDServerConfig struct {
access sync.RWMutex config *tls.Config
config *tls.Config logger log.Logger
logger log.Logger acmeService adapter.SimpleLifecycle
acmeService adapter.SimpleLifecycle certificate []byte
certificate []byte key []byte
key []byte certificatePath string
certificatePath string keyPath string
keyPath string echKeyPath string
clientCertificatePath []string watcher *fswatch.Watcher
echKeyPath string
watcher *fswatch.Watcher
} }
func (c *STDServerConfig) ServerName() string { func (c *STDServerConfig) ServerName() string {
c.access.RLock()
defer c.access.RUnlock()
return c.config.ServerName return c.config.ServerName
} }
func (c *STDServerConfig) SetServerName(serverName string) { func (c *STDServerConfig) SetServerName(serverName string) {
c.access.Lock() c.config.ServerName = serverName
defer c.access.Unlock()
config := c.config.Clone()
config.ServerName = serverName
c.config = config
} }
func (c *STDServerConfig) NextProtos() []string { func (c *STDServerConfig) NextProtos() []string {
c.access.RLock()
defer c.access.RUnlock()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
return c.config.NextProtos[1:] return c.config.NextProtos[1:]
} else { } else {
@@ -61,18 +48,14 @@ func (c *STDServerConfig) NextProtos() []string {
} }
func (c *STDServerConfig) SetNextProtos(nextProto []string) { func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.access.Lock()
defer c.access.Unlock()
config := c.config.Clone()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
config.NextProtos = append(c.config.NextProtos[:1], nextProto...) c.config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
} else { } else {
config.NextProtos = nextProto c.config.NextProtos = nextProto
} }
c.config = config
} }
func (c *STDServerConfig) STDConfig() (*STDConfig, error) { func (c *STDServerConfig) Config() (*STDConfig, error) {
return c.config, nil return c.config, nil
} }
@@ -94,6 +77,9 @@ func (c *STDServerConfig) Start() error {
if c.acmeService != nil { if c.acmeService != nil {
return c.acmeService.Start() return c.acmeService.Start()
} else { } else {
if c.certificatePath == "" && c.keyPath == "" {
return nil
}
err := c.startWatcher() err := c.startWatcher()
if err != nil { if err != nil {
c.logger.Warn("create fsnotify watcher: ", err) c.logger.Warn("create fsnotify watcher: ", err)
@@ -113,12 +99,6 @@ func (c *STDServerConfig) startWatcher() error {
if c.echKeyPath != "" { if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath) watchPath = append(watchPath, c.echKeyPath)
} }
if len(c.clientCertificatePath) > 0 {
watchPath = append(watchPath, c.clientCertificatePath...)
}
if len(watchPath) == 0 {
return nil
}
watcher, err := fswatch.NewWatcher(fswatch.Options{ watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath, Path: watchPath,
Callback: func(path string) { Callback: func(path string) {
@@ -158,42 +138,10 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
if err != nil { if err != nil {
return E.Cause(err, "reload key pair") return E.Cause(err, "reload key pair")
} }
c.access.Lock() c.config.Certificates = []tls.Certificate{keyPair}
config := c.config.Clone()
config.Certificates = []tls.Certificate{keyPair}
c.config = config
c.access.Unlock()
c.logger.Info("reloaded TLS certificate") c.logger.Info("reloaded TLS certificate")
} else if common.Contains(c.clientCertificatePath, path) {
clientCertificateCA := x509.NewCertPool()
var reloaded bool
for _, certPath := range c.clientCertificatePath {
content, err := os.ReadFile(certPath)
if err != nil {
c.logger.Error(E.Cause(err, "reload certificate from ", c.clientCertificatePath))
continue
}
if !clientCertificateCA.AppendCertsFromPEM(content) {
c.logger.Error(E.New("invalid client certificate file: ", certPath))
continue
}
reloaded = true
}
if !reloaded {
return E.New("client certificates is empty")
}
c.access.Lock()
config := c.config.Clone()
config.ClientCAs = clientCertificateCA
c.config = config
c.access.Unlock()
c.logger.Info("reloaded client certificates")
} else if path == c.echKeyPath { } else if path == c.echKeyPath {
echKey, err := os.ReadFile(c.echKeyPath) err := reloadECHKeys(c.echKeyPath, c.config)
if err != nil {
return E.Cause(err, "reload ECH keys from ", c.echKeyPath)
}
err = c.setECHServerConfig(echKey)
if err != nil { if err != nil {
return err return err
} }
@@ -212,7 +160,7 @@ func (c *STDServerConfig) Close() error {
return nil return nil
} }
func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { func NewSTDServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
if !options.Enabled { if !options.Enabled {
return nil, nil return nil, nil
} }
@@ -264,14 +212,8 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
return nil, E.New("unknown cipher_suite: ", cipherSuite) return nil, E.New("unknown cipher_suite: ", cipherSuite)
} }
} }
for _, curveID := range options.CurvePreferences { var certificate []byte
tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curveID)) var key []byte
}
tlsConfig.ClientAuth = tls.ClientAuthType(options.ClientAuthentication)
var (
certificate []byte
key []byte
)
if acmeService == nil { if acmeService == nil {
if len(options.Certificate) > 0 { if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n")) certificate = []byte(strings.Join(options.Certificate, "\n"))
@@ -313,43 +255,6 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
tlsConfig.Certificates = []tls.Certificate{keyPair} tlsConfig.Certificates = []tls.Certificate{keyPair}
} }
} }
if len(options.ClientCertificate) > 0 || len(options.ClientCertificatePath) > 0 {
if tlsConfig.ClientAuth == tls.NoClientCert {
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
}
}
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
if len(options.ClientCertificate) > 0 {
clientCertificateCA := x509.NewCertPool()
if !clientCertificateCA.AppendCertsFromPEM([]byte(strings.Join(options.ClientCertificate, "\n"))) {
return nil, E.New("invalid client certificate strings")
}
tlsConfig.ClientCAs = clientCertificateCA
} else if len(options.ClientCertificatePath) > 0 {
clientCertificateCA := x509.NewCertPool()
for _, path := range options.ClientCertificatePath {
content, err := os.ReadFile(path)
if err != nil {
return nil, E.Cause(err, "read client certificate from ", path)
}
if !clientCertificateCA.AppendCertsFromPEM(content) {
return nil, E.New("invalid client certificate file: ", path)
}
}
tlsConfig.ClientCAs = clientCertificateCA
} else if len(options.ClientCertificatePublicKeySHA256) > 0 {
if tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
tlsConfig.ClientAuth = tls.RequireAnyClientCert
} else if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven {
tlsConfig.ClientAuth = tls.RequestClientCert
}
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
return verifyPublicKeySHA256(options.ClientCertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
}
} else {
return nil, E.New("missing client_certificate, client_certificate_path or client_certificate_public_key_sha256 for client authentication")
}
}
var echKeyPath string var echKeyPath string
if options.ECH != nil && options.ECH.Enabled { if options.ECH != nil && options.ECH.Enabled {
err = parseECHServerConfig(ctx, options, tlsConfig, &echKeyPath) err = parseECHServerConfig(ctx, options, tlsConfig, &echKeyPath)
@@ -357,33 +262,14 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
return nil, err return nil, err
} }
} }
serverConfig := &STDServerConfig{ return &STDServerConfig{
config: tlsConfig, config: tlsConfig,
logger: logger, logger: logger,
acmeService: acmeService, acmeService: acmeService,
certificate: certificate, certificate: certificate,
key: key, key: key,
certificatePath: options.CertificatePath, certificatePath: options.CertificatePath,
clientCertificatePath: options.ClientCertificatePath, keyPath: options.KeyPath,
keyPath: options.KeyPath, echKeyPath: echKeyPath,
echKeyPath: echKeyPath, }, nil
}
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
serverConfig.access.Lock()
defer serverConfig.access.Unlock()
return serverConfig.config, nil
}
var config ServerConfig = serverConfig
if options.KernelTx || options.KernelRx {
if !C.IsLinux {
return nil, E.New("kTLS is only supported on Linux")
}
config = &KTlSServerConfig{
ServerConfig: config,
logger: logger,
kernelTx: options.KernelTx,
kernelRx: options.KernelRx,
}
}
return config, nil
} }

View File

@@ -11,13 +11,10 @@ type TimeServiceWrapper struct {
} }
func (w *TimeServiceWrapper) TimeFunc() func() time.Time { func (w *TimeServiceWrapper) TimeFunc() func() time.Time {
return func() time.Time { if w.TimeService == nil {
if w.TimeService != nil { return nil
return w.TimeService.TimeFunc()()
} else {
return time.Now()
}
} }
return w.TimeService.TimeFunc()
} }
func (w *TimeServiceWrapper) Upstream() any { func (w *TimeServiceWrapper) Upstream() any {

View File

@@ -14,11 +14,8 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tlsfragment" "github.com/sagernet/sing-box/common/tlsfragment"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
utls "github.com/metacubex/utls" utls "github.com/metacubex/utls"
@@ -53,7 +50,7 @@ func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto c.config.NextProtos = nextProto
} }
func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) { func (c *UTLSClientConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for uTLS") return nil, E.New("unsupported usage for uTLS")
} }
@@ -109,14 +106,6 @@ func (c *utlsConnWrapper) Upstream() any {
return c.UConn return c.UConn
} }
func (c *utlsConnWrapper) ReaderReplaceable() bool {
return true
}
func (c *utlsConnWrapper) WriterReplaceable() bool {
return true
}
type utlsALPNWrapper struct { type utlsALPNWrapper struct {
utlsConnWrapper utlsConnWrapper
nextProtocols []string nextProtocols []string
@@ -142,7 +131,7 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
return c.UConn.HandshakeContext(ctx) return c.UConn.HandshakeContext(ctx)
} }
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
var serverName string var serverName string
if options.ServerName != "" { if options.ServerName != "" {
serverName = options.ServerName serverName = options.ServerName
@@ -156,25 +145,11 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
var tlsConfig utls.Config var tlsConfig utls.Config
tlsConfig.Time = ntp.TimeFuncFromContext(ctx) tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx) tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
if !options.DisableSNI { tlsConfig.ServerName = serverName
tlsConfig.ServerName = serverName
}
if options.Insecure { if options.Insecure {
tlsConfig.InsecureSkipVerify = options.Insecure tlsConfig.InsecureSkipVerify = options.Insecure
} else if options.DisableSNI { } else if options.DisableSNI {
if options.Reality != nil && options.Reality.Enabled { return nil, E.New("disable_sni is unsupported in uTLS")
return nil, E.New("disable_sni is unsupported in reality")
}
tlsConfig.InsecureServerNameToVerify = serverName
}
if len(options.CertificatePublicKeySHA256) > 0 {
if len(options.Certificate) > 0 || options.CertificatePath != "" {
return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path")
}
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
}
} }
if len(options.ALPN) > 0 { if len(options.ALPN) > 0 {
tlsConfig.NextProtos = options.ALPN tlsConfig.NextProtos = options.ALPN
@@ -226,28 +201,15 @@ func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
if err != nil { if err != nil {
return nil, err return nil, err
} }
var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment} uConfig := &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
if options.ECH != nil && options.ECH.Enabled { if options.ECH != nil && options.ECH.Enabled {
if options.Reality != nil && options.Reality.Enabled { if options.Reality != nil && options.Reality.Enabled {
return nil, E.New("Reality is conflict with ECH") return nil, E.New("Reality is conflict with ECH")
} }
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options) return parseECHClientConfig(ctx, uConfig, options)
if err != nil { } else {
return nil, err return uConfig, nil
}
} }
if (options.KernelRx || options.KernelTx) && !common.PtrValueOrDefault(options.Reality).Enabled {
if !C.IsLinux {
return nil, E.New("kTLS is only supported on Linux")
}
config = &KTLSClientConfig{
Config: config,
logger: logger,
kernelTx: options.KernelTx,
kernelRx: options.KernelRx,
}
}
return config, nil
} }
var ( var (

View File

@@ -8,14 +8,13 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
) )
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`) return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
} }
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`) return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
} }

View File

@@ -45,7 +45,7 @@ func (c *Conn) Write(b []byte) (n int, err error) {
defer func() { defer func() {
c.firstPacketWritten = true c.firstPacketWritten = true
}() }()
serverName := IndexTLSServerName(b) serverName := indexTLSServerName(b)
if serverName != nil { if serverName != nil {
if c.splitPacket { if c.splitPacket {
if c.tcpConn != nil { if c.tcpConn != nil {
@@ -109,9 +109,6 @@ func (c *Conn) Write(b []byte) (n int, err error) {
if err != nil { if err != nil {
return return
} }
if i != len(splitIndexes) {
time.Sleep(c.fallbackDelay)
}
} }
} }
} }

View File

@@ -22,13 +22,13 @@ const (
tls13 uint16 = 0x0304 tls13 uint16 = 0x0304
) )
type MyServerName struct { type myServerName struct {
Index int Index int
Length int Length int
ServerName string ServerName string
} }
func IndexTLSServerName(payload []byte) *MyServerName { func indexTLSServerName(payload []byte) *myServerName {
if len(payload) < recordLayerHeaderLen || payload[0] != contentType { if len(payload) < recordLayerHeaderLen || payload[0] != contentType {
return nil return nil
} }
@@ -36,48 +36,47 @@ func IndexTLSServerName(payload []byte) *MyServerName {
if len(payload) < recordLayerHeaderLen+int(segmentLen) { if len(payload) < recordLayerHeaderLen+int(segmentLen) {
return nil return nil
} }
serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen:]) serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)])
if serverName == nil { if serverName == nil {
return nil return nil
} }
serverName.Index += recordLayerHeaderLen serverName.Length += recordLayerHeaderLen
return serverName return serverName
} }
func indexTLSServerNameFromHandshake(handshake []byte) *MyServerName { func indexTLSServerNameFromHandshake(hs []byte) *myServerName {
if len(handshake) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen { if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
return nil return nil
} }
if handshake[0] != handshakeType { if hs[0] != handshakeType {
return nil return nil
} }
handshakeLen := uint32(handshake[1])<<16 | uint32(handshake[2])<<8 | uint32(handshake[3]) handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
if len(handshake[4:]) != int(handshakeLen) { if len(hs[4:]) != int(handshakeLen) {
return nil return nil
} }
tlsVersion := uint16(handshake[4])<<8 | uint16(handshake[5]) tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 { if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
return nil return nil
} }
sessionIDLen := handshake[38] sessionIDLen := hs[38]
currentIndex := handshakeHeaderLen + randomDataLen + sessionIDHeaderLen + int(sessionIDLen) if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
if len(handshake) < currentIndex {
return nil return nil
} }
cipherSuites := handshake[currentIndex:] cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
if len(cipherSuites) < cipherSuiteHeaderLen { if len(cs) < cipherSuiteHeaderLen {
return nil return nil
} }
csLen := uint16(cipherSuites[0])<<8 | uint16(cipherSuites[1]) csLen := uint16(cs[0])<<8 | uint16(cs[1])
if len(cipherSuites) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen { if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
return nil return nil
} }
compressMethodLen := uint16(cipherSuites[cipherSuiteHeaderLen+int(csLen)]) compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
currentIndex += cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen) if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
if len(handshake) < currentIndex {
return nil return nil
} }
serverName := indexTLSServerNameFromExtensions(handshake[currentIndex:]) currentIndex := cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen)
serverName := indexTLSServerNameFromExtensions(cs[currentIndex:])
if serverName == nil { if serverName == nil {
return nil return nil
} }
@@ -85,7 +84,7 @@ func indexTLSServerNameFromHandshake(handshake []byte) *MyServerName {
return serverName return serverName
} }
func indexTLSServerNameFromExtensions(exs []byte) *MyServerName { func indexTLSServerNameFromExtensions(exs []byte) *myServerName {
if len(exs) == 0 { if len(exs) == 0 {
return nil return nil
} }
@@ -119,8 +118,7 @@ func indexTLSServerNameFromExtensions(exs []byte) *MyServerName {
} }
sniLen := uint16(sex[3])<<8 | uint16(sex[4]) sniLen := uint16(sex[3])<<8 | uint16(sex[4])
sex = sex[sniExtensionHeaderLen:] sex = sex[sniExtensionHeaderLen:]
return &myServerName{
return &MyServerName{
Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen, Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen,
Length: int(sniLen), Length: int(sniLen),
ServerName: string(sex), ServerName: string(sex),

View File

@@ -1,20 +0,0 @@
package tf_test
import (
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/common/tlsfragment"
"github.com/stretchr/testify/require"
)
func TestIndexTLSServerName(t *testing.T) {
t.Parallel()
payload, err := hex.DecodeString("16030105f8010005f403036e35de7389a679c54029cf452611f2211c70d9ac3897271de589ab6155f8e4ab20637d225f1ef969ad87ed78bfb9d171300bcb1703b6f314ccefb964f79b7d0961002a0a0a130213031301c02cc02bcca9c030c02fcca8c00ac009c014c013009d009c0035002fc008c012000a01000581baba00000000000f000d00000a6769746875622e636f6d00170000ff01000100000a000e000c3a3a11ec001d001700180019000b000201000010000e000c02683208687474702f312e31000500050100000000000d00160014040308040401050308050805050108060601020100120000003304ef04ed3a3a00010011ec04c0aeb2250c092a3463161cccb29d9183331a424964248579507ed23a180b0ceab2a5f5d9ce41547e497a89055471ea572867ba3a1fc3c9e45025274a20f60c6b60e62476b6afed0403af59ab83660ef4112ae20386a602010d0a5d454c0ed34c84ed4423e750213e6a2baab1bf9c4367a6007ab40a33d95220c2dcaa44f257024a5626b545db0510f4311b1a60714154909c6a61fdfca011fb2626d657aeb6070bf078508babe3b584555013e34acc56198ed4663742b3155a664a9901794c4586820a7dc162c01827291f3792e1237f801a8d1ef096013c181c4a58d2f6859ba75022d18cc4418bd4f351d5c18f83a58857d05af860c4b9ac018a5b63f17184e591532c6bc2cf2215d4a282c8a8a4f6f7aee110422c8bc9ebd3b1d609c568523aaae555db320e6c269473d87af38c256cbb9febc20aea6380c32a8916f7a373c8b1e37554e3260bf6621f6b804ee80b3c516b1d01985bf4c603b6daa9a5991de6a7a29f3a7122b8afb843a7660110fce62b43c615f5bcc2db688ba012649c0952b0a2c031e732d2b454c6b2968683cb8d244be2c9a7fa163222979eaf92722b92b862d81a3d94450c2b60c318421ebb4307c42d1f0473592a5c30e42039cc68cda9721e61aa63f49def17c15221680ed444896340133bbee67556f56b9f9d78a4df715f926a12add0cc9c862e46ea8b7316ae468282c18601b2771c9c9322f982228cf93effaacd3f80cbd12bce5fc36f56e2a3caf91e578a5fae00c9b23a8ed1a66764f4433c3628a70b8f0a6196adc60a4cb4226f07ba4c6b363fe9065563bfc1347452946386bab488686e837ab979c64f9047417fca635fe1bb4f074f256cc8af837c7b455e280426547755af90a61640169ef180aea3a77e662bb6dac1b6c3696027129b1a5edf495314e9c7f4b6110e16378ec893fa24642330a40aba1a85326101acb97c620fd8d71389e69eaed7bdb01bbe1fd428d66191150c7b2cd1ad4257391676a82ba8ce07fb2667c3b289f159003a7c7bc31d361b7b7f49a802961739d950dfcc0fa1c7abce5abdd2245101da391151490862028110465950b9e9c03d08a90998ab83267838d2e74a0593bc81f74cdf734519a05b351c0e5488c68dd810e6e9142ccc1e2f4a7f464297eb340e27acc6b9d64e12e38cce8492b3d939140b5a9e149a75597f10a23874c84323a07cdd657274378f887c85c4259b9c04cd33ba58ed630ef2a744f8e19dd34843dff331d2a6be7e2332c599289cd248a611c73d7481cd4a9bd43449a3836f14b2af18a1739e17999e4c67e85cc5bcecabb14185e5bcaff3c96098f03dc5aba819f29587758f49f940585354a2a780830528d68ccd166920dadcaa25cab5fc1907272a826aba3f08bc6b88757776812ecb6c7cec69a223ec0a13a7b62a2349a0f63ed7a27a3b15ba21d71fe6864ec6e089ae17cadd433fa3138f7ee24353c11365818f8fc34f43a05542d18efaac24bfccc1f748a0cc1a67ad379468b76fd34973dba785f5c91d618333cd810fe0700d1bbc8422029782628070a624c52c5309a4a64d625b11f8033ab28df34a1add297517fcc06b92b6817b3c5144438cf260867c57bde68c8c4b82e6a135ef676a52fbae5708002a404e6189a60e2836de565ad1b29e3819e5ed49f6810bcb28e1bd6de57306f94b79d9dae1cc4624d2a068499beef81cd5fe4b76dcbfff2a2008001d002001976128c6d5a934533f28b9914d2480aab2a8c1ab03d212529ce8b27640a716002d00020101002b000706caca03040303001b00030200015a5a000100")
require.NoError(t, err)
serverName := tf.IndexTLSServerName(payload)
require.NotNil(t, serverName)
require.Equal(t, serverName.ServerName, string(payload[serverName.Index:serverName.Index+serverName.Length]))
require.Equal(t, "github.com", serverName.ServerName)
}

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