mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-07-31 19:14:17 +03:00
Compare commits
165 Commits
v1.12.1-ex
...
v1.12.22-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d16078651 | ||
|
|
18b1101fbe | ||
|
|
4ebe870306 | ||
|
|
50c5e9df0d | ||
|
|
c8a993834e | ||
|
|
260bbbfb45 | ||
|
|
82337299b9 | ||
|
|
c229c79dcc | ||
|
|
f63091d14d | ||
|
|
1c4a01ee90 | ||
|
|
4d7f99310c | ||
|
|
6fc511f56e | ||
|
|
d18d2b352a | ||
|
|
534128bba9 | ||
|
|
736a7368c6 | ||
|
|
e7a9c90213 | ||
|
|
0f3774e501 | ||
|
|
2f8e656522 | ||
|
|
3ba30e3f00 | ||
|
|
f2639a5829 | ||
|
|
69bebbda82 | ||
|
|
00b2c042ee | ||
|
|
d9eb8f3ab6 | ||
|
|
58025a01f8 | ||
|
|
99cad72ea8 | ||
|
|
6e96d620fe | ||
|
|
596291567f | ||
|
|
a2a5f46cb6 | ||
|
|
f6da8e52b4 | ||
|
|
51ce402dbb | ||
|
|
8b404b5a4c | ||
|
|
b27d707668 | ||
|
|
3ce94d50dd | ||
|
|
29d56fca9c | ||
|
|
ab18010ee1 | ||
|
|
e69c202c79 | ||
|
|
0a812f2a46 | ||
|
|
fffe9fc566 | ||
|
|
6fdf27a701 | ||
|
|
7fa7d4f0a9 | ||
|
|
f511ebc1d4 | ||
|
|
84bbdc2eba | ||
|
|
568612fc70 | ||
|
|
d78828fd81 | ||
|
|
f56d9ab945 | ||
|
|
86fabd6a22 | ||
|
|
24a1e7cee4 | ||
|
|
223dd8bb1a | ||
|
|
68448de7d0 | ||
|
|
1ebff74c21 | ||
|
|
f0cd3422c1 | ||
|
|
e385a98ced | ||
|
|
670f32baee | ||
|
|
287fe834db | ||
|
|
d7f0cea4ff | ||
|
|
d8b470d1ba | ||
|
|
984fc295b3 | ||
|
|
2747a00ba2 | ||
|
|
48e76038d0 | ||
|
|
6421252d44 | ||
|
|
216c4c8bd4 | ||
|
|
5841d410a1 | ||
|
|
63c8207d7a | ||
|
|
6e4b7ed744 | ||
|
|
855d400654 | ||
|
|
4c5e2c6645 | ||
|
|
725eccdea8 | ||
|
|
2ff042abd2 | ||
|
|
ffb282e47e | ||
|
|
54ed58499d | ||
|
|
b1bdc18c85 | ||
|
|
a38030cc0b | ||
|
|
4626aa2cb0 | ||
|
|
5a40b673a4 | ||
|
|
541f63fee4 | ||
|
|
5de6f4a14f | ||
|
|
5658830077 | ||
|
|
0e50edc009 | ||
|
|
444f454810 | ||
|
|
d0e1fd6c7e | ||
|
|
17b4d1e010 | ||
|
|
06791470c9 | ||
|
|
ef14c8ca0e | ||
|
|
36dc883c7c | ||
|
|
6557bd7029 | ||
|
|
41b30c91d9 | ||
|
|
0f767d5ce1 | ||
|
|
328a6de797 | ||
|
|
886be6414d | ||
|
|
9362d3cab3 | ||
|
|
ced2e39dbf | ||
|
|
2159d8877b | ||
|
|
cb7dba3eff | ||
|
|
d9d7f7880d | ||
|
|
a031aaf2c0 | ||
|
|
4bca951773 | ||
|
|
140735dbde | ||
|
|
714a68bba1 | ||
|
|
91f9134379 | ||
|
|
5a4de7b242 | ||
|
|
bc91312a73 | ||
|
|
1d603e24fd | ||
|
|
573c6179ab | ||
|
|
510bf05e36 | ||
|
|
ae852e0be4 | ||
|
|
1955002ed8 | ||
|
|
44559fb7b9 | ||
|
|
0977c5cf73 | ||
|
|
07697bf931 | ||
|
|
5d1d1a1456 | ||
|
|
146383499e | ||
|
|
e81a76fdf9 | ||
|
|
de13137418 | ||
|
|
e42b818c2a | ||
|
|
fcde0c94e0 | ||
|
|
1af83e997d | ||
|
|
59ee7be72a | ||
|
|
c331ee3d5c | ||
|
|
36babe4bef | ||
|
|
c5f2cea802 | ||
|
|
8a200bf913 | ||
|
|
f16468e74f | ||
|
|
79c0b9f51d | ||
|
|
f98a3a4f65 | ||
|
|
b14cecaeb2 | ||
|
|
2594745ef8 | ||
|
|
cc3041322e | ||
|
|
f352f84483 | ||
|
|
cbf48e9b8c | ||
|
|
0ef7e8eca2 | ||
|
|
1a18e43a88 | ||
|
|
6849288d6d | ||
|
|
2edfed7d91 | ||
|
|
30c069f5b7 | ||
|
|
649163cb7b | ||
|
|
980e96250b | ||
|
|
963bc4b647 | ||
|
|
031f25c1c1 | ||
|
|
b40f642fa4 | ||
|
|
22782ca6fc | ||
|
|
1468d83895 | ||
|
|
97f0dc8a60 | ||
|
|
ee02532ab5 | ||
|
|
f1dd0dba78 | ||
|
|
f4ed684146 | ||
|
|
83f02d0bfb | ||
|
|
52fa5f20a3 | ||
|
|
f462ce5615 | ||
|
|
cef3e538ba | ||
|
|
acda4ce985 | ||
|
|
354ece2bdf | ||
|
|
de10bb00a9 | ||
|
|
fdc181106d | ||
|
|
8752b631bd | ||
|
|
378e39f70c | ||
|
|
043a2e7a07 | ||
|
|
7e190e92ca | ||
|
|
5eb318ba06 | ||
|
|
4a209f1afb | ||
|
|
c0ac3c748c | ||
|
|
a65d3e040a | ||
|
|
2358efe44a | ||
|
|
09d3b8f2c2 | ||
|
|
531de77124 | ||
|
|
44981fd803 |
@@ -1,25 +1,27 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
VERSION="1.23.6"
|
VERSION="1.25.7"
|
||||||
|
|
||||||
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_legacy
|
mv go go_win7
|
||||||
cd go_legacy
|
cd go_win7
|
||||||
|
|
||||||
# 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.23.x
|
# this patch file only works on golang1.25.x
|
||||||
# that means after golang1.24 release it must be changed
|
# that means after golang1.26 release it must be changed
|
||||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
|
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
||||||
# 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"
|
||||||
|
|
||||||
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
|
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||||
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
|
|
||||||
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
|
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.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/6788c4c6f9fafb56729bad6b660f7ee2272d699f.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
|
||||||
77
.github/workflows/build.yml
vendored
77
.github/workflows/build.yml
vendored
@@ -40,13 +40,13 @@ jobs:
|
|||||||
version: ${{ steps.outputs.outputs.version }}
|
version: ${{ steps.outputs.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
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.24.6
|
go-version: ^1.25.7
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -88,13 +88,14 @@ 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_go: true }
|
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
|
||||||
- { os: windows, arch: "386" }
|
- { os: windows, arch: "386" }
|
||||||
- { os: windows, arch: "386", legacy_go: true }
|
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
|
||||||
- { 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" }
|
||||||
@@ -102,31 +103,36 @@ 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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
if: ${{ ! matrix.legacy_go }}
|
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24.6
|
go-version: ^1.25.7
|
||||||
- name: Cache Legacy Go
|
- name: Setup Go 1.24
|
||||||
if: matrix.require_legacy_go
|
if: matrix.legacy_go124
|
||||||
id: cache-legacy-go
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ~1.24.10
|
||||||
|
- 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_legacy
|
~/go/go_win7
|
||||||
key: go_legacy_1236
|
key: go_win7_1255
|
||||||
- name: Setup Legacy Go
|
- name: Setup Go for Windows 7
|
||||||
if: matrix.legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
||||||
run: |-
|
run: |-
|
||||||
.github/setup_legacy_go.sh
|
.github/setup_go_for_windows7.sh
|
||||||
- name: Setup Legacy Go 2
|
- name: Setup Go for Windows 7
|
||||||
if: matrix.legacy_go
|
if: matrix.legacy_win7
|
||||||
run: |-
|
run: |-
|
||||||
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
|
echo "PATH=$HOME/go/go_win7/bin:$PATH" >> $GITHUB_ENV
|
||||||
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
|
echo "GOROOT=$HOME/go/go_win7" >> $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
|
||||||
@@ -184,8 +190,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 [[ "${{ matrix.legacy_go }}" == 'true' ]]; then
|
elif [[ -n "${{ matrix.legacy_name }}" ]]; then
|
||||||
DIR_NAME="${DIR_NAME}-legacy"
|
DIR_NAME="${DIR_NAME}-legacy-${{ matrix.legacy_name }}"
|
||||||
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 }}"
|
||||||
@@ -277,7 +283,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_go && '-legacy' || '' }}
|
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) }}
|
||||||
path: "dist"
|
path: "dist"
|
||||||
build_android:
|
build_android:
|
||||||
name: Build Android
|
name: Build Android
|
||||||
@@ -287,14 +293,14 @@ jobs:
|
|||||||
- calculate_version
|
- calculate_version
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
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.24.6
|
go-version: ^1.25.7
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -367,14 +373,14 @@ jobs:
|
|||||||
- calculate_version
|
- calculate_version
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
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.24.6
|
go-version: ^1.25.7
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -426,7 +432,8 @@ 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-15
|
runs-on: macos-26
|
||||||
|
if: false
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
strategy:
|
strategy:
|
||||||
@@ -464,7 +471,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
@@ -472,15 +479,7 @@ jobs:
|
|||||||
if: matrix.if
|
if: matrix.if
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24.6
|
go-version: ^1.25.7
|
||||||
- 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: |-
|
||||||
@@ -624,7 +623,7 @@ jobs:
|
|||||||
- build_apple
|
- build_apple
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Cache ghr
|
- name: Cache ghr
|
||||||
@@ -647,7 +646,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@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: dist
|
path: dist
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
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@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
|
|||||||
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@@ -22,15 +22,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
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.24.6
|
go-version: ~1.24.10
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v8
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout=30m
|
args: --timeout=30m
|
||||||
|
|||||||
21
.github/workflows/linux.yml
vendored
21
.github/workflows/linux.yml
vendored
@@ -7,6 +7,11 @@ 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
|
||||||
@@ -19,13 +24,13 @@ jobs:
|
|||||||
version: ${{ steps.outputs.outputs.version }}
|
version: ${{ steps.outputs.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
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.24.6
|
go-version: ^1.25.7
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -60,13 +65,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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
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.24.6
|
go-version: ^1.25.7
|
||||||
- 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
|
||||||
@@ -99,11 +104,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, '-') }}
|
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
|
||||||
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, '-')
|
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta
|
||||||
run: |-
|
run: |-
|
||||||
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||||
- name: Set version
|
- name: Set version
|
||||||
@@ -166,7 +171,7 @@ jobs:
|
|||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
@@ -175,7 +180,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@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: dist
|
path: dist
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -15,4 +15,6 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/config.d/
|
/config.d/
|
||||||
/venv/
|
/venv/
|
||||||
|
CLAUDE.md
|
||||||
|
AGENTS.md
|
||||||
|
/.claude/
|
||||||
|
|||||||
@@ -1,27 +1,6 @@
|
|||||||
linters:
|
version: "2"
|
||||||
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.23"
|
go: "1.24"
|
||||||
build-tags:
|
build-tags:
|
||||||
- with_gvisor
|
- with_gvisor
|
||||||
- with_quic
|
- with_quic
|
||||||
@@ -30,7 +9,51 @@ run:
|
|||||||
- with_utls
|
- with_utls
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
|
linters:
|
||||||
issues:
|
default: none
|
||||||
exclude-dirs:
|
enable:
|
||||||
- transport/simple-obfs
|
- govet
|
||||||
|
- 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$
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
|
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="shtorm-7"
|
||||||
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
|
||||||
ARG TARGETOS TARGETARCH
|
ARG TARGETOS TARGETARCH
|
||||||
@@ -18,10 +18,8 @@ RUN set -ex \
|
|||||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
-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="shtorm-7"
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& apk upgrade \
|
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
||||||
&& apk add bash tzdata ca-certificates nftables \
|
|
||||||
&& rm -rf /var/cache/apk/*
|
|
||||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
||||||
ENTRYPOINT ["sing-box"]
|
ENTRYPOINT ["sing-box"]
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -17,6 +17,10 @@ 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) && \
|
||||||
@@ -45,7 +49,7 @@ lint:
|
|||||||
GOOS=freebsd golangci-lint run ./...
|
GOOS=freebsd golangci-lint run ./...
|
||||||
|
|
||||||
lint_install:
|
lint_install:
|
||||||
go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||||
|
|
||||||
proto:
|
proto:
|
||||||
@go run ./cmd/internal/protogen
|
@go run ./cmd/internal/protogen
|
||||||
@@ -245,8 +249,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.7
|
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.8
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.7
|
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.8
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
venv/bin/mkdocs serve
|
venv/bin/mkdocs serve
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ Sing-box with extended features.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Amnezia
|
* Amnezia 1.5
|
||||||
* WARP
|
* WARP
|
||||||
* Tunneling
|
* Tunneling
|
||||||
* Mieru
|
* Mieru
|
||||||
* XHTTP
|
* XHTTP
|
||||||
* SDNS (DNSCrypt)
|
* SDNS (DNSCrypt)
|
||||||
|
* Extended Wireguard options
|
||||||
* Unified delay
|
* Unified delay
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ type DNSClient interface {
|
|||||||
Start()
|
Start()
|
||||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||||
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
|
|
||||||
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
|
|
||||||
ClearCache()
|
ClearCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ type InboundContext struct {
|
|||||||
Domain string
|
Domain string
|
||||||
Client string
|
Client string
|
||||||
SniffContext any
|
SniffContext any
|
||||||
|
SnifferNames []string
|
||||||
SniffError error
|
SniffError error
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
@@ -137,8 +138,7 @@ 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 {
|
||||||
var newMetadata InboundContext
|
newMetadata := *metadata
|
||||||
newMetadata = *metadata
|
|
||||||
return WithContext(ctx, &newMetadata)
|
return WithContext(ctx, &newMetadata)
|
||||||
}
|
}
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ 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
|
||||||
|
|||||||
@@ -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 adapter.Outbound
|
defaultOutboundFallback func() (adapter.Outbound, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 adapter.Outbound) {
|
func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, error)) {
|
||||||
m.defaultOutboundFallback = defaultOutboundFallback
|
m.defaultOutboundFallback = defaultOutboundFallback
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,18 +55,31 @@ 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 {
|
||||||
@@ -187,11 +200,7 @@ 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()
|
||||||
if m.defaultOutbound != nil {
|
return m.defaultOutbound
|
||||||
return m.defaultOutbound
|
|
||||||
} else {
|
|
||||||
return m.defaultOutboundFallback
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Remove(tag string) error {
|
func (m *Manager) Remove(tag string) error {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func NewUpstreamContextHandlerEx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
myMetadata := ContextFrom(ctx)
|
_, myMetadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
myMetadata := ContextFrom(ctx)
|
_, myMetadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ type routeContextHandlerWrapperEx struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
metadata := ContextFrom(ctx)
|
_, metadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
metadata.Source = source
|
metadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
metadata := ContextFrom(ctx)
|
_, metadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
metadata.Source = source
|
metadata.Source = source
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
Source: metadata.Source.Unwrap(),
|
||||||
Destination: metadata.Destination,
|
Destination: metadata.Destination.Unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
box.go
8
box.go
@@ -318,15 +318,15 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outboundManager.Initialize(common.Must1(
|
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||||
direct.NewOutbound(
|
return direct.NewOutbound(
|
||||||
ctx,
|
ctx,
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger("outbound/direct"),
|
logFactory.NewLogger("outbound/direct"),
|
||||||
"direct",
|
"direct",
|
||||||
option.DirectOutboundOptions{},
|
option.DirectOutboundOptions{},
|
||||||
),
|
)
|
||||||
))
|
})
|
||||||
dnsTransportManager.Initialize(common.Must1(
|
dnsTransportManager.Initialize(common.Must1(
|
||||||
local.NewTransport(
|
local.NewTransport(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
Submodule clients/android updated: 6db8e06e8d...eb87216961
Submodule clients/apple updated: c5734677bd...97402ba8b6
@@ -134,6 +134,7 @@ 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 {
|
||||||
@@ -145,12 +146,13 @@ func publishTestflight(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
build := builds.Data[0]
|
build := builds.Data[0]
|
||||||
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
|
if !waitingForProcess && (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
|
||||||
|
|||||||
@@ -107,13 +107,13 @@ func buildAndroid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
|
sharedFlags[3] = sharedFlags[3] + " -checklinkname=0"
|
||||||
args = append(args, sharedFlags...)
|
args = append(args, sharedFlags...)
|
||||||
} else {
|
} else {
|
||||||
|
debugFlags[1] = debugFlags[1] + " -checklinkname=0"
|
||||||
args = append(args, debugFlags...)
|
args = append(args, debugFlags...)
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, "-ldflags", "-checklinkname=0")
|
|
||||||
|
|
||||||
tags := append(sharedTags, memcTags...)
|
tags := append(sharedTags, memcTags...)
|
||||||
if debugEnabled {
|
if debugEnabled {
|
||||||
tags = append(tags, debugTags...)
|
tags = append(tags, debugTags...)
|
||||||
|
|||||||
@@ -151,9 +151,7 @@ func testOnce(boxPath string, stackName string, mtu int, multiThread bool, flags
|
|||||||
var sudoArgs []string
|
var sudoArgs []string
|
||||||
if len(flags) > 0 {
|
if len(flags) > 0 {
|
||||||
sudoArgs = append(sudoArgs, "env")
|
sudoArgs = append(sudoArgs, "env")
|
||||||
for _, flag := range flags {
|
sudoArgs = append(sudoArgs, flags...)
|
||||||
sudoArgs = append(sudoArgs, flag)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sudoArgs = append(sudoArgs, boxPath, "run", "-c", tempConfig.Name())
|
sudoArgs = append(sudoArgs, boxPath, "run", "-c", tempConfig.Name())
|
||||||
boxProcess := shell.Exec("sudo", sudoArgs...)
|
boxProcess := shell.Exec("sudo", sudoArgs...)
|
||||||
|
|||||||
@@ -128,6 +128,10 @@ func (c *ReadWaitConn) Upstream() any {
|
|||||||
return c.Conn
|
return c.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ReadWaitConn) ReaderReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
|
var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -6,22 +6,26 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
_ "unsafe"
|
_ "unsafe"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
|
|
||||||
"github.com/metacubex/utls"
|
"github.com/metacubex/utls"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
||||||
tlsConn, loaded := common.Cast[*tls.UConn](conn)
|
switch tlsConn := conn.(type) {
|
||||||
if !loaded {
|
case *tls.UConn:
|
||||||
return
|
return true, func() error {
|
||||||
|
return utlsReadRecord(tlsConn.Conn)
|
||||||
|
}, func() error {
|
||||||
|
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
|
||||||
|
}
|
||||||
|
case *tls.Conn:
|
||||||
|
return true, func() error {
|
||||||
|
return utlsReadRecord(tlsConn)
|
||||||
|
}, func() error {
|
||||||
|
return utlsHandlePostHandshakeMessage(tlsConn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true, func() error {
|
return
|
||||||
return utlsReadRecord(tlsConn.Conn)
|
|
||||||
}, func() error {
|
|
||||||
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ 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"
|
||||||
@@ -21,6 +22,7 @@ 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
|
||||||
@@ -115,10 +117,14 @@ 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()
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
"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"
|
||||||
@@ -43,7 +42,7 @@ type DefaultDialer struct {
|
|||||||
networkType []C.InterfaceType
|
networkType []C.InterfaceType
|
||||||
fallbackNetworkType []C.InterfaceType
|
fallbackNetworkType []C.InterfaceType
|
||||||
networkFallbackDelay time.Duration
|
networkFallbackDelay time.Duration
|
||||||
networkLastFallback atomic.TypedValue[time.Time]
|
networkLastFallback common.TypedValue[time.Time]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
||||||
@@ -89,37 +88,35 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
|
|
||||||
if networkManager != nil {
|
if networkManager != nil {
|
||||||
defaultOptions := networkManager.DefaultOptions()
|
defaultOptions := networkManager.DefaultOptions()
|
||||||
if !disableDefaultBind {
|
if defaultOptions.BindInterface != "" {
|
||||||
if defaultOptions.BindInterface != "" {
|
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
||||||
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
|
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 {
|
||||||
@@ -127,6 +124,11 @@ 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())
|
||||||
}
|
}
|
||||||
@@ -271,7 +273,7 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
|||||||
} else {
|
} else {
|
||||||
dialer = d.udpDialer4
|
dialer = d.udpDialer4
|
||||||
}
|
}
|
||||||
fastFallback := time.Now().Sub(d.networkLastFallback.Load()) < C.TCPTimeout
|
fastFallback := time.Since(d.networkLastFallback.Load()) < C.TCPTimeout
|
||||||
var (
|
var (
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
isPrimary bool
|
isPrimary bool
|
||||||
|
|||||||
@@ -52,14 +52,6 @@ func (d *DetourDialer) init() {
|
|||||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !d.legacyDNSDialer {
|
|
||||||
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
|
||||||
if directDialer.IsEmpty() {
|
|
||||||
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.dialer = dialer
|
d.dialer = dialer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ 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"
|
||||||
@@ -22,7 +24,7 @@ type slowOpenConn struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
network string
|
network string
|
||||||
destination M.Socksaddr
|
destination M.Socksaddr
|
||||||
conn net.Conn
|
conn atomic.Pointer[net.TCPConn]
|
||||||
create chan struct{}
|
create chan struct{}
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
@@ -50,22 +52,25 @@ func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, des
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
select {
|
if conn != nil {
|
||||||
case <-c.create:
|
return conn.Read(b)
|
||||||
if c.err != nil {
|
}
|
||||||
return 0, c.err
|
select {
|
||||||
}
|
case <-c.create:
|
||||||
case <-c.done:
|
if c.err != nil {
|
||||||
return 0, os.ErrClosed
|
return 0, c.err
|
||||||
}
|
}
|
||||||
|
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) {
|
||||||
if c.conn != nil {
|
tcpConn := c.conn.Load()
|
||||||
return c.conn.Write(b)
|
if tcpConn != nil {
|
||||||
|
return tcpConn.Write(b)
|
||||||
}
|
}
|
||||||
c.access.Lock()
|
c.access.Lock()
|
||||||
defer c.access.Unlock()
|
defer c.access.Unlock()
|
||||||
@@ -74,7 +79,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.Write(b)
|
return c.conn.Load().Write(b)
|
||||||
case <-c.done:
|
case <-c.done:
|
||||||
return 0, os.ErrClosed
|
return 0, os.ErrClosed
|
||||||
default:
|
default:
|
||||||
@@ -83,7 +88,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 = conn
|
c.conn.Store(conn.(*net.TCPConn))
|
||||||
}
|
}
|
||||||
n = len(b)
|
n = len(b)
|
||||||
close(c.create)
|
close(c.create)
|
||||||
@@ -93,70 +98,77 @@ 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)
|
||||||
if c.conn != nil {
|
conn := c.conn.Load()
|
||||||
c.conn.Close()
|
if conn != nil {
|
||||||
|
conn.Close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) LocalAddr() net.Addr {
|
func (c *slowOpenConn) LocalAddr() net.Addr {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
return M.Socksaddr{}
|
return M.Socksaddr{}
|
||||||
}
|
}
|
||||||
return c.conn.LocalAddr()
|
return conn.LocalAddr()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) RemoteAddr() net.Addr {
|
func (c *slowOpenConn) RemoteAddr() net.Addr {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
return M.Socksaddr{}
|
return M.Socksaddr{}
|
||||||
}
|
}
|
||||||
return c.conn.RemoteAddr()
|
return conn.RemoteAddr()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) SetDeadline(t time.Time) error {
|
func (c *slowOpenConn) SetDeadline(t time.Time) error {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return c.conn.SetDeadline(t)
|
return conn.SetDeadline(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) SetReadDeadline(t time.Time) error {
|
func (c *slowOpenConn) SetReadDeadline(t time.Time) error {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return c.conn.SetReadDeadline(t)
|
return conn.SetReadDeadline(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) SetWriteDeadline(t time.Time) error {
|
func (c *slowOpenConn) SetWriteDeadline(t time.Time) error {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return c.conn.SetWriteDeadline(t)
|
return conn.SetWriteDeadline(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) Upstream() any {
|
func (c *slowOpenConn) Upstream() any {
|
||||||
return c.conn
|
return common.PtrOrNil(c.conn.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) ReaderReplaceable() bool {
|
func (c *slowOpenConn) ReaderReplaceable() bool {
|
||||||
return c.conn != nil
|
return c.conn.Load() != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) WriterReplaceable() bool {
|
func (c *slowOpenConn) WriterReplaceable() bool {
|
||||||
return c.conn != nil
|
return c.conn.Load() != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) LazyHeadroom() bool {
|
func (c *slowOpenConn) LazyHeadroom() bool {
|
||||||
return c.conn == nil
|
return c.conn.Load() == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) NeedHandshake() bool {
|
func (c *slowOpenConn) NeedHandshake() bool {
|
||||||
return c.conn == nil
|
return c.conn.Load() == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
|
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
if c.conn == nil {
|
conn := c.conn.Load()
|
||||||
|
if conn == nil {
|
||||||
select {
|
select {
|
||||||
case <-c.create:
|
case <-c.create:
|
||||||
if c.err != nil {
|
if c.err != nil {
|
||||||
@@ -166,5 +178,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)
|
return bufio.Copy(w, c.conn.Load())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package listener
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ func (l *Listener) ListenTCP() (net.Listener, 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, !M.ParseSocksaddr(address).IsIPv4(), false)
|
return redir.TProxy(fd, !strings.HasSuffix(network, "4"), false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -41,7 +42,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, !M.ParseSocksaddr(address).IsIPv4(), true)
|
return redir.TProxy(fd, !strings.HasSuffix(network, "4"), true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,8 +303,6 @@ find:
|
|||||||
metadata.Protocol = C.ProtocolQUIC
|
metadata.Protocol = C.ProtocolQUIC
|
||||||
fingerprint, err := ja3.Compute(buffer.Bytes())
|
fingerprint, err := ja3.Compute(buffer.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metadata.Protocol = C.ProtocolQUIC
|
|
||||||
metadata.Client = C.ClientChromium
|
|
||||||
metadata.SniffContext = fragments
|
metadata.SniffContext = fragments
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return E.Cause1(ErrNeedMoreData, err)
|
||||||
}
|
}
|
||||||
@@ -334,7 +332,7 @@ find:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
|
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
|
||||||
if maybeUQUIC(fingerprint) {
|
if isQUICGo(fingerprint) {
|
||||||
metadata.Client = C.ClientQUICGo
|
metadata.Client = C.ClientQUICGo
|
||||||
} else {
|
} else {
|
||||||
metadata.Client = C.ClientChromium
|
metadata.Client = C.ClientChromium
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
package sniff
|
package sniff
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/ja3"
|
"github.com/sagernet/sing-box/common/ja3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
|
const (
|
||||||
// The cronet without this behavior does not have version 115
|
// X25519Kyber768Draft00 - post-quantum curve used by Go crypto/tls
|
||||||
var uQUICChrome115 = &ja3.ClientHello{
|
x25519Kyber768Draft00 uint16 = 0x11EC // 4588
|
||||||
Version: tls.VersionTLS12,
|
// renegotiation_info extension used by Go crypto/tls
|
||||||
CipherSuites: []uint16{4865, 4866, 4867},
|
extensionRenegotiationInfo uint16 = 0xFF01 // 65281
|
||||||
Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513},
|
)
|
||||||
EllipticCurves: []uint16{29, 23, 24},
|
|
||||||
SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},
|
|
||||||
}
|
|
||||||
|
|
||||||
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
|
// isQUICGo detects native quic-go by checking for Go crypto/tls specific features.
|
||||||
if uQUICChrome115.Equals(fingerprint, true) {
|
// Note: uQUIC with Chromium mimicry cannot be reliably distinguished from real Chromium
|
||||||
return true
|
// since it uses the same TLS fingerprint, so it will be identified as Chromium.
|
||||||
|
func isQUICGo(fingerprint *ja3.ClientHello) bool {
|
||||||
|
for _, curve := range fingerprint.EllipticCurves {
|
||||||
|
if curve == x25519Kyber768Draft00 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ext := range fingerprint.Extensions {
|
||||||
|
if ext == extensionRenegotiationInfo {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
188
common/sniff/quic_capture_test.go
Normal file
188
common/sniff/quic_capture_test.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package sniff_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSniffQUICQuicGoFingerprint(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const testSNI = "test.example.com"
|
||||||
|
|
||||||
|
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer udpConn.Close()
|
||||||
|
|
||||||
|
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
||||||
|
packetsChan := make(chan [][]byte, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var packets [][]byte
|
||||||
|
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
n, _, err := udpConn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
packets = append(packets, buf[:n])
|
||||||
|
}
|
||||||
|
packetsChan <- packets
|
||||||
|
}()
|
||||||
|
|
||||||
|
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: testSNI,
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
NextProtos: []string{"h3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
||||||
|
|
||||||
|
select {
|
||||||
|
case packets := <-packetsChan:
|
||||||
|
t.Logf("Captured %d packets", len(packets))
|
||||||
|
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
for i, pkt := range packets {
|
||||||
|
err := sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
t.Logf("Packet %d: err=%v, domain=%s, client=%s", i, err, metadata.Domain, metadata.Client)
|
||||||
|
if metadata.Domain != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("\n=== quic-go TLS Fingerprint Analysis ===")
|
||||||
|
t.Logf("Domain: %s", metadata.Domain)
|
||||||
|
t.Logf("Client: %s", metadata.Client)
|
||||||
|
t.Logf("Protocol: %s", metadata.Protocol)
|
||||||
|
|
||||||
|
// The client should be identified as quic-go, not chromium
|
||||||
|
// Current issue: it's being identified as chromium
|
||||||
|
if metadata.Client == "chromium" {
|
||||||
|
t.Log("WARNING: quic-go is being misidentified as chromium!")
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("Timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSniffQUICInitialFromQuicGo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const testSNI = "test.example.com"
|
||||||
|
|
||||||
|
// Create UDP listener to capture ALL initial packets
|
||||||
|
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer udpConn.Close()
|
||||||
|
|
||||||
|
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
||||||
|
|
||||||
|
// Channel to receive captured packets
|
||||||
|
packetsChan := make(chan [][]byte, 1)
|
||||||
|
|
||||||
|
// Start goroutine to capture packets
|
||||||
|
go func() {
|
||||||
|
var packets [][]byte
|
||||||
|
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||||
|
for i := 0; i < 5; i++ { // Capture up to 5 packets
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
n, _, err := udpConn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
packets = append(packets, buf[:n])
|
||||||
|
}
|
||||||
|
packetsChan <- packets
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create QUIC client connection (will fail but we capture the initial packet)
|
||||||
|
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: testSNI,
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
NextProtos: []string{"h3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// This will fail (no server) but sends initial packet
|
||||||
|
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
||||||
|
|
||||||
|
// Wait for captured packets
|
||||||
|
select {
|
||||||
|
case packets := <-packetsChan:
|
||||||
|
t.Logf("Captured %d QUIC packets", len(packets))
|
||||||
|
|
||||||
|
for i, packet := range packets {
|
||||||
|
t.Logf("Packet %d: length=%d, first 30 bytes: %x", i, len(packet), packet[:min(30, len(packet))])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test sniffer with first packet
|
||||||
|
if len(packets) > 0 {
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err := sniff.QUICClientHello(context.Background(), &metadata, packets[0])
|
||||||
|
|
||||||
|
t.Logf("First packet sniff error: %v", err)
|
||||||
|
t.Logf("Protocol: %s", metadata.Protocol)
|
||||||
|
t.Logf("Domain: %s", metadata.Domain)
|
||||||
|
t.Logf("Client: %s", metadata.Client)
|
||||||
|
|
||||||
|
// If first packet needs more data, try with subsequent packets
|
||||||
|
// IMPORTANT: reuse metadata to accumulate CRYPTO fragments via SniffContext
|
||||||
|
if errors.Is(err, sniff.ErrNeedMoreData) && len(packets) > 1 {
|
||||||
|
t.Log("First packet needs more data, trying subsequent packets with shared context...")
|
||||||
|
for i := 1; i < len(packets); i++ {
|
||||||
|
// Reuse same metadata to accumulate fragments
|
||||||
|
err = sniff.QUICClientHello(context.Background(), &metadata, packets[i])
|
||||||
|
t.Logf("Packet %d sniff result: err=%v, domain=%s, sniffCtx=%v", i, err, metadata.Domain, metadata.SniffContext != nil)
|
||||||
|
if metadata.Domain != "" || (err != nil && !errors.Is(err, sniff.ErrNeedMoreData)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print hex dump for debugging
|
||||||
|
t.Logf("First packet hex:\n%s", hex.Dump(packets[0][:min(256, len(packets[0]))]))
|
||||||
|
|
||||||
|
// Log final results
|
||||||
|
t.Logf("Final: Protocol=%s, Domain=%s, Client=%s", metadata.Protocol, metadata.Domain, metadata.Client)
|
||||||
|
|
||||||
|
// Verify SNI extraction
|
||||||
|
if metadata.Domain == "" {
|
||||||
|
t.Errorf("Failed to extract SNI, expected: %s", testSNI)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, testSNI, metadata.Domain, "SNI should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check client identification - quic-go should be identified as quic-go, not chromium
|
||||||
|
t.Logf("Client identified as: %s (expected: quic-go)", metadata.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("Timeout waiting for QUIC packets")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ func TestSniffQUICChromeNew(t *testing.T) {
|
|||||||
var metadata adapter.InboundContext
|
var metadata adapter.InboundContext
|
||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
require.Empty(t, metadata.Client)
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||||
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -39,7 +39,7 @@ func TestSniffQUICChromium(t *testing.T) {
|
|||||||
var metadata adapter.InboundContext
|
var metadata adapter.InboundContext
|
||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
require.Empty(t, metadata.Client)
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||||
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -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.ClientQUICGo)
|
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||||
require.Equal(t, metadata.Domain, "www.google.com")
|
require.Equal(t, metadata.Domain, "www.google.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,26 +53,48 @@ func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, e
|
|||||||
return tlsConn, nil
|
return tlsConn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Dialer struct {
|
type Dialer interface {
|
||||||
|
N.Dialer
|
||||||
|
DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultDialer struct {
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
config Config
|
config Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDialer(dialer N.Dialer, config Config) N.Dialer {
|
func NewDialer(dialer N.Dialer, config Config) Dialer {
|
||||||
return &Dialer{dialer, config}
|
return &defaultDialer{dialer, config}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d *defaultDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
if network != N.NetworkTCP {
|
if N.NetworkName(network) != N.NetworkTCP {
|
||||||
return nil, os.ErrInvalid
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
conn, err := d.dialer.DialContext(ctx, network, destination)
|
return d.DialTLSContext(ctx, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *defaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
|
||||||
|
return d.dialContext(ctx, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
|
||||||
|
conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ClientHandshake(ctx, conn, d.config)
|
tlsConn, err := aTLS.ClientHandshake(ctx, conn, d.config)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tlsConn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *defaultDialer) Upstream() any {
|
||||||
return nil, os.ErrInvalid
|
return d.dialer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,11 +69,7 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
|
|||||||
} else {
|
} else {
|
||||||
return E.New("missing ECH keys")
|
return E.New("missing ECH keys")
|
||||||
}
|
}
|
||||||
block, rest := pem.Decode(echKey)
|
echKeys, err := parseECHKeys(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")
|
||||||
}
|
}
|
||||||
@@ -85,21 +81,29 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
|
||||||
echKey, err := os.ReadFile(echKeyPath)
|
echKeys, err := parseECHKeys(echKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "reload ECH keys from ", echKeyPath)
|
return err
|
||||||
}
|
}
|
||||||
|
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 E.New("invalid ECH keys pem")
|
return nil, E.New("invalid ECH keys pem")
|
||||||
}
|
}
|
||||||
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "parse ECH keys")
|
return nil, E.Cause(err, "parse ECH keys")
|
||||||
}
|
}
|
||||||
tlsConfig.EncryptedClientHelloKeys = echKeys
|
return echKeys, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ECHClientConfig struct {
|
type ECHClientConfig struct {
|
||||||
@@ -125,7 +129,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.Now().Sub(s.lastUpdate) > s.lastTTL {
|
if len(s.ECHConfigList()) == 0 || s.lastTTL == 0 || time.Since(s.lastUpdate) > s.lastTTL {
|
||||||
message := &mDNS.Msg{
|
message := &mDNS.Msg{
|
||||||
MsgHdr: mDNS.MsgHdr{
|
MsgHdr: mDNS.MsgHdr{
|
||||||
RecursionDesired: true,
|
RecursionDesired: true,
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
|
|||||||
return E.New("ECH requires go1.24, please recompile your binary.")
|
return E.New("ECH requires go1.24, please recompile your binary.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
|
||||||
return E.New("ECH requires go1.24, please recompile your binary.")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,3 +307,11 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -206,3 +206,11 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/fswatch"
|
"github.com/sagernet/fswatch"
|
||||||
@@ -20,6 +21,7 @@ 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
|
||||||
@@ -32,14 +34,22 @@ type STDServerConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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.config.ServerName = serverName
|
c.access.Lock()
|
||||||
|
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 {
|
||||||
@@ -48,11 +58,15 @@ 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 {
|
||||||
c.config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
||||||
} else {
|
} else {
|
||||||
c.config.NextProtos = nextProto
|
config.NextProtos = nextProto
|
||||||
}
|
}
|
||||||
|
c.config = config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) Config() (*STDConfig, error) {
|
func (c *STDServerConfig) Config() (*STDConfig, error) {
|
||||||
@@ -77,9 +91,6 @@ 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)
|
||||||
@@ -99,6 +110,9 @@ func (c *STDServerConfig) startWatcher() error {
|
|||||||
if c.echKeyPath != "" {
|
if c.echKeyPath != "" {
|
||||||
watchPath = append(watchPath, c.echKeyPath)
|
watchPath = append(watchPath, c.echKeyPath)
|
||||||
}
|
}
|
||||||
|
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) {
|
||||||
@@ -138,10 +152,18 @@ 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.config.Certificates = []tls.Certificate{keyPair}
|
c.access.Lock()
|
||||||
|
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 path == c.echKeyPath {
|
} else if path == c.echKeyPath {
|
||||||
err := reloadECHKeys(c.echKeyPath, c.config)
|
echKey, err := os.ReadFile(c.echKeyPath)
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -262,7 +284,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &STDServerConfig{
|
serverConfig := &STDServerConfig{
|
||||||
config: tlsConfig,
|
config: tlsConfig,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
acmeService: acmeService,
|
acmeService: acmeService,
|
||||||
@@ -271,5 +293,11 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
|||||||
certificatePath: options.CertificatePath,
|
certificatePath: options.CertificatePath,
|
||||||
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
|
||||||
|
}
|
||||||
|
return serverConfig, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,14 @@ 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
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
||||||
|
_, err := conn.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
time.Sleep(fallbackDelay)
|
time.Sleep(fallbackDelay)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fal
|
|||||||
err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload)
|
err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||||
|
if _, err := conn.Write(payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
time.Sleep(fallbackDelay)
|
time.Sleep(fallbackDelay)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,15 +47,15 @@ func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory
|
|||||||
func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
delete(s.delayHistory, tag)
|
delete(s.delayHistory, tag)
|
||||||
s.access.Unlock()
|
|
||||||
s.notifyUpdated()
|
s.notifyUpdated()
|
||||||
|
s.access.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
|
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
s.delayHistory[tag] = history
|
s.delayHistory[tag] = history
|
||||||
s.access.Unlock()
|
|
||||||
s.notifyUpdated()
|
s.notifyUpdated()
|
||||||
|
s.access.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HistoryStorage) notifyUpdated() {
|
func (s *HistoryStorage) notifyUpdated() {
|
||||||
@@ -69,6 +69,8 @@ func (s *HistoryStorage) notifyUpdated() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *HistoryStorage) Close() error {
|
func (s *HistoryStorage) Close() error {
|
||||||
|
s.access.Lock()
|
||||||
|
defer s.access.Unlock()
|
||||||
s.updateHook = nil
|
s.updateHook = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const (
|
|||||||
Size = 8192
|
Size = 8192
|
||||||
)
|
)
|
||||||
|
|
||||||
var zero = [Size * 10]byte{0}
|
var ErrBufferFull = E.New("buffer is full")
|
||||||
|
|
||||||
var pool = bytespool.GetPool(Size)
|
var pool = bytespool.GetPool(Size)
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ func (b *Buffer) Bytes() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extend increases the buffer size by n bytes, and returns the extended part.
|
// Extend increases the buffer size by n bytes, and returns the extended part.
|
||||||
// It panics if result size is larger than buf.Size.
|
// It panics if result size is larger than size of this buffer.
|
||||||
func (b *Buffer) Extend(n int32) []byte {
|
func (b *Buffer) Extend(n int32) []byte {
|
||||||
end := b.end + n
|
end := b.end + n
|
||||||
if end > int32(len(b.v)) {
|
if end > int32(len(b.v)) {
|
||||||
@@ -152,7 +152,7 @@ func (b *Buffer) Extend(n int32) []byte {
|
|||||||
}
|
}
|
||||||
ext := b.v[b.end:end]
|
ext := b.v[b.end:end]
|
||||||
b.end = end
|
b.end = end
|
||||||
copy(ext, zero[:])
|
clear(ext)
|
||||||
return ext
|
return ext
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ func (b *Buffer) Resize(from, to int32) {
|
|||||||
b.start += from
|
b.start += from
|
||||||
b.Check()
|
b.Check()
|
||||||
if b.end > oldEnd {
|
if b.end > oldEnd {
|
||||||
copy(b.v[oldEnd:b.end], zero[:])
|
clear(b.v[oldEnd:b.end])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +244,14 @@ func (b *Buffer) Cap() int32 {
|
|||||||
return int32(len(b.v))
|
return int32(len(b.v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Available returns the available capacity of the buffer content.
|
||||||
|
func (b *Buffer) Available() int32 {
|
||||||
|
if b == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int32(len(b.v)) - b.end
|
||||||
|
}
|
||||||
|
|
||||||
// IsEmpty returns true if the buffer is empty.
|
// IsEmpty returns true if the buffer is empty.
|
||||||
func (b *Buffer) IsEmpty() bool {
|
func (b *Buffer) IsEmpty() bool {
|
||||||
return b.Len() == 0
|
return b.Len() == 0
|
||||||
@@ -258,13 +266,16 @@ func (b *Buffer) IsFull() bool {
|
|||||||
func (b *Buffer) Write(data []byte) (int, error) {
|
func (b *Buffer) Write(data []byte) (int, error) {
|
||||||
nBytes := copy(b.v[b.end:], data)
|
nBytes := copy(b.v[b.end:], data)
|
||||||
b.end += int32(nBytes)
|
b.end += int32(nBytes)
|
||||||
|
if nBytes < len(data) {
|
||||||
|
return nBytes, ErrBufferFull
|
||||||
|
}
|
||||||
return nBytes, nil
|
return nBytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteByte writes a single byte into the buffer.
|
// WriteByte writes a single byte into the buffer.
|
||||||
func (b *Buffer) WriteByte(v byte) error {
|
func (b *Buffer) WriteByte(v byte) error {
|
||||||
if b.IsFull() {
|
if b.IsFull() {
|
||||||
return E.New("buffer full")
|
return ErrBufferFull
|
||||||
}
|
}
|
||||||
b.v[b.end] = v
|
b.v[b.end] = v
|
||||||
b.end++
|
b.end++
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ var ErrReadTimeout = E.New("IO timeout")
|
|||||||
|
|
||||||
// TimeoutReader is a reader that returns error if Read() operation takes longer than the given timeout.
|
// TimeoutReader is a reader that returns error if Read() operation takes longer than the given timeout.
|
||||||
type TimeoutReader interface {
|
type TimeoutReader interface {
|
||||||
|
Reader
|
||||||
ReadMultiBufferTimeout(time.Duration) (MultiBuffer, error)
|
ReadMultiBufferTimeout(time.Duration) (MultiBuffer, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func Compact(mb MultiBuffer) MultiBuffer {
|
|||||||
|
|
||||||
for i := 1; i < len(mb); i++ {
|
for i := 1; i < len(mb); i++ {
|
||||||
curr := mb[i]
|
curr := mb[i]
|
||||||
if last.Len()+curr.Len() > Size {
|
if curr.Len() > last.Available() {
|
||||||
mb2 = append(mb2, last)
|
mb2 = append(mb2, last)
|
||||||
last = curr
|
last = curr
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ func (w *BufferToBytesWriter) ReadFrom(reader io.Reader) (int64, error) {
|
|||||||
// BufferedWriter is a Writer with internal buffer.
|
// BufferedWriter is a Writer with internal buffer.
|
||||||
type BufferedWriter struct {
|
type BufferedWriter struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
writer Writer
|
writer Writer
|
||||||
buffer *Buffer
|
buffer *Buffer
|
||||||
buffered bool
|
buffered bool
|
||||||
|
flushNext bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBufferedWriter creates a new BufferedWriter.
|
// NewBufferedWriter creates a new BufferedWriter.
|
||||||
@@ -161,6 +162,12 @@ func (w *BufferedWriter) WriteMultiBuffer(b MultiBuffer) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if w.flushNext {
|
||||||
|
w.buffered = false
|
||||||
|
w.flushNext = false
|
||||||
|
return w.flushInternal()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +208,13 @@ func (w *BufferedWriter) SetBuffered(f bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFlushNext will wait the next WriteMultiBuffer to flush and set buffered = false
|
||||||
|
func (w *BufferedWriter) SetFlushNext() {
|
||||||
|
w.Lock()
|
||||||
|
defer w.Unlock()
|
||||||
|
w.flushNext = true
|
||||||
|
}
|
||||||
|
|
||||||
// ReadFrom implements io.ReaderFrom.
|
// ReadFrom implements io.ReaderFrom.
|
||||||
func (w *BufferedWriter) ReadFrom(reader io.Reader) (int64, error) {
|
func (w *BufferedWriter) ReadFrom(reader io.Reader) (int64, error) {
|
||||||
if err := w.SetBuffered(false); err != nil {
|
if err := w.SetBuffered(false); err != nil {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
// Must panics if err is not nil.
|
// Must panics if err is not nil.
|
||||||
func Must(err error) {
|
func Must(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -17,3 +19,14 @@ func Must2(v interface{}, err error) interface{} {
|
|||||||
func Error2(v interface{}, err error) error {
|
func Error2(v interface{}, err error) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseIfExists call obj.Close() if obj is not nil.
|
||||||
|
func CloseIfExists(obj any) error {
|
||||||
|
if obj != nil {
|
||||||
|
v := reflect.ValueOf(obj)
|
||||||
|
if !v.IsNil() {
|
||||||
|
return Close(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ func RandBetween(from int64, to int64) int64 {
|
|||||||
if from == to {
|
if from == to {
|
||||||
return from
|
return from
|
||||||
}
|
}
|
||||||
|
if from > to {
|
||||||
|
from, to = to, from
|
||||||
|
}
|
||||||
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
|
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
|
||||||
return from + bigInt.Int64()
|
return from + bigInt.Int64()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ func (c *Range) Build() *Range {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Range) MarshalJSON() ([]byte, error) {
|
func (c *Range) MarshalJSON() ([]byte, error) {
|
||||||
|
if c.From == c.To {
|
||||||
|
return json.Marshal(c.From)
|
||||||
|
}
|
||||||
return json.Marshal(fmt.Sprintf("%d-%d", c.From, c.To))
|
return json.Marshal(fmt.Sprintf("%d-%d", c.From, c.To))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,21 +36,32 @@ func (c *Range) UnmarshalJSON(content []byte) error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
parts := strings.Split(stringValue, "-")
|
parts := strings.Split(stringValue, "-")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return E.New("invalid length of range parts")
|
from, err := strconv.ParseInt(parts[0], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rangeValue.From, rangeValue.To = int32(from), int32(from)
|
||||||
|
} else {
|
||||||
|
from, err := strconv.ParseInt(parts[0], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
to, err := strconv.ParseInt(parts[1], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rangeValue.From, rangeValue.To = int32(from), int32(to)
|
||||||
}
|
}
|
||||||
from, err := strconv.ParseInt(parts[0], 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
to, err := strconv.ParseInt(parts[1], 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rangeValue.From, rangeValue.To = int32(from), int32(to)
|
|
||||||
} else {
|
} else {
|
||||||
err := json.Unmarshal(content, &rangeValue)
|
var int32Value int32
|
||||||
if err != nil {
|
err := json.Unmarshal(content, &int32Value)
|
||||||
return err
|
if err == nil {
|
||||||
|
rangeValue.From, rangeValue.To = int32Value, int32Value
|
||||||
|
} else {
|
||||||
|
err := json.Unmarshal(content, &rangeValue)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if rangeValue.From > rangeValue.To {
|
if rangeValue.From > rangeValue.To {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package pipe
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -136,11 +135,10 @@ func (p *pipe) writeMultiBufferInternal(mb buf.MultiBuffer) error {
|
|||||||
|
|
||||||
if p.data == nil {
|
if p.data == nil {
|
||||||
p.data = mb
|
p.data = mb
|
||||||
return nil
|
} else {
|
||||||
|
p.data, _ = buf.MergeMulti(p.data, mb)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
p.data, _ = buf.MergeMulti(p.data, mb)
|
|
||||||
return errSlowDown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||||
@@ -155,30 +153,23 @@ func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == errSlowDown {
|
if err == errBufferFull {
|
||||||
p.readSignal.Signal()
|
if p.option.discardOverflow {
|
||||||
|
buf.ReleaseMulti(mb)
|
||||||
// Yield current goroutine. Hopefully the reading counterpart can pick up the payload.
|
return nil
|
||||||
runtime.Gosched()
|
}
|
||||||
return nil
|
select {
|
||||||
|
case <-p.writeSignal.Wait():
|
||||||
|
continue
|
||||||
|
case <-p.done.Wait():
|
||||||
|
buf.ReleaseMulti(mb)
|
||||||
|
return io.ErrClosedPipe
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == errBufferFull && p.option.discardOverflow {
|
buf.ReleaseMulti(mb)
|
||||||
buf.ReleaseMulti(mb)
|
p.readSignal.Signal()
|
||||||
return nil
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if err != errBufferFull {
|
|
||||||
buf.ReleaseMulti(mb)
|
|
||||||
p.readSignal.Signal()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-p.writeSignal.Wait():
|
|
||||||
case <-p.done.Wait():
|
|
||||||
return io.ErrClosedPipe
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,16 +191,19 @@ func (p *pipe) Interrupt() {
|
|||||||
p.Lock()
|
p.Lock()
|
||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
|
|
||||||
|
if !p.data.IsEmpty() {
|
||||||
|
buf.ReleaseMulti(p.data)
|
||||||
|
p.data = nil
|
||||||
|
if p.state == closed {
|
||||||
|
p.state = errord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if p.state == closed || p.state == errord {
|
if p.state == closed || p.state == errord {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.state = errord
|
p.state = errord
|
||||||
|
|
||||||
if !p.data.IsEmpty() {
|
|
||||||
buf.ReleaseMulti(p.data)
|
|
||||||
p.data = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
common.Must(p.done.Close())
|
common.Must(p.done.Close())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package signal
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray"
|
"github.com/sagernet/sing-box/common/xray"
|
||||||
@@ -14,10 +15,12 @@ type ActivityUpdater interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActivityTimer struct {
|
type ActivityTimer struct {
|
||||||
sync.RWMutex
|
mu sync.RWMutex
|
||||||
updated chan struct{}
|
updated chan struct{}
|
||||||
checkTask *task.Periodic
|
checkTask *task.Periodic
|
||||||
onTimeout func()
|
onTimeout func()
|
||||||
|
consumed atomic.Bool
|
||||||
|
once sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ActivityTimer) Update() {
|
func (t *ActivityTimer) Update() {
|
||||||
@@ -37,39 +40,39 @@ func (t *ActivityTimer) check() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *ActivityTimer) finish() {
|
func (t *ActivityTimer) finish() {
|
||||||
t.Lock()
|
t.once.Do(func() {
|
||||||
defer t.Unlock()
|
t.consumed.Store(true)
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
if t.onTimeout != nil {
|
common.CloseIfExists(t.checkTask)
|
||||||
t.onTimeout()
|
t.onTimeout()
|
||||||
t.onTimeout = nil
|
})
|
||||||
}
|
|
||||||
if t.checkTask != nil {
|
|
||||||
t.checkTask.Close()
|
|
||||||
t.checkTask = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
|
func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
|
||||||
|
if t.consumed.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
t.finish()
|
t.finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
checkTask := &task.Periodic{
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
// double check, just in case
|
||||||
|
if t.consumed.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newCheckTask := &task.Periodic{
|
||||||
Interval: timeout,
|
Interval: timeout,
|
||||||
Execute: t.check,
|
Execute: t.check,
|
||||||
}
|
}
|
||||||
|
common.CloseIfExists(t.checkTask)
|
||||||
t.Lock()
|
t.checkTask = newCheckTask
|
||||||
|
|
||||||
if t.checkTask != nil {
|
|
||||||
t.checkTask.Close()
|
|
||||||
}
|
|
||||||
t.checkTask = checkTask
|
|
||||||
t.Unlock()
|
|
||||||
t.Update()
|
t.Update()
|
||||||
common.Must(checkTask.Start())
|
common.Must(newCheckTask.Start())
|
||||||
}
|
}
|
||||||
|
|
||||||
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {
|
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {
|
||||||
|
|||||||
28
common/xray/utils/browser.go
Normal file
28
common/xray/utils/browser.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/klauspost/cpuid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ChromeVersion() int {
|
||||||
|
// Use only CPU info as seed for PRNG
|
||||||
|
seed := int64(cpuid.CPU.Family + cpuid.CPU.Model + cpuid.CPU.PhysicalCores + cpuid.CPU.LogicalCores + cpuid.CPU.CacheLine)
|
||||||
|
rng := rand.New(rand.NewSource(seed))
|
||||||
|
// Start from Chrome 144 released on 2026.1.13
|
||||||
|
releaseDate := time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC)
|
||||||
|
version := 144
|
||||||
|
now := time.Now()
|
||||||
|
// Each version has random 25-45 day interval
|
||||||
|
for releaseDate.Before(now) {
|
||||||
|
releaseDate = releaseDate.AddDate(0, 0, rng.Intn(21)+25)
|
||||||
|
version++
|
||||||
|
}
|
||||||
|
return version - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChromeUA provides default browser User-Agent based on CPU-seeded PRNG.
|
||||||
|
var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(ChromeVersion()) + ".0.0.0 Safari/537.36"
|
||||||
24
common/xray/utils/padding.go
Normal file
24
common/xray/utils/padding.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// 8 ÷ (397/62)
|
||||||
|
h2packCorrectionFactor = 1.2493702770780857
|
||||||
|
base62TotalCharsNum = 62
|
||||||
|
base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
)
|
||||||
|
|
||||||
|
// H2Base62Pad generates a base62 padding string for HTTP/2 header
|
||||||
|
// The total len will be slightly longer than the input to match the length after h2(h3 also) header huffman encoding
|
||||||
|
func H2Base62Pad[T int32 | int64 | int](expectedLen T) string {
|
||||||
|
actualLenFloat := float64(expectedLen) * h2packCorrectionFactor
|
||||||
|
actualLen := int(actualLenFloat)
|
||||||
|
result := make([]byte, actualLen)
|
||||||
|
for i := range actualLen {
|
||||||
|
result[i] = base62Chars[rand.N(base62TotalCharsNum)]
|
||||||
|
}
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
@@ -85,10 +85,14 @@ func ParseString(str string) (UUID, error) {
|
|||||||
b := uuid.Bytes()
|
b := uuid.Bytes()
|
||||||
|
|
||||||
for _, byteGroup := range byteGroups {
|
for _, byteGroup := range byteGroups {
|
||||||
if text[0] == '-' {
|
if len(text) > 0 && text[0] == '-' {
|
||||||
text = text[1:]
|
text = text[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(text) < byteGroup {
|
||||||
|
return uuid, E.New("invalid UUID: ", str)
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil {
|
if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil {
|
||||||
return uuid, err
|
return uuid, err
|
||||||
}
|
}
|
||||||
|
|||||||
228
dns/client.go
228
dns/client.go
@@ -2,12 +2,14 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/compatible"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -17,7 +19,7 @@ import (
|
|||||||
"github.com/sagernet/sing/contrab/freelru"
|
"github.com/sagernet/sing/contrab/freelru"
|
||||||
"github.com/sagernet/sing/contrab/maphash"
|
"github.com/sagernet/sing/contrab/maphash"
|
||||||
|
|
||||||
dns "github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -30,16 +32,18 @@ var (
|
|||||||
var _ adapter.DNSClient = (*Client)(nil)
|
var _ adapter.DNSClient = (*Client)(nil)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
disableCache bool
|
disableCache bool
|
||||||
disableExpire bool
|
disableExpire bool
|
||||||
independentCache bool
|
independentCache bool
|
||||||
clientSubnet netip.Prefix
|
clientSubnet netip.Prefix
|
||||||
rdrc adapter.RDRCStore
|
rdrc adapter.RDRCStore
|
||||||
initRDRCFunc func() adapter.RDRCStore
|
initRDRCFunc func() adapter.RDRCStore
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
cache freelru.Cache[dns.Question, *dns.Msg]
|
cache freelru.Cache[dns.Question, *dns.Msg]
|
||||||
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
|
cacheLock compatible.Map[dns.Question, chan struct{}]
|
||||||
|
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
|
||||||
|
transportCacheLock compatible.Map[dns.Question, chan struct{}]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientOptions struct {
|
type ClientOptions struct {
|
||||||
@@ -91,22 +95,34 @@ func (c *Client) Start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
||||||
|
for _, record := range response.Ns {
|
||||||
|
if soa, isSOA := record.(*dns.SOA); isSOA {
|
||||||
|
soaTTL := soa.Header().Ttl
|
||||||
|
soaMinimum := soa.Minttl
|
||||||
|
if soaTTL < soaMinimum {
|
||||||
|
return soaTTL, true
|
||||||
|
}
|
||||||
|
return soaMinimum, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
|
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
|
||||||
if len(message.Question) == 0 {
|
if len(message.Question) == 0 {
|
||||||
if c.logger != nil {
|
if c.logger != nil {
|
||||||
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
||||||
}
|
}
|
||||||
responseMessage := dns.Msg{
|
return FixedResponseStatus(message, dns.RcodeFormatError), nil
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Response: true,
|
|
||||||
Rcode: dns.RcodeFormatError,
|
|
||||||
},
|
|
||||||
Question: message.Question,
|
|
||||||
}
|
|
||||||
return &responseMessage, nil
|
|
||||||
}
|
}
|
||||||
question := message.Question[0]
|
question := message.Question[0]
|
||||||
|
if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only {
|
||||||
|
if c.logger != nil {
|
||||||
|
c.logger.DebugContext(ctx, "strategy rejected")
|
||||||
|
}
|
||||||
|
return FixedResponseStatus(message, dns.RcodeSuccess), nil
|
||||||
|
}
|
||||||
clientSubnet := options.ClientSubnet
|
clientSubnet := options.ClientSubnet
|
||||||
if !clientSubnet.IsValid() {
|
if !clientSubnet.IsValid() {
|
||||||
clientSubnet = c.clientSubnet
|
clientSubnet = c.clientSubnet
|
||||||
@@ -114,12 +130,46 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
if clientSubnet.IsValid() {
|
if clientSubnet.IsValid() {
|
||||||
message = SetClientSubnet(message, clientSubnet)
|
message = SetClientSubnet(message, clientSubnet)
|
||||||
}
|
}
|
||||||
|
|
||||||
isSimpleRequest := len(message.Question) == 1 &&
|
isSimpleRequest := len(message.Question) == 1 &&
|
||||||
len(message.Ns) == 0 &&
|
len(message.Ns) == 0 &&
|
||||||
len(message.Extra) == 0 &&
|
(len(message.Extra) == 0 || len(message.Extra) == 1 &&
|
||||||
|
message.Extra[0].Header().Rrtype == dns.TypeOPT &&
|
||||||
|
message.Extra[0].Header().Class > 0 &&
|
||||||
|
message.Extra[0].Header().Ttl == 0 &&
|
||||||
|
len(message.Extra[0].(*dns.OPT).Option) == 0) &&
|
||||||
!options.ClientSubnet.IsValid()
|
!options.ClientSubnet.IsValid()
|
||||||
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
||||||
if !disableCache {
|
if !disableCache {
|
||||||
|
if c.cache != nil {
|
||||||
|
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
|
||||||
|
if loaded {
|
||||||
|
select {
|
||||||
|
case <-cond:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defer func() {
|
||||||
|
c.cacheLock.Delete(question)
|
||||||
|
close(cond)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
} else if c.transportCache != nil {
|
||||||
|
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
|
||||||
|
if loaded {
|
||||||
|
select {
|
||||||
|
case <-cond:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defer func() {
|
||||||
|
c.transportCacheLock.Delete(question)
|
||||||
|
close(cond)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
response, ttl := c.loadResponse(question, transport)
|
response, ttl := c.loadResponse(question, transport)
|
||||||
if response != nil {
|
if response != nil {
|
||||||
logCachedResponse(c.logger, ctx, response, ttl)
|
logCachedResponse(c.logger, ctx, response, ttl)
|
||||||
@@ -127,27 +177,14 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only {
|
|
||||||
responseMessage := dns.Msg{
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Response: true,
|
|
||||||
Rcode: dns.RcodeSuccess,
|
|
||||||
},
|
|
||||||
Question: []dns.Question{question},
|
|
||||||
}
|
|
||||||
if c.logger != nil {
|
|
||||||
c.logger.DebugContext(ctx, "strategy rejected")
|
|
||||||
}
|
|
||||||
return &responseMessage, nil
|
|
||||||
}
|
|
||||||
messageId := message.Id
|
messageId := message.Id
|
||||||
contextTransport, clientSubnetLoaded := transportTagFromContext(ctx)
|
contextTransport, clientSubnetLoaded := transportTagFromContext(ctx)
|
||||||
if clientSubnetLoaded && transport.Tag() == contextTransport {
|
if clientSubnetLoaded && transport.Tag() == contextTransport {
|
||||||
return nil, E.New("DNS query loopback in transport[", contextTransport, "]")
|
return nil, E.New("DNS query loopback in transport[", contextTransport, "]")
|
||||||
}
|
}
|
||||||
ctx = contextWithTransportTag(ctx, transport.Tag())
|
ctx = contextWithTransportTag(ctx, transport.Tag())
|
||||||
if responseChecker != nil && c.rdrc != nil {
|
if !disableCache && responseChecker != nil && c.rdrc != nil {
|
||||||
rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype)
|
rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype)
|
||||||
if rejected {
|
if rejected {
|
||||||
return nil, ErrResponseRejectedCached
|
return nil, ErrResponseRejectedCached
|
||||||
@@ -157,7 +194,12 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
response, err := transport.Exchange(ctx, message)
|
response, err := transport.Exchange(ctx, message)
|
||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
var rcodeError RcodeError
|
||||||
|
if errors.As(err, &rcodeError) {
|
||||||
|
response = FixedResponseStatus(message, int(rcodeError))
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
|
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
|
||||||
validResponse := response
|
validResponse := response
|
||||||
@@ -194,15 +236,17 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
response.Answer = append(response.Answer, validResponse.Answer...)
|
response.Answer = append(response.Answer, validResponse.Answer...)
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
|
||||||
if responseChecker != nil {
|
if responseChecker != nil {
|
||||||
var rejected bool
|
var rejected bool
|
||||||
if !(response.Rcode == dns.RcodeSuccess || response.Rcode == dns.RcodeNameError) {
|
// TODO: add accept_any rule and support to check response instead of addresses
|
||||||
|
if response.Rcode != dns.RcodeSuccess || len(response.Answer) == 0 {
|
||||||
rejected = true
|
rejected = true
|
||||||
} else {
|
} else {
|
||||||
rejected = !responseChecker(MessageToAddresses(response))
|
rejected = !responseChecker(MessageToAddresses(response))
|
||||||
}
|
}
|
||||||
if rejected {
|
if rejected {
|
||||||
if c.rdrc != nil {
|
if !disableCache && c.rdrc != nil {
|
||||||
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
|
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
|
||||||
}
|
}
|
||||||
logRejectedResponse(c.logger, ctx, response)
|
logRejectedResponse(c.logger, ctx, response)
|
||||||
@@ -229,10 +273,17 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var timeToLive uint32
|
var timeToLive uint32
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
if len(response.Answer) == 0 {
|
||||||
for _, record := range recordList {
|
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
|
||||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
timeToLive = soaTTL
|
||||||
timeToLive = record.Header().Ttl
|
}
|
||||||
|
}
|
||||||
|
if timeToLive == 0 {
|
||||||
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
|
for _, record := range recordList {
|
||||||
|
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||||
|
timeToLive = record.Header().Ttl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +310,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
logExchangedResponse(c.logger, ctx, response, timeToLive)
|
logExchangedResponse(c.logger, ctx, response, timeToLive)
|
||||||
return response, err
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||||
@@ -305,70 +356,11 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
|||||||
func (c *Client) ClearCache() {
|
func (c *Client) ClearCache() {
|
||||||
if c.cache != nil {
|
if c.cache != nil {
|
||||||
c.cache.Purge()
|
c.cache.Purge()
|
||||||
}
|
} else if c.transportCache != nil {
|
||||||
if c.transportCache != nil {
|
|
||||||
c.transportCache.Purge()
|
c.transportCache.Purge()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool) {
|
|
||||||
if c.disableCache || c.independentCache {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if dns.IsFqdn(domain) {
|
|
||||||
domain = domain[:len(domain)-1]
|
|
||||||
}
|
|
||||||
dnsName := dns.Fqdn(domain)
|
|
||||||
if strategy == C.DomainStrategyIPv4Only {
|
|
||||||
response, err := c.questionCache(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
if err != ErrNotCached {
|
|
||||||
return response, true
|
|
||||||
}
|
|
||||||
} else if strategy == C.DomainStrategyIPv6Only {
|
|
||||||
response, err := c.questionCache(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeAAAA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
if err != ErrNotCached {
|
|
||||||
return response, true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
response4, _ := c.questionCache(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
response6, _ := c.questionCache(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeAAAA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
if len(response4) > 0 || len(response6) > 0 {
|
|
||||||
return sortAddresses(response4, response6, strategy), true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool) {
|
|
||||||
if c.disableCache || c.independentCache || len(message.Question) != 1 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
question := message.Question[0]
|
|
||||||
response, ttl := c.loadResponse(question, nil)
|
|
||||||
if response == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
logCachedResponse(c.logger, ctx, response, ttl)
|
|
||||||
response.Id = message.Id
|
|
||||||
return response, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
|
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
|
||||||
if strategy == C.DomainStrategyPreferIPv6 {
|
if strategy == C.DomainStrategyPreferIPv6 {
|
||||||
return append(response6, response4...)
|
return append(response6, response4...)
|
||||||
@@ -390,15 +382,15 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
|
|||||||
transportTag: transport.Tag(),
|
transportTag: transport.Tag(),
|
||||||
}, message)
|
}, message)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
if !c.independentCache {
|
|
||||||
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
|
|
||||||
} else {
|
} else {
|
||||||
c.transportCache.AddWithLifetime(transportCacheKey{
|
if !c.independentCache {
|
||||||
Question: question,
|
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
|
||||||
transportTag: transport.Tag(),
|
} else {
|
||||||
}, message, time.Second*time.Duration(timeToLive))
|
c.transportCache.AddWithLifetime(transportCacheKey{
|
||||||
|
Question: question,
|
||||||
|
transportTag: transport.Tag(),
|
||||||
|
}, message, time.Second*time.Duration(timeToLive))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,6 +509,9 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MessageToAddresses(response *dns.Msg) []netip.Addr {
|
func MessageToAddresses(response *dns.Msg) []netip.Addr {
|
||||||
|
if response == nil || response.Rcode != dns.RcodeSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||||
for _, rawAnswer := range response.Answer {
|
for _, rawAnswer := range response.Answer {
|
||||||
switch answer := rawAnswer.(type) {
|
switch answer := rawAnswer.(type) {
|
||||||
@@ -561,9 +556,12 @@ func transportTagFromContext(ctx context.Context) (string, bool) {
|
|||||||
func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg {
|
func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg {
|
||||||
return &dns.Msg{
|
return &dns.Msg{
|
||||||
MsgHdr: dns.MsgHdr{
|
MsgHdr: dns.MsgHdr{
|
||||||
Id: message.Id,
|
Id: message.Id,
|
||||||
Rcode: rcode,
|
Response: true,
|
||||||
Response: true,
|
Authoritative: true,
|
||||||
|
RecursionDesired: true,
|
||||||
|
RecursionAvailable: true,
|
||||||
|
Rcode: rcode,
|
||||||
},
|
},
|
||||||
Question: message.Question,
|
Question: message.Question,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
|
|||||||
}
|
}
|
||||||
responseLen := response.Len()
|
responseLen := response.Len()
|
||||||
if responseLen > maxLen {
|
if responseLen > maxLen {
|
||||||
copyResponse := *response
|
response = response.Copy()
|
||||||
response = ©Response
|
|
||||||
response.Truncate(maxLen)
|
response.Truncate(maxLen)
|
||||||
}
|
}
|
||||||
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
RcodeSuccess RcodeError = mDNS.RcodeSuccess
|
||||||
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
||||||
RcodeNameError RcodeError = mDNS.RcodeNameError
|
RcodeNameError RcodeError = mDNS.RcodeNameError
|
||||||
RcodeRefused RcodeError = mDNS.RcodeRefused
|
RcodeRefused RcodeError = mDNS.RcodeRefused
|
||||||
|
|||||||
188
dns/router.go
188
dns/router.go
@@ -214,102 +214,95 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
|||||||
}
|
}
|
||||||
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
||||||
var (
|
var (
|
||||||
|
response *mDNS.Msg
|
||||||
transport adapter.DNSTransport
|
transport adapter.DNSTransport
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
response, cached := r.client.ExchangeCache(ctx, message)
|
var metadata *adapter.InboundContext
|
||||||
if !cached {
|
ctx, metadata = adapter.ExtendContext(ctx)
|
||||||
var metadata *adapter.InboundContext
|
metadata.Destination = M.Socksaddr{}
|
||||||
ctx, metadata = adapter.ExtendContext(ctx)
|
metadata.QueryType = message.Question[0].Qtype
|
||||||
metadata.Destination = M.Socksaddr{}
|
switch metadata.QueryType {
|
||||||
metadata.QueryType = message.Question[0].Qtype
|
case mDNS.TypeA:
|
||||||
switch metadata.QueryType {
|
metadata.IPVersion = 4
|
||||||
case mDNS.TypeA:
|
case mDNS.TypeAAAA:
|
||||||
metadata.IPVersion = 4
|
metadata.IPVersion = 6
|
||||||
case mDNS.TypeAAAA:
|
}
|
||||||
metadata.IPVersion = 6
|
metadata.Domain = FqdnToDomain(message.Question[0].Name)
|
||||||
}
|
if options.Transport != nil {
|
||||||
metadata.Domain = FqdnToDomain(message.Question[0].Name)
|
transport = options.Transport
|
||||||
if options.Transport != nil {
|
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||||
transport = options.Transport
|
|
||||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
|
||||||
options.Strategy = legacyTransport.LegacyStrategy()
|
|
||||||
}
|
|
||||||
if !options.ClientSubnet.IsValid() {
|
|
||||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
options.Strategy = r.defaultDomainStrategy
|
options.Strategy = legacyTransport.LegacyStrategy()
|
||||||
}
|
}
|
||||||
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
if !options.ClientSubnet.IsValid() {
|
||||||
} else {
|
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||||
var (
|
|
||||||
rule adapter.DNSRule
|
|
||||||
ruleIndex int
|
|
||||||
)
|
|
||||||
ruleIndex = -1
|
|
||||||
for {
|
|
||||||
dnsCtx := adapter.OverrideContext(ctx)
|
|
||||||
dnsOptions := options
|
|
||||||
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
|
||||||
if rule != nil {
|
|
||||||
switch action := rule.Action().(type) {
|
|
||||||
case *R.RuleActionReject:
|
|
||||||
switch action.Method {
|
|
||||||
case C.RuleActionRejectMethodDefault:
|
|
||||||
return &mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Rcode: mDNS.RcodeRefused,
|
|
||||||
Response: true,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{message.Question[0]},
|
|
||||||
}, nil
|
|
||||||
case C.RuleActionRejectMethodDrop:
|
|
||||||
return nil, tun.ErrDrop
|
|
||||||
}
|
|
||||||
case *R.RuleActionPredefined:
|
|
||||||
return action.Response(message), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var responseCheck func(responseAddrs []netip.Addr) bool
|
|
||||||
if rule != nil && rule.WithAddressLimit() {
|
|
||||||
responseCheck = func(responseAddrs []netip.Addr) bool {
|
|
||||||
metadata.DestinationAddresses = responseAddrs
|
|
||||||
return rule.MatchAddressLimit(metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
|
||||||
dnsOptions.Strategy = r.defaultDomainStrategy
|
|
||||||
}
|
|
||||||
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
|
|
||||||
var rejected bool
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, ErrResponseRejectedCached) {
|
|
||||||
rejected = true
|
|
||||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
|
|
||||||
} else if errors.Is(err, ErrResponseRejected) {
|
|
||||||
rejected = true
|
|
||||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
|
||||||
/*} else if responseCheck!= nil && errors.Is(err, RcodeError(mDNS.RcodeNameError)) {
|
|
||||||
rejected = true
|
|
||||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
|
||||||
*/
|
|
||||||
} else if len(message.Question) > 0 {
|
|
||||||
rejected = true
|
|
||||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
|
||||||
} else {
|
|
||||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if responseCheck != nil && rejected {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
|
options.Strategy = r.defaultDomainStrategy
|
||||||
|
}
|
||||||
|
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
||||||
|
} else {
|
||||||
|
var (
|
||||||
|
rule adapter.DNSRule
|
||||||
|
ruleIndex int
|
||||||
|
)
|
||||||
|
ruleIndex = -1
|
||||||
|
for {
|
||||||
|
dnsCtx := adapter.OverrideContext(ctx)
|
||||||
|
dnsOptions := options
|
||||||
|
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
||||||
|
if rule != nil {
|
||||||
|
switch action := rule.Action().(type) {
|
||||||
|
case *R.RuleActionReject:
|
||||||
|
switch action.Method {
|
||||||
|
case C.RuleActionRejectMethodDefault:
|
||||||
|
return &mDNS.Msg{
|
||||||
|
MsgHdr: mDNS.MsgHdr{
|
||||||
|
Id: message.Id,
|
||||||
|
Rcode: mDNS.RcodeRefused,
|
||||||
|
Response: true,
|
||||||
|
},
|
||||||
|
Question: []mDNS.Question{message.Question[0]},
|
||||||
|
}, nil
|
||||||
|
case C.RuleActionRejectMethodDrop:
|
||||||
|
return nil, tun.ErrDrop
|
||||||
|
}
|
||||||
|
case *R.RuleActionPredefined:
|
||||||
|
return action.Response(message), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||||
|
if rule != nil && rule.WithAddressLimit() {
|
||||||
|
responseCheck = func(responseAddrs []netip.Addr) bool {
|
||||||
|
metadata.DestinationAddresses = responseAddrs
|
||||||
|
return rule.MatchAddressLimit(metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||||
|
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||||
|
}
|
||||||
|
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
|
||||||
|
var rejected bool
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrResponseRejectedCached) {
|
||||||
|
rejected = true
|
||||||
|
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
|
||||||
|
} else if errors.Is(err, ErrResponseRejected) {
|
||||||
|
rejected = true
|
||||||
|
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
||||||
|
} else if len(message.Question) > 0 {
|
||||||
|
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
||||||
|
} else {
|
||||||
|
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if responseCheck != nil && rejected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -332,7 +325,6 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
|||||||
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||||
var (
|
var (
|
||||||
responseAddrs []netip.Addr
|
responseAddrs []netip.Addr
|
||||||
cached bool
|
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
printResult := func() {
|
printResult := func() {
|
||||||
@@ -352,13 +344,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
|||||||
err = E.Cause(err, "lookup ", domain)
|
err = E.Cause(err, "lookup ", domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
|
|
||||||
if cached {
|
|
||||||
if len(responseAddrs) == 0 {
|
|
||||||
return nil, E.New("lookup ", domain, ": empty result (cached)")
|
|
||||||
}
|
|
||||||
return responseAddrs, nil
|
|
||||||
}
|
|
||||||
r.logger.DebugContext(ctx, "lookup domain ", domain)
|
r.logger.DebugContext(ctx, "lookup domain ", domain)
|
||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
metadata.Destination = M.Socksaddr{}
|
metadata.Destination = M.Socksaddr{}
|
||||||
@@ -391,16 +376,13 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
|||||||
if rule != nil {
|
if rule != nil {
|
||||||
switch action := rule.Action().(type) {
|
switch action := rule.Action().(type) {
|
||||||
case *R.RuleActionReject:
|
case *R.RuleActionReject:
|
||||||
switch action.Method {
|
return nil, &R.RejectedError{Cause: action.Error(ctx)}
|
||||||
case C.RuleActionRejectMethodDefault:
|
|
||||||
return nil, nil
|
|
||||||
case C.RuleActionRejectMethodDrop:
|
|
||||||
return nil, tun.ErrDrop
|
|
||||||
}
|
|
||||||
case *R.RuleActionPredefined:
|
case *R.RuleActionPredefined:
|
||||||
|
responseAddrs = nil
|
||||||
if action.Rcode != mDNS.RcodeSuccess {
|
if action.Rcode != mDNS.RcodeSuccess {
|
||||||
err = RcodeError(action.Rcode)
|
err = RcodeError(action.Rcode)
|
||||||
} else {
|
} else {
|
||||||
|
err = nil
|
||||||
for _, answer := range action.Answer {
|
for _, answer := range action.Answer {
|
||||||
switch record := answer.(type) {
|
switch record := answer.(type) {
|
||||||
case *mDNS.A:
|
case *mDNS.A:
|
||||||
|
|||||||
@@ -2,17 +2,18 @@ package dhcp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/dns"
|
"github.com/sagernet/sing-box/dns"
|
||||||
"github.com/sagernet/sing-box/dns/transport"
|
|
||||||
"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"
|
"github.com/sagernet/sing-tun"
|
||||||
@@ -29,6 +30,7 @@ import (
|
|||||||
|
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||||
@@ -45,9 +47,12 @@ type Transport struct {
|
|||||||
networkManager adapter.NetworkManager
|
networkManager adapter.NetworkManager
|
||||||
interfaceName string
|
interfaceName string
|
||||||
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
|
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
|
||||||
transports []adapter.DNSTransport
|
transportLock sync.RWMutex
|
||||||
updateAccess sync.Mutex
|
|
||||||
updatedAt time.Time
|
updatedAt time.Time
|
||||||
|
servers []M.Socksaddr
|
||||||
|
search []string
|
||||||
|
ndots int
|
||||||
|
attempts int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) {
|
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
@@ -62,27 +67,40 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
networkManager: service.FromContext[adapter.NetworkManager](ctx),
|
networkManager: service.FromContext[adapter.NetworkManager](ctx),
|
||||||
interfaceName: options.Interface,
|
interfaceName: options.Interface,
|
||||||
|
ndots: 1,
|
||||||
|
attempts: 2,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewRawTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) *Transport {
|
||||||
|
return &Transport{
|
||||||
|
TransportAdapter: transportAdapter,
|
||||||
|
ctx: ctx,
|
||||||
|
dialer: dialer,
|
||||||
|
logger: logger,
|
||||||
|
networkManager: service.FromContext[adapter.NetworkManager](ctx),
|
||||||
|
ndots: 1,
|
||||||
|
attempts: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||||
if stage != adapter.StartStateStart {
|
if stage != adapter.StartStateStart {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := t.fetchServers()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if t.interfaceName == "" {
|
if t.interfaceName == "" {
|
||||||
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
|
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
|
||||||
}
|
}
|
||||||
|
go func() {
|
||||||
|
_, err := t.Fetch()
|
||||||
|
if err != nil {
|
||||||
|
t.logger.Error(E.Cause(err, "fetch DNS servers"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Close() error {
|
func (t *Transport) Close() error {
|
||||||
for _, transport := range t.transports {
|
|
||||||
transport.Close()
|
|
||||||
}
|
|
||||||
if t.interfaceCallback != nil {
|
if t.interfaceCallback != nil {
|
||||||
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
|
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
|
||||||
}
|
}
|
||||||
@@ -90,23 +108,44 @@ func (t *Transport) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
err := t.fetchServers()
|
servers, err := t.Fetch()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(servers) == 0 {
|
||||||
if len(t.transports) == 0 {
|
|
||||||
return nil, E.New("dhcp: empty DNS servers from response")
|
return nil, E.New("dhcp: empty DNS servers from response")
|
||||||
}
|
}
|
||||||
|
return t.Exchange0(ctx, message, servers)
|
||||||
|
}
|
||||||
|
|
||||||
var response *mDNS.Msg
|
func (t *Transport) Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error) {
|
||||||
for _, transport := range t.transports {
|
question := message.Question[0]
|
||||||
response, err = transport.Exchange(ctx, message)
|
domain := dns.FqdnToDomain(question.Name)
|
||||||
if err == nil {
|
if len(servers) == 1 || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
|
||||||
return response, nil
|
return t.exchangeSingleRequest(ctx, servers, message, domain)
|
||||||
}
|
} else {
|
||||||
|
return t.exchangeParallel(ctx, servers, message, domain)
|
||||||
}
|
}
|
||||||
return nil, err
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Fetch() ([]M.Socksaddr, error) {
|
||||||
|
t.transportLock.RLock()
|
||||||
|
updatedAt := t.updatedAt
|
||||||
|
servers := t.servers
|
||||||
|
t.transportLock.RUnlock()
|
||||||
|
if time.Since(updatedAt) < C.DHCPTTL {
|
||||||
|
return servers, nil
|
||||||
|
}
|
||||||
|
t.transportLock.Lock()
|
||||||
|
defer t.transportLock.Unlock()
|
||||||
|
if time.Since(t.updatedAt) < C.DHCPTTL {
|
||||||
|
return t.servers, nil
|
||||||
|
}
|
||||||
|
err := t.updateServers()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return t.servers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) fetchInterface() (*control.Interface, error) {
|
func (t *Transport) fetchInterface() (*control.Interface, error) {
|
||||||
@@ -124,18 +163,6 @@ func (t *Transport) fetchInterface() (*control.Interface, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) fetchServers() error {
|
|
||||||
if time.Since(t.updatedAt) < C.DHCPTTL {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
t.updateAccess.Lock()
|
|
||||||
defer t.updateAccess.Unlock()
|
|
||||||
if time.Since(t.updatedAt) < C.DHCPTTL {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return t.updateServers()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) updateServers() error {
|
func (t *Transport) updateServers() error {
|
||||||
iface, err := t.fetchInterface()
|
iface, err := t.fetchInterface()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -148,7 +175,7 @@ func (t *Transport) updateServers() error {
|
|||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if len(t.transports) == 0 {
|
} else if len(t.servers) == 0 {
|
||||||
return E.New("dhcp: empty DNS servers response")
|
return E.New("dhcp: empty DNS servers response")
|
||||||
} else {
|
} else {
|
||||||
t.updatedAt = time.Now()
|
t.updatedAt = time.Now()
|
||||||
@@ -171,13 +198,27 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *control.Interface)
|
|||||||
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
||||||
listenAddr = "255.255.255.255:68"
|
listenAddr = "255.255.255.255:68"
|
||||||
}
|
}
|
||||||
packetConn, err := listener.ListenPacket(t.ctx, "udp4", listenAddr)
|
var (
|
||||||
|
packetConn net.PacketConn
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
packetConn, err = listener.ListenPacket(t.ctx, "udp4", listenAddr)
|
||||||
|
if err == nil || !errors.Is(err, syscall.EADDRINUSE) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer packetConn.Close()
|
defer packetConn.Close()
|
||||||
|
|
||||||
discovery, err := dhcpv4.NewDiscovery(iface.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer))
|
discovery, err := dhcpv4.NewDiscovery(iface.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(
|
||||||
|
dhcpv4.OptionDomainName,
|
||||||
|
dhcpv4.OptionDomainNameServer,
|
||||||
|
dhcpv4.OptionDNSDomainSearchList,
|
||||||
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -202,8 +243,12 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
|
|||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
buffer.Reset()
|
||||||
_, _, err := buffer.ReadPacketFrom(packetConn)
|
_, _, err := buffer.ReadPacketFrom(packetConn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, io.ErrShortBuffer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,31 +268,23 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
dns := dhcpPacket.DNS()
|
return t.recreateServers(iface, dhcpPacket)
|
||||||
if len(dns) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return t.recreateServers(iface, common.Map(dns, func(it net.IP) M.Socksaddr {
|
|
||||||
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []M.Socksaddr) error {
|
func (t *Transport) recreateServers(iface *control.Interface, dhcpPacket *dhcpv4.DHCPv4) error {
|
||||||
if len(serverAddrs) > 0 {
|
searchList := dhcpPacket.DomainSearch()
|
||||||
t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "]")
|
if searchList != nil && len(searchList.Labels) > 0 {
|
||||||
|
t.search = searchList.Labels
|
||||||
|
} else if dhcpPacket.DomainName() != "" {
|
||||||
|
t.search = []string{dhcpPacket.DomainName()}
|
||||||
}
|
}
|
||||||
serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{
|
serverAddrs := common.Map(dhcpPacket.DNS(), func(it net.IP) M.Socksaddr {
|
||||||
BindInterface: iface.Name,
|
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
|
||||||
UDPFragmentDefault: true,
|
})
|
||||||
}))
|
if len(serverAddrs) > 0 && !slices.Equal(t.servers, serverAddrs) {
|
||||||
var transports []adapter.DNSTransport
|
t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "], search: [", strings.Join(t.search, ","), "]")
|
||||||
for _, serverAddr := range serverAddrs {
|
|
||||||
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, serverAddr))
|
|
||||||
}
|
}
|
||||||
for _, transport := range t.transports {
|
t.servers = serverAddrs
|
||||||
transport.Close()
|
|
||||||
}
|
|
||||||
t.transports = transports
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
213
dns/transport/dhcp/dhcp_shared.go
Normal file
213
dns/transport/dhcp/dhcp_shared.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package dhcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Transport) exchangeSingleRequest(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||||
|
var lastErr error
|
||||||
|
for _, fqdn := range t.nameList(domain) {
|
||||||
|
response, err := t.tryOneName(ctx, servers, fqdn, message)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
return nil, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||||
|
returned := make(chan struct{})
|
||||||
|
defer close(returned)
|
||||||
|
type queryResult struct {
|
||||||
|
response *mDNS.Msg
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
results := make(chan queryResult)
|
||||||
|
startRacer := func(ctx context.Context, fqdn string) {
|
||||||
|
response, err := t.tryOneName(ctx, servers, fqdn, message)
|
||||||
|
if err == nil {
|
||||||
|
if response.Rcode != mDNS.RcodeSuccess {
|
||||||
|
err = dns.RcodeError(response.Rcode)
|
||||||
|
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||||
|
err = dns.RcodeSuccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case results <- queryResult{response, err}:
|
||||||
|
case <-returned:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queryCtx, queryCancel := context.WithCancel(ctx)
|
||||||
|
defer queryCancel()
|
||||||
|
var nameCount int
|
||||||
|
for _, fqdn := range t.nameList(domain) {
|
||||||
|
nameCount++
|
||||||
|
go startRacer(queryCtx, fqdn)
|
||||||
|
}
|
||||||
|
var errors []error
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case result := <-results:
|
||||||
|
if result.err == nil {
|
||||||
|
return result.response, nil
|
||||||
|
}
|
||||||
|
errors = append(errors, result.err)
|
||||||
|
if len(errors) == nameCount {
|
||||||
|
return nil, E.Errors(errors...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
sLen := len(servers)
|
||||||
|
var lastErr error
|
||||||
|
for i := 0; i < t.attempts; i++ {
|
||||||
|
for j := 0; j < sLen; j++ {
|
||||||
|
server := servers[j]
|
||||||
|
question := message.Question[0]
|
||||||
|
question.Name = fqdn
|
||||||
|
response, err := t.exchangeOne(ctx, server, question)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, E.Cause(lastErr, fqdn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question) (*mDNS.Msg, error) {
|
||||||
|
if server.Port == 0 {
|
||||||
|
server.Port = 53
|
||||||
|
}
|
||||||
|
request := &mDNS.Msg{
|
||||||
|
MsgHdr: mDNS.MsgHdr{
|
||||||
|
Id: uint16(rand.Uint32()),
|
||||||
|
RecursionDesired: true,
|
||||||
|
AuthenticatedData: true,
|
||||||
|
},
|
||||||
|
Question: []mDNS.Question{question},
|
||||||
|
Compress: true,
|
||||||
|
}
|
||||||
|
request.SetEdns0(buf.UDPBufferSize, false)
|
||||||
|
return t.exchangeUDP(ctx, server, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||||
|
conn.SetDeadline(deadline)
|
||||||
|
}
|
||||||
|
buffer := buf.Get(buf.UDPBufferSize)
|
||||||
|
defer buf.Put(buffer)
|
||||||
|
rawMessage, err := request.PackBuffer(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "pack request")
|
||||||
|
}
|
||||||
|
_, err = conn.Write(rawMessage)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, syscall.EMSGSIZE) {
|
||||||
|
return t.exchangeTCP(ctx, server, request)
|
||||||
|
}
|
||||||
|
return nil, E.Cause(err, "write request")
|
||||||
|
}
|
||||||
|
n, err := conn.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, syscall.EMSGSIZE) {
|
||||||
|
return t.exchangeTCP(ctx, server, request)
|
||||||
|
}
|
||||||
|
return nil, E.Cause(err, "read response")
|
||||||
|
}
|
||||||
|
var response mDNS.Msg
|
||||||
|
err = response.Unpack(buffer[:n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "unpack response")
|
||||||
|
}
|
||||||
|
if response.Truncated {
|
||||||
|
return t.exchangeTCP(ctx, server, request)
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||||
|
conn.SetDeadline(deadline)
|
||||||
|
}
|
||||||
|
err = transport.WriteMessage(conn, 0, request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return transport.ReadMessage(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) nameList(name string) []string {
|
||||||
|
l := len(name)
|
||||||
|
rooted := l > 0 && name[l-1] == '.'
|
||||||
|
if l > 254 || l == 254 && !rooted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if rooted {
|
||||||
|
if avoidDNS(name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{name}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNdots := strings.Count(name, ".") >= t.ndots
|
||||||
|
name += "."
|
||||||
|
// l++
|
||||||
|
|
||||||
|
names := make([]string, 0, 1+len(t.search))
|
||||||
|
if hasNdots && !avoidDNS(name) {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
for _, suffix := range t.search {
|
||||||
|
fqdn := name + suffix
|
||||||
|
if !avoidDNS(fqdn) && len(fqdn) <= 254 {
|
||||||
|
names = append(names, fqdn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasNdots && !avoidDNS(name) {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func avoidDNS(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if name[len(name)-1] == '.' {
|
||||||
|
name = name[:len(name)-1]
|
||||||
|
}
|
||||||
|
return strings.HasSuffix(name, ".onion")
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -26,7 +25,6 @@ import (
|
|||||||
"github.com/sagernet/sing/common/logger"
|
"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"
|
|
||||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
@@ -48,7 +46,7 @@ type HTTPSTransport struct {
|
|||||||
destination *url.URL
|
destination *url.URL
|
||||||
headers http.Header
|
headers http.Header
|
||||||
transportAccess sync.Mutex
|
transportAccess sync.Mutex
|
||||||
transport *http.Transport
|
transport *HTTPSTransportWrapper
|
||||||
transportResetAt time.Time
|
transportResetAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,11 +61,8 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if common.Error(tlsConfig.Config()) == nil && !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
if len(tlsConfig.NextProtos()) == 0 {
|
||||||
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), http2.NextProtoTLS))
|
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
|
||||||
}
|
|
||||||
if !common.Contains(tlsConfig.NextProtos(), "http/1.1") {
|
|
||||||
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), "http/1.1"))
|
|
||||||
}
|
}
|
||||||
headers := options.Headers.Build()
|
headers := options.Headers.Build()
|
||||||
host := headers.Get("Host")
|
host := headers.Get("Host")
|
||||||
@@ -125,37 +120,13 @@ func NewHTTPSRaw(
|
|||||||
serverAddr M.Socksaddr,
|
serverAddr M.Socksaddr,
|
||||||
tlsConfig tls.Config,
|
tlsConfig tls.Config,
|
||||||
) *HTTPSTransport {
|
) *HTTPSTransport {
|
||||||
var transport *http.Transport
|
|
||||||
if tlsConfig != nil {
|
|
||||||
transport = &http.Transport{
|
|
||||||
ForceAttemptHTTP2: true,
|
|
||||||
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
tcpConn, hErr := dialer.DialContext(ctx, network, serverAddr)
|
|
||||||
if hErr != nil {
|
|
||||||
return nil, hErr
|
|
||||||
}
|
|
||||||
tlsConn, hErr := aTLS.ClientHandshake(ctx, tcpConn, tlsConfig)
|
|
||||||
if hErr != nil {
|
|
||||||
tcpConn.Close()
|
|
||||||
return nil, hErr
|
|
||||||
}
|
|
||||||
return tlsConn, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
transport = &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return dialer.DialContext(ctx, network, serverAddr)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &HTTPSTransport{
|
return &HTTPSTransport{
|
||||||
TransportAdapter: adapter,
|
TransportAdapter: adapter,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
destination: destination,
|
destination: destination,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
transport: transport,
|
transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +149,7 @@ func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
|||||||
startAt := time.Now()
|
startAt := time.Now()
|
||||||
response, err := t.exchange(ctx, message)
|
response, err := t.exchange(ctx, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
t.transportAccess.Lock()
|
t.transportAccess.Lock()
|
||||||
defer t.transportAccess.Unlock()
|
defer t.transportAccess.Unlock()
|
||||||
if t.transportResetAt.After(startAt) {
|
if t.transportResetAt.After(startAt) {
|
||||||
|
|||||||
80
dns/transport/https_transport.go
Normal file
80
dns/transport/https_transport.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package transport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errFallback = E.New("fallback to HTTP/1.1")
|
||||||
|
|
||||||
|
type HTTPSTransportWrapper struct {
|
||||||
|
http2Transport *http2.Transport
|
||||||
|
httpTransport *http.Transport
|
||||||
|
fallback *atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper {
|
||||||
|
var fallback atomic.Bool
|
||||||
|
return &HTTPSTransportWrapper{
|
||||||
|
http2Transport: &http2.Transport{
|
||||||
|
DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) {
|
||||||
|
tlsConn, err := dialer.DialTLSContext(ctx, serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
state := tlsConn.ConnectionState()
|
||||||
|
if state.NegotiatedProtocol == http2.NextProtoTLS {
|
||||||
|
return tlsConn, nil
|
||||||
|
}
|
||||||
|
tlsConn.Close()
|
||||||
|
fallback.Store(true)
|
||||||
|
return nil, errFallback
|
||||||
|
},
|
||||||
|
},
|
||||||
|
httpTransport: &http.Transport{
|
||||||
|
DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
return dialer.DialTLSContext(ctx, serverAddr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fallback: &fallback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
if h.fallback.Load() {
|
||||||
|
return h.httpTransport.RoundTrip(request)
|
||||||
|
} else {
|
||||||
|
response, err := h.http2Transport.RoundTrip(request)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errFallback) {
|
||||||
|
return h.httpTransport.RoundTrip(request)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
|
||||||
|
h.http2Transport.CloseIdleConnections()
|
||||||
|
h.httpTransport.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
|
||||||
|
return &HTTPSTransportWrapper{
|
||||||
|
httpTransport: h.httpTransport,
|
||||||
|
http2Transport: &http2.Transport{
|
||||||
|
DialTLSContext: h.http2Transport.DialTLSContext,
|
||||||
|
},
|
||||||
|
fallback: h.fallback,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,15 @@ package local
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/dns"
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport"
|
||||||
"github.com/sagernet/sing-box/dns/transport/hosts"
|
"github.com/sagernet/sing-box/dns/transport/hosts"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
@@ -51,18 +54,17 @@ func (t *Transport) Close() error {
|
|||||||
|
|
||||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
question := message.Question[0]
|
question := message.Question[0]
|
||||||
domain := dns.FqdnToDomain(question.Name)
|
|
||||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||||
addresses := t.hosts.Lookup(domain)
|
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
|
||||||
if len(addresses) > 0 {
|
if len(addresses) > 0 {
|
||||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
systemConfig := getSystemDNSConfig(t.ctx)
|
systemConfig := getSystemDNSConfig(t.ctx)
|
||||||
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
|
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
|
||||||
return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
|
return t.exchangeSingleRequest(ctx, systemConfig, message, question.Name)
|
||||||
} else {
|
} else {
|
||||||
return t.exchangeParallel(ctx, systemConfig, message, domain)
|
return t.exchangeParallel(ctx, systemConfig, message, question.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +95,7 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
|
|||||||
if response.Rcode != mDNS.RcodeSuccess {
|
if response.Rcode != mDNS.RcodeSuccess {
|
||||||
err = dns.RcodeError(response.Rcode)
|
err = dns.RcodeError(response.Rcode)
|
||||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||||
err = E.New(fqdn, ": empty result")
|
err = dns.RcodeSuccess
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
@@ -149,12 +151,6 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
|
|||||||
if server.Port == 0 {
|
if server.Port == 0 {
|
||||||
server.Port = 53
|
server.Port = 53
|
||||||
}
|
}
|
||||||
var networks []string
|
|
||||||
if useTCP {
|
|
||||||
networks = []string{N.NetworkTCP}
|
|
||||||
} else {
|
|
||||||
networks = []string{N.NetworkUDP, N.NetworkTCP}
|
|
||||||
}
|
|
||||||
request := &mDNS.Msg{
|
request := &mDNS.Msg{
|
||||||
MsgHdr: mDNS.MsgHdr{
|
MsgHdr: mDNS.MsgHdr{
|
||||||
Id: uint16(rand.Uint32()),
|
Id: uint16(rand.Uint32()),
|
||||||
@@ -164,41 +160,74 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
|
|||||||
Question: []mDNS.Question{question},
|
Question: []mDNS.Question{question},
|
||||||
Compress: true,
|
Compress: true,
|
||||||
}
|
}
|
||||||
request.SetEdns0(maxDNSPacketSize, false)
|
request.SetEdns0(buf.UDPBufferSize, false)
|
||||||
|
if !useTCP {
|
||||||
|
return t.exchangeUDP(ctx, server, request, timeout)
|
||||||
|
} else {
|
||||||
|
return t.exchangeTCP(ctx, server, request, timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
|
||||||
|
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||||
|
newDeadline := time.Now().Add(timeout)
|
||||||
|
if deadline.After(newDeadline) {
|
||||||
|
deadline = newDeadline
|
||||||
|
}
|
||||||
|
conn.SetDeadline(deadline)
|
||||||
|
}
|
||||||
buffer := buf.Get(buf.UDPBufferSize)
|
buffer := buf.Get(buf.UDPBufferSize)
|
||||||
defer buf.Put(buffer)
|
defer buf.Put(buffer)
|
||||||
for _, network := range networks {
|
rawMessage, err := request.PackBuffer(buffer)
|
||||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
|
if err != nil {
|
||||||
defer cancel()
|
return nil, E.Cause(err, "pack request")
|
||||||
conn, err := t.dialer.DialContext(ctx, network, server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
|
||||||
conn.SetDeadline(deadline)
|
|
||||||
}
|
|
||||||
rawMessage, err := request.PackBuffer(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "pack request")
|
|
||||||
}
|
|
||||||
_, err = conn.Write(rawMessage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "write request")
|
|
||||||
}
|
|
||||||
n, err := conn.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read response")
|
|
||||||
}
|
|
||||||
var response mDNS.Msg
|
|
||||||
err = response.Unpack(buffer[:n])
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "unpack response")
|
|
||||||
}
|
|
||||||
if response.Truncated && network == N.NetworkUDP {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return &response, nil
|
|
||||||
}
|
}
|
||||||
panic("unexpected")
|
_, err = conn.Write(rawMessage)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, syscall.EMSGSIZE) {
|
||||||
|
return t.exchangeTCP(ctx, server, request, timeout)
|
||||||
|
}
|
||||||
|
return nil, E.Cause(err, "write request")
|
||||||
|
}
|
||||||
|
n, err := conn.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, syscall.EMSGSIZE) {
|
||||||
|
return t.exchangeTCP(ctx, server, request, timeout)
|
||||||
|
}
|
||||||
|
return nil, E.Cause(err, "read response")
|
||||||
|
}
|
||||||
|
var response mDNS.Msg
|
||||||
|
err = response.Unpack(buffer[:n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "unpack response")
|
||||||
|
}
|
||||||
|
if response.Truncated {
|
||||||
|
return t.exchangeTCP(ctx, server, request, timeout)
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
|
||||||
|
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||||
|
newDeadline := time.Now().Add(timeout)
|
||||||
|
if deadline.After(newDeadline) {
|
||||||
|
deadline = newDeadline
|
||||||
|
}
|
||||||
|
conn.SetDeadline(deadline)
|
||||||
|
}
|
||||||
|
err = transport.WriteMessage(conn, 0, request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return transport.ReadMessage(conn)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
|
|||||||
return f.DNSTransport.Exchange(ctx, message)
|
return f.DNSTransport.Exchange(ctx, message)
|
||||||
}
|
}
|
||||||
question := message.Question[0]
|
question := message.Question[0]
|
||||||
domain := dns.FqdnToDomain(question.Name)
|
|
||||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||||
var network string
|
var network string
|
||||||
if question.Qtype == mDNS.TypeA {
|
if question.Qtype == mDNS.TypeA {
|
||||||
@@ -75,7 +74,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
|
|||||||
} else {
|
} else {
|
||||||
network = "ip6"
|
network = "ip6"
|
||||||
}
|
}
|
||||||
addresses, err := f.resolver.LookupNetIP(ctx, network, domain)
|
addresses, err := f.resolver.LookupNetIP(ctx, network, question.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var dnsError *net.DNSError
|
var dnsError *net.DNSError
|
||||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||||
@@ -85,7 +84,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
|
|||||||
}
|
}
|
||||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||||
} else if question.Qtype == mDNS.TypeNS {
|
} else if question.Qtype == mDNS.TypeNS {
|
||||||
records, err := f.resolver.LookupNS(ctx, domain)
|
records, err := f.resolver.LookupNS(ctx, question.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var dnsError *net.DNSError
|
var dnsError *net.DNSError
|
||||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||||
@@ -114,7 +113,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
|
|||||||
}
|
}
|
||||||
return response, nil
|
return response, nil
|
||||||
} else if question.Qtype == mDNS.TypeCNAME {
|
} else if question.Qtype == mDNS.TypeCNAME {
|
||||||
cname, err := f.resolver.LookupCNAME(ctx, domain)
|
cname, err := f.resolver.LookupCNAME(ctx, question.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var dnsError *net.DNSError
|
var dnsError *net.DNSError
|
||||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||||
@@ -142,7 +141,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
} else if question.Qtype == mDNS.TypeTXT {
|
} else if question.Qtype == mDNS.TypeTXT {
|
||||||
records, err := f.resolver.LookupTXT(ctx, domain)
|
records, err := f.resolver.LookupTXT(ctx, question.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var dnsError *net.DNSError
|
var dnsError *net.DNSError
|
||||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||||
@@ -170,7 +169,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
} else if question.Qtype == mDNS.TypeMX {
|
} else if question.Qtype == mDNS.TypeMX {
|
||||||
records, err := f.resolver.LookupMX(ctx, domain)
|
records, err := f.resolver.LookupMX(ctx, question.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var dnsError *net.DNSError
|
var dnsError *net.DNSError
|
||||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// net.maxDNSPacketSize
|
|
||||||
maxDNSPacketSize = 1232
|
|
||||||
)
|
|
||||||
|
|
||||||
type resolverConfig struct {
|
type resolverConfig struct {
|
||||||
initOnce sync.Once
|
initOnce sync.Once
|
||||||
ch chan struct{}
|
ch chan struct{}
|
||||||
@@ -104,7 +99,7 @@ func (c *dnsConfig) serverOffset() uint32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conf *dnsConfig) nameList(name string) []string {
|
func (c *dnsConfig) nameList(name string) []string {
|
||||||
l := len(name)
|
l := len(name)
|
||||||
rooted := l > 0 && name[l-1] == '.'
|
rooted := l > 0 && name[l-1] == '.'
|
||||||
if l > 254 || l == 254 && !rooted {
|
if l > 254 || l == 254 && !rooted {
|
||||||
@@ -118,15 +113,15 @@ func (conf *dnsConfig) nameList(name string) []string {
|
|||||||
return []string{name}
|
return []string{name}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasNdots := strings.Count(name, ".") >= conf.ndots
|
hasNdots := strings.Count(name, ".") >= c.ndots
|
||||||
name += "."
|
name += "."
|
||||||
// l++
|
// l++
|
||||||
|
|
||||||
names := make([]string, 0, 1+len(conf.search))
|
names := make([]string, 0, 1+len(c.search))
|
||||||
if hasNdots && !avoidDNS(name) {
|
if hasNdots && !avoidDNS(name) {
|
||||||
names = append(names, name)
|
names = append(names, name)
|
||||||
}
|
}
|
||||||
for _, suffix := range conf.search {
|
for _, suffix := range c.search {
|
||||||
fqdn := name + suffix
|
fqdn := name + suffix
|
||||||
if !avoidDNS(fqdn) && len(fqdn) <= 254 {
|
if !avoidDNS(fqdn) && len(fqdn) <= 254 {
|
||||||
names = append(names, fqdn)
|
names = append(names, fqdn)
|
||||||
|
|||||||
@@ -2,6 +2,112 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### 1.12.22
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.21
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.20
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.19
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.18
|
||||||
|
|
||||||
|
* Add fallback routing rule for `auto_redirect` **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
|
||||||
|
ensuring traffic is routed to the sing-box table when no route is found in system tables.
|
||||||
|
|
||||||
|
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
|
||||||
|
|
||||||
|
#### 1.12.17
|
||||||
|
|
||||||
|
* Update uTLS to v1.8.2 **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
This update fixes missing padding extension for Chrome 120+ fingerprints.
|
||||||
|
|
||||||
|
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
|
||||||
|
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
|
||||||
|
use NaiveProxy instead for TLS fingerprint resistance.
|
||||||
|
|
||||||
|
#### 1.12.16
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.15
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.14
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.13
|
||||||
|
|
||||||
|
* Fix naive inbound
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
__Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:
|
||||||
|
because system extensions require signatures to function, we have had to temporarily halt its release.__
|
||||||
|
|
||||||
|
__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then,
|
||||||
|
only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__
|
||||||
|
|
||||||
|
#### 1.12.12
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.11
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.10
|
||||||
|
|
||||||
|
* Update uTLS to v1.8.1 **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected,
|
||||||
|
see https://github.com/refraction-networking/utls/pull/375.
|
||||||
|
|
||||||
|
#### 1.12.9
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.8
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.5
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.4
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.3
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.2
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.12.1
|
#### 1.12.1
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
@@ -72,7 +178,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
|
|||||||
|
|
||||||
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
|
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
|
||||||
|
|
||||||
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
|
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches
|
||||||
|
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
|
||||||
|
|
||||||
**7**:
|
**7**:
|
||||||
|
|
||||||
@@ -134,7 +241,8 @@ See [Tun](/configuration/inbound/tun/#loopback_address).
|
|||||||
|
|
||||||
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
|
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
|
||||||
|
|
||||||
The following data was tested using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro.
|
The following data was tested
|
||||||
|
using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro.
|
||||||
|
|
||||||
| Version | Stack | MTU | Upload | Download |
|
| Version | Stack | MTU | Upload | Download |
|
||||||
|-------------|--------|-------|--------|----------|
|
|-------------|--------|-------|--------|----------|
|
||||||
@@ -153,8 +261,8 @@ The following data was tested using [tun_bench](https://github.com/SagerNet/sing
|
|||||||
|
|
||||||
**18**:
|
**18**:
|
||||||
|
|
||||||
We continue to experience issues updating our sing-box apps on the App Store and Play Store.
|
We continue to experience issues updating our sing-box apps on the App Store and Play Store.
|
||||||
Until we rewrite and resubmit the apps, they are considered irrecoverable.
|
Until we rewrite and resubmit the apps, they are considered irrecoverable.
|
||||||
Therefore, after this release, we will not be repeating this notice unless there is new information.
|
Therefore, after this release, we will not be repeating this notice unless there is new information.
|
||||||
|
|
||||||
### 1.11.15
|
### 1.11.15
|
||||||
@@ -435,7 +543,8 @@ See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/conf
|
|||||||
|
|
||||||
**2**:
|
**2**:
|
||||||
|
|
||||||
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions, see [Route Action](/configuration/route/rule_action).
|
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions,
|
||||||
|
see [Route Action](/configuration/route/rule_action).
|
||||||
|
|
||||||
**3**:
|
**3**:
|
||||||
|
|
||||||
@@ -466,7 +575,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
|
|||||||
|
|
||||||
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
|
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
|
||||||
|
|
||||||
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
|
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches
|
||||||
|
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
|
||||||
|
|
||||||
### 1.11.3
|
### 1.11.3
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
|
|||||||
|
|
||||||
!!! failure ""
|
!!! failure ""
|
||||||
|
|
||||||
We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected).
|
Due to non-technical reasons, we are temporarily unable to update the sing-box app on the App Store and release the standalone version of the macOS client (TestFlight users are not affected)
|
||||||
|
|
||||||
## :material-graph: Requirements
|
## :material-graph: Requirements
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
|
|||||||
|
|
||||||
## :material-download: Download
|
## :material-download: Download
|
||||||
|
|
||||||
* [App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)
|
* ~~[App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)~~
|
||||||
* TestFlight (Beta)
|
* TestFlight (Beta)
|
||||||
|
|
||||||
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
|
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
|
||||||
@@ -26,15 +26,15 @@ TestFlight quota is only available to [sponsors](https://github.com/sponsors/nek
|
|||||||
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
|
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
|
||||||
or sending us your Apple ID [via email](mailto:contact@sagernet.org).
|
or sending us your Apple ID [via email](mailto:contact@sagernet.org).
|
||||||
|
|
||||||
## :material-file-download: Download (macOS standalone version)
|
## ~~:material-file-download: Download (macOS standalone version)~~
|
||||||
|
|
||||||
* [Homebrew Cask](https://formulae.brew.sh/cask/sfm)
|
* ~~[Homebrew Cask](https://formulae.brew.sh/cask/sfm)~~
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install sfm
|
# brew install sfm
|
||||||
```
|
```
|
||||||
|
|
||||||
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases)
|
* ~~[GitHub Releases](https://github.com/SagerNet/sing-box/releases)~~
|
||||||
|
|
||||||
## :material-source-repository: Source code
|
## :material-source-repository: Source code
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ WireGuard MTU。
|
|||||||
|
|
||||||
==必填==
|
==必填==
|
||||||
|
|
||||||
接口的 IPv4/IPv6 地址或地址段的列表您。
|
接口的 IPv4/IPv6 地址或地址段的列表。
|
||||||
|
|
||||||
要分配给接口的 IP(v4 或 v6)地址段列表。
|
要分配给接口的 IP(v4 或 v6)地址段列表。
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
"method": "2022-blake3-aes-128-gcm",
|
"method": "2022-blake3-aes-128-gcm",
|
||||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||||
|
"managed": false,
|
||||||
"multiplex": {}
|
"multiplex": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -86,6 +87,10 @@ Both if empty.
|
|||||||
| 2022 methods | `sing-box generate rand --base64 <Key Length>` |
|
| 2022 methods | `sing-box generate rand --base64 <Key Length>` |
|
||||||
| other methods | any string |
|
| other methods | any string |
|
||||||
|
|
||||||
|
#### managed
|
||||||
|
|
||||||
|
Defaults to `false`. Enable this when the inbound is managed by the [SSM API](/configuration/service/ssm-api) for dynamic user.
|
||||||
|
|
||||||
#### multiplex
|
#### multiplex
|
||||||
|
|
||||||
See [Multiplex](/configuration/shared/multiplex#inbound) for details.
|
See [Multiplex](/configuration/shared/multiplex#inbound) for details.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
"method": "2022-blake3-aes-128-gcm",
|
"method": "2022-blake3-aes-128-gcm",
|
||||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||||
|
"managed": false,
|
||||||
"multiplex": {}
|
"multiplex": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -86,6 +87,10 @@ See [Listen Fields](/configuration/shared/listen/) for details.
|
|||||||
| 2022 methods | `sing-box generate rand --base64 <密钥长度>` |
|
| 2022 methods | `sing-box generate rand --base64 <密钥长度>` |
|
||||||
| other methods | 任意字符串 |
|
| other methods | 任意字符串 |
|
||||||
|
|
||||||
|
#### managed
|
||||||
|
|
||||||
|
默认为 `false`。当该入站需要由 [SSM API](/zh/configuration/service/ssm-api) 管理用户时必须启用此字段。
|
||||||
|
|
||||||
#### multiplex
|
#### multiplex
|
||||||
|
|
||||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.12.18"
|
||||||
|
|
||||||
|
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.0"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-plus: [loopback_address](#loopback_address)
|
:material-plus: [loopback_address](#loopback_address)
|
||||||
@@ -63,6 +67,7 @@ icon: material/new-box
|
|||||||
"auto_redirect": true,
|
"auto_redirect": true,
|
||||||
"auto_redirect_input_mark": "0x2023",
|
"auto_redirect_input_mark": "0x2023",
|
||||||
"auto_redirect_output_mark": "0x2024",
|
"auto_redirect_output_mark": "0x2024",
|
||||||
|
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
||||||
"loopback_address": [
|
"loopback_address": [
|
||||||
"10.7.0.1"
|
"10.7.0.1"
|
||||||
],
|
],
|
||||||
@@ -278,6 +283,17 @@ Connection output mark used by `auto_redirect`.
|
|||||||
|
|
||||||
`0x2024` is used by default.
|
`0x2024` is used by default.
|
||||||
|
|
||||||
|
#### auto_redirect_iproute2_fallback_rule_index
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.12.18"
|
||||||
|
|
||||||
|
Linux iproute2 fallback rule index generated by `auto_redirect`.
|
||||||
|
|
||||||
|
This rule is checked after system default rules (32766: main, 32767: default),
|
||||||
|
routing traffic to the sing-box table only when no route is found in system tables.
|
||||||
|
|
||||||
|
`32768` is used by default.
|
||||||
|
|
||||||
#### loopback_address
|
#### loopback_address
|
||||||
|
|
||||||
!!! question "Since sing-box 1.12.0"
|
!!! question "Since sing-box 1.12.0"
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.12.18 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
||||||
|
|
||||||
!!! quote "sing-box 1.12.0 中的更改"
|
!!! quote "sing-box 1.12.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [loopback_address](#loopback_address)
|
:material-plus: [loopback_address](#loopback_address)
|
||||||
@@ -63,6 +67,7 @@ icon: material/new-box
|
|||||||
"auto_redirect": true,
|
"auto_redirect": true,
|
||||||
"auto_redirect_input_mark": "0x2023",
|
"auto_redirect_input_mark": "0x2023",
|
||||||
"auto_redirect_output_mark": "0x2024",
|
"auto_redirect_output_mark": "0x2024",
|
||||||
|
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
||||||
"loopback_address": [
|
"loopback_address": [
|
||||||
"10.7.0.1"
|
"10.7.0.1"
|
||||||
],
|
],
|
||||||
@@ -277,6 +282,17 @@ tun 接口的 IPv6 前缀。
|
|||||||
|
|
||||||
默认使用 `0x2024`。
|
默认使用 `0x2024`。
|
||||||
|
|
||||||
|
#### auto_redirect_iproute2_fallback_rule_index
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.12.18 起"
|
||||||
|
|
||||||
|
`auto_redirect` 生成的 iproute2 回退规则索引。
|
||||||
|
|
||||||
|
此规则在系统默认规则(32766: main,32767: default)之后检查,
|
||||||
|
仅当系统路由表中未找到路由时才将流量路由到 sing-box 路由表。
|
||||||
|
|
||||||
|
默认使用 `32768`。
|
||||||
|
|
||||||
#### loopback_address
|
#### loopback_address
|
||||||
|
|
||||||
!!! question "自 sing-box 1.12.0 起"
|
!!! question "自 sing-box 1.12.0 起"
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ icon: material/delete-clock
|
|||||||
|
|
||||||
==必填==
|
==必填==
|
||||||
|
|
||||||
接口的 IPv4/IPv6 地址或地址段的列表您。
|
接口的 IPv4/IPv6 地址或地址段的列表。
|
||||||
|
|
||||||
要分配给接口的 IP(v4 或 v6)地址段列表。
|
要分配给接口的 IP(v4 或 v6)地址段列表。
|
||||||
|
|
||||||
|
|||||||
@@ -230,9 +230,18 @@ The path to the server private key, in PEM format.
|
|||||||
|
|
||||||
==Client only==
|
==Client only==
|
||||||
|
|
||||||
!!! failure ""
|
!!! failure "Not Recommended"
|
||||||
|
|
||||||
There is no evidence that GFW detects and blocks servers based on TLS client fingerprinting, and using an imperfect emulation that has not been security reviewed could pose security risks.
|
uTLS has had repeated fingerprinting vulnerabilities discovered by researchers.
|
||||||
|
|
||||||
|
uTLS is a Go library that attempts to imitate browser TLS fingerprints by copying
|
||||||
|
ClientHello structure. However, browsers use completely different TLS stacks
|
||||||
|
(Chrome uses BoringSSL, Firefox uses NSS) with distinct implementation behaviors
|
||||||
|
that cannot be replicated by simply copying the handshake format, making detection possible.
|
||||||
|
Additionally, the library lacks active maintenance and has poor code quality,
|
||||||
|
making it unsuitable for censorship circumvention.
|
||||||
|
|
||||||
|
For TLS fingerprint resistance, use [NaiveProxy](/configuration/inbound/naive/) instead.
|
||||||
|
|
||||||
uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance.
|
uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance.
|
||||||
|
|
||||||
|
|||||||
@@ -220,9 +220,16 @@ TLS 版本值:
|
|||||||
|
|
||||||
==仅客户端==
|
==仅客户端==
|
||||||
|
|
||||||
!!! failure ""
|
!!! failure "不推荐"
|
||||||
|
|
||||||
没有证据表明 GFW 根据 TLS 客户端指纹检测并阻止服务器,并且,使用一个未经安全审查的不完美模拟可能带来安全隐患。
|
uTLS 已被研究人员多次发现其指纹可被识别的漏洞。
|
||||||
|
|
||||||
|
uTLS 是一个试图通过复制 ClientHello 结构来模仿浏览器 TLS 指纹的 Go 库。
|
||||||
|
然而,浏览器使用完全不同的 TLS 实现(Chrome 使用 BoringSSL,Firefox 使用 NSS),
|
||||||
|
其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。
|
||||||
|
此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。
|
||||||
|
|
||||||
|
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/configuration/inbound/naive/)。
|
||||||
|
|
||||||
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。
|
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ icon: material/horse
|
|||||||
|
|
||||||
# Trojan
|
# Trojan
|
||||||
|
|
||||||
Torjan is the most commonly used TLS proxy made in China. It can be used in various combinations,
|
Trojan is the most commonly used TLS proxy made in China. It can be used in various combinations.
|
||||||
but only the combination of uTLS and multiplexing is recommended.
|
|
||||||
|
|
||||||
| Protocol and implementation combination | Specification | Resists passive detection | Resists active probes |
|
| Protocol and implementation combination | Specification | Resists passive detection | Resists active probes |
|
||||||
|-----------------------------------------|----------------------------------------------------------------------|---------------------------|-----------------------|
|
|-----------------------------------------|----------------------------------------------------------------------|---------------------------|-----------------------|
|
||||||
@@ -140,11 +139,7 @@ but only the combination of uTLS and multiplexing is recommended.
|
|||||||
"password": "password",
|
"password": "password",
|
||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.org",
|
"server_name": "example.org"
|
||||||
"utls": {
|
|
||||||
"enabled": true,
|
|
||||||
"fingerprint": "firefox"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"multiplex": {
|
"multiplex": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
@@ -171,11 +166,7 @@ but only the combination of uTLS and multiplexing is recommended.
|
|||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.org",
|
"server_name": "example.org",
|
||||||
"certificate_path": "/path/to/certificate.pem",
|
"certificate_path": "/path/to/certificate.pem"
|
||||||
"utls": {
|
|
||||||
"enabled": true,
|
|
||||||
"fingerprint": "firefox"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"multiplex": {
|
"multiplex": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
@@ -198,11 +189,7 @@ but only the combination of uTLS and multiplexing is recommended.
|
|||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.org",
|
"server_name": "example.org",
|
||||||
"insecure": true,
|
"insecure": true
|
||||||
"utls": {
|
|
||||||
"enabled": true,
|
|
||||||
"fingerprint": "firefox"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"multiplex": {
|
"multiplex": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ flowchart TB
|
|||||||
"inbounds": [
|
"inbounds": [
|
||||||
{
|
{
|
||||||
"type": "tun",
|
"type": "tun",
|
||||||
"inet4_address": "172.19.0.1/30",
|
"address": ["172.19.0.1/30"],
|
||||||
"auto_route": true,
|
"auto_route": true,
|
||||||
// "auto_redirect": true, // On linux
|
// "auto_redirect": true, // On linux
|
||||||
"strict_route": true
|
"strict_route": true
|
||||||
@@ -162,8 +162,7 @@ flowchart TB
|
|||||||
"inbounds": [
|
"inbounds": [
|
||||||
{
|
{
|
||||||
"type": "tun",
|
"type": "tun",
|
||||||
"inet4_address": "172.19.0.1/30",
|
"address": ["172.19.0.1/30", "fdfe:dcba:9876::1/126"],
|
||||||
"inet6_address": "fdfe:dcba:9876::1/126",
|
|
||||||
"auto_route": true,
|
"auto_route": true,
|
||||||
// "auto_redirect": true, // On linux
|
// "auto_redirect": true, // On linux
|
||||||
"strict_route": true
|
"strict_route": true
|
||||||
@@ -233,8 +232,7 @@ flowchart TB
|
|||||||
"inbounds": [
|
"inbounds": [
|
||||||
{
|
{
|
||||||
"type": "tun",
|
"type": "tun",
|
||||||
"inet4_address": "172.19.0.1/30",
|
"address": ["172.19.0.1/30","fdfe:dcba:9876::1/126"],
|
||||||
"inet6_address": "fdfe:dcba:9876::1/126",
|
|
||||||
"auto_route": true,
|
"auto_route": true,
|
||||||
// "auto_redirect": true, // On linux
|
// "auto_redirect": true, // On linux
|
||||||
"strict_route": true
|
"strict_route": true
|
||||||
|
|||||||
@@ -11,16 +11,22 @@ the project maintainer via [GitHub Sponsors](https://github.com/sponsors/nekohas
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Special Sponsors
|
## Commercial Sponsors
|
||||||
|
|
||||||
**Viral Tech, Inc.**
|
> [Warp](https://go.warp.dev/sing-box), Built for coding with multiple AI agents.
|
||||||
|
|
||||||
|
[](https://go.warp.dev/sing-box)
|
||||||
|
|
||||||
|
## Special Sponsors
|
||||||
|
|
||||||
|
> Viral Tech, Inc.
|
||||||
|
|
||||||
Helping us re-list sing-box apps on the Apple Store.
|
Helping us re-list sing-box apps on the Apple Store.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[](https://www.jetbrains.com)
|
> [JetBrains](https://www.jetbrains.com)
|
||||||
|
|
||||||
Free license for the amazing IDEs.
|
Free license for the amazing IDEs.
|
||||||
|
|
||||||
---
|
[](https://www.jetbrains.com)
|
||||||
|
|||||||
@@ -30,10 +30,23 @@
|
|||||||
"jc": 120,
|
"jc": 120,
|
||||||
"jmin": 23,
|
"jmin": 23,
|
||||||
"jmax": 911,
|
"jmax": 911,
|
||||||
|
"s1": 1,
|
||||||
|
"s2": 2,
|
||||||
|
"s3": 3,
|
||||||
|
"s4": 4,
|
||||||
"h1": 1,
|
"h1": 1,
|
||||||
"h2": 2,
|
"h2": 2,
|
||||||
"h3": 3,
|
"h3": 3,
|
||||||
"h4": 4
|
"h4": 4,
|
||||||
|
"i1": "<b 0xc70000000108...",
|
||||||
|
"i2": "<b 0xc70000000108...",
|
||||||
|
"i3": "<b 0xc70000000108...",
|
||||||
|
"i4": "<b 0xc70000000108...",
|
||||||
|
"i5": "<b 0xc70000000108...",
|
||||||
|
"j1": "<b 0xc70000000108...",
|
||||||
|
"j2": "<b 0xc70000000108...",
|
||||||
|
"j3": "<b 0xc70000000108...",
|
||||||
|
"itime": 50,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"detour": "direct",
|
"detour": "direct",
|
||||||
// for getting existing WARP device profile
|
// For getting existing WARP device profile, else sing-box will create new profile
|
||||||
"id": "",
|
"id": "",
|
||||||
"private_key": "",
|
"private_key": "",
|
||||||
"auth_token": ""
|
"auth_token": ""
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"experimental": {
|
"experimental": {
|
||||||
"cache_file": {
|
"cache_file": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"store_warp_config": true
|
"store_warp_config": true // For saving WARP device profiles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
52
examples/wireguard/client.json
Normal file
52
examples/wireguard/client.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "wireguard",
|
||||||
|
"tag": "wireguard-out",
|
||||||
|
"mtu": 1408,
|
||||||
|
"address": null,
|
||||||
|
"private_key": "",
|
||||||
|
"listen_port": 10000,
|
||||||
|
"peers": [
|
||||||
|
{
|
||||||
|
"address": "example.com",
|
||||||
|
"port": 10001,
|
||||||
|
"reserved": "AAAA"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"udp_timeout": "5m0s",
|
||||||
|
// Extended options
|
||||||
|
"preallocated_buffers_per_pool": 256, // Set limit for preallocated buffers (can be useful for devices with low RAM)
|
||||||
|
"disable_pauses": true, // Disable pauses when android device in sleep mode
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "wireguard-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"server": "example.com",
|
"server": "example.com",
|
||||||
"server_port": 443,
|
"server_port": 443,
|
||||||
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668",
|
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668",
|
||||||
"tls": {
|
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.com",
|
"server_name": "example.com",
|
||||||
"alpn": "h2" // h3 for QUIC
|
"alpn": "h2" // h3 for QUIC
|
||||||
@@ -39,34 +39,65 @@
|
|||||||
"host": "example.com",
|
"host": "example.com",
|
||||||
"path": "/xhttp",
|
"path": "/xhttp",
|
||||||
"domain_strategy": "prefer_ipv4",
|
"domain_strategy": "prefer_ipv4",
|
||||||
"xmux": {
|
"x_padding_bytes": "100-1000",
|
||||||
"max_concurrency": "0-1",
|
"no_grpc_header": false, // stream-up/one, client only
|
||||||
"max_connections": "0-1",
|
"sc_max_each_post_bytes": 1000000, // packet-up only
|
||||||
"c_max_reuse_times": "0-1",
|
"sc_min_posts_interval_ms": 30, // packet-up, client only
|
||||||
"h_max_request_times": "0-1",
|
"xmux": { // h2/h3 mainly, client only
|
||||||
"h_max_reusable_secs": "0-1",
|
"max_concurrency": "16-32",
|
||||||
"h_keep_alive_period": 60
|
"max_connections": 0,
|
||||||
|
"c_max_reuse_times": 0,
|
||||||
|
"h_max_request_times": "600-900",
|
||||||
|
"h_max_reusable_secs": "1800-3000",
|
||||||
|
"h_keep_alive_period": 0
|
||||||
},
|
},
|
||||||
|
"x_padding_obfs_mode": false,
|
||||||
|
"x_padding_key": "",
|
||||||
|
"x_padding_header": "",
|
||||||
|
"x_padding_placement": "",
|
||||||
|
"x_padding_method": "",
|
||||||
|
"uplink_http_method": "",
|
||||||
|
"session_placement": "",
|
||||||
|
"session_key": "",
|
||||||
|
"seq_placement": "",
|
||||||
|
"seq_key": "",
|
||||||
|
"uplink_data_placement": "",
|
||||||
|
"uplink_data_key": "",
|
||||||
|
"uplink_chunk_size": 0,
|
||||||
|
"server": "example.com",
|
||||||
|
"server_port": 443,
|
||||||
"download": {
|
"download": {
|
||||||
"mode": "",
|
|
||||||
"host": "example.com",
|
"host": "example.com",
|
||||||
"path": "/xhttp",
|
"path": "/xhttp",
|
||||||
"domain_strategy": "prefer_ipv4",
|
"domain_strategy": "prefer_ipv4",
|
||||||
"x_padding_bytes": "0-0",
|
"x_padding_bytes": "100-1000",
|
||||||
"sc_max_each_post_bytes": "0-0",
|
"no_grpc_header": false, // stream-up/one, client only
|
||||||
"sc_min_posts_interval_ms": "0-0",
|
"sc_max_each_post_bytes": 1000000, // packet-up only
|
||||||
"sc_stream_up_server_secs": "0-0",
|
"sc_min_posts_interval_ms": 30, // packet-up, client only
|
||||||
"xmux": {
|
"xmux": { // h2/h3 mainly, client only
|
||||||
"max_concurrency": "0-1",
|
"max_concurrency": "16-32",
|
||||||
"max_connections": "0-1",
|
"max_connections": 0,
|
||||||
"c_max_reuse_times": "0-1",
|
"c_max_reuse_times": 0,
|
||||||
"h_max_request_times": "0-1",
|
"h_max_request_times": "600-900",
|
||||||
"h_max_reusable_secs": "0-1",
|
"h_max_reusable_secs": "1800-3000",
|
||||||
"h_keep_alive_period": 60
|
"h_keep_alive_period": 0
|
||||||
},
|
},
|
||||||
|
"x_padding_obfs_mode": false,
|
||||||
|
"x_padding_key": "",
|
||||||
|
"x_padding_header": "",
|
||||||
|
"x_padding_placement": "",
|
||||||
|
"x_padding_method": "",
|
||||||
|
"uplink_http_method": "",
|
||||||
|
"session_placement": "",
|
||||||
|
"session_key": "",
|
||||||
|
"seq_placement": "",
|
||||||
|
"seq_key": "",
|
||||||
|
"uplink_data_placement": "",
|
||||||
|
"uplink_data_key": "",
|
||||||
|
"uplink_chunk_size": 0,
|
||||||
"server": "example.com",
|
"server": "example.com",
|
||||||
"server_port": 443,
|
"server_port": 443,
|
||||||
"tls": {
|
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.com",
|
"server_name": "example.com",
|
||||||
"alpn": "h2" // h3 for QUIC
|
"alpn": "h2" // h3 for QUIC
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668"
|
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tls": {
|
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#inbound
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.com",
|
"server_name": "example.com",
|
||||||
"alpn": "h2", // h3 for QUIC
|
"alpn": "h2", // h3 for QUIC
|
||||||
@@ -33,6 +33,23 @@
|
|||||||
"type": "xhttp",
|
"type": "xhttp",
|
||||||
"mode": "stream-up",
|
"mode": "stream-up",
|
||||||
"path": "/xhttp",
|
"path": "/xhttp",
|
||||||
|
"x_padding_bytes": "100-1000",
|
||||||
|
"no_sse_header": false, // server only
|
||||||
|
"sc_max_buffered_posts": 30, // packet-up, server only
|
||||||
|
"sc_stream_up_server_secs": "20-80", // stream-up, server only
|
||||||
|
"x_padding_obfs_mode": false,
|
||||||
|
"x_padding_key": "",
|
||||||
|
"x_padding_header": "",
|
||||||
|
"x_padding_placement": "",
|
||||||
|
"x_padding_method": "",
|
||||||
|
"uplink_http_method": "",
|
||||||
|
"session_placement": "",
|
||||||
|
"session_key": "",
|
||||||
|
"seq_placement": "",
|
||||||
|
"seq_key": "",
|
||||||
|
"uplink_data_placement": "",
|
||||||
|
"uplink_data_key": "",
|
||||||
|
"uplink_chunk_size": 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package trafficontrol
|
|||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/compatible"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/experimental/clashapi/compatible"
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/atomic"
|
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package trafficontrol
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/atomic"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user