mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-11-06 10:12:37 +00:00
Compare commits
256
Commits
v1.7.7
...
v1.10.0-beta.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68ec00aaa5 | ||
|
|
a192e43eee | ||
|
|
793bf8e2a6 | ||
|
|
cab3a0b871 | ||
|
|
148e60fdb6 | ||
|
|
fde50b8851 | ||
|
|
331645fee3 | ||
|
|
6010158e29 | ||
|
|
a070057431 | ||
|
|
862697453f | ||
|
|
1f3e7fa754 | ||
|
|
c543993221 | ||
|
|
fe58a10753 | ||
|
|
1867689018 | ||
|
|
fe654b5665 | ||
|
|
918d955ec4 | ||
|
|
ba33b1320e | ||
|
|
3bcf0de6c1 | ||
|
|
fc92fe685b | ||
|
|
7ac2d247ee | ||
|
|
23af5afd30 | ||
|
|
eb6a6d310b | ||
|
|
3d098da6cd | ||
|
|
0f521bd702 | ||
|
|
07891d5c7a | ||
|
|
f1147965dd | ||
|
|
45f3234c73 | ||
|
|
aae3fded32 | ||
|
|
090494faf5 | ||
|
|
db5719e22f | ||
|
|
064fb9b873 | ||
|
|
f6a1e123fc | ||
|
|
3066dfe3b3 | ||
|
|
1128fdd8c7 | ||
|
|
cfd9879b17 | ||
|
|
9ceb660c57 | ||
|
|
7d00d7df28 | ||
|
|
21b1ac26b9 | ||
|
|
7fec8d842e | ||
|
|
07c678fb85 | ||
|
|
baecfc7778 | ||
|
|
07de36ecdb | ||
|
|
2c8a8303cd | ||
|
|
e5991cae0b | ||
|
|
1349acfd5a | ||
|
|
98ff897f35 | ||
|
|
6144c8e340 | ||
|
|
c8caac9f67 | ||
|
|
81e9eda357 | ||
|
|
7cba3da108 | ||
|
|
82d06b43e7 | ||
|
|
a7ac91f573 | ||
|
|
0540a95a43 | ||
|
|
94707dfcdd | ||
|
|
8a17043502 | ||
|
|
b0aaa86806 | ||
|
|
8a2d3fbb28 | ||
|
|
4652019608 | ||
|
|
06fa5abf63 | ||
|
|
996fbbf0c3 | ||
|
|
142ff1b455 | ||
|
|
74d662f7a3 | ||
|
|
085f603377 | ||
|
|
460fae83dc | ||
|
|
bb9bd9bff6 | ||
|
|
c2354ebf25 | ||
|
|
c1f4755c4e | ||
|
|
0ca5909b06 | ||
|
|
e77a8114c5 | ||
|
|
f1393235ff | ||
|
|
bdba2365de | ||
|
|
ce0da5b557 | ||
|
|
3853201412 | ||
|
|
7003ef40a3 | ||
|
|
59ec92228c | ||
|
|
0eeb2da323 | ||
|
|
977b0fac02 | ||
|
|
51964801ff | ||
|
|
e08c052fc9 | ||
|
|
53927d8bbd | ||
|
|
968b9bc217 | ||
|
|
69dc87aa6d | ||
|
|
4193df375f | ||
|
|
5ff7006326 | ||
|
|
a89107ea9d | ||
|
|
9ffdbba2ed | ||
|
|
65c71049ea | ||
|
|
7d4e6a7f4e | ||
|
|
d612620c5d | ||
|
|
8a9a77a438 | ||
|
|
a2098c18e1 | ||
|
|
cf2181dd3a | ||
|
|
5899e95ff1 | ||
|
|
d7160c19cf | ||
|
|
da9e22b4e6 | ||
|
|
0e120f8a44 | ||
|
|
d918863ac5 | ||
|
|
2ae192305c | ||
|
|
71d1879bd6 | ||
|
|
917514e09f | ||
|
|
5327aeaea4 | ||
|
|
93ae3f7a1e | ||
|
|
f24a2aed7d | ||
|
|
0517ceef76 | ||
|
|
830ea46932 | ||
|
|
cd0fcd5ddc | ||
|
|
003176f069 | ||
|
|
71d92518c1 | ||
|
|
b5dcd6bf59 | ||
|
|
11c7b4a866 | ||
|
|
ee14135298 | ||
|
|
cbcf005f37 | ||
|
|
daee0b154e | ||
|
|
d530c724c0 | ||
|
|
7f698c1104 | ||
|
|
7a4a44c6d2 | ||
|
|
44277e5dd2 | ||
|
|
1f470c69c4 | ||
|
|
742adacce7 | ||
|
|
32e1d5a5e2 | ||
|
|
cb9f4ce597 | ||
|
|
4b1a6185ba | ||
|
|
8d85c92356 | ||
|
|
c6164c9eca | ||
|
|
3c85b8bc48 | ||
|
|
8b8fb4344c | ||
|
|
e85a38e059 | ||
|
|
f3ac91673a | ||
|
|
0f1e58b917 | ||
|
|
c4cfe24aef | ||
|
|
3d73b159ba | ||
|
|
0ae1afef44 | ||
|
|
a5e2a4073b | ||
|
|
b6cb3948a3 | ||
|
|
7b0f5061dc | ||
|
|
76f20482f7 | ||
|
|
e735a5bdc8 | ||
|
|
70381e93c8 | ||
|
|
07a40716e8 | ||
|
|
5fea5956db | ||
|
|
d20a389043 | ||
|
|
4a4180bde5 | ||
|
|
7ecb6daabb | ||
|
|
712bdd9ae5 | ||
|
|
a3b74591a7 | ||
|
|
2f4abc6523 | ||
|
|
965ab075d9 | ||
|
|
ed2f8b9637 | ||
|
|
0f71ce5120 | ||
|
|
f8085ab111 | ||
|
|
f61b272cbf | ||
|
|
59d437b9d2 | ||
|
|
a7338fdc2b | ||
|
|
d88860928e | ||
|
|
20a2e38f47 | ||
|
|
acd438be23 | ||
|
|
e27fb51b54 | ||
|
|
adc38b26eb | ||
|
|
7e943e743a | ||
|
|
ceffcc0ad2 | ||
|
|
fdc451f7c6 | ||
|
|
b48c471e6a | ||
|
|
4b1fabd007 | ||
|
|
2b5eb1c59e | ||
|
|
e2d3862e64 | ||
|
|
4f5e7b974d | ||
|
|
21dedddd93 | ||
|
|
e02502bec0 | ||
|
|
ba67633ee8 | ||
|
|
7fd9abe802 | ||
|
|
78a5f59202 | ||
|
|
8d0da685d2 | ||
|
|
e6644f784e | ||
|
|
2b93b74d38 | ||
|
|
dd52c26ae1 | ||
|
|
f288e3898b | ||
|
|
1bc893a73a | ||
|
|
7359fdf195 | ||
|
|
02b7041de6 | ||
|
|
96ac931b11 | ||
|
|
3077a82650 | ||
|
|
de998c5119 | ||
|
|
d32c30c4b7 | ||
|
|
4823023806 | ||
|
|
bb355d17b2 | ||
|
|
aaf30bf92b | ||
|
|
f8c400cffc | ||
|
|
3c24411e14 | ||
|
|
4a44aa3c21 | ||
|
|
8db2ae0c83 | ||
|
|
80d1aebcb7 | ||
|
|
5583e01c99 | ||
|
|
bca0b86549 | ||
|
|
8332878cdc | ||
|
|
d0ba69ad22 | ||
|
|
31b8834427 | ||
|
|
d0f7a59e9b | ||
|
|
71e7d517a8 | ||
|
|
e6885e9967 | ||
|
|
e2090923db | ||
|
|
46be319976 | ||
|
|
b27bc45cf2 | ||
|
|
3d735281f4 | ||
|
|
8760a0d94d | ||
|
|
2239b59933 | ||
|
|
425a63f59d | ||
|
|
b85725c009 | ||
|
|
17aebc56c1 | ||
|
|
f76b21b02c | ||
|
|
704545a2ec | ||
|
|
dc7b7afc06 | ||
|
|
e478d3c2dc | ||
|
|
c8318058bb | ||
|
|
abca2118e7 | ||
|
|
a8ee41715a | ||
|
|
94f76d6671 | ||
|
|
bf6cc8903c | ||
|
|
1b15e1692a | ||
|
|
017372db25 | ||
|
|
216a0380fe | ||
|
|
71b9e4ff17 | ||
|
|
9b7deb5246 | ||
|
|
a850a73e1a | ||
|
|
c4d9be9e0d | ||
|
|
f31c604b3d | ||
|
|
4c8a50a52b | ||
|
|
b326e60998 | ||
|
|
11bec79a06 | ||
|
|
16eff06c37 | ||
|
|
2911eba236 | ||
|
|
2e607118c3 | ||
|
|
89c723e3e4 | ||
|
|
35fd9de3ff | ||
|
|
6ddcd3954d | ||
|
|
36b0f2e91a | ||
|
|
fe053e26b5 | ||
|
|
269434cfe6 | ||
|
|
88495a24dc | ||
|
|
d131a7c10a | ||
|
|
744a5d703b | ||
|
|
09421b6378 | ||
|
|
21283b554a | ||
|
|
25810b50c1 | ||
|
|
f1e3a59db3 | ||
|
|
a99deb2cb5 | ||
|
|
38d28e0763 | ||
|
|
e09a94bb9e | ||
|
|
a21c5324fd | ||
|
|
4b43acfec0 | ||
|
|
7df151e820 | ||
|
|
5948ffb965 | ||
|
|
bf4e556f67 | ||
|
|
e3f8567690 | ||
|
|
40c7f3e170 | ||
|
|
c506255e0f | ||
|
|
87c6fd4c0f |
@@ -0,0 +1 @@
|
|||||||
|
github: nekohasekai
|
||||||
@@ -65,6 +65,12 @@ body:
|
|||||||
For Apple platform clients, please check `Settings - View Service Log` for crash logs.
|
For Apple platform clients, please check `Settings - View Service Log` for crash logs.
|
||||||
For the Android client, please check the `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` file for crash logs.
|
For the Android client, please check the `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` file for crash logs.
|
||||||
render: shell
|
render: shell
|
||||||
|
- type: checkboxes
|
||||||
|
id: supporter
|
||||||
|
attributes:
|
||||||
|
label: Supporter
|
||||||
|
options:
|
||||||
|
- label: I am a [sponsor](https://github.com/sponsors/nekohasekai/)
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Integrity requirements
|
label: Integrity requirements
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ body:
|
|||||||
对于 Apple 平台图形客户端程序,请检查 `Settings - View Service Log` 以导出崩溃日志。
|
对于 Apple 平台图形客户端程序,请检查 `Settings - View Service Log` 以导出崩溃日志。
|
||||||
对于 Android 图形客户端程序,请检查 `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` 文件以导出崩溃日志。
|
对于 Android 图形客户端程序,请检查 `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` 文件以导出崩溃日志。
|
||||||
render: shell
|
render: shell
|
||||||
|
- type: checkboxes
|
||||||
|
id: supporter
|
||||||
|
attributes:
|
||||||
|
label: 支持我们
|
||||||
|
options:
|
||||||
|
- label: 我已经 [赞助](https://github.com/sponsors/nekohasekai/)
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: 完整性要求
|
label: 完整性要求
|
||||||
|
|||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
PROJECTS=$(dirname "$0")/../..
|
||||||
|
|
||||||
|
function updateClient() {
|
||||||
|
pushd clients/$1
|
||||||
|
git fetch
|
||||||
|
git reset FETCH_HEAD --hard
|
||||||
|
popd
|
||||||
|
git add clients/$1
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClient "apple"
|
||||||
|
updateClient "android"
|
||||||
+50
-53
@@ -22,67 +22,74 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Get latest go version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
echo go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') >> $GITHUB_OUTPUT
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ steps.version.outputs.go_version }}
|
go-version: ^1.23
|
||||||
- name: Add cache to Go proxy
|
|
||||||
run: |
|
|
||||||
version=`git rev-parse HEAD`
|
|
||||||
mkdir build
|
|
||||||
pushd build
|
|
||||||
go mod init build
|
|
||||||
go get -v github.com/sagernet/sing-box@$version
|
|
||||||
popd
|
|
||||||
continue-on-error: true
|
|
||||||
- name: Run Test
|
- name: Run Test
|
||||||
run: |
|
run: |
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
build_go118:
|
|
||||||
name: Debug build (Go 1.18)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: 1.18.10
|
|
||||||
- name: Cache go module
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: go118-${{ hashFiles('**/go.sum') }}
|
|
||||||
- name: Run Test
|
|
||||||
run: make ci_build_go118
|
|
||||||
build_go120:
|
build_go120:
|
||||||
name: Debug build (Go 1.20)
|
name: Debug build (Go 1.20)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.20.7
|
go-version: ~1.20
|
||||||
- name: Cache go module
|
- name: Cache go module
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: go118-${{ hashFiles('**/go.sum') }}
|
key: go120-${{ hashFiles('**/go.sum') }}
|
||||||
|
- name: Run Test
|
||||||
|
run: make ci_build_go120
|
||||||
|
build_go121:
|
||||||
|
name: Debug build (Go 1.21)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ~1.21
|
||||||
|
- name: Cache go module
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: go121-${{ hashFiles('**/go.sum') }}
|
||||||
|
- name: Run Test
|
||||||
|
run: make ci_build
|
||||||
|
build_go122:
|
||||||
|
name: Debug build (Go 1.22)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ~1.22
|
||||||
|
- name: Cache go module
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: go122-${{ hashFiles('**/go.sum') }}
|
||||||
- name: Run Test
|
- name: Run Test
|
||||||
run: make ci_build
|
run: make ci_build
|
||||||
cross:
|
cross:
|
||||||
@@ -188,8 +195,7 @@ jobs:
|
|||||||
- name: freebsd-arm64
|
- name: freebsd-arm64
|
||||||
goos: freebsd
|
goos: freebsd
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
|
fail-fast: true
|
||||||
fail-fast: false
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
@@ -201,22 +207,13 @@ jobs:
|
|||||||
TAGS: with_clash_api,with_quic
|
TAGS: with_clash_api,with_quic
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Get latest go version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
echo go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') >> $GITHUB_OUTPUT
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ steps.version.outputs.go_version }}
|
go-version: ^1.21
|
||||||
- name: Build
|
- name: Build
|
||||||
id: build
|
id: build
|
||||||
run: make
|
run: make
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: sing-box-${{ matrix.name }}
|
|
||||||
path: sing-box*
|
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
name: Build Docker Images
|
name: Build Docker Images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- released
|
||||||
|
- prereleased
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
@@ -8,8 +13,27 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Get commit to build
|
||||||
|
id: ref
|
||||||
|
run: |-
|
||||||
|
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
|
||||||
|
ref="${{ github.ref_name }}"
|
||||||
|
else
|
||||||
|
ref="${{ github.event.inputs.tag }}"
|
||||||
|
fi
|
||||||
|
echo "ref=$ref"
|
||||||
|
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||||
|
if [[ $ref == *"-"* ]]; then
|
||||||
|
latest=latest-beta
|
||||||
|
else
|
||||||
|
latest=latest
|
||||||
|
fi
|
||||||
|
echo "latest=$latest"
|
||||||
|
echo "latest=$latest" >> $GITHUB_OUTPUT
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||||
|
with:
|
||||||
|
ref: ${{ steps.ref.outputs.ref }}
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Setup QEMU for Docker Buildx
|
- name: Setup QEMU for Docker Buildx
|
||||||
@@ -25,23 +49,15 @@ jobs:
|
|||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/sagernet/sing-box
|
images: ghcr.io/sagernet/sing-box
|
||||||
- name: Get tag to build
|
|
||||||
id: tag
|
|
||||||
run: |
|
|
||||||
echo "latest=ghcr.io/sagernet/sing-box:latest" >> $GITHUB_OUTPUT
|
|
||||||
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
|
|
||||||
echo "versioned=ghcr.io/sagernet/sing-box:${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "versioned=ghcr.io/sagernet/sing-box:${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
- name: Build and release Docker images
|
- name: Build and release Docker images
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
|
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
|
||||||
|
context: .
|
||||||
target: dist
|
target: dist
|
||||||
build-args: |
|
build-args: |
|
||||||
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||||
tags: |
|
tags: |
|
||||||
${{ steps.tag.outputs.latest }}
|
ghcr.io/sagernet/sing-box:${{ steps.ref.outputs.latest }}
|
||||||
${{ steps.tag.outputs.versioned }}
|
ghcr.io/sagernet/sing-box:${{ steps.ref.outputs.ref }}
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
@@ -22,19 +22,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Get latest go version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
echo go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') >> $GITHUB_OUTPUT
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ steps.version.outputs.go_version }}
|
go-version: ^1.23
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout=30m
|
args: --timeout=30m
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: Release to Linux repository
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.23
|
||||||
|
- name: Extract signing key
|
||||||
|
run: |-
|
||||||
|
mkdir -p $HOME/.gnupg
|
||||||
|
cat > $HOME/.gnupg/sagernet.key <<EOF
|
||||||
|
${{ secrets.GPG_KEY }}
|
||||||
|
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
||||||
|
EOF
|
||||||
|
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
||||||
|
- name: Publish release
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
distribution: goreleaser-pro
|
||||||
|
version: latest
|
||||||
|
args: release -f .goreleaser.fury.yaml --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||||
|
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
|
||||||
|
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||||
|
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
@@ -12,4 +12,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
days-before-close: 5
|
days-before-close: 5
|
||||||
|
exempt-issue-labels: 'bug,enhancement'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
/vendor/
|
/vendor/
|
||||||
/*.json
|
/*.json
|
||||||
|
/*.srs
|
||||||
/*.db
|
/*.db
|
||||||
/site/
|
/site/
|
||||||
/bin/
|
/bin/
|
||||||
@@ -13,3 +14,5 @@
|
|||||||
/*.xcframework/
|
/*.xcframework/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/config.d/
|
/config.d/
|
||||||
|
/venv/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[submodule "clients/apple"]
|
||||||
|
path = clients/apple
|
||||||
|
url = https://github.com/SagerNet/sing-box-for-apple.git
|
||||||
|
[submodule "clients/android"]
|
||||||
|
path = clients/android
|
||||||
|
url = https://github.com/SagerNet/sing-box-for-android.git
|
||||||
+11
-9
@@ -6,14 +6,7 @@ linters:
|
|||||||
- gci
|
- gci
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- paralleltest
|
- paralleltest
|
||||||
|
- ineffassign
|
||||||
run:
|
|
||||||
skip-dirs:
|
|
||||||
- transport/simple-obfs
|
|
||||||
- transport/clashssr
|
|
||||||
- transport/cloudflaretls
|
|
||||||
- transport/shadowtls/tls
|
|
||||||
- transport/shadowtls/tls_go119
|
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
gci:
|
gci:
|
||||||
@@ -23,4 +16,13 @@ linters-settings:
|
|||||||
- prefix(github.com/sagernet/)
|
- prefix(github.com/sagernet/)
|
||||||
- default
|
- default
|
||||||
staticcheck:
|
staticcheck:
|
||||||
go: '1.20'
|
checks:
|
||||||
|
- all
|
||||||
|
- -SA1003
|
||||||
|
|
||||||
|
run:
|
||||||
|
go: "1.23"
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-dirs:
|
||||||
|
- transport/simple-obfs
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
project_name: sing-box
|
||||||
|
builds:
|
||||||
|
- id: main
|
||||||
|
main: ./cmd/sing-box
|
||||||
|
flags:
|
||||||
|
- -v
|
||||||
|
- -trimpath
|
||||||
|
ldflags:
|
||||||
|
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
||||||
|
tags:
|
||||||
|
- with_gvisor
|
||||||
|
- with_quic
|
||||||
|
- with_dhcp
|
||||||
|
- with_wireguard
|
||||||
|
- with_ech
|
||||||
|
- with_utls
|
||||||
|
- with_reality_server
|
||||||
|
- with_acme
|
||||||
|
- with_clash_api
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
targets:
|
||||||
|
- linux_386
|
||||||
|
- linux_amd64_v1
|
||||||
|
- linux_arm64
|
||||||
|
- linux_arm_7
|
||||||
|
- linux_s390x
|
||||||
|
- linux_riscv64
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ .Version }}.{{ .ShortCommit }}"
|
||||||
|
nfpms:
|
||||||
|
- &template
|
||||||
|
id: package
|
||||||
|
package_name: sing-box
|
||||||
|
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
|
builds:
|
||||||
|
- main
|
||||||
|
homepage: https://sing-box.sagernet.org/
|
||||||
|
maintainer: nekohasekai <contact-git@sekai.icu>
|
||||||
|
description: The universal proxy platform.
|
||||||
|
license: GPLv3 or later
|
||||||
|
formats:
|
||||||
|
- deb
|
||||||
|
- rpm
|
||||||
|
priority: extra
|
||||||
|
contents:
|
||||||
|
- src: release/config/config.json
|
||||||
|
dst: /etc/sing-box/config.json
|
||||||
|
type: config
|
||||||
|
- src: release/config/sing-box.service
|
||||||
|
dst: /usr/lib/systemd/system/sing-box.service
|
||||||
|
- src: release/config/sing-box@.service
|
||||||
|
dst: /usr/lib/systemd/system/sing-box@.service
|
||||||
|
- src: LICENSE
|
||||||
|
dst: /usr/share/licenses/sing-box/LICENSE
|
||||||
|
deb:
|
||||||
|
signature:
|
||||||
|
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||||
|
fields:
|
||||||
|
Bugs: https://github.com/SagerNet/sing-box/issues
|
||||||
|
rpm:
|
||||||
|
signature:
|
||||||
|
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||||
|
conflicts:
|
||||||
|
- sing-box-beta
|
||||||
|
- id: package_beta
|
||||||
|
<<: *template
|
||||||
|
package_name: sing-box-beta
|
||||||
|
file_name_template: '{{ .ProjectName }}-beta_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
|
formats:
|
||||||
|
- deb
|
||||||
|
- rpm
|
||||||
|
conflicts:
|
||||||
|
- sing-box
|
||||||
|
release:
|
||||||
|
disable: true
|
||||||
|
furies:
|
||||||
|
- account: sagernet
|
||||||
|
ids:
|
||||||
|
- package
|
||||||
|
disable: "{{ not (not .Prerelease) }}"
|
||||||
|
- account: sagernet
|
||||||
|
ids:
|
||||||
|
- package_beta
|
||||||
|
disable: "{{ not .Prerelease }}"
|
||||||
+48
-59
@@ -1,16 +1,16 @@
|
|||||||
project_name: sing-box
|
project_name: sing-box
|
||||||
builds:
|
builds:
|
||||||
- id: main
|
- &template
|
||||||
|
id: main
|
||||||
main: ./cmd/sing-box
|
main: ./cmd/sing-box
|
||||||
flags:
|
flags:
|
||||||
- -v
|
- -v
|
||||||
- -trimpath
|
- -trimpath
|
||||||
asmflags:
|
|
||||||
- all=-trimpath={{.Env.GOPATH}}
|
|
||||||
gcflags:
|
|
||||||
- all=-trimpath={{.Env.GOPATH}}
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||||
|
- -s
|
||||||
|
- -buildid=
|
||||||
|
- -checklinkname=0
|
||||||
tags:
|
tags:
|
||||||
- with_gvisor
|
- with_gvisor
|
||||||
- with_quic
|
- with_quic
|
||||||
@@ -30,65 +30,39 @@ builds:
|
|||||||
- linux_arm64
|
- linux_arm64
|
||||||
- linux_arm_7
|
- linux_arm_7
|
||||||
- linux_s390x
|
- linux_s390x
|
||||||
|
- linux_riscv64
|
||||||
- windows_amd64_v1
|
- windows_amd64_v1
|
||||||
- windows_amd64_v3
|
- windows_amd64_v3
|
||||||
- windows_386
|
- windows_386
|
||||||
- windows_arm64
|
- windows_arm64
|
||||||
- darwin_amd64_v1
|
- darwin_amd64_v1
|
||||||
- darwin_amd64_v3
|
|
||||||
- darwin_arm64
|
- darwin_arm64
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
- id: legacy
|
- id: legacy
|
||||||
main: ./cmd/sing-box
|
<<: *template
|
||||||
flags:
|
|
||||||
- -v
|
|
||||||
- -trimpath
|
|
||||||
asmflags:
|
|
||||||
- all=-trimpath={{.Env.GOPATH}}
|
|
||||||
gcflags:
|
|
||||||
- all=-trimpath={{.Env.GOPATH}}
|
|
||||||
ldflags:
|
|
||||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
|
||||||
tags:
|
tags:
|
||||||
- with_gvisor
|
- with_gvisor
|
||||||
- with_quic
|
- with_quic
|
||||||
- with_dhcp
|
- with_dhcp
|
||||||
- with_wireguard
|
- with_wireguard
|
||||||
- with_ech
|
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
|
ldflags:
|
||||||
|
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||||
|
- -s
|
||||||
|
- -buildid=
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GOROOT=/nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/share/go
|
- GOROOT={{ .Env.GOPATH }}/go1.20.14
|
||||||
gobinary: /nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/bin/go
|
gobinary: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
|
||||||
targets:
|
targets:
|
||||||
- windows_amd64_v1
|
- windows_amd64_v1
|
||||||
- windows_386
|
- windows_386
|
||||||
- darwin_amd64_v1
|
- darwin_amd64_v1
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
- id: android
|
- id: android
|
||||||
main: ./cmd/sing-box
|
<<: *template
|
||||||
flags:
|
|
||||||
- -v
|
|
||||||
- -trimpath
|
|
||||||
asmflags:
|
|
||||||
- all=-trimpath={{.Env.GOPATH}}
|
|
||||||
gcflags:
|
|
||||||
- all=-trimpath={{.Env.GOPATH}}
|
|
||||||
ldflags:
|
|
||||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
|
||||||
tags:
|
|
||||||
- with_gvisor
|
|
||||||
- with_quic
|
|
||||||
- with_dhcp
|
|
||||||
- with_wireguard
|
|
||||||
- with_ech
|
|
||||||
- with_utls
|
|
||||||
- with_reality_server
|
|
||||||
- with_acme
|
|
||||||
- with_clash_api
|
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=1
|
||||||
overrides:
|
overrides:
|
||||||
@@ -96,8 +70,8 @@ builds:
|
|||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: 7
|
goarm: 7
|
||||||
env:
|
env:
|
||||||
- CC=armv7a-linux-androideabi19-clang
|
- CC=armv7a-linux-androideabi21-clang
|
||||||
- CXX=armv7a-linux-androideabi19-clang++
|
- CXX=armv7a-linux-androideabi21-clang++
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
env:
|
env:
|
||||||
@@ -106,8 +80,8 @@ builds:
|
|||||||
- goos: android
|
- goos: android
|
||||||
goarch: 386
|
goarch: 386
|
||||||
env:
|
env:
|
||||||
- CC=i686-linux-android19-clang
|
- CC=i686-linux-android21-clang
|
||||||
- CXX=i686-linux-android19-clang++
|
- CXX=i686-linux-android21-clang++
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
goamd64: v1
|
goamd64: v1
|
||||||
@@ -119,11 +93,11 @@ builds:
|
|||||||
- android_arm64
|
- android_arm64
|
||||||
- android_386
|
- android_386
|
||||||
- android_amd64
|
- android_amd64
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
snapshot:
|
snapshot:
|
||||||
name_template: "{{ .Version }}.{{ .ShortCommit }}"
|
name_template: "{{ .Version }}.{{ .ShortCommit }}"
|
||||||
archives:
|
archives:
|
||||||
- id: archive
|
- &template
|
||||||
|
id: archive
|
||||||
builds:
|
builds:
|
||||||
- main
|
- main
|
||||||
- android
|
- android
|
||||||
@@ -136,21 +110,16 @@ archives:
|
|||||||
- LICENSE
|
- LICENSE
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
- id: archive-legacy
|
- id: archive-legacy
|
||||||
|
<<: *template
|
||||||
builds:
|
builds:
|
||||||
- legacy
|
- legacy
|
||||||
format: tar.gz
|
|
||||||
format_overrides:
|
|
||||||
- goos: windows
|
|
||||||
format: zip
|
|
||||||
wrap_in_directory: true
|
|
||||||
files:
|
|
||||||
- LICENSE
|
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
|
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
|
||||||
nfpms:
|
nfpms:
|
||||||
- id: package
|
- id: package
|
||||||
package_name: sing-box
|
package_name: sing-box
|
||||||
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
vendor: sagernet
|
builds:
|
||||||
|
- main
|
||||||
homepage: https://sing-box.sagernet.org/
|
homepage: https://sing-box.sagernet.org/
|
||||||
maintainer: nekohasekai <contact-git@sekai.icu>
|
maintainer: nekohasekai <contact-git@sekai.icu>
|
||||||
description: The universal proxy platform.
|
description: The universal proxy platform.
|
||||||
@@ -165,11 +134,27 @@ nfpms:
|
|||||||
dst: /etc/sing-box/config.json
|
dst: /etc/sing-box/config.json
|
||||||
type: config
|
type: config
|
||||||
- src: release/config/sing-box.service
|
- src: release/config/sing-box.service
|
||||||
dst: /etc/systemd/system/sing-box.service
|
dst: /usr/lib/systemd/system/sing-box.service
|
||||||
- src: release/config/sing-box@.service
|
- src: release/config/sing-box@.service
|
||||||
dst: /etc/systemd/system/sing-box@.service
|
dst: /usr/lib/systemd/system/sing-box@.service
|
||||||
- src: LICENSE
|
- src: LICENSE
|
||||||
dst: /usr/share/licenses/sing-box/LICENSE
|
dst: /usr/share/licenses/sing-box/LICENSE
|
||||||
|
deb:
|
||||||
|
signature:
|
||||||
|
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||||
|
fields:
|
||||||
|
Bugs: https://github.com/SagerNet/sing-box/issues
|
||||||
|
rpm:
|
||||||
|
signature:
|
||||||
|
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||||
|
overrides:
|
||||||
|
deb:
|
||||||
|
conflicts:
|
||||||
|
- sing-box-beta
|
||||||
|
rpm:
|
||||||
|
conflicts:
|
||||||
|
- sing-box-beta
|
||||||
|
|
||||||
source:
|
source:
|
||||||
enabled: false
|
enabled: false
|
||||||
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
||||||
@@ -183,6 +168,10 @@ release:
|
|||||||
github:
|
github:
|
||||||
owner: SagerNet
|
owner: SagerNet
|
||||||
name: sing-box
|
name: sing-box
|
||||||
name_template: '{{ if .IsSnapshot }}{{ nightly }}{{ else }}{{ .Version }}{{ end }}'
|
|
||||||
draft: true
|
draft: true
|
||||||
mode: replace
|
prerelease: auto
|
||||||
|
mode: replace
|
||||||
|
ids:
|
||||||
|
- archive
|
||||||
|
- package
|
||||||
|
skip_upload: true
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
|
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
COPY . /go/src/github.com/sagernet/sing-box
|
COPY . /go/src/github.com/sagernet/sing-box
|
||||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||||
@@ -15,7 +15,7 @@ RUN set -ex \
|
|||||||
&& go build -v -trimpath -tags \
|
&& go build -v -trimpath -tags \
|
||||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
|
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
|
||||||
-o /go/bin/sing-box \
|
-o /go/bin/sing-box \
|
||||||
-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= -checklinkname=0" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
NAME = sing-box
|
NAME = sing-box
|
||||||
COMMIT = $(shell git rev-parse --short HEAD)
|
COMMIT = $(shell git rev-parse --short HEAD)
|
||||||
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api
|
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
|
||||||
TAGS_GO120 = with_quic,with_ech
|
TAGS_GO121 = with_ech
|
||||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
|
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
|
||||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
|
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
|
||||||
|
|
||||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||||
@@ -14,14 +14,14 @@ MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
|
|||||||
MAIN = ./cmd/sing-box
|
MAIN = ./cmd/sing-box
|
||||||
PREFIX ?= $(shell go env GOPATH)
|
PREFIX ?= $(shell go env GOPATH)
|
||||||
|
|
||||||
.PHONY: test release docs
|
.PHONY: test release docs build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
ci_build_go118:
|
ci_build_go120:
|
||||||
go build $(PARAMS) $(MAIN)
|
go build $(PARAMS) $(MAIN)
|
||||||
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
|
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
|
||||||
|
|
||||||
ci_build:
|
ci_build:
|
||||||
go build $(PARAMS) $(MAIN)
|
go build $(PARAMS) $(MAIN)
|
||||||
@@ -59,25 +59,35 @@ proto_install:
|
|||||||
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
|
|
||||||
release:
|
release:
|
||||||
go run ./cmd/internal/build goreleaser release --clean --skip-publish || exit 1
|
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
||||||
mkdir dist/release
|
mkdir dist/release
|
||||||
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/*.pkg.tar.zst dist/release
|
mv dist/*.tar.gz \
|
||||||
|
dist/*.zip \
|
||||||
|
dist/*.deb \
|
||||||
|
dist/*.rpm \
|
||||||
|
dist/*_amd64.pkg.tar.zst \
|
||||||
|
dist/*_amd64v3.pkg.tar.zst \
|
||||||
|
dist/*_arm64.pkg.tar.zst \
|
||||||
|
dist/release
|
||||||
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
|
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
|
||||||
rm -r dist/release
|
rm -r dist/release
|
||||||
|
|
||||||
|
release_repo:
|
||||||
|
go run ./cmd/internal/build goreleaser release -f .goreleaser.fury.yaml --clean
|
||||||
|
|
||||||
release_install:
|
release_install:
|
||||||
go install -v github.com/goreleaser/goreleaser@latest
|
|
||||||
go install -v github.com/tcnksm/ghr@latest
|
go install -v github.com/tcnksm/ghr@latest
|
||||||
|
|
||||||
update_android_version:
|
update_android_version:
|
||||||
go run ./cmd/internal/update_android_version
|
go run ./cmd/internal/update_android_version
|
||||||
|
|
||||||
build_android:
|
build_android:
|
||||||
cd ../sing-box-for-android && ./gradlew :app:assemblePlayRelease && ./gradlew --stop
|
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease :app:assembleOtherRelease && ./gradlew --stop
|
||||||
|
|
||||||
upload_android:
|
upload_android:
|
||||||
mkdir -p dist/release_android
|
mkdir -p dist/release_android
|
||||||
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android
|
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android
|
||||||
|
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*-universal.apk dist/release_android
|
||||||
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release_android
|
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release_android
|
||||||
rm -rf dist/release_android
|
rm -rf dist/release_android
|
||||||
|
|
||||||
@@ -178,18 +188,19 @@ lib:
|
|||||||
go run ./cmd/internal/build_libbox -target ios
|
go run ./cmd/internal/build_libbox -target ios
|
||||||
|
|
||||||
lib_install:
|
lib_install:
|
||||||
go get -v -d
|
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.3
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230915142329-c6740b6d2950
|
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.3
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230915142329-c6740b6d2950
|
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
mkdocs serve
|
venv/bin/mkdocs serve
|
||||||
|
|
||||||
publish_docs:
|
publish_docs:
|
||||||
mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
||||||
|
|
||||||
docs_install:
|
docs_install:
|
||||||
pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
|
python -m venv venv
|
||||||
|
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin dist sing-box
|
rm -rf bin dist sing-box
|
||||||
rm -f $(shell go env GOPATH)/sing-box
|
rm -f $(shell go env GOPATH)/sing-box
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ The universal proxy platform.
|
|||||||
|
|
||||||
[](https://repology.org/project/sing-box/versions)
|
[](https://repology.org/project/sing-box/versions)
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
https://sing-box.sagernet.org
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
https://community.sagernet.org/c/sing-box/
|
https://community.sagernet.org/c/sing-box/
|
||||||
|
|||||||
+68
-5
@@ -1,11 +1,16 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/urltest"
|
"github.com/sagernet/sing-box/common/urltest"
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/varbin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClashServer interface {
|
type ClashServer interface {
|
||||||
@@ -13,22 +18,80 @@ type ClashServer interface {
|
|||||||
PreStarter
|
PreStarter
|
||||||
Mode() string
|
Mode() string
|
||||||
ModeList() []string
|
ModeList() []string
|
||||||
StoreSelected() bool
|
|
||||||
StoreFakeIP() bool
|
|
||||||
CacheFile() ClashCacheFile
|
|
||||||
HistoryStorage() *urltest.HistoryStorage
|
HistoryStorage() *urltest.HistoryStorage
|
||||||
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
|
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
|
||||||
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
|
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClashCacheFile interface {
|
type CacheFile interface {
|
||||||
|
Service
|
||||||
|
PreStarter
|
||||||
|
|
||||||
|
StoreFakeIP() bool
|
||||||
|
FakeIPStorage
|
||||||
|
|
||||||
|
StoreRDRC() bool
|
||||||
|
dns.RDRCStore
|
||||||
|
|
||||||
LoadMode() string
|
LoadMode() string
|
||||||
StoreMode(mode string) error
|
StoreMode(mode string) error
|
||||||
LoadSelected(group string) string
|
LoadSelected(group string) string
|
||||||
StoreSelected(group string, selected string) error
|
StoreSelected(group string, selected string) error
|
||||||
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
||||||
StoreGroupExpand(group string, expand bool) error
|
StoreGroupExpand(group string, expand bool) error
|
||||||
FakeIPStorage
|
LoadRuleSet(tag string) *SavedRuleSet
|
||||||
|
SaveRuleSet(tag string, set *SavedRuleSet) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SavedRuleSet struct {
|
||||||
|
Content []byte
|
||||||
|
LastUpdated time.Time
|
||||||
|
LastEtag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = varbin.Write(&buffer, binary.BigEndian, s.Content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
var version uint8
|
||||||
|
err := binary.Read(reader, binary.BigEndian, &version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = varbin.Read(reader, binary.BigEndian, &s.Content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var lastUpdated int64
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &lastUpdated)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.LastUpdated = time.Unix(lastUpdated, 0)
|
||||||
|
err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tracker interface {
|
type Tracker interface {
|
||||||
|
|||||||
+37
-4
@@ -31,11 +31,16 @@ type InboundContext struct {
|
|||||||
Network string
|
Network string
|
||||||
Source M.Socksaddr
|
Source M.Socksaddr
|
||||||
Destination M.Socksaddr
|
Destination M.Socksaddr
|
||||||
Domain string
|
|
||||||
Protocol string
|
|
||||||
User string
|
User string
|
||||||
Outbound string
|
Outbound string
|
||||||
|
|
||||||
|
// sniffer
|
||||||
|
|
||||||
|
Protocol string
|
||||||
|
Domain string
|
||||||
|
Client string
|
||||||
|
SniffContext any
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|
||||||
InboundDetour string
|
InboundDetour string
|
||||||
@@ -46,11 +51,30 @@ type InboundContext struct {
|
|||||||
SourceGeoIPCode string
|
SourceGeoIPCode string
|
||||||
GeoIPCode string
|
GeoIPCode string
|
||||||
ProcessInfo *process.Info
|
ProcessInfo *process.Info
|
||||||
|
QueryType uint16
|
||||||
FakeIP bool
|
FakeIP bool
|
||||||
|
|
||||||
// dns cache
|
// rule cache
|
||||||
|
|
||||||
QueryType uint16
|
IPCIDRMatchSource bool
|
||||||
|
IPCIDRAcceptEmpty bool
|
||||||
|
|
||||||
|
SourceAddressMatch bool
|
||||||
|
SourcePortMatch bool
|
||||||
|
DestinationAddressMatch bool
|
||||||
|
DestinationPortMatch bool
|
||||||
|
DidMatch bool
|
||||||
|
IgnoreDestinationIPCIDRMatch bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *InboundContext) ResetRuleCache() {
|
||||||
|
c.IPCIDRMatchSource = false
|
||||||
|
c.IPCIDRAcceptEmpty = false
|
||||||
|
c.SourceAddressMatch = false
|
||||||
|
c.SourcePortMatch = false
|
||||||
|
c.DestinationAddressMatch = false
|
||||||
|
c.DestinationPortMatch = false
|
||||||
|
c.DidMatch = false
|
||||||
}
|
}
|
||||||
|
|
||||||
type inboundContextKey struct{}
|
type inboundContextKey struct{}
|
||||||
@@ -83,3 +107,12 @@ func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
|
|||||||
}
|
}
|
||||||
return WithContext(ctx, &newMetadata), &newMetadata
|
return WithContext(ctx, &newMetadata), &newMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func OverrideContext(ctx context.Context) context.Context {
|
||||||
|
if metadata := ContextFrom(ctx); metadata != nil {
|
||||||
|
var newMetadata InboundContext
|
||||||
|
newMetadata = *metadata
|
||||||
|
return WithContext(ctx, &newMetadata)
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|||||||
+51
-4
@@ -2,24 +2,30 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/geoip"
|
"github.com/sagernet/sing-box/common/geoip"
|
||||||
"github.com/sagernet/sing-dns"
|
"github.com/sagernet/sing-dns"
|
||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-tun"
|
||||||
"github.com/sagernet/sing/common/control"
|
"github.com/sagernet/sing/common/control"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/x/list"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
mdns "github.com/miekg/dns"
|
mdns "github.com/miekg/dns"
|
||||||
|
"go4.org/netipx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Router interface {
|
type Router interface {
|
||||||
Service
|
Service
|
||||||
|
PreStarter
|
||||||
PostStarter
|
PostStarter
|
||||||
|
Cleanup() error
|
||||||
|
|
||||||
Outbounds() []Outbound
|
Outbounds() []Outbound
|
||||||
Outbound(tag string) (Outbound, bool)
|
Outbound(tag string) (Outbound, bool)
|
||||||
DefaultOutbound(network string) Outbound
|
DefaultOutbound(network string) (Outbound, error)
|
||||||
|
|
||||||
FakeIPStore() FakeIPStore
|
FakeIPStore() FakeIPStore
|
||||||
|
|
||||||
@@ -28,6 +34,10 @@ type Router interface {
|
|||||||
GeoIPReader() *geoip.Reader
|
GeoIPReader() *geoip.Reader
|
||||||
LoadGeosite(code string) (Rule, error)
|
LoadGeosite(code string) (Rule, error)
|
||||||
|
|
||||||
|
RuleSet(tag string) (RuleSet, bool)
|
||||||
|
|
||||||
|
NeedWIFIState() bool
|
||||||
|
|
||||||
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
||||||
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
||||||
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
||||||
@@ -38,7 +48,9 @@ type Router interface {
|
|||||||
DefaultInterface() string
|
DefaultInterface() string
|
||||||
AutoDetectInterface() bool
|
AutoDetectInterface() bool
|
||||||
AutoDetectInterfaceFunc() control.Func
|
AutoDetectInterfaceFunc() control.Func
|
||||||
DefaultMark() int
|
DefaultMark() uint32
|
||||||
|
RegisterAutoRedirectOutputMark(mark uint32) error
|
||||||
|
AutoRedirectOutputMark() uint32
|
||||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||||
PackageManager() tun.PackageManager
|
PackageManager() tun.PackageManager
|
||||||
@@ -62,19 +74,54 @@ func RouterFromContext(ctx context.Context) Router {
|
|||||||
return service.FromContext[Router](ctx)
|
return service.FromContext[Router](ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HeadlessRule interface {
|
||||||
|
Match(metadata *InboundContext) bool
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
type Rule interface {
|
type Rule interface {
|
||||||
|
HeadlessRule
|
||||||
Service
|
Service
|
||||||
Type() string
|
Type() string
|
||||||
UpdateGeosite() error
|
UpdateGeosite() error
|
||||||
Match(metadata *InboundContext) bool
|
|
||||||
Outbound() string
|
Outbound() string
|
||||||
String() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSRule interface {
|
type DNSRule interface {
|
||||||
Rule
|
Rule
|
||||||
DisableCache() bool
|
DisableCache() bool
|
||||||
RewriteTTL() *uint32
|
RewriteTTL() *uint32
|
||||||
|
ClientSubnet() *netip.Prefix
|
||||||
|
WithAddressLimit() bool
|
||||||
|
MatchAddressLimit(metadata *InboundContext) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleSet interface {
|
||||||
|
Name() string
|
||||||
|
StartContext(ctx context.Context, startContext RuleSetStartContext) error
|
||||||
|
PostStart() error
|
||||||
|
Metadata() RuleSetMetadata
|
||||||
|
ExtractIPSet() []*netipx.IPSet
|
||||||
|
IncRef()
|
||||||
|
DecRef()
|
||||||
|
Cleanup()
|
||||||
|
RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback]
|
||||||
|
UnregisterCallback(element *list.Element[RuleSetUpdateCallback])
|
||||||
|
Close() error
|
||||||
|
HeadlessRule
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleSetUpdateCallback func(it RuleSet)
|
||||||
|
|
||||||
|
type RuleSetMetadata struct {
|
||||||
|
ContainsProcessRule bool
|
||||||
|
ContainsWIFIRule bool
|
||||||
|
ContainsIPCIDRRule bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleSetStartContext interface {
|
||||||
|
HTTPClient(detour string, dialer N.Dialer) *http.Client
|
||||||
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceUpdateListener interface {
|
type InterfaceUpdateListener interface {
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ type V2RayServerTransportHandler interface {
|
|||||||
|
|
||||||
type V2RayClientTransport interface {
|
type V2RayClientTransport interface {
|
||||||
DialContext(ctx context.Context) (net.Conn, error)
|
DialContext(ctx context.Context) (net.Conn, error)
|
||||||
|
Close() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/experimental"
|
"github.com/sagernet/sing-box/experimental"
|
||||||
|
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
"github.com/sagernet/sing-box/inbound"
|
"github.com/sagernet/sing-box/inbound"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@@ -32,7 +35,8 @@ type Box struct {
|
|||||||
outbounds []adapter.Outbound
|
outbounds []adapter.Outbound
|
||||||
logFactory log.Factory
|
logFactory log.Factory
|
||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
preServices map[string]adapter.Service
|
preServices1 map[string]adapter.Service
|
||||||
|
preServices2 map[string]adapter.Service
|
||||||
postServices map[string]adapter.Service
|
postServices map[string]adapter.Service
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
@@ -45,17 +49,21 @@ type Options struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(options Options) (*Box, error) {
|
func New(options Options) (*Box, error) {
|
||||||
|
createdAt := time.Now()
|
||||||
ctx := options.Context
|
ctx := options.Context
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
ctx = service.ContextWithDefaultRegistry(ctx)
|
ctx = service.ContextWithDefaultRegistry(ctx)
|
||||||
ctx = pause.ContextWithDefaultManager(ctx)
|
ctx = pause.WithDefaultManager(ctx)
|
||||||
createdAt := time.Now()
|
|
||||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||||
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||||
|
var needCacheFile bool
|
||||||
var needClashAPI bool
|
var needClashAPI bool
|
||||||
var needV2RayAPI bool
|
var needV2RayAPI bool
|
||||||
|
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
|
||||||
|
needCacheFile = true
|
||||||
|
}
|
||||||
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
|
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
|
||||||
needClashAPI = true
|
needClashAPI = true
|
||||||
}
|
}
|
||||||
@@ -103,6 +111,7 @@ func New(options Options) (*Box, error) {
|
|||||||
ctx,
|
ctx,
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
inboundOptions,
|
inboundOptions,
|
||||||
options.PlatformInterface,
|
options.PlatformInterface,
|
||||||
)
|
)
|
||||||
@@ -145,8 +154,17 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize platform interface")
|
return nil, E.Cause(err, "initialize platform interface")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preServices := make(map[string]adapter.Service)
|
preServices1 := make(map[string]adapter.Service)
|
||||||
|
preServices2 := make(map[string]adapter.Service)
|
||||||
postServices := make(map[string]adapter.Service)
|
postServices := make(map[string]adapter.Service)
|
||||||
|
if needCacheFile {
|
||||||
|
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
||||||
|
if cacheFile == nil {
|
||||||
|
cacheFile = cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||||
|
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||||
|
}
|
||||||
|
preServices1["cache file"] = cacheFile
|
||||||
|
}
|
||||||
if needClashAPI {
|
if needClashAPI {
|
||||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||||
@@ -155,7 +173,7 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "create clash api server")
|
return nil, E.Cause(err, "create clash api server")
|
||||||
}
|
}
|
||||||
router.SetClashServer(clashServer)
|
router.SetClashServer(clashServer)
|
||||||
preServices["clash api"] = clashServer
|
preServices2["clash api"] = clashServer
|
||||||
}
|
}
|
||||||
if needV2RayAPI {
|
if needV2RayAPI {
|
||||||
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
||||||
@@ -163,7 +181,7 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "create v2ray api server")
|
return nil, E.Cause(err, "create v2ray api server")
|
||||||
}
|
}
|
||||||
router.SetV2RayServer(v2rayServer)
|
router.SetV2RayServer(v2rayServer)
|
||||||
preServices["v2ray api"] = v2rayServer
|
preServices2["v2ray api"] = v2rayServer
|
||||||
}
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
router: router,
|
router: router,
|
||||||
@@ -172,7 +190,8 @@ func New(options Options) (*Box, error) {
|
|||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
logFactory: logFactory,
|
logFactory: logFactory,
|
||||||
logger: logFactory.Logger(),
|
logger: logFactory.Logger(),
|
||||||
preServices: preServices,
|
preServices1: preServices1,
|
||||||
|
preServices2: preServices2,
|
||||||
postServices: postServices,
|
postServices: postServices,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -185,7 +204,7 @@ func (s *Box) PreStart() error {
|
|||||||
defer func() {
|
defer func() {
|
||||||
v := recover()
|
v := recover()
|
||||||
if v != nil {
|
if v != nil {
|
||||||
log.Error(E.Cause(err, "origin error"))
|
println(err.Error())
|
||||||
debug.PrintStack()
|
debug.PrintStack()
|
||||||
panic("panic on early close: " + fmt.Sprint(v))
|
panic("panic on early close: " + fmt.Sprint(v))
|
||||||
}
|
}
|
||||||
@@ -204,9 +223,9 @@ func (s *Box) Start() error {
|
|||||||
defer func() {
|
defer func() {
|
||||||
v := recover()
|
v := recover()
|
||||||
if v != nil {
|
if v != nil {
|
||||||
log.Error(E.Cause(err, "origin error"))
|
println(err.Error())
|
||||||
debug.PrintStack()
|
debug.PrintStack()
|
||||||
panic("panic on early close: " + fmt.Sprint(v))
|
println("panic on early start: " + fmt.Sprint(v))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
s.Close()
|
s.Close()
|
||||||
@@ -217,16 +236,38 @@ func (s *Box) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) preStart() error {
|
func (s *Box) preStart() error {
|
||||||
for serviceName, service := range s.preServices {
|
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||||
|
monitor.Start("start logger")
|
||||||
|
err := s.logFactory.Start()
|
||||||
|
monitor.Finish()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "start logger")
|
||||||
|
}
|
||||||
|
for serviceName, service := range s.preServices1 {
|
||||||
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
||||||
s.logger.Trace("pre-start ", serviceName)
|
monitor.Start("pre-start ", serviceName)
|
||||||
err := preService.PreStart()
|
err := preService.PreStart()
|
||||||
|
monitor.Finish()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "pre-starting ", serviceName)
|
return E.Cause(err, "pre-start ", serviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := s.startOutbounds()
|
for serviceName, service := range s.preServices2 {
|
||||||
|
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
||||||
|
monitor.Start("pre-start ", serviceName)
|
||||||
|
err := preService.PreStart()
|
||||||
|
monitor.Finish()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "pre-start ", serviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = s.router.PreStart()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "pre-start router")
|
||||||
|
}
|
||||||
|
err = s.startOutbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -238,8 +279,13 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for serviceName, service := range s.preServices {
|
for serviceName, service := range s.preServices1 {
|
||||||
s.logger.Trace("starting ", serviceName)
|
err = service.Start()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "start ", serviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for serviceName, service := range s.preServices2 {
|
||||||
err = service.Start()
|
err = service.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "start ", serviceName)
|
return E.Cause(err, "start ", serviceName)
|
||||||
@@ -252,34 +298,47 @@ func (s *Box) start() error {
|
|||||||
} else {
|
} else {
|
||||||
tag = in.Tag()
|
tag = in.Tag()
|
||||||
}
|
}
|
||||||
s.logger.Trace("initializing inbound/", in.Type(), "[", tag, "]")
|
|
||||||
err = in.Start()
|
err = in.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s.postStart()
|
err = s.postStart()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.router.Cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) postStart() error {
|
func (s *Box) postStart() error {
|
||||||
for serviceName, service := range s.postServices {
|
for serviceName, service := range s.postServices {
|
||||||
s.logger.Trace("starting ", service)
|
|
||||||
err := service.Start()
|
err := service.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "start ", serviceName)
|
return E.Cause(err, "start ", serviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, outbound := range s.outbounds {
|
// TODO: reorganize ALL start order
|
||||||
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound {
|
for _, out := range s.outbounds {
|
||||||
s.logger.Trace("post-starting outbound/", outbound.Tag())
|
if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound {
|
||||||
err := lateOutbound.PostStart()
|
err := lateOutbound.PostStart()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "post-start outbound/", outbound.Tag())
|
return E.Cause(err, "post-start outbound/", out.Tag())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.Trace("post-starting router")
|
err := s.router.PostStart()
|
||||||
return s.router.PostStart()
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, in := range s.inbounds {
|
||||||
|
if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound {
|
||||||
|
err = lateInbound.PostStart()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "post-start inbound/", in.Tag())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) Close() error {
|
func (s *Box) Close() error {
|
||||||
@@ -289,41 +348,53 @@ func (s *Box) Close() error {
|
|||||||
default:
|
default:
|
||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
|
monitor := taskmonitor.New(s.logger, C.StopTimeout)
|
||||||
var errors error
|
var errors error
|
||||||
for serviceName, service := range s.postServices {
|
for serviceName, service := range s.postServices {
|
||||||
s.logger.Trace("closing ", serviceName)
|
monitor.Start("close ", serviceName)
|
||||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", serviceName)
|
return E.Cause(err, "close ", serviceName)
|
||||||
})
|
})
|
||||||
|
monitor.Finish()
|
||||||
}
|
}
|
||||||
for i, in := range s.inbounds {
|
for i, in := range s.inbounds {
|
||||||
s.logger.Trace("closing inbound/", in.Type(), "[", i, "]")
|
monitor.Start("close inbound/", in.Type(), "[", i, "]")
|
||||||
errors = E.Append(errors, in.Close(), func(err error) error {
|
errors = E.Append(errors, in.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
|
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
|
||||||
})
|
})
|
||||||
|
monitor.Finish()
|
||||||
}
|
}
|
||||||
for i, out := range s.outbounds {
|
for i, out := range s.outbounds {
|
||||||
s.logger.Trace("closing outbound/", out.Type(), "[", i, "]")
|
monitor.Start("close outbound/", out.Type(), "[", i, "]")
|
||||||
errors = E.Append(errors, common.Close(out), func(err error) error {
|
errors = E.Append(errors, common.Close(out), func(err error) error {
|
||||||
return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
|
return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
|
||||||
})
|
})
|
||||||
|
monitor.Finish()
|
||||||
}
|
}
|
||||||
s.logger.Trace("closing router")
|
monitor.Start("close router")
|
||||||
if err := common.Close(s.router); err != nil {
|
if err := common.Close(s.router); err != nil {
|
||||||
errors = E.Append(errors, err, func(err error) error {
|
errors = E.Append(errors, err, func(err error) error {
|
||||||
return E.Cause(err, "close router")
|
return E.Cause(err, "close router")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for serviceName, service := range s.preServices {
|
monitor.Finish()
|
||||||
s.logger.Trace("closing ", serviceName)
|
for serviceName, service := range s.preServices1 {
|
||||||
|
monitor.Start("close ", serviceName)
|
||||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", serviceName)
|
return E.Cause(err, "close ", serviceName)
|
||||||
})
|
})
|
||||||
|
monitor.Finish()
|
||||||
|
}
|
||||||
|
for serviceName, service := range s.preServices2 {
|
||||||
|
monitor.Start("close ", serviceName)
|
||||||
|
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||||
|
return E.Cause(err, "close ", serviceName)
|
||||||
|
})
|
||||||
|
monitor.Finish()
|
||||||
}
|
}
|
||||||
s.logger.Trace("closing log factory")
|
|
||||||
if err := common.Close(s.logFactory); err != nil {
|
if err := common.Close(s.logFactory); err != nil {
|
||||||
errors = E.Append(errors, err, func(err error) error {
|
errors = E.Append(errors, err, func(err error) error {
|
||||||
return E.Cause(err, "close log factory")
|
return E.Cause(err, "close logger")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return errors
|
return errors
|
||||||
|
|||||||
+8
-2
@@ -4,12 +4,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
|
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"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Box) startOutbounds() error {
|
func (s *Box) startOutbounds() error {
|
||||||
|
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||||
outboundTags := make(map[adapter.Outbound]string)
|
outboundTags := make(map[adapter.Outbound]string)
|
||||||
outbounds := make(map[string]adapter.Outbound)
|
outbounds := make(map[string]adapter.Outbound)
|
||||||
for i, outboundToStart := range s.outbounds {
|
for i, outboundToStart := range s.outbounds {
|
||||||
@@ -42,9 +45,12 @@ func (s *Box) startOutbounds() error {
|
|||||||
}
|
}
|
||||||
started[outboundTag] = true
|
started[outboundTag] = true
|
||||||
canContinue = true
|
canContinue = true
|
||||||
if starter, isStarter := outboundToStart.(common.Starter); isStarter {
|
if starter, isStarter := outboundToStart.(interface {
|
||||||
s.logger.Trace("initializing outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
Start() error
|
||||||
|
}); isStarter {
|
||||||
|
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||||
err := starter.Start()
|
err := starter.Start()
|
||||||
|
monitor.Finish()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule
+1
Submodule clients/android added at 440aaa9a1a
Submodule
+1
Submodule clients/apple added at aa4ce98421
@@ -7,7 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
_ "github.com/sagernet/gomobile/event/key"
|
_ "github.com/sagernet/gomobile"
|
||||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
@@ -46,13 +46,13 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
sharedFlags = append(sharedFlags, "-trimpath")
|
sharedFlags = append(sharedFlags, "-trimpath")
|
||||||
sharedFlags = append(sharedFlags, "-ldflags")
|
sharedFlags = append(sharedFlags, "-buildvcs=false")
|
||||||
currentTag, err := build_shared.ReadTag()
|
currentTag, err := build_shared.ReadTag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
currentTag = "unknown"
|
currentTag = "unknown"
|
||||||
}
|
}
|
||||||
sharedFlags = append(sharedFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||||
debugFlags = append(debugFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||||
|
|
||||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
|
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
|
||||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
||||||
@@ -93,7 +93,7 @@ func buildAndroid() {
|
|||||||
|
|
||||||
const name = "libbox.aar"
|
const name = "libbox.aar"
|
||||||
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
|
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
|
||||||
if rw.FileExists(copyPath) {
|
if rw.IsDir(copyPath) {
|
||||||
copyPath, _ = filepath.Abs(copyPath)
|
copyPath, _ = filepath.Abs(copyPath)
|
||||||
err = rw.CopyFile(name, filepath.Join(copyPath, name))
|
err = rw.CopyFile(name, filepath.Join(copyPath, name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -134,7 +134,7 @@ func buildiOS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
copyPath := filepath.Join("..", "sing-box-for-apple")
|
copyPath := filepath.Join("..", "sing-box-for-apple")
|
||||||
if rw.FileExists(copyPath) {
|
if rw.IsDir(copyPath) {
|
||||||
targetDir := filepath.Join(copyPath, "Libbox.xcframework")
|
targetDir := filepath.Join(copyPath, "Libbox.xcframework")
|
||||||
targetDir, _ = filepath.Abs(targetDir)
|
targetDir, _ = filepath.Abs(targetDir)
|
||||||
os.RemoveAll(targetDir)
|
os.RemoveAll(targetDir)
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
"github.com/sagernet/sing/common/shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -28,7 +30,7 @@ func FindSDK() {
|
|||||||
}
|
}
|
||||||
for _, path := range searchPath {
|
for _, path := range searchPath {
|
||||||
path = os.ExpandEnv(path)
|
path = os.ExpandEnv(path)
|
||||||
if rw.FileExists(path + "/licenses/android-sdk-license") {
|
if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) {
|
||||||
androidSDKPath = path
|
androidSDKPath = path
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -40,6 +42,14 @@ func FindSDK() {
|
|||||||
log.Fatal("android NDK not found")
|
log.Fatal("android NDK not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
javaVersion, err := shell.Exec("java", "--version").ReadOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(E.Cause(err, "check java version"))
|
||||||
|
}
|
||||||
|
if !strings.Contains(javaVersion, "openjdk 17") {
|
||||||
|
log.Fatal("java version should be openjdk 17")
|
||||||
|
}
|
||||||
|
|
||||||
os.Setenv("ANDROID_HOME", androidSDKPath)
|
os.Setenv("ANDROID_HOME", androidSDKPath)
|
||||||
os.Setenv("ANDROID_SDK_HOME", androidSDKPath)
|
os.Setenv("ANDROID_SDK_HOME", androidSDKPath)
|
||||||
os.Setenv("ANDROID_NDK_HOME", androidNDKPath)
|
os.Setenv("ANDROID_NDK_HOME", androidNDKPath)
|
||||||
@@ -48,11 +58,13 @@ func FindSDK() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findNDK() bool {
|
func findNDK() bool {
|
||||||
if rw.FileExists(androidSDKPath + "/ndk/25.1.8937393") {
|
const fixedVersion = "26.2.11394342"
|
||||||
androidNDKPath = androidSDKPath + "/ndk/25.1.8937393"
|
const versionFile = "source.properties"
|
||||||
|
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
|
||||||
|
androidNDKPath = fixedPath
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
ndkVersions, err := os.ReadDir(androidSDKPath + "/ndk")
|
ndkVersions, err := os.ReadDir(filepath.Join(androidSDKPath, "ndk"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -73,8 +85,10 @@ func findNDK() bool {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
for _, versionName := range versionNames {
|
for _, versionName := range versionNames {
|
||||||
if rw.FileExists(androidSDKPath + "/ndk/" + versionName) {
|
currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName)
|
||||||
androidNDKPath = androidSDKPath + "/ndk/" + versionName
|
if rw.IsFile(filepath.Join(androidSDKPath, versionFile)) {
|
||||||
|
androidNDKPath = currentNDKPath
|
||||||
|
log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,8 +99,14 @@ var GoBinPath string
|
|||||||
|
|
||||||
func FindMobile() {
|
func FindMobile() {
|
||||||
goBin := filepath.Join(build.Default.GOPATH, "bin")
|
goBin := filepath.Join(build.Default.GOPATH, "bin")
|
||||||
if !rw.FileExists(goBin + "/" + "gobind") {
|
if runtime.GOOS == "windows" {
|
||||||
log.Fatal("missing gomobile installation")
|
if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) {
|
||||||
|
log.Fatal("missing gomobile installation")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !rw.IsFile(filepath.Join(goBin, "gobind")) {
|
||||||
|
log.Fatal("missing gomobile installation")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
GoBinPath = goBin
|
GoBinPath = goBin
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -18,34 +19,46 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
common.Must(os.Chdir(androidPath))
|
common.Must(os.Chdir(androidPath))
|
||||||
localProps := common.Must1(os.ReadFile("local.properties"))
|
localProps := common.Must1(os.ReadFile("version.properties"))
|
||||||
var propsList [][]string
|
var propsList [][]string
|
||||||
for _, propLine := range strings.Split(string(localProps), "\n") {
|
for _, propLine := range strings.Split(string(localProps), "\n") {
|
||||||
propsList = append(propsList, strings.Split(propLine, "="))
|
propsList = append(propsList, strings.Split(propLine, "="))
|
||||||
}
|
}
|
||||||
|
var (
|
||||||
|
versionUpdated bool
|
||||||
|
goVersionUpdated bool
|
||||||
|
)
|
||||||
for _, propPair := range propsList {
|
for _, propPair := range propsList {
|
||||||
if propPair[0] == "VERSION_NAME" {
|
switch propPair[0] {
|
||||||
if propPair[1] == newVersion.String() {
|
case "VERSION_NAME":
|
||||||
log.Info("version not changed")
|
if propPair[1] != newVersion.String() {
|
||||||
return
|
versionUpdated = true
|
||||||
|
propPair[1] = newVersion.String()
|
||||||
|
log.Info("updated version to ", newVersion.String())
|
||||||
|
}
|
||||||
|
case "GO_VERSION":
|
||||||
|
if propPair[1] != runtime.Version() {
|
||||||
|
goVersionUpdated = true
|
||||||
|
propPair[1] = runtime.Version()
|
||||||
|
log.Info("updated Go version to ", runtime.Version())
|
||||||
}
|
}
|
||||||
propPair[1] = newVersion.String()
|
|
||||||
log.Info("updated version to ", newVersion.String())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !(versionUpdated || goVersionUpdated) {
|
||||||
|
log.Info("version not changed")
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, propPair := range propsList {
|
for _, propPair := range propsList {
|
||||||
switch propPair[0] {
|
switch propPair[0] {
|
||||||
case "VERSION_CODE":
|
case "VERSION_CODE":
|
||||||
versionCode := common.Must1(strconv.ParseInt(propPair[1], 10, 64))
|
versionCode := common.Must1(strconv.ParseInt(propPair[1], 10, 64))
|
||||||
propPair[1] = strconv.Itoa(int(versionCode + 1))
|
propPair[1] = strconv.Itoa(int(versionCode + 1))
|
||||||
log.Info("updated version code to ", propPair[1])
|
log.Info("updated version code to ", propPair[1])
|
||||||
case "RELEASE_NOTES":
|
|
||||||
propPair[1] = "sing-box " + newVersion.String()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var newProps []string
|
var newProps []string
|
||||||
for _, propPair := range propsList {
|
for _, propPair := range propsList {
|
||||||
newProps = append(newProps, strings.Join(propPair, "="))
|
newProps = append(newProps, strings.Join(propPair, "="))
|
||||||
}
|
}
|
||||||
common.Must(os.WriteFile("local.properties", []byte(strings.Join(newProps, "\n")), 0o644))
|
common.Must(os.WriteFile("version.properties", []byte(strings.Join(newProps, "\n")), 0o644))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/json"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -38,6 +38,10 @@ func format() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, optionsEntry := range optionsList {
|
for _, optionsEntry := range optionsList {
|
||||||
|
optionsEntry.options, err = badjson.Omitempty(optionsEntry.options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
encoder := json.NewEncoder(buffer)
|
encoder := json.NewEncoder(buffer)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
@@ -69,41 +73,3 @@ func format() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatOne(configPath string) error {
|
|
||||||
configContent, err := os.ReadFile(configPath)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read config")
|
|
||||||
}
|
|
||||||
var options option.Options
|
|
||||||
err = options.UnmarshalJSON(configContent)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "decode config")
|
|
||||||
}
|
|
||||||
buffer := new(bytes.Buffer)
|
|
||||||
encoder := json.NewEncoder(buffer)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
err = encoder.Encode(options)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "encode config")
|
|
||||||
}
|
|
||||||
if !commandFormatFlagWrite {
|
|
||||||
os.Stdout.WriteString(buffer.String() + "\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if bytes.Equal(configContent, buffer.Bytes()) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
output, err := os.Create(configPath)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "open output")
|
|
||||||
}
|
|
||||||
_, err = output.Write(buffer.Bytes())
|
|
||||||
output.Close()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "write output")
|
|
||||||
}
|
|
||||||
outputPath, _ := filepath.Abs(configPath)
|
|
||||||
os.Stderr.WriteString(outputPath + "\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/oschwald/maxminddb-golang"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
geoipReader *maxminddb.Reader
|
||||||
|
commandGeoIPFlagFile string
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandGeoip = &cobra.Command{
|
||||||
|
Use: "geoip",
|
||||||
|
Short: "GeoIP tools",
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := geoipPreRun()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file")
|
||||||
|
mainCommand.AddCommand(commandGeoip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func geoipPreRun() error {
|
||||||
|
reader, err := maxminddb.Open(commandGeoIPFlagFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if reader.Metadata.DatabaseType != "sing-geoip" {
|
||||||
|
reader.Close()
|
||||||
|
return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType)
|
||||||
|
}
|
||||||
|
geoipReader = reader
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
|
"github.com/oschwald/maxminddb-golang"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var flagGeoipExportOutput string
|
||||||
|
|
||||||
|
const flagGeoipExportDefaultOutput = "geoip-<country>.srs"
|
||||||
|
|
||||||
|
var commandGeoipExport = &cobra.Command{
|
||||||
|
Use: "export <country>",
|
||||||
|
Short: "Export geoip country as rule-set",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := geoipExport(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path")
|
||||||
|
commandGeoip.AddCommand(commandGeoipExport)
|
||||||
|
}
|
||||||
|
|
||||||
|
func geoipExport(countryCode string) error {
|
||||||
|
networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks)
|
||||||
|
countryMap := make(map[string][]*net.IPNet)
|
||||||
|
var (
|
||||||
|
ipNet *net.IPNet
|
||||||
|
nextCountryCode string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for networks.Next() {
|
||||||
|
ipNet, err = networks.Network(&nextCountryCode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet)
|
||||||
|
}
|
||||||
|
ipNets := countryMap[strings.ToLower(countryCode)]
|
||||||
|
if len(ipNets) == 0 {
|
||||||
|
return E.New("country code not found: ", countryCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
outputFile *os.File
|
||||||
|
outputWriter io.Writer
|
||||||
|
)
|
||||||
|
if flagGeoipExportOutput == "stdout" {
|
||||||
|
outputWriter = os.Stdout
|
||||||
|
} else if flagGeoipExportOutput == flagGeoipExportDefaultOutput {
|
||||||
|
outputFile, err = os.Create("geoip-" + countryCode + ".json")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outputFile.Close()
|
||||||
|
outputWriter = outputFile
|
||||||
|
} else {
|
||||||
|
outputFile, err = os.Create(flagGeoipExportOutput)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outputFile.Close()
|
||||||
|
outputWriter = outputFile
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(outputWriter)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
var headlessRule option.DefaultHeadlessRule
|
||||||
|
headlessRule.IPCIDR = make([]string, 0, len(ipNets))
|
||||||
|
for _, cidr := range ipNets {
|
||||||
|
headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String())
|
||||||
|
}
|
||||||
|
var plainRuleSet option.PlainRuleSetCompat
|
||||||
|
plainRuleSet.Version = C.RuleSetVersion2
|
||||||
|
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: headlessRule,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return encoder.Encode(plainRuleSet)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandGeoipList = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List geoip country codes",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := listGeoip()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandGeoip.AddCommand(commandGeoipList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listGeoip() error {
|
||||||
|
for _, code := range geoipReader.Metadata.Languages {
|
||||||
|
os.Stdout.WriteString(code + "\n")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandGeoipLookup = &cobra.Command{
|
||||||
|
Use: "lookup <address>",
|
||||||
|
Short: "Lookup if an IP address is contained in the GeoIP database",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := geoipLookup(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandGeoip.AddCommand(commandGeoipLookup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func geoipLookup(address string) error {
|
||||||
|
addr, err := netip.ParseAddr(address)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "parse address")
|
||||||
|
}
|
||||||
|
if !N.IsPublicAddr(addr) {
|
||||||
|
os.Stdout.WriteString("private\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var code string
|
||||||
|
_ = geoipReader.Lookup(addr.AsSlice(), &code)
|
||||||
|
if code != "" {
|
||||||
|
os.Stdout.WriteString(code + "\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
os.Stdout.WriteString("unknown\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/common/geosite"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
commandGeoSiteFlagFile string
|
||||||
|
geositeReader *geosite.Reader
|
||||||
|
geositeCodeList []string
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandGeoSite = &cobra.Command{
|
||||||
|
Use: "geosite",
|
||||||
|
Short: "Geosite tools",
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := geositePreRun()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file")
|
||||||
|
mainCommand.AddCommand(commandGeoSite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func geositePreRun() error {
|
||||||
|
reader, codeList, err := geosite.Open(commandGeoSiteFlagFile)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "open geosite file")
|
||||||
|
}
|
||||||
|
geositeReader = reader
|
||||||
|
geositeCodeList = codeList
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/geosite"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandGeositeExportOutput string
|
||||||
|
|
||||||
|
const commandGeositeExportDefaultOutput = "geosite-<category>.json"
|
||||||
|
|
||||||
|
var commandGeositeExport = &cobra.Command{
|
||||||
|
Use: "export <category>",
|
||||||
|
Short: "Export geosite category as rule-set",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := geositeExport(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path")
|
||||||
|
commandGeoSite.AddCommand(commandGeositeExport)
|
||||||
|
}
|
||||||
|
|
||||||
|
func geositeExport(category string) error {
|
||||||
|
sourceSet, err := geositeReader.Read(category)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
outputFile *os.File
|
||||||
|
outputWriter io.Writer
|
||||||
|
)
|
||||||
|
if commandGeositeExportOutput == "stdout" {
|
||||||
|
outputWriter = os.Stdout
|
||||||
|
} else if commandGeositeExportOutput == commandGeositeExportDefaultOutput {
|
||||||
|
outputFile, err = os.Create("geosite-" + category + ".json")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outputFile.Close()
|
||||||
|
outputWriter = outputFile
|
||||||
|
} else {
|
||||||
|
outputFile, err = os.Create(commandGeositeExportOutput)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outputFile.Close()
|
||||||
|
outputWriter = outputFile
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(outputWriter)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
var headlessRule option.DefaultHeadlessRule
|
||||||
|
defaultRule := geosite.Compile(sourceSet)
|
||||||
|
headlessRule.Domain = defaultRule.Domain
|
||||||
|
headlessRule.DomainSuffix = defaultRule.DomainSuffix
|
||||||
|
headlessRule.DomainKeyword = defaultRule.DomainKeyword
|
||||||
|
headlessRule.DomainRegex = defaultRule.DomainRegex
|
||||||
|
var plainRuleSet option.PlainRuleSetCompat
|
||||||
|
plainRuleSet.Version = C.RuleSetVersion2
|
||||||
|
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: headlessRule,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return encoder.Encode(plainRuleSet)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandGeositeList = &cobra.Command{
|
||||||
|
Use: "list <category>",
|
||||||
|
Short: "List geosite categories",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := geositeList()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandGeoSite.AddCommand(commandGeositeList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func geositeList() error {
|
||||||
|
var geositeEntry []struct {
|
||||||
|
category string
|
||||||
|
items int
|
||||||
|
}
|
||||||
|
for _, category := range geositeCodeList {
|
||||||
|
sourceSet, err := geositeReader.Read(category)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
geositeEntry = append(geositeEntry, struct {
|
||||||
|
category string
|
||||||
|
items int
|
||||||
|
}{category, len(sourceSet)})
|
||||||
|
}
|
||||||
|
sort.SliceStable(geositeEntry, func(i, j int) bool {
|
||||||
|
return geositeEntry[i].items < geositeEntry[j].items
|
||||||
|
})
|
||||||
|
for _, entry := range geositeEntry {
|
||||||
|
os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandGeositeLookup = &cobra.Command{
|
||||||
|
Use: "lookup [category] <domain>",
|
||||||
|
Short: "Check if a domain is in the geosite",
|
||||||
|
Args: cobra.RangeArgs(1, 2),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var (
|
||||||
|
source string
|
||||||
|
target string
|
||||||
|
)
|
||||||
|
switch len(args) {
|
||||||
|
case 1:
|
||||||
|
target = args[0]
|
||||||
|
case 2:
|
||||||
|
source = args[0]
|
||||||
|
target = args[1]
|
||||||
|
}
|
||||||
|
err := geositeLookup(source, target)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandGeoSite.AddCommand(commandGeositeLookup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func geositeLookup(source string, target string) error {
|
||||||
|
var sourceMatcherList []struct {
|
||||||
|
code string
|
||||||
|
matcher *searchGeositeMatcher
|
||||||
|
}
|
||||||
|
if source != "" {
|
||||||
|
sourceSet, err := geositeReader.Read(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sourceMatcher, err := newSearchGeositeMatcher(sourceSet)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "compile code: "+source)
|
||||||
|
}
|
||||||
|
sourceMatcherList = []struct {
|
||||||
|
code string
|
||||||
|
matcher *searchGeositeMatcher
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
code: source,
|
||||||
|
matcher: sourceMatcher,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
for _, code := range geositeCodeList {
|
||||||
|
sourceSet, err := geositeReader.Read(code)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sourceMatcher, err := newSearchGeositeMatcher(sourceSet)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "compile code: "+code)
|
||||||
|
}
|
||||||
|
sourceMatcherList = append(sourceMatcherList, struct {
|
||||||
|
code string
|
||||||
|
matcher *searchGeositeMatcher
|
||||||
|
}{
|
||||||
|
code: code,
|
||||||
|
matcher: sourceMatcher,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.SliceStable(sourceMatcherList, func(i, j int) bool {
|
||||||
|
return sourceMatcherList[i].code < sourceMatcherList[j].code
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, matcherItem := range sourceMatcherList {
|
||||||
|
if matchRule := matcherItem.matcher.Match(target); matchRule != "" {
|
||||||
|
os.Stdout.WriteString("Match code (")
|
||||||
|
os.Stdout.WriteString(matcherItem.code)
|
||||||
|
os.Stdout.WriteString(") ")
|
||||||
|
os.Stdout.WriteString(matchRule)
|
||||||
|
os.Stdout.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/geosite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type searchGeositeMatcher struct {
|
||||||
|
domainMap map[string]bool
|
||||||
|
suffixList []string
|
||||||
|
keywordList []string
|
||||||
|
regexList []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) {
|
||||||
|
options := geosite.Compile(items)
|
||||||
|
domainMap := make(map[string]bool)
|
||||||
|
for _, domain := range options.Domain {
|
||||||
|
domainMap[domain] = true
|
||||||
|
}
|
||||||
|
rule := &searchGeositeMatcher{
|
||||||
|
domainMap: domainMap,
|
||||||
|
suffixList: options.DomainSuffix,
|
||||||
|
keywordList: options.DomainKeyword,
|
||||||
|
regexList: options.DomainRegex,
|
||||||
|
}
|
||||||
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *searchGeositeMatcher) Match(domain string) string {
|
||||||
|
if r.domainMap[domain] {
|
||||||
|
return "domain=" + domain
|
||||||
|
}
|
||||||
|
for _, suffix := range r.suffixList {
|
||||||
|
if strings.HasSuffix(domain, suffix) {
|
||||||
|
return "domain_suffix=" + suffix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, keyword := range r.keywordList {
|
||||||
|
if strings.Contains(domain, keyword) {
|
||||||
|
return "domain_keyword=" + keyword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, regexStr := range r.regexList {
|
||||||
|
regex, err := regexp.Compile(regexStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if regex.MatchString(domain) {
|
||||||
|
return "domain_regex=" + regexStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
+20
-40
@@ -6,19 +6,19 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/json"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var commandMerge = &cobra.Command{
|
var commandMerge = &cobra.Command{
|
||||||
Use: "merge [output]",
|
Use: "merge <output>",
|
||||||
Short: "Merge configurations",
|
Short: "Merge configurations",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
err := merge(args[0])
|
err := merge(args[0])
|
||||||
@@ -54,7 +54,11 @@ func merge(outputPath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = rw.WriteFile(outputPath, buffer.Bytes())
|
err = rw.MkdirParent(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -65,50 +69,26 @@ func merge(outputPath string) error {
|
|||||||
|
|
||||||
func mergePathResources(options *option.Options) error {
|
func mergePathResources(options *option.Options) error {
|
||||||
for index, inbound := range options.Inbounds {
|
for index, inbound := range options.Inbounds {
|
||||||
switch inbound.Type {
|
rawOptions, err := inbound.RawOptions()
|
||||||
case C.TypeHTTP:
|
if err != nil {
|
||||||
inbound.HTTPOptions.TLS = mergeTLSInboundOptions(inbound.HTTPOptions.TLS)
|
return err
|
||||||
case C.TypeMixed:
|
}
|
||||||
inbound.MixedOptions.TLS = mergeTLSInboundOptions(inbound.MixedOptions.TLS)
|
if tlsOptions, containsTLSOptions := rawOptions.(option.InboundTLSOptionsWrapper); containsTLSOptions {
|
||||||
case C.TypeVMess:
|
tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions()))
|
||||||
inbound.VMessOptions.TLS = mergeTLSInboundOptions(inbound.VMessOptions.TLS)
|
|
||||||
case C.TypeTrojan:
|
|
||||||
inbound.TrojanOptions.TLS = mergeTLSInboundOptions(inbound.TrojanOptions.TLS)
|
|
||||||
case C.TypeNaive:
|
|
||||||
inbound.NaiveOptions.TLS = mergeTLSInboundOptions(inbound.NaiveOptions.TLS)
|
|
||||||
case C.TypeHysteria:
|
|
||||||
inbound.HysteriaOptions.TLS = mergeTLSInboundOptions(inbound.HysteriaOptions.TLS)
|
|
||||||
case C.TypeVLESS:
|
|
||||||
inbound.VLESSOptions.TLS = mergeTLSInboundOptions(inbound.VLESSOptions.TLS)
|
|
||||||
case C.TypeTUIC:
|
|
||||||
inbound.TUICOptions.TLS = mergeTLSInboundOptions(inbound.TUICOptions.TLS)
|
|
||||||
case C.TypeHysteria2:
|
|
||||||
inbound.Hysteria2Options.TLS = mergeTLSInboundOptions(inbound.Hysteria2Options.TLS)
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
options.Inbounds[index] = inbound
|
options.Inbounds[index] = inbound
|
||||||
}
|
}
|
||||||
for index, outbound := range options.Outbounds {
|
for index, outbound := range options.Outbounds {
|
||||||
|
rawOptions, err := outbound.RawOptions()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
switch outbound.Type {
|
switch outbound.Type {
|
||||||
case C.TypeHTTP:
|
|
||||||
outbound.HTTPOptions.TLS = mergeTLSOutboundOptions(outbound.HTTPOptions.TLS)
|
|
||||||
case C.TypeVMess:
|
|
||||||
outbound.VMessOptions.TLS = mergeTLSOutboundOptions(outbound.VMessOptions.TLS)
|
|
||||||
case C.TypeTrojan:
|
|
||||||
outbound.TrojanOptions.TLS = mergeTLSOutboundOptions(outbound.TrojanOptions.TLS)
|
|
||||||
case C.TypeHysteria:
|
|
||||||
outbound.HysteriaOptions.TLS = mergeTLSOutboundOptions(outbound.HysteriaOptions.TLS)
|
|
||||||
case C.TypeSSH:
|
case C.TypeSSH:
|
||||||
outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions)
|
outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions)
|
||||||
case C.TypeVLESS:
|
}
|
||||||
outbound.VLESSOptions.TLS = mergeTLSOutboundOptions(outbound.VLESSOptions.TLS)
|
if tlsOptions, containsTLSOptions := rawOptions.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
|
||||||
case C.TypeTUIC:
|
tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions()))
|
||||||
outbound.TUICOptions.TLS = mergeTLSOutboundOptions(outbound.TUICOptions.TLS)
|
|
||||||
case C.TypeHysteria2:
|
|
||||||
outbound.Hysteria2Options.TLS = mergeTLSOutboundOptions(outbound.Hysteria2Options.TLS)
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
options.Outbounds[index] = outbound
|
options.Outbounds[index] = outbound
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandRuleSet = &cobra.Command{
|
||||||
|
Use: "rule-set",
|
||||||
|
Short: "Manage rule-sets",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mainCommand.AddCommand(commandRuleSet)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var flagRuleSetCompileOutput string
|
||||||
|
|
||||||
|
const flagRuleSetCompileDefaultOutput = "<file_name>.srs"
|
||||||
|
|
||||||
|
var commandRuleSetCompile = &cobra.Command{
|
||||||
|
Use: "compile [source-path]",
|
||||||
|
Short: "Compile rule-set json to binary",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := compileRuleSet(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandRuleSet.AddCommand(commandRuleSetCompile)
|
||||||
|
commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileRuleSet(sourcePath string) error {
|
||||||
|
var (
|
||||||
|
reader io.Reader
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sourcePath == "stdin" {
|
||||||
|
reader = os.Stdin
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ruleSet, err := plainRuleSet.Upgrade()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var outputPath string
|
||||||
|
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
|
||||||
|
if strings.HasSuffix(sourcePath, ".json") {
|
||||||
|
outputPath = sourcePath[:len(sourcePath)-5] + ".srs"
|
||||||
|
} else {
|
||||||
|
outputPath = sourcePath + ".srs"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputPath = flagRuleSetCompileOutput
|
||||||
|
}
|
||||||
|
outputFile, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = srs.Write(outputFile, ruleSet, plainRuleSet.Version == C.RuleSetVersion2)
|
||||||
|
if err != nil {
|
||||||
|
outputFile.Close()
|
||||||
|
os.Remove(outputPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outputFile.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
|
||||||
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagRuleSetConvertType string
|
||||||
|
flagRuleSetConvertOutput string
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandRuleSetConvert = &cobra.Command{
|
||||||
|
Use: "convert [source-path]",
|
||||||
|
Short: "Convert adguard DNS filter to rule-set",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := convertRuleSet(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandRuleSet.AddCommand(commandRuleSetConvert)
|
||||||
|
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertType, "type", "t", "", "Source type, available: adguard")
|
||||||
|
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertRuleSet(sourcePath string) error {
|
||||||
|
var (
|
||||||
|
reader io.Reader
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sourcePath == "stdin" {
|
||||||
|
reader = os.Stdin
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var rules []option.HeadlessRule
|
||||||
|
switch flagRuleSetConvertType {
|
||||||
|
case "adguard":
|
||||||
|
rules, err = adguard.Convert(reader)
|
||||||
|
case "":
|
||||||
|
return E.New("source type is required")
|
||||||
|
default:
|
||||||
|
return E.New("unsupported source type: ", flagRuleSetConvertType)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var outputPath string
|
||||||
|
if flagRuleSetConvertOutput == flagRuleSetCompileDefaultOutput {
|
||||||
|
if strings.HasSuffix(sourcePath, ".txt") {
|
||||||
|
outputPath = sourcePath[:len(sourcePath)-4] + ".srs"
|
||||||
|
} else {
|
||||||
|
outputPath = sourcePath + ".srs"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputPath = flagRuleSetConvertOutput
|
||||||
|
}
|
||||||
|
outputFile, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outputFile.Close()
|
||||||
|
err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, true)
|
||||||
|
if err != nil {
|
||||||
|
outputFile.Close()
|
||||||
|
os.Remove(outputPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outputFile.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var flagRuleSetDecompileOutput string
|
||||||
|
|
||||||
|
const flagRuleSetDecompileDefaultOutput = "<file_name>.json"
|
||||||
|
|
||||||
|
var commandRuleSetDecompile = &cobra.Command{
|
||||||
|
Use: "decompile [binary-path]",
|
||||||
|
Short: "Decompile rule-set binary to json",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := decompileRuleSet(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandRuleSet.AddCommand(commandRuleSetDecompile)
|
||||||
|
commandRuleSetDecompile.Flags().StringVarP(&flagRuleSetDecompileOutput, "output", "o", flagRuleSetDecompileDefaultOutput, "Output file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func decompileRuleSet(sourcePath string) error {
|
||||||
|
var (
|
||||||
|
reader io.Reader
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sourcePath == "stdin" {
|
||||||
|
reader = os.Stdin
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plainRuleSet, err := srs.Read(reader, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ruleSet := option.PlainRuleSetCompat{
|
||||||
|
Version: C.RuleSetVersion1,
|
||||||
|
Options: plainRuleSet,
|
||||||
|
}
|
||||||
|
var outputPath string
|
||||||
|
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
|
||||||
|
if strings.HasSuffix(sourcePath, ".srs") {
|
||||||
|
outputPath = sourcePath[:len(sourcePath)-4] + ".json"
|
||||||
|
} else {
|
||||||
|
outputPath = sourcePath + ".json"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputPath = flagRuleSetDecompileOutput
|
||||||
|
}
|
||||||
|
outputFile, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoder := json.NewEncoder(outputFile)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
err = encoder.Encode(ruleSet)
|
||||||
|
if err != nil {
|
||||||
|
outputFile.Close()
|
||||||
|
os.Remove(outputPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outputFile.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandRuleSetFormatFlagWrite bool
|
||||||
|
|
||||||
|
var commandRuleSetFormat = &cobra.Command{
|
||||||
|
Use: "format <source-path>",
|
||||||
|
Short: "Format rule-set json",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := formatRuleSet(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
||||||
|
commandRuleSet.AddCommand(commandRuleSetFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRuleSet(sourcePath string) error {
|
||||||
|
var (
|
||||||
|
reader io.Reader
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sourcePath == "stdin" {
|
||||||
|
reader = os.Stdin
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
err = encoder.Encode(plainRuleSet)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "encode config")
|
||||||
|
}
|
||||||
|
outputPath, _ := filepath.Abs(sourcePath)
|
||||||
|
if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" {
|
||||||
|
os.Stdout.WriteString(buffer.String() + "\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if bytes.Equal(content, buffer.Bytes()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
output, err := os.Create(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "open output")
|
||||||
|
}
|
||||||
|
_, err = output.Write(buffer.Bytes())
|
||||||
|
output.Close()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "write output")
|
||||||
|
}
|
||||||
|
os.Stderr.WriteString(outputPath + "\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-box/route"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var flagRuleSetMatchFormat string
|
||||||
|
|
||||||
|
var commandRuleSetMatch = &cobra.Command{
|
||||||
|
Use: "match <rule-set path> <IP address/domain>",
|
||||||
|
Short: "Check if an IP address or a domain matches the rule-set",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := ruleSetMatch(args[0], args[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandRuleSetMatch.Flags().StringVarP(&flagRuleSetMatchFormat, "format", "f", "source", "rule-set format")
|
||||||
|
commandRuleSet.AddCommand(commandRuleSetMatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ruleSetMatch(sourcePath string, domain string) error {
|
||||||
|
var (
|
||||||
|
reader io.Reader
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sourcePath == "stdin" {
|
||||||
|
reader = os.Stdin
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read rule-set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read rule-set")
|
||||||
|
}
|
||||||
|
var plainRuleSet option.PlainRuleSet
|
||||||
|
switch flagRuleSetMatchFormat {
|
||||||
|
case C.RuleSetFormatSource:
|
||||||
|
var compat option.PlainRuleSetCompat
|
||||||
|
compat, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
plainRuleSet, err = compat.Upgrade()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case C.RuleSetFormatBinary:
|
||||||
|
plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return E.New("unknown rule-set format: ", flagRuleSetMatchFormat)
|
||||||
|
}
|
||||||
|
ipAddress := M.ParseAddr(domain)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
if ipAddress.IsValid() {
|
||||||
|
metadata.Destination = M.SocksaddrFrom(ipAddress, 0)
|
||||||
|
} else {
|
||||||
|
metadata.Domain = domain
|
||||||
|
}
|
||||||
|
for i, ruleOptions := range plainRuleSet.Rules {
|
||||||
|
var currentRule adapter.HeadlessRule
|
||||||
|
currentRule, err = route.NewHeadlessRule(nil, ruleOptions)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||||
|
}
|
||||||
|
if currentRule.Match(&metadata) {
|
||||||
|
println(F.ToString("match rules.[", i, "]: ", currentRule))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandRuleSetUpgradeFlagWrite bool
|
||||||
|
|
||||||
|
var commandRuleSetUpgrade = &cobra.Command{
|
||||||
|
Use: "upgrade <source-path>",
|
||||||
|
Short: "Upgrade rule-set json",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := upgradeRuleSet(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandRuleSetUpgrade.Flags().BoolVarP(&commandRuleSetUpgradeFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
||||||
|
commandRuleSet.AddCommand(commandRuleSetUpgrade)
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgradeRuleSet(sourcePath string) error {
|
||||||
|
var (
|
||||||
|
reader io.Reader
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sourcePath == "stdin" {
|
||||||
|
reader = os.Stdin
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
plainRuleSetCompat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch plainRuleSetCompat.Version {
|
||||||
|
case C.RuleSetVersion1:
|
||||||
|
default:
|
||||||
|
log.Info("already up-to-date")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
plainRuleSet, err := plainRuleSetCompat.Upgrade()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
encoder := json.NewEncoder(buffer)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
err = encoder.Encode(plainRuleSet)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "encode config")
|
||||||
|
}
|
||||||
|
outputPath, _ := filepath.Abs(sourcePath)
|
||||||
|
if !commandRuleSetUpgradeFlagWrite || sourcePath == "stdin" {
|
||||||
|
os.Stdout.WriteString(buffer.String() + "\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if bytes.Equal(content, buffer.Bytes()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
output, err := os.Create(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "open output")
|
||||||
|
}
|
||||||
|
_, err = output.Write(buffer.Bytes())
|
||||||
|
output.Close()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "write output")
|
||||||
|
}
|
||||||
|
os.Stderr.WriteString(outputPath + "\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+17
-8
@@ -13,10 +13,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box"
|
"github.com/sagernet/sing-box"
|
||||||
"github.com/sagernet/sing-box/common/badjsonmerge"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -55,8 +57,7 @@ func readConfigAt(path string) (*OptionsEntry, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "read config at ", path)
|
return nil, E.Cause(err, "read config at ", path)
|
||||||
}
|
}
|
||||||
var options option.Options
|
options, err := json.UnmarshalExtended[option.Options](configContent)
|
||||||
err = options.UnmarshalJSON(configContent)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "decode config at ", path)
|
return nil, E.Cause(err, "decode config at ", path)
|
||||||
}
|
}
|
||||||
@@ -106,13 +107,18 @@ func readConfigAndMerge() (option.Options, error) {
|
|||||||
if len(optionsList) == 1 {
|
if len(optionsList) == 1 {
|
||||||
return optionsList[0].options, nil
|
return optionsList[0].options, nil
|
||||||
}
|
}
|
||||||
var mergedOptions option.Options
|
var mergedMessage json.RawMessage
|
||||||
for _, options := range optionsList {
|
for _, options := range optionsList {
|
||||||
mergedOptions, err = badjsonmerge.MergeOptions(options.options, mergedOptions)
|
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return option.Options{}, E.Cause(err, "merge config at ", options.path)
|
return option.Options{}, E.Cause(err, "merge config at ", options.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var mergedOptions option.Options
|
||||||
|
err = mergedOptions.UnmarshalJSON(mergedMessage)
|
||||||
|
if err != nil {
|
||||||
|
return option.Options{}, E.Cause(err, "unmarshal merged config")
|
||||||
|
}
|
||||||
return mergedOptions, nil
|
return mergedOptions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +133,7 @@ func create() (*box.Box, context.CancelFunc, error) {
|
|||||||
}
|
}
|
||||||
options.Log.DisableColor = true
|
options.Log.DisableColor = true
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(globalCtx)
|
||||||
instance, err := box.New(box.Options{
|
instance, err := box.New(box.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Options: options,
|
Options: options,
|
||||||
@@ -182,9 +188,12 @@ func run() error {
|
|||||||
cancel()
|
cancel()
|
||||||
closeCtx, closed := context.WithCancel(context.Background())
|
closeCtx, closed := context.WithCancel(context.Background())
|
||||||
go closeMonitor(closeCtx)
|
go closeMonitor(closeCtx)
|
||||||
instance.Close()
|
err = instance.Close()
|
||||||
closed()
|
closed()
|
||||||
if osSignal != syscall.SIGHUP {
|
if osSignal != syscall.SIGHUP {
|
||||||
|
if err != nil {
|
||||||
|
log.Error(E.Cause(err, "sing-box did not closed properly"))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -193,7 +202,7 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func closeMonitor(ctx context.Context) {
|
func closeMonitor(ctx context.Context) {
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(C.FatalStopTimeout)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -38,11 +38,7 @@ func createPreStartedClient() (*box.Box, error) {
|
|||||||
|
|
||||||
func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
|
func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
|
||||||
if outboundTag == "" {
|
if outboundTag == "" {
|
||||||
outbound := instance.Router().DefaultOutbound(N.NetworkName(network))
|
return instance.Router().DefaultOutbound(N.NetworkName(network))
|
||||||
if outbound == nil {
|
|
||||||
return nil, E.New("missing default outbound")
|
|
||||||
}
|
|
||||||
return outbound, nil
|
|
||||||
} else {
|
} else {
|
||||||
outbound, loaded := instance.Router().Outbound(outboundTag)
|
outbound, loaded := instance.Router().Outbound(outboundTag)
|
||||||
if !loaded {
|
if !loaded {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
var commandConnectFlagNetwork string
|
var commandConnectFlagNetwork string
|
||||||
|
|
||||||
var commandConnect = &cobra.Command{
|
var commandConnect = &cobra.Command{
|
||||||
Use: "connect [address]",
|
Use: "connect <address>",
|
||||||
Short: "Connect to an address",
|
Short: "Connect to an address",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -32,7 +34,10 @@ func init() {
|
|||||||
commandTools.AddCommand(commandFetch)
|
commandTools.AddCommand(commandFetch)
|
||||||
}
|
}
|
||||||
|
|
||||||
var httpClient *http.Client
|
var (
|
||||||
|
httpClient *http.Client
|
||||||
|
http3Client *http.Client
|
||||||
|
)
|
||||||
|
|
||||||
func fetch(args []string) error {
|
func fetch(args []string) error {
|
||||||
instance, err := createPreStartedClient()
|
instance, err := createPreStartedClient()
|
||||||
@@ -53,8 +58,16 @@ func fetch(args []string) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
defer httpClient.CloseIdleConnections()
|
defer httpClient.CloseIdleConnections()
|
||||||
|
if C.WithQUIC {
|
||||||
|
err = initializeHTTP3Client(instance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer http3Client.CloseIdleConnections()
|
||||||
|
}
|
||||||
for _, urlString := range args {
|
for _, urlString := range args {
|
||||||
parsedURL, err := url.Parse(urlString)
|
var parsedURL *url.URL
|
||||||
|
parsedURL, err = url.Parse(urlString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -63,16 +76,27 @@ func fetch(args []string) error {
|
|||||||
parsedURL.Scheme = "http"
|
parsedURL.Scheme = "http"
|
||||||
fallthrough
|
fallthrough
|
||||||
case "http", "https":
|
case "http", "https":
|
||||||
err = fetchHTTP(parsedURL)
|
err = fetchHTTP(httpClient, parsedURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case "http3":
|
||||||
|
if !C.WithQUIC {
|
||||||
|
return C.ErrQUICNotIncluded
|
||||||
|
}
|
||||||
|
parsedURL.Scheme = "https"
|
||||||
|
err = fetchHTTP(http3Client, parsedURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return E.New("unsupported scheme: ", parsedURL.Scheme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchHTTP(parsedURL *url.URL) error {
|
func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
|
||||||
request, err := http.NewRequest("GET", parsedURL.String(), nil)
|
request, err := http.NewRequest("GET", parsedURL.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/quic-go/http3"
|
||||||
|
box "github.com/sagernet/sing-box"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeHTTP3Client(instance *box.Box) error {
|
||||||
|
dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http3Client = &http.Client{
|
||||||
|
Transport: &http3.RoundTripper{
|
||||||
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
destination := M.ParseSocksaddr(addr)
|
||||||
|
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||||
|
if dErr != nil {
|
||||||
|
return nil, dErr
|
||||||
|
}
|
||||||
|
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build !with_quic
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
box "github.com/sagernet/sing-box"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeHTTP3Client(instance *box.Box) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchHTTP3(parsedURL *url.URL) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
package adguard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type agdguardRuleLine struct {
|
||||||
|
ruleLine string
|
||||||
|
isRawDomain bool
|
||||||
|
isExclude bool
|
||||||
|
isSuffix bool
|
||||||
|
hasStart bool
|
||||||
|
hasEnd bool
|
||||||
|
isRegexp bool
|
||||||
|
isImportant bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Convert(reader io.Reader) ([]option.HeadlessRule, error) {
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
var (
|
||||||
|
ruleLines []agdguardRuleLine
|
||||||
|
ignoredLines int
|
||||||
|
)
|
||||||
|
parseLine:
|
||||||
|
for scanner.Scan() {
|
||||||
|
ruleLine := scanner.Text()
|
||||||
|
if ruleLine == "" || ruleLine[0] == '!' || ruleLine[0] == '#' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
originRuleLine := ruleLine
|
||||||
|
if M.IsDomainName(ruleLine) {
|
||||||
|
ruleLines = append(ruleLines, agdguardRuleLine{
|
||||||
|
ruleLine: ruleLine,
|
||||||
|
isRawDomain: true,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hostLine, err := parseAdGuardHostLine(ruleLine)
|
||||||
|
if err == nil {
|
||||||
|
if hostLine != "" {
|
||||||
|
ruleLines = append(ruleLines, agdguardRuleLine{
|
||||||
|
ruleLine: hostLine,
|
||||||
|
isRawDomain: true,
|
||||||
|
hasStart: true,
|
||||||
|
hasEnd: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(ruleLine, "|") {
|
||||||
|
ruleLine = ruleLine[:len(ruleLine)-1]
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
isExclude bool
|
||||||
|
isSuffix bool
|
||||||
|
hasStart bool
|
||||||
|
hasEnd bool
|
||||||
|
isRegexp bool
|
||||||
|
isImportant bool
|
||||||
|
)
|
||||||
|
if !strings.HasPrefix(ruleLine, "/") && strings.Contains(ruleLine, "$") {
|
||||||
|
params := common.SubstringAfter(ruleLine, "$")
|
||||||
|
for _, param := range strings.Split(params, ",") {
|
||||||
|
paramParts := strings.Split(param, "=")
|
||||||
|
var ignored bool
|
||||||
|
if len(paramParts) > 0 && len(paramParts) <= 2 {
|
||||||
|
switch paramParts[0] {
|
||||||
|
case "app", "network":
|
||||||
|
// maybe support by package_name/process_name
|
||||||
|
case "dnstype":
|
||||||
|
// maybe support by query_type
|
||||||
|
case "important":
|
||||||
|
ignored = true
|
||||||
|
isImportant = true
|
||||||
|
case "dnsrewrite":
|
||||||
|
if len(paramParts) == 2 && M.ParseAddr(paramParts[1]).IsUnspecified() {
|
||||||
|
ignored = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ignored {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with modifier: ", paramParts[0], ": ", ruleLine)
|
||||||
|
continue parseLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ruleLine = common.SubstringBefore(ruleLine, "$")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(ruleLine, "@@") {
|
||||||
|
ruleLine = ruleLine[2:]
|
||||||
|
isExclude = true
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(ruleLine, "|") {
|
||||||
|
ruleLine = ruleLine[:len(ruleLine)-1]
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(ruleLine, "||") {
|
||||||
|
ruleLine = ruleLine[2:]
|
||||||
|
isSuffix = true
|
||||||
|
} else if strings.HasPrefix(ruleLine, "|") {
|
||||||
|
ruleLine = ruleLine[1:]
|
||||||
|
hasStart = true
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(ruleLine, "^") {
|
||||||
|
ruleLine = ruleLine[:len(ruleLine)-1]
|
||||||
|
hasEnd = true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(ruleLine, "/") && strings.HasSuffix(ruleLine, "/") {
|
||||||
|
ruleLine = ruleLine[1 : len(ruleLine)-1]
|
||||||
|
if ignoreIPCIDRRegexp(ruleLine) {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with IPCIDR regexp: ", ruleLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
isRegexp = true
|
||||||
|
} else {
|
||||||
|
if strings.Contains(ruleLine, "://") {
|
||||||
|
ruleLine = common.SubstringAfter(ruleLine, "://")
|
||||||
|
}
|
||||||
|
if strings.Contains(ruleLine, "/") {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with path: ", ruleLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(ruleLine, "##") {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(ruleLine, "#$#") {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with element hiding: ", ruleLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var domainCheck string
|
||||||
|
if strings.HasPrefix(ruleLine, ".") || strings.HasPrefix(ruleLine, "-") {
|
||||||
|
domainCheck = "r" + ruleLine
|
||||||
|
} else {
|
||||||
|
domainCheck = ruleLine
|
||||||
|
}
|
||||||
|
if ruleLine == "" {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with empty domain", originRuleLine)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
domainCheck = strings.ReplaceAll(domainCheck, "*", "x")
|
||||||
|
if !M.IsDomainName(domainCheck) {
|
||||||
|
_, ipErr := parseADGuardIPCIDRLine(ruleLine)
|
||||||
|
if ipErr == nil {
|
||||||
|
ignoredLines++
|
||||||
|
log.Debug("ignored unsupported rule with IPCIDR: ", ruleLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if M.ParseSocksaddr(domainCheck).Port != 0 {
|
||||||
|
log.Debug("ignored unsupported rule with port: ", ruleLine)
|
||||||
|
} else {
|
||||||
|
log.Debug("ignored unsupported rule with invalid domain: ", ruleLine)
|
||||||
|
}
|
||||||
|
ignoredLines++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ruleLines = append(ruleLines, agdguardRuleLine{
|
||||||
|
ruleLine: ruleLine,
|
||||||
|
isExclude: isExclude,
|
||||||
|
isSuffix: isSuffix,
|
||||||
|
hasStart: hasStart,
|
||||||
|
hasEnd: hasEnd,
|
||||||
|
isRegexp: isRegexp,
|
||||||
|
isImportant: isImportant,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(ruleLines) == 0 {
|
||||||
|
return nil, E.New("AdGuard rule-set is empty or all rules are unsupported")
|
||||||
|
}
|
||||||
|
if common.All(ruleLines, func(it agdguardRuleLine) bool {
|
||||||
|
return it.isRawDomain
|
||||||
|
}) {
|
||||||
|
return []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultHeadlessRule{
|
||||||
|
Domain: common.Map(ruleLines, func(it agdguardRuleLine) string {
|
||||||
|
return it.ruleLine
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
mapDomain := func(it agdguardRuleLine) string {
|
||||||
|
ruleLine := it.ruleLine
|
||||||
|
if it.isSuffix {
|
||||||
|
ruleLine = "||" + ruleLine
|
||||||
|
} else if it.hasStart {
|
||||||
|
ruleLine = "|" + ruleLine
|
||||||
|
}
|
||||||
|
if it.hasEnd {
|
||||||
|
ruleLine += "^"
|
||||||
|
}
|
||||||
|
return ruleLine
|
||||||
|
}
|
||||||
|
|
||||||
|
importantDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain)
|
||||||
|
importantDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && !it.isExclude }), mapDomain)
|
||||||
|
importantExcludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && !it.isRegexp && it.isExclude }), mapDomain)
|
||||||
|
importantExcludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return it.isImportant && it.isRegexp && it.isExclude }), mapDomain)
|
||||||
|
domain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && !it.isExclude }), mapDomain)
|
||||||
|
domainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && !it.isExclude }), mapDomain)
|
||||||
|
excludeDomain := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && !it.isRegexp && it.isExclude }), mapDomain)
|
||||||
|
excludeDomainRegex := common.Map(common.Filter(ruleLines, func(it agdguardRuleLine) bool { return !it.isImportant && it.isRegexp && it.isExclude }), mapDomain)
|
||||||
|
currentRule := option.HeadlessRule{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultHeadlessRule{
|
||||||
|
AdGuardDomain: domain,
|
||||||
|
DomainRegex: domainRegex,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if len(excludeDomain) > 0 || len(excludeDomainRegex) > 0 {
|
||||||
|
currentRule = option.HeadlessRule{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalHeadlessRule{
|
||||||
|
Mode: C.LogicalTypeAnd,
|
||||||
|
Rules: []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultHeadlessRule{
|
||||||
|
AdGuardDomain: excludeDomain,
|
||||||
|
DomainRegex: excludeDomainRegex,
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentRule,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(importantDomain) > 0 || len(importantDomainRegex) > 0 {
|
||||||
|
currentRule = option.HeadlessRule{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalHeadlessRule{
|
||||||
|
Mode: C.LogicalTypeOr,
|
||||||
|
Rules: []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultHeadlessRule{
|
||||||
|
AdGuardDomain: importantDomain,
|
||||||
|
DomainRegex: importantDomainRegex,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentRule,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(importantExcludeDomain) > 0 || len(importantExcludeDomainRegex) > 0 {
|
||||||
|
currentRule = option.HeadlessRule{
|
||||||
|
Type: C.RuleTypeLogical,
|
||||||
|
LogicalOptions: option.LogicalHeadlessRule{
|
||||||
|
Mode: C.LogicalTypeAnd,
|
||||||
|
Rules: []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultHeadlessRule{
|
||||||
|
AdGuardDomain: importantExcludeDomain,
|
||||||
|
DomainRegex: importantExcludeDomainRegex,
|
||||||
|
Invert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
currentRule,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info("parsed rules: ", len(ruleLines), "/", len(ruleLines)+ignoredLines)
|
||||||
|
return []option.HeadlessRule{currentRule}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ignoreIPCIDRRegexp(ruleLine string) bool {
|
||||||
|
if strings.HasPrefix(ruleLine, "(http?:\\/\\/)") {
|
||||||
|
ruleLine = ruleLine[12:]
|
||||||
|
} else if strings.HasPrefix(ruleLine, "(https?:\\/\\/)") {
|
||||||
|
ruleLine = ruleLine[13:]
|
||||||
|
} else if strings.HasPrefix(ruleLine, "^") {
|
||||||
|
ruleLine = ruleLine[1:]
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, parseErr := strconv.ParseUint(common.SubstringBefore(ruleLine, "\\."), 10, 8)
|
||||||
|
return parseErr == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAdGuardHostLine(ruleLine string) (string, error) {
|
||||||
|
idx := strings.Index(ruleLine, " ")
|
||||||
|
if idx == -1 {
|
||||||
|
return "", os.ErrInvalid
|
||||||
|
}
|
||||||
|
address, err := netip.ParseAddr(ruleLine[:idx])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !address.IsUnspecified() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
domain := ruleLine[idx+1:]
|
||||||
|
if !M.IsDomainName(domain) {
|
||||||
|
return "", E.New("invalid domain name: ", domain)
|
||||||
|
}
|
||||||
|
return domain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseADGuardIPCIDRLine(ruleLine string) (netip.Prefix, error) {
|
||||||
|
var isPrefix bool
|
||||||
|
if strings.HasSuffix(ruleLine, ".") {
|
||||||
|
isPrefix = true
|
||||||
|
ruleLine = ruleLine[:len(ruleLine)-1]
|
||||||
|
}
|
||||||
|
ruleStringParts := strings.Split(ruleLine, ".")
|
||||||
|
if len(ruleStringParts) > 4 || len(ruleStringParts) < 4 && !isPrefix {
|
||||||
|
return netip.Prefix{}, os.ErrInvalid
|
||||||
|
}
|
||||||
|
ruleParts := make([]uint8, 0, len(ruleStringParts))
|
||||||
|
for _, part := range ruleStringParts {
|
||||||
|
rulePart, err := strconv.ParseUint(part, 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
return netip.Prefix{}, err
|
||||||
|
}
|
||||||
|
ruleParts = append(ruleParts, uint8(rulePart))
|
||||||
|
}
|
||||||
|
bitLen := len(ruleParts) * 8
|
||||||
|
for len(ruleParts) < 4 {
|
||||||
|
ruleParts = append(ruleParts, 0)
|
||||||
|
}
|
||||||
|
return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ruleParts)), bitLen), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package adguard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/route"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConverter(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
rules, err := Convert(strings.NewReader(`
|
||||||
|
||example.org^
|
||||||
|
|example.com^
|
||||||
|
example.net^
|
||||||
|
||example.edu
|
||||||
|
||example.edu.tw^
|
||||||
|
|example.gov
|
||||||
|
example.arpa
|
||||||
|
@@|sagernet.example.org|
|
||||||
|
||sagernet.org^$important
|
||||||
|
@@|sing-box.sagernet.org^$important
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, rules, 1)
|
||||||
|
rule, err := route.NewHeadlessRule(nil, rules[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
matchDomain := []string{
|
||||||
|
"example.org",
|
||||||
|
"www.example.org",
|
||||||
|
"example.com",
|
||||||
|
"example.net",
|
||||||
|
"isexample.net",
|
||||||
|
"www.example.net",
|
||||||
|
"example.edu",
|
||||||
|
"example.edu.cn",
|
||||||
|
"example.edu.tw",
|
||||||
|
"www.example.edu",
|
||||||
|
"www.example.edu.cn",
|
||||||
|
"example.gov",
|
||||||
|
"example.gov.cn",
|
||||||
|
"example.arpa",
|
||||||
|
"www.example.arpa",
|
||||||
|
"isexample.arpa",
|
||||||
|
"example.arpa.cn",
|
||||||
|
"www.example.arpa.cn",
|
||||||
|
"isexample.arpa.cn",
|
||||||
|
"sagernet.org",
|
||||||
|
"www.sagernet.org",
|
||||||
|
}
|
||||||
|
notMatchDomain := []string{
|
||||||
|
"example.org.cn",
|
||||||
|
"notexample.org",
|
||||||
|
"example.com.cn",
|
||||||
|
"www.example.com.cn",
|
||||||
|
"example.net.cn",
|
||||||
|
"notexample.edu",
|
||||||
|
"notexample.edu.cn",
|
||||||
|
"www.example.gov",
|
||||||
|
"notexample.gov",
|
||||||
|
"sagernet.example.org",
|
||||||
|
"sing-box.sagernet.org",
|
||||||
|
}
|
||||||
|
for _, domain := range matchDomain {
|
||||||
|
require.True(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
for _, domain := range notMatchDomain {
|
||||||
|
require.False(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHosts(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
rules, err := Convert(strings.NewReader(`
|
||||||
|
127.0.0.1 localhost
|
||||||
|
::1 localhost #[IPv6]
|
||||||
|
0.0.0.0 google.com
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, rules, 1)
|
||||||
|
rule, err := route.NewHeadlessRule(nil, rules[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
matchDomain := []string{
|
||||||
|
"google.com",
|
||||||
|
}
|
||||||
|
notMatchDomain := []string{
|
||||||
|
"www.google.com",
|
||||||
|
"notgoogle.com",
|
||||||
|
"localhost",
|
||||||
|
}
|
||||||
|
for _, domain := range matchDomain {
|
||||||
|
require.True(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
for _, domain := range notMatchDomain {
|
||||||
|
require.False(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleHosts(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
rules, err := Convert(strings.NewReader(`
|
||||||
|
example.com
|
||||||
|
www.example.org
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, rules, 1)
|
||||||
|
rule, err := route.NewHeadlessRule(nil, rules[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
matchDomain := []string{
|
||||||
|
"example.com",
|
||||||
|
"www.example.org",
|
||||||
|
}
|
||||||
|
notMatchDomain := []string{
|
||||||
|
"example.com.cn",
|
||||||
|
"www.example.com",
|
||||||
|
"notexample.com",
|
||||||
|
"example.org",
|
||||||
|
}
|
||||||
|
for _, domain := range matchDomain {
|
||||||
|
require.True(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
for _, domain := range notMatchDomain {
|
||||||
|
require.False(t, rule.Match(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}), domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-3
@@ -1,16 +1,21 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/sagernet/sing-box/include"
|
_ "github.com/sagernet/sing-box/include"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/service/filemanager"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
globalCtx context.Context
|
||||||
configPaths []string
|
configPaths []string
|
||||||
configDirectories []string
|
configDirectories []string
|
||||||
workingDir string
|
workingDir string
|
||||||
@@ -36,15 +41,30 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func preRun(cmd *cobra.Command, args []string) {
|
func preRun(cmd *cobra.Command, args []string) {
|
||||||
|
globalCtx = context.Background()
|
||||||
|
sudoUser := os.Getenv("SUDO_USER")
|
||||||
|
sudoUID, _ := strconv.Atoi(os.Getenv("SUDO_UID"))
|
||||||
|
sudoGID, _ := strconv.Atoi(os.Getenv("SUDO_GID"))
|
||||||
|
if sudoUID == 0 && sudoGID == 0 && sudoUser != "" {
|
||||||
|
sudoUserObject, _ := user.Lookup(sudoUser)
|
||||||
|
if sudoUserObject != nil {
|
||||||
|
sudoUID, _ = strconv.Atoi(sudoUserObject.Uid)
|
||||||
|
sudoGID, _ = strconv.Atoi(sudoUserObject.Gid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sudoUID > 0 && sudoGID > 0 {
|
||||||
|
globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
|
||||||
|
}
|
||||||
if disableColor {
|
if disableColor {
|
||||||
log.SetStdLogger(log.NewFactory(log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, nil).Logger())
|
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
|
||||||
}
|
}
|
||||||
if workingDir != "" {
|
if workingDir != "" {
|
||||||
_, err := os.Stat(workingDir)
|
_, err := os.Stat(workingDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.MkdirAll(workingDir, 0o777)
|
filemanager.MkdirAll(globalCtx, workingDir, 0o777)
|
||||||
}
|
}
|
||||||
if err := os.Chdir(workingDir); err != nil {
|
err = os.Chdir(workingDir)
|
||||||
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package badjson
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/json"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type JSONArray []any
|
|
||||||
|
|
||||||
func (a JSONArray) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal([]any(a))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *JSONArray) UnmarshalJSON(content []byte) error {
|
|
||||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
|
||||||
arrayStart, err := decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if arrayStart != json.Delim('[') {
|
|
||||||
return E.New("excepted array start, but got ", arrayStart)
|
|
||||||
}
|
|
||||||
err = a.decodeJSON(decoder)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
arrayEnd, err := decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if arrayEnd != json.Delim(']') {
|
|
||||||
return E.New("excepted array end, but got ", arrayEnd)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *JSONArray) decodeJSON(decoder *json.Decoder) error {
|
|
||||||
for decoder.More() {
|
|
||||||
item, err := decodeJSON(decoder)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*a = append(*a, item)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package badjson
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/json"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Decode(content []byte) (any, error) {
|
|
||||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
|
||||||
return decodeJSON(decoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeJSON(decoder *json.Decoder) (any, error) {
|
|
||||||
rawToken, err := decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch token := rawToken.(type) {
|
|
||||||
case json.Delim:
|
|
||||||
switch token {
|
|
||||||
case '{':
|
|
||||||
var object JSONObject
|
|
||||||
err = object.decodeJSON(decoder)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rawToken, err = decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if rawToken != json.Delim('}') {
|
|
||||||
return nil, E.New("excepted object end, but got ", rawToken)
|
|
||||||
}
|
|
||||||
return &object, nil
|
|
||||||
case '[':
|
|
||||||
var array JSONArray
|
|
||||||
err = array.decodeJSON(decoder)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rawToken, err = decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if rawToken != json.Delim(']') {
|
|
||||||
return nil, E.New("excepted array end, but got ", rawToken)
|
|
||||||
}
|
|
||||||
return array, nil
|
|
||||||
default:
|
|
||||||
return nil, E.New("excepted object or array end: ", token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rawToken, nil
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package badjson
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/json"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/x/linkedhashmap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type JSONObject struct {
|
|
||||||
linkedhashmap.Map[string, any]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m JSONObject) MarshalJSON() ([]byte, error) {
|
|
||||||
buffer := new(bytes.Buffer)
|
|
||||||
buffer.WriteString("{")
|
|
||||||
items := m.Entries()
|
|
||||||
iLen := len(items)
|
|
||||||
for i, entry := range items {
|
|
||||||
keyContent, err := json.Marshal(entry.Key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.WriteString(strings.TrimSpace(string(keyContent)))
|
|
||||||
buffer.WriteString(": ")
|
|
||||||
valueContent, err := json.Marshal(entry.Value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.WriteString(strings.TrimSpace(string(valueContent)))
|
|
||||||
if i < iLen-1 {
|
|
||||||
buffer.WriteString(", ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buffer.WriteString("}")
|
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *JSONObject) UnmarshalJSON(content []byte) error {
|
|
||||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
|
||||||
m.Clear()
|
|
||||||
objectStart, err := decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if objectStart != json.Delim('{') {
|
|
||||||
return E.New("expected json object start, but starts with ", objectStart)
|
|
||||||
}
|
|
||||||
err = m.decodeJSON(decoder)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "decode json object content")
|
|
||||||
}
|
|
||||||
objectEnd, err := decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if objectEnd != json.Delim('}') {
|
|
||||||
return E.New("expected json object end, but ends with ", objectEnd)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *JSONObject) decodeJSON(decoder *json.Decoder) error {
|
|
||||||
for decoder.More() {
|
|
||||||
var entryKey string
|
|
||||||
keyToken, err := decoder.Token()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
entryKey = keyToken.(string)
|
|
||||||
var entryValue any
|
|
||||||
entryValue, err = decodeJSON(decoder)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "decode value for ", entryKey)
|
|
||||||
}
|
|
||||||
m.Put(entryKey, entryValue)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package badjsonmerge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/badjson"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
func MergeOptions(source option.Options, destination option.Options) (option.Options, error) {
|
|
||||||
rawSource, err := json.Marshal(source)
|
|
||||||
if err != nil {
|
|
||||||
return option.Options{}, E.Cause(err, "marshal source")
|
|
||||||
}
|
|
||||||
rawDestination, err := json.Marshal(destination)
|
|
||||||
if err != nil {
|
|
||||||
return option.Options{}, E.Cause(err, "marshal destination")
|
|
||||||
}
|
|
||||||
rawMerged, err := MergeJSON(rawSource, rawDestination)
|
|
||||||
if err != nil {
|
|
||||||
return option.Options{}, E.Cause(err, "merge options")
|
|
||||||
}
|
|
||||||
var merged option.Options
|
|
||||||
err = json.Unmarshal(rawMerged, &merged)
|
|
||||||
if err != nil {
|
|
||||||
return option.Options{}, E.Cause(err, "unmarshal merged options")
|
|
||||||
}
|
|
||||||
return merged, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeJSON(rawSource json.RawMessage, rawDestination json.RawMessage) (json.RawMessage, error) {
|
|
||||||
source, err := badjson.Decode(rawSource)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "decode source")
|
|
||||||
}
|
|
||||||
destination, err := badjson.Decode(rawDestination)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "decode destination")
|
|
||||||
}
|
|
||||||
merged, err := mergeJSON(source, destination)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return json.Marshal(merged)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeJSON(anySource any, anyDestination any) (any, error) {
|
|
||||||
switch destination := anyDestination.(type) {
|
|
||||||
case badjson.JSONArray:
|
|
||||||
switch source := anySource.(type) {
|
|
||||||
case badjson.JSONArray:
|
|
||||||
destination = append(destination, source...)
|
|
||||||
default:
|
|
||||||
destination = append(destination, source)
|
|
||||||
}
|
|
||||||
return destination, nil
|
|
||||||
case *badjson.JSONObject:
|
|
||||||
switch source := anySource.(type) {
|
|
||||||
case *badjson.JSONObject:
|
|
||||||
for _, entry := range source.Entries() {
|
|
||||||
oldValue, loaded := destination.Get(entry.Key)
|
|
||||||
if loaded {
|
|
||||||
var err error
|
|
||||||
entry.Value, err = mergeJSON(entry.Value, oldValue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "merge object item ", entry.Key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destination.Put(entry.Key, entry.Value)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, E.New("cannot merge json object into ", reflect.TypeOf(destination))
|
|
||||||
}
|
|
||||||
return destination, nil
|
|
||||||
default:
|
|
||||||
return destination, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package badjsonmerge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMergeJSON(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
options := option.Options{
|
|
||||||
Log: &option.LogOptions{
|
|
||||||
Level: "info",
|
|
||||||
},
|
|
||||||
Route: &option.RouteOptions{
|
|
||||||
Rules: []option.Rule{
|
|
||||||
{
|
|
||||||
Type: C.RuleTypeDefault,
|
|
||||||
DefaultOptions: option.DefaultRule{
|
|
||||||
Network: []string{N.NetworkTCP},
|
|
||||||
Outbound: "direct",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
anotherOptions := option.Options{
|
|
||||||
Outbounds: []option.Outbound{
|
|
||||||
{
|
|
||||||
Type: C.TypeDirect,
|
|
||||||
Tag: "direct",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
thirdOptions := option.Options{
|
|
||||||
Route: &option.RouteOptions{
|
|
||||||
Rules: []option.Rule{
|
|
||||||
{
|
|
||||||
Type: C.RuleTypeDefault,
|
|
||||||
DefaultOptions: option.DefaultRule{
|
|
||||||
Network: []string{N.NetworkUDP},
|
|
||||||
Outbound: "direct",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mergeOptions, err := MergeOptions(options, anotherOptions)
|
|
||||||
require.NoError(t, err)
|
|
||||||
mergeOptions, err = MergeOptions(thirdOptions, mergeOptions)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "info", mergeOptions.Log.Level)
|
|
||||||
require.Equal(t, 2, len(mergeOptions.Route.Rules))
|
|
||||||
require.Equal(t, C.TypeDirect, mergeOptions.Outbounds[0].Type)
|
|
||||||
}
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
//go:build go1.20 && !go1.21
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
aTLS "github.com/sagernet/sing/common/tls"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Conn struct {
|
|
||||||
*tls.Conn
|
|
||||||
writer N.ExtendedWriter
|
|
||||||
isHandshakeComplete *atomic.Bool
|
|
||||||
activeCall *atomic.Int32
|
|
||||||
closeNotifySent *bool
|
|
||||||
version *uint16
|
|
||||||
rand io.Reader
|
|
||||||
halfAccess *sync.Mutex
|
|
||||||
halfError *error
|
|
||||||
cipher cipher.AEAD
|
|
||||||
explicitNonceLen int
|
|
||||||
halfPtr uintptr
|
|
||||||
halfSeq []byte
|
|
||||||
halfScratchBuf []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func TryCreate(conn aTLS.Conn) aTLS.Conn {
|
|
||||||
tlsConn, ok := conn.(*tls.Conn)
|
|
||||||
if !ok {
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
badConn, err := Create(tlsConn)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("initialize badtls: ", err)
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
return badConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func Create(conn *tls.Conn) (aTLS.Conn, error) {
|
|
||||||
rawConn := reflect.Indirect(reflect.ValueOf(conn))
|
|
||||||
rawIsHandshakeComplete := rawConn.FieldByName("isHandshakeComplete")
|
|
||||||
if !rawIsHandshakeComplete.IsValid() || rawIsHandshakeComplete.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid isHandshakeComplete")
|
|
||||||
}
|
|
||||||
isHandshakeComplete := (*atomic.Bool)(unsafe.Pointer(rawIsHandshakeComplete.UnsafeAddr()))
|
|
||||||
if !isHandshakeComplete.Load() {
|
|
||||||
return nil, E.New("handshake not finished")
|
|
||||||
}
|
|
||||||
rawActiveCall := rawConn.FieldByName("activeCall")
|
|
||||||
if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid active call")
|
|
||||||
}
|
|
||||||
activeCall := (*atomic.Int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr()))
|
|
||||||
rawHalfConn := rawConn.FieldByName("out")
|
|
||||||
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid half conn")
|
|
||||||
}
|
|
||||||
rawVersion := rawConn.FieldByName("vers")
|
|
||||||
if !rawVersion.IsValid() || rawVersion.Kind() != reflect.Uint16 {
|
|
||||||
return nil, E.New("badtls: invalid version")
|
|
||||||
}
|
|
||||||
version := (*uint16)(unsafe.Pointer(rawVersion.UnsafeAddr()))
|
|
||||||
rawCloseNotifySent := rawConn.FieldByName("closeNotifySent")
|
|
||||||
if !rawCloseNotifySent.IsValid() || rawCloseNotifySent.Kind() != reflect.Bool {
|
|
||||||
return nil, E.New("badtls: invalid notify")
|
|
||||||
}
|
|
||||||
closeNotifySent := (*bool)(unsafe.Pointer(rawCloseNotifySent.UnsafeAddr()))
|
|
||||||
rawConfig := reflect.Indirect(rawConn.FieldByName("config"))
|
|
||||||
if !rawConfig.IsValid() || rawConfig.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: bad config")
|
|
||||||
}
|
|
||||||
config := (*tls.Config)(unsafe.Pointer(rawConfig.UnsafeAddr()))
|
|
||||||
randReader := config.Rand
|
|
||||||
if randReader == nil {
|
|
||||||
randReader = rand.Reader
|
|
||||||
}
|
|
||||||
rawHalfMutex := rawHalfConn.FieldByName("Mutex")
|
|
||||||
if !rawHalfMutex.IsValid() || rawHalfMutex.Kind() != reflect.Struct {
|
|
||||||
return nil, E.New("badtls: invalid half mutex")
|
|
||||||
}
|
|
||||||
halfAccess := (*sync.Mutex)(unsafe.Pointer(rawHalfMutex.UnsafeAddr()))
|
|
||||||
rawHalfError := rawHalfConn.FieldByName("err")
|
|
||||||
if !rawHalfError.IsValid() || rawHalfError.Kind() != reflect.Interface {
|
|
||||||
return nil, E.New("badtls: invalid half error")
|
|
||||||
}
|
|
||||||
halfError := (*error)(unsafe.Pointer(rawHalfError.UnsafeAddr()))
|
|
||||||
rawHalfCipherInterface := rawHalfConn.FieldByName("cipher")
|
|
||||||
if !rawHalfCipherInterface.IsValid() || rawHalfCipherInterface.Kind() != reflect.Interface {
|
|
||||||
return nil, E.New("badtls: invalid cipher interface")
|
|
||||||
}
|
|
||||||
rawHalfCipher := rawHalfCipherInterface.Elem()
|
|
||||||
aeadCipher, loaded := valueInterface(rawHalfCipher, false).(cipher.AEAD)
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("badtls: invalid AEAD cipher")
|
|
||||||
}
|
|
||||||
var explicitNonceLen int
|
|
||||||
switch cipherName := reflect.Indirect(rawHalfCipher).Type().String(); cipherName {
|
|
||||||
case "tls.prefixNonceAEAD":
|
|
||||||
explicitNonceLen = aeadCipher.NonceSize()
|
|
||||||
case "tls.xorNonceAEAD":
|
|
||||||
default:
|
|
||||||
return nil, E.New("badtls: unknown cipher type: ", cipherName)
|
|
||||||
}
|
|
||||||
rawHalfSeq := rawHalfConn.FieldByName("seq")
|
|
||||||
if !rawHalfSeq.IsValid() || rawHalfSeq.Kind() != reflect.Array {
|
|
||||||
return nil, E.New("badtls: invalid seq")
|
|
||||||
}
|
|
||||||
halfSeq := rawHalfSeq.Bytes()
|
|
||||||
rawHalfScratchBuf := rawHalfConn.FieldByName("scratchBuf")
|
|
||||||
if !rawHalfScratchBuf.IsValid() || rawHalfScratchBuf.Kind() != reflect.Array {
|
|
||||||
return nil, E.New("badtls: invalid scratchBuf")
|
|
||||||
}
|
|
||||||
halfScratchBuf := rawHalfScratchBuf.Bytes()
|
|
||||||
return &Conn{
|
|
||||||
Conn: conn,
|
|
||||||
writer: bufio.NewExtendedWriter(conn.NetConn()),
|
|
||||||
isHandshakeComplete: isHandshakeComplete,
|
|
||||||
activeCall: activeCall,
|
|
||||||
closeNotifySent: closeNotifySent,
|
|
||||||
version: version,
|
|
||||||
halfAccess: halfAccess,
|
|
||||||
halfError: halfError,
|
|
||||||
cipher: aeadCipher,
|
|
||||||
explicitNonceLen: explicitNonceLen,
|
|
||||||
rand: randReader,
|
|
||||||
halfPtr: rawHalfConn.UnsafeAddr(),
|
|
||||||
halfSeq: halfSeq,
|
|
||||||
halfScratchBuf: halfScratchBuf,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) WriteBuffer(buffer *buf.Buffer) error {
|
|
||||||
if buffer.Len() > maxPlaintext {
|
|
||||||
defer buffer.Release()
|
|
||||||
return common.Error(c.Write(buffer.Bytes()))
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
x := c.activeCall.Load()
|
|
||||||
if x&1 != 0 {
|
|
||||||
return net.ErrClosed
|
|
||||||
}
|
|
||||||
if c.activeCall.CompareAndSwap(x, x+2) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer c.activeCall.Add(-2)
|
|
||||||
c.halfAccess.Lock()
|
|
||||||
defer c.halfAccess.Unlock()
|
|
||||||
if err := *c.halfError; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *c.closeNotifySent {
|
|
||||||
return errShutdown
|
|
||||||
}
|
|
||||||
dataLen := buffer.Len()
|
|
||||||
dataBytes := buffer.Bytes()
|
|
||||||
outBuf := buffer.ExtendHeader(recordHeaderLen + c.explicitNonceLen)
|
|
||||||
outBuf[0] = 23
|
|
||||||
version := *c.version
|
|
||||||
if version == 0 {
|
|
||||||
version = tls.VersionTLS10
|
|
||||||
} else if version == tls.VersionTLS13 {
|
|
||||||
version = tls.VersionTLS12
|
|
||||||
}
|
|
||||||
binary.BigEndian.PutUint16(outBuf[1:], version)
|
|
||||||
var nonce []byte
|
|
||||||
if c.explicitNonceLen > 0 {
|
|
||||||
nonce = outBuf[5 : 5+c.explicitNonceLen]
|
|
||||||
if c.explicitNonceLen < 16 {
|
|
||||||
copy(nonce, c.halfSeq)
|
|
||||||
} else {
|
|
||||||
if _, err := io.ReadFull(c.rand, nonce); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(nonce) == 0 {
|
|
||||||
nonce = c.halfSeq
|
|
||||||
}
|
|
||||||
if *c.version == tls.VersionTLS13 {
|
|
||||||
buffer.FreeBytes()[0] = 23
|
|
||||||
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+1+c.cipher.Overhead()))
|
|
||||||
c.cipher.Seal(outBuf, nonce, outBuf[recordHeaderLen:recordHeaderLen+c.explicitNonceLen+dataLen+1], outBuf[:recordHeaderLen])
|
|
||||||
buffer.Extend(1 + c.cipher.Overhead())
|
|
||||||
} else {
|
|
||||||
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen))
|
|
||||||
additionalData := append(c.halfScratchBuf[:0], c.halfSeq...)
|
|
||||||
additionalData = append(additionalData, outBuf[:recordHeaderLen]...)
|
|
||||||
c.cipher.Seal(outBuf, nonce, dataBytes, additionalData)
|
|
||||||
buffer.Extend(c.cipher.Overhead())
|
|
||||||
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+c.explicitNonceLen+c.cipher.Overhead()))
|
|
||||||
}
|
|
||||||
incSeq(c.halfPtr)
|
|
||||||
log.Trace("badtls write ", buffer.Len())
|
|
||||||
return c.writer.WriteBuffer(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) FrontHeadroom() int {
|
|
||||||
return recordHeaderLen + c.explicitNonceLen
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) RearHeadroom() int {
|
|
||||||
return 1 + c.cipher.Overhead()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) WriterMTU() int {
|
|
||||||
return maxPlaintext
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) Upstream() any {
|
|
||||||
return c.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) UpstreamWriter() any {
|
|
||||||
return c.NetConn()
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
//go:build !go1.19 || go1.21
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
aTLS "github.com/sagernet/sing/common/tls"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Create(conn *tls.Conn) (aTLS.Conn, error) {
|
|
||||||
return nil, os.ErrInvalid
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
//go:build go1.20 && !go.1.21
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
_ "unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxPlaintext = 16384 // maximum plaintext payload length
|
|
||||||
recordHeaderLen = 5 // record header length
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:linkname errShutdown crypto/tls.errShutdown
|
|
||||||
var errShutdown error
|
|
||||||
|
|
||||||
//go:linkname incSeq crypto/tls.(*halfConn).incSeq
|
|
||||||
func incSeq(conn uintptr)
|
|
||||||
|
|
||||||
//go:linkname valueInterface reflect.valueInterface
|
|
||||||
func valueInterface(v reflect.Value, safe bool) any
|
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
//go:build go1.21 && !without_badtls
|
||||||
|
|
||||||
|
package badtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ N.ReadWaiter = (*ReadWaitConn)(nil)
|
||||||
|
|
||||||
|
type ReadWaitConn struct {
|
||||||
|
tls.Conn
|
||||||
|
halfAccess *sync.Mutex
|
||||||
|
rawInput *bytes.Buffer
|
||||||
|
input *bytes.Reader
|
||||||
|
hand *bytes.Buffer
|
||||||
|
readWaitOptions N.ReadWaitOptions
|
||||||
|
tlsReadRecord func() error
|
||||||
|
tlsHandlePostHandshakeMessage func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
|
||||||
|
var (
|
||||||
|
loaded bool
|
||||||
|
tlsReadRecord func() error
|
||||||
|
tlsHandlePostHandshakeMessage func() error
|
||||||
|
)
|
||||||
|
for _, tlsCreator := range tlsRegistry {
|
||||||
|
loaded, tlsReadRecord, tlsHandlePostHandshakeMessage = tlsCreator(conn)
|
||||||
|
if loaded {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !loaded {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
rawConn := reflect.Indirect(reflect.ValueOf(conn))
|
||||||
|
rawHalfConn := rawConn.FieldByName("in")
|
||||||
|
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("badtls: invalid half conn")
|
||||||
|
}
|
||||||
|
rawHalfMutex := rawHalfConn.FieldByName("Mutex")
|
||||||
|
if !rawHalfMutex.IsValid() || rawHalfMutex.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("badtls: invalid half mutex")
|
||||||
|
}
|
||||||
|
halfAccess := (*sync.Mutex)(unsafe.Pointer(rawHalfMutex.UnsafeAddr()))
|
||||||
|
rawRawInput := rawConn.FieldByName("rawInput")
|
||||||
|
if !rawRawInput.IsValid() || rawRawInput.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("badtls: invalid raw input")
|
||||||
|
}
|
||||||
|
rawInput := (*bytes.Buffer)(unsafe.Pointer(rawRawInput.UnsafeAddr()))
|
||||||
|
rawInput0 := rawConn.FieldByName("input")
|
||||||
|
if !rawInput0.IsValid() || rawInput0.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("badtls: invalid input")
|
||||||
|
}
|
||||||
|
input := (*bytes.Reader)(unsafe.Pointer(rawInput0.UnsafeAddr()))
|
||||||
|
rawHand := rawConn.FieldByName("hand")
|
||||||
|
if !rawHand.IsValid() || rawHand.Kind() != reflect.Struct {
|
||||||
|
return nil, E.New("badtls: invalid hand")
|
||||||
|
}
|
||||||
|
hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
|
||||||
|
return &ReadWaitConn{
|
||||||
|
Conn: conn,
|
||||||
|
halfAccess: halfAccess,
|
||||||
|
rawInput: rawInput,
|
||||||
|
input: input,
|
||||||
|
hand: hand,
|
||||||
|
tlsReadRecord: tlsReadRecord,
|
||||||
|
tlsHandlePostHandshakeMessage: tlsHandlePostHandshakeMessage,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) {
|
||||||
|
c.readWaitOptions = options
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
|
||||||
|
err = c.HandshakeContext(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.halfAccess.Lock()
|
||||||
|
defer c.halfAccess.Unlock()
|
||||||
|
for c.input.Len() == 0 {
|
||||||
|
err = c.tlsReadRecord()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for c.hand.Len() > 0 {
|
||||||
|
err = c.tlsHandlePostHandshakeMessage()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer = c.readWaitOptions.NewBuffer()
|
||||||
|
n, err := c.input.Read(buffer.FreeBytes())
|
||||||
|
if err != nil {
|
||||||
|
buffer.Release()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buffer.Truncate(n)
|
||||||
|
|
||||||
|
if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 &&
|
||||||
|
// recordType(c.rawInput.Bytes()[0]) == recordTypeAlert {
|
||||||
|
c.rawInput.Bytes()[0] == 21 {
|
||||||
|
_ = c.tlsReadRecord()
|
||||||
|
// return n, err // will be io.EOF on closeNotify
|
||||||
|
}
|
||||||
|
|
||||||
|
c.readWaitOptions.PostReturn(buffer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ReadWaitConn) Upstream() any {
|
||||||
|
return c.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
||||||
|
tlsConn, loaded := conn.(*tls.STDConn)
|
||||||
|
if !loaded {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return true, func() error {
|
||||||
|
return stdTLSReadRecord(tlsConn)
|
||||||
|
}, func() error {
|
||||||
|
return stdTLSHandlePostHandshakeMessage(tlsConn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord
|
||||||
|
func stdTLSReadRecord(c *tls.STDConn) error
|
||||||
|
|
||||||
|
//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
|
||||||
|
func stdTLSHandlePostHandshakeMessage(c *tls.STDConn) error
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//go:build go1.21 && !without_badtls && with_ech
|
||||||
|
|
||||||
|
package badtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
_ "unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/cloudflare-tls"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
||||||
|
tlsConn, loaded := common.Cast[*tls.Conn](conn)
|
||||||
|
if !loaded {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return true, func() error {
|
||||||
|
return echReadRecord(tlsConn)
|
||||||
|
}, func() error {
|
||||||
|
return echHandlePostHandshakeMessage(tlsConn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
|
||||||
|
func echReadRecord(c *tls.Conn) error
|
||||||
|
|
||||||
|
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
|
||||||
|
func echHandlePostHandshakeMessage(c *tls.Conn) error
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build !go1.21 || without_badtls
|
||||||
|
|
||||||
|
package badtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//go:build go1.21 && !without_badtls && with_utls
|
||||||
|
|
||||||
|
package badtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
_ "unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/utls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
||||||
|
tlsConn, loaded := common.Cast[*tls.UConn](conn)
|
||||||
|
if !loaded {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return true, func() error {
|
||||||
|
return utlsReadRecord(tlsConn.Conn)
|
||||||
|
}, func() error {
|
||||||
|
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord
|
||||||
|
func utlsReadRecord(c *tls.Conn) error
|
||||||
|
|
||||||
|
//go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage
|
||||||
|
func utlsHandlePostHandshakeMessage(c *tls.Conn) error
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package badversion
|
package badversion
|
||||||
|
|
||||||
import "github.com/sagernet/sing-box/common/json"
|
import "github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
func (v Version) MarshalJSON() ([]byte, error) {
|
func (v Version) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(v.String())
|
return json.Marshal(v.String())
|
||||||
|
|||||||
+48
-12
@@ -15,38 +15,61 @@ import (
|
|||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ WireGuardListener = (*DefaultDialer)(nil)
|
||||||
|
|
||||||
type DefaultDialer struct {
|
type DefaultDialer struct {
|
||||||
dialer4 tcpDialer
|
dialer4 tcpDialer
|
||||||
dialer6 tcpDialer
|
dialer6 tcpDialer
|
||||||
udpDialer4 net.Dialer
|
udpDialer4 net.Dialer
|
||||||
udpDialer6 net.Dialer
|
udpDialer6 net.Dialer
|
||||||
udpListener net.ListenConfig
|
udpListener net.ListenConfig
|
||||||
udpAddr4 string
|
udpAddr4 string
|
||||||
udpAddr6 string
|
udpAddr6 string
|
||||||
|
isWireGuardListener bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) {
|
func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) {
|
||||||
var dialer net.Dialer
|
var dialer net.Dialer
|
||||||
var listener net.ListenConfig
|
var listener net.ListenConfig
|
||||||
if options.BindInterface != "" {
|
if options.BindInterface != "" {
|
||||||
bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1)
|
var interfaceFinder control.InterfaceFinder
|
||||||
|
if router != nil {
|
||||||
|
interfaceFinder = router.InterfaceFinder()
|
||||||
|
} else {
|
||||||
|
interfaceFinder = control.NewDefaultInterfaceFinder()
|
||||||
|
}
|
||||||
|
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
|
||||||
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 router.AutoDetectInterface() {
|
} else if router != nil && router.AutoDetectInterface() {
|
||||||
bindFunc := router.AutoDetectInterfaceFunc()
|
bindFunc := router.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 router.DefaultInterface() != "" {
|
} else if router != nil && router.DefaultInterface() != "" {
|
||||||
bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1)
|
bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1)
|
||||||
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)
|
||||||
}
|
}
|
||||||
if options.RoutingMark != 0 {
|
var autoRedirectOutputMark uint32
|
||||||
|
if router != nil {
|
||||||
|
autoRedirectOutputMark = router.AutoRedirectOutputMark()
|
||||||
|
}
|
||||||
|
if autoRedirectOutputMark > 0 {
|
||||||
|
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
|
||||||
|
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
|
||||||
|
}
|
||||||
|
if options.RoutingMark > 0 {
|
||||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
|
dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
|
||||||
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
|
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
|
||||||
} else if router.DefaultMark() != 0 {
|
if autoRedirectOutputMark > 0 {
|
||||||
|
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`")
|
||||||
|
}
|
||||||
|
} else if router != nil && router.DefaultMark() > 0 {
|
||||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
|
dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
|
||||||
listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
|
listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
|
||||||
|
if autoRedirectOutputMark > 0 {
|
||||||
|
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if options.ReuseAddr {
|
if options.ReuseAddr {
|
||||||
listener.Control = control.Append(listener.Control, control.ReuseAddr())
|
listener.Control = control.Append(listener.Control, control.ReuseAddr())
|
||||||
@@ -60,6 +83,9 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
} else {
|
} else {
|
||||||
dialer.Timeout = C.TCPTimeout
|
dialer.Timeout = C.TCPTimeout
|
||||||
}
|
}
|
||||||
|
// TODO: Add an option to customize the keep alive period
|
||||||
|
dialer.KeepAlive = C.TCPKeepAliveInitial
|
||||||
|
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval))
|
||||||
var udpFragment bool
|
var udpFragment bool
|
||||||
if options.UDPFragment != nil {
|
if options.UDPFragment != nil {
|
||||||
udpFragment = *options.UDPFragment
|
udpFragment = *options.UDPFragment
|
||||||
@@ -98,6 +124,11 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
}
|
}
|
||||||
setMultiPathTCP(&dialer4)
|
setMultiPathTCP(&dialer4)
|
||||||
}
|
}
|
||||||
|
if options.IsWireGuardListener {
|
||||||
|
for _, controlFn := range wgControlFns {
|
||||||
|
listener.Control = control.Append(listener.Control, controlFn)
|
||||||
|
}
|
||||||
|
}
|
||||||
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -114,6 +145,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
listener,
|
listener,
|
||||||
udpAddr4,
|
udpAddr4,
|
||||||
udpAddr6,
|
udpAddr6,
|
||||||
|
options.IsWireGuardListener,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +178,10 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
|
||||||
|
return trackPacketConn(d.udpListener.ListenPacket(context.Background(), network, address))
|
||||||
|
}
|
||||||
|
|
||||||
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
||||||
if !conntrack.Enabled || err != nil {
|
if !conntrack.Enabled || err != nil {
|
||||||
return conn, err
|
return conn, err
|
||||||
|
|||||||
@@ -6,15 +6,16 @@ import (
|
|||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-dns"
|
"github.com/sagernet/sing-dns"
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MustNew(router adapter.Router, options option.DialerOptions) N.Dialer {
|
|
||||||
return common.Must1(New(router, options))
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) {
|
func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) {
|
||||||
|
if options.IsWireGuardListener {
|
||||||
|
return NewDefault(router, options)
|
||||||
|
}
|
||||||
|
if router == nil {
|
||||||
|
return NewDefault(nil, options)
|
||||||
|
}
|
||||||
var (
|
var (
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
err error
|
err error
|
||||||
@@ -27,13 +28,12 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error)
|
|||||||
} else {
|
} else {
|
||||||
dialer = NewDetour(router, options.Detour)
|
dialer = NewDetour(router, options.Detour)
|
||||||
}
|
}
|
||||||
domainStrategy := dns.DomainStrategy(options.DomainStrategy)
|
if options.Detour == "" {
|
||||||
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
|
|
||||||
dialer = NewResolveDialer(
|
dialer = NewResolveDialer(
|
||||||
router,
|
router,
|
||||||
dialer,
|
dialer,
|
||||||
options.Detour == "" && !options.TCPFastOpen,
|
options.Detour == "" && !options.TCPFastOpen,
|
||||||
domainStrategy,
|
dns.DomainStrategy(options.DomainStrategy),
|
||||||
time.Duration(options.FallbackDelay))
|
time.Duration(options.FallbackDelay))
|
||||||
}
|
}
|
||||||
return dialer, nil
|
return dialer, nil
|
||||||
|
|||||||
+10
-2
@@ -18,11 +18,19 @@ func NewRouter(router adapter.Router) N.Dialer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
return d.router.DefaultOutbound(network).DialContext(ctx, network, destination)
|
dialer, err := d.router.DefaultOutbound(network)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination)
|
dialer, err := d.router.DefaultOutbound(N.NetworkUDP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RouterDialer) Upstream() any {
|
func (d *RouterDialer) Upstream() any {
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
|||||||
c.conn = nil
|
c.conn = nil
|
||||||
c.err = E.Cause(err, "dial tcp fast open")
|
c.err = E.Cause(err, "dial tcp fast open")
|
||||||
}
|
}
|
||||||
|
n = len(b)
|
||||||
close(c.create)
|
close(c.create)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WireGuardListener interface {
|
||||||
|
ListenPacketCompat(network, address string) (net.PacketConn, error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build with_wireguard
|
||||||
|
|
||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/wireguard-go/conn"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ WireGuardListener = (conn.Listener)(nil)
|
||||||
|
|
||||||
|
var wgControlFns = conn.ControlFns
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !with_wireguard
|
||||||
|
|
||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing/common/control"
|
||||||
|
)
|
||||||
|
|
||||||
|
var wgControlFns []control.Func
|
||||||
@@ -32,3 +32,7 @@ func (r *Reader) Lookup(addr netip.Addr) string {
|
|||||||
}
|
}
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reader) Close() error {
|
||||||
|
return r.reader.Close()
|
||||||
|
}
|
||||||
|
|||||||
+46
-28
@@ -1,17 +1,24 @@
|
|||||||
package geosite
|
package geosite
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/varbin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reader struct {
|
type Reader struct {
|
||||||
reader io.ReadSeeker
|
access sync.Mutex
|
||||||
domainIndex map[string]int
|
reader io.ReadSeeker
|
||||||
domainLength map[string]int
|
bufferedReader *bufio.Reader
|
||||||
|
metadataIndex int64
|
||||||
|
domainIndex map[string]int
|
||||||
|
domainLength map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
func Open(path string) (*Reader, []string, error) {
|
func Open(path string) (*Reader, []string, error) {
|
||||||
@@ -34,15 +41,23 @@ func Open(path string) (*Reader, []string, error) {
|
|||||||
return reader, codes, nil
|
return reader, codes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type geositeMetadata struct {
|
||||||
|
Code string
|
||||||
|
Index uint64
|
||||||
|
Length uint64
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Reader) readMetadata() error {
|
func (r *Reader) readMetadata() error {
|
||||||
version, err := rw.ReadByte(r.reader)
|
counter := &readCounter{Reader: r.reader}
|
||||||
|
reader := bufio.NewReader(counter)
|
||||||
|
version, err := reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if version != 0 {
|
if version != 0 {
|
||||||
return E.New("unknown version")
|
return E.New("unknown version")
|
||||||
}
|
}
|
||||||
entryLength, err := rw.ReadUVariant(r.reader)
|
entryLength, err := binary.ReadUvarint(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -55,16 +70,16 @@ func (r *Reader) readMetadata() error {
|
|||||||
codeIndex uint64
|
codeIndex uint64
|
||||||
codeLength uint64
|
codeLength uint64
|
||||||
)
|
)
|
||||||
code, err = rw.ReadVString(r.reader)
|
code, err = varbin.ReadValue[string](reader, binary.BigEndian)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
keys[i] = code
|
keys[i] = code
|
||||||
codeIndex, err = rw.ReadUVariant(r.reader)
|
codeIndex, err = binary.ReadUvarint(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
codeLength, err = rw.ReadUVariant(r.reader)
|
codeLength, err = binary.ReadUvarint(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -73,6 +88,8 @@ func (r *Reader) readMetadata() error {
|
|||||||
}
|
}
|
||||||
r.domainIndex = domainIndex
|
r.domainIndex = domainIndex
|
||||||
r.domainLength = domainLength
|
r.domainLength = domainLength
|
||||||
|
r.metadataIndex = counter.count - int64(reader.Buffered())
|
||||||
|
r.bufferedReader = reader
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,31 +98,32 @@ func (r *Reader) Read(code string) ([]Item, error) {
|
|||||||
if !exists {
|
if !exists {
|
||||||
return nil, E.New("code ", code, " not exists!")
|
return nil, E.New("code ", code, " not exists!")
|
||||||
}
|
}
|
||||||
_, err := r.reader.Seek(int64(index), io.SeekCurrent)
|
_, err := r.reader.Seek(r.metadataIndex+int64(index), io.SeekStart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
counter := &rw.ReadCounter{Reader: r.reader}
|
r.bufferedReader.Reset(r.reader)
|
||||||
domain := make([]Item, r.domainLength[code])
|
itemList := make([]Item, r.domainLength[code])
|
||||||
for i := range domain {
|
err = varbin.Read(r.bufferedReader, binary.BigEndian, &itemList)
|
||||||
var (
|
if err != nil {
|
||||||
item Item
|
return nil, err
|
||||||
err error
|
|
||||||
)
|
|
||||||
item.Type, err = rw.ReadByte(counter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
item.Value, err = rw.ReadVString(counter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
domain[i] = item
|
|
||||||
}
|
}
|
||||||
_, err = r.reader.Seek(int64(-index)-counter.Count(), io.SeekCurrent)
|
return itemList, nil
|
||||||
return domain, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) Upstream() any {
|
func (r *Reader) Upstream() any {
|
||||||
return r.reader
|
return r.reader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type readCounter struct {
|
||||||
|
io.Reader
|
||||||
|
count int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *readCounter) Read(p []byte) (n int, err error) {
|
||||||
|
n, err = r.Reader.Read(p)
|
||||||
|
if n > 0 {
|
||||||
|
atomic.AddInt64(&r.count, int64(n))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
+11
-14
@@ -2,13 +2,13 @@ package geosite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"encoding/binary"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/varbin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Write(writer io.Writer, domains map[string][]Item) error {
|
func Write(writer varbin.Writer, domains map[string][]Item) error {
|
||||||
keys := make([]string, 0, len(domains))
|
keys := make([]string, 0, len(domains))
|
||||||
for code := range domains {
|
for code := range domains {
|
||||||
keys = append(keys, code)
|
keys = append(keys, code)
|
||||||
@@ -19,35 +19,32 @@ func Write(writer io.Writer, domains map[string][]Item) error {
|
|||||||
index := make(map[string]int)
|
index := make(map[string]int)
|
||||||
for _, code := range keys {
|
for _, code := range keys {
|
||||||
index[code] = content.Len()
|
index[code] = content.Len()
|
||||||
for _, domain := range domains[code] {
|
err := varbin.Write(content, binary.BigEndian, domains[code])
|
||||||
content.WriteByte(domain.Type)
|
if err != nil {
|
||||||
err := rw.WriteVString(content, domain.Value)
|
return err
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := rw.WriteByte(writer, 0)
|
err := writer.WriteByte(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = rw.WriteUVariant(writer, uint64(len(keys)))
|
_, err = varbin.WriteUvarint(writer, uint64(len(keys)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, code := range keys {
|
for _, code := range keys {
|
||||||
err = rw.WriteVString(writer, code)
|
err = varbin.Write(writer, binary.BigEndian, code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = rw.WriteUVariant(writer, uint64(index[code]))
|
_, err = varbin.WriteUvarint(writer, uint64(index[code]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = rw.WriteUVariant(writer, uint64(len(domains[code])))
|
_, err = varbin.WriteUvarint(writer, uint64(len(domains[code])))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2018, Open Systems AG
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# JA3
|
||||||
|
|
||||||
|
mod from: https://github.com/open-ch/ja3
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) 2018, Open Systems AG. All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license
|
||||||
|
// that can be found in the LICENSE file in the root of the source
|
||||||
|
// tree.
|
||||||
|
|
||||||
|
package ja3
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Error types
|
||||||
|
const (
|
||||||
|
LengthErr string = "length check %v failed"
|
||||||
|
ContentTypeErr string = "content type not matching"
|
||||||
|
VersionErr string = "version check %v failed"
|
||||||
|
HandshakeTypeErr string = "handshake type not matching"
|
||||||
|
SNITypeErr string = "SNI type not supported"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseError can be encountered while parsing a segment
|
||||||
|
type ParseError struct {
|
||||||
|
errType string
|
||||||
|
check int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ParseError) Error() string {
|
||||||
|
if e.errType == LengthErr || e.errType == VersionErr {
|
||||||
|
return fmt.Sprintf(e.errType, e.check)
|
||||||
|
}
|
||||||
|
return fmt.Sprint(e.errType)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// Copyright (c) 2018, Open Systems AG. All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license
|
||||||
|
// that can be found in the LICENSE file in the root of the source
|
||||||
|
// tree.
|
||||||
|
|
||||||
|
package ja3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientHello struct {
|
||||||
|
Version uint16
|
||||||
|
CipherSuites []uint16
|
||||||
|
Extensions []uint16
|
||||||
|
EllipticCurves []uint16
|
||||||
|
EllipticCurvePF []uint8
|
||||||
|
Versions []uint16
|
||||||
|
SignatureAlgorithms []uint16
|
||||||
|
ServerName string
|
||||||
|
ja3ByteString []byte
|
||||||
|
ja3Hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ClientHello) Equals(another *ClientHello, ignoreExtensionsSequence bool) bool {
|
||||||
|
if j.Version != another.Version {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !slices.Equal(j.CipherSuites, another.CipherSuites) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.Extensions) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if ignoreExtensionsSequence && !slices.Equal(j.Extensions, another.sortedExtensions()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !slices.Equal(j.EllipticCurves, another.EllipticCurves) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !slices.Equal(j.EllipticCurvePF, another.EllipticCurvePF) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !slices.Equal(j.SignatureAlgorithms, another.SignatureAlgorithms) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ClientHello) sortedExtensions() []uint16 {
|
||||||
|
extensions := make([]uint16, len(j.Extensions))
|
||||||
|
copy(extensions, j.Extensions)
|
||||||
|
slices.Sort(extensions)
|
||||||
|
return extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
func Compute(payload []byte) (*ClientHello, error) {
|
||||||
|
ja3 := ClientHello{}
|
||||||
|
err := ja3.parseSegment(payload)
|
||||||
|
return &ja3, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ClientHello) String() string {
|
||||||
|
if j.ja3ByteString == nil {
|
||||||
|
j.marshalJA3()
|
||||||
|
}
|
||||||
|
return string(j.ja3ByteString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ClientHello) Hash() string {
|
||||||
|
if j.ja3ByteString == nil {
|
||||||
|
j.marshalJA3()
|
||||||
|
}
|
||||||
|
if j.ja3Hash == "" {
|
||||||
|
h := md5.Sum(j.ja3ByteString)
|
||||||
|
j.ja3Hash = hex.EncodeToString(h[:])
|
||||||
|
}
|
||||||
|
return j.ja3Hash
|
||||||
|
}
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
// Copyright (c) 2018, Open Systems AG. All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license
|
||||||
|
// that can be found in the LICENSE file in the root of the source
|
||||||
|
// tree.
|
||||||
|
|
||||||
|
package ja3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Constants used for parsing
|
||||||
|
recordLayerHeaderLen int = 5
|
||||||
|
handshakeHeaderLen int = 6
|
||||||
|
randomDataLen int = 32
|
||||||
|
sessionIDHeaderLen int = 1
|
||||||
|
cipherSuiteHeaderLen int = 2
|
||||||
|
compressMethodHeaderLen int = 1
|
||||||
|
extensionsHeaderLen int = 2
|
||||||
|
extensionHeaderLen int = 4
|
||||||
|
sniExtensionHeaderLen int = 5
|
||||||
|
ecExtensionHeaderLen int = 2
|
||||||
|
ecpfExtensionHeaderLen int = 1
|
||||||
|
versionExtensionHeaderLen int = 1
|
||||||
|
signatureAlgorithmsExtensionHeaderLen int = 2
|
||||||
|
contentType uint8 = 22
|
||||||
|
handshakeType uint8 = 1
|
||||||
|
sniExtensionType uint16 = 0
|
||||||
|
sniNameDNSHostnameType uint8 = 0
|
||||||
|
ecExtensionType uint16 = 10
|
||||||
|
ecpfExtensionType uint16 = 11
|
||||||
|
versionExtensionType uint16 = 43
|
||||||
|
signatureAlgorithmsExtensionType uint16 = 13
|
||||||
|
|
||||||
|
// Versions
|
||||||
|
// The bitmask covers the versions SSL3.0 to TLS1.2
|
||||||
|
tlsVersionBitmask uint16 = 0xFFFC
|
||||||
|
tls13 uint16 = 0x0304
|
||||||
|
|
||||||
|
// GREASE values
|
||||||
|
// The bitmask covers all GREASE values
|
||||||
|
GreaseBitmask uint16 = 0x0F0F
|
||||||
|
|
||||||
|
// Constants used for marshalling
|
||||||
|
dashByte = byte(45)
|
||||||
|
commaByte = byte(44)
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseSegment to populate the corresponding ClientHello object or return an error
|
||||||
|
func (j *ClientHello) parseSegment(segment []byte) error {
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(segment) < recordLayerHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have "Content Type: Handshake (22)"
|
||||||
|
contType := uint8(segment[0])
|
||||||
|
if contType != contentType {
|
||||||
|
return &ParseError{errType: ContentTypeErr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if TLS record layer version is supported
|
||||||
|
tlsRecordVersion := uint16(segment[1])<<8 | uint16(segment[2])
|
||||||
|
if tlsRecordVersion&tlsVersionBitmask != 0x0300 && tlsRecordVersion != tls13 {
|
||||||
|
return &ParseError{VersionErr, 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the Handshake is as long as expected from the length field
|
||||||
|
segmentLen := uint16(segment[3])<<8 | uint16(segment[4])
|
||||||
|
if len(segment[recordLayerHeaderLen:]) < int(segmentLen) {
|
||||||
|
return &ParseError{LengthErr, 2}
|
||||||
|
}
|
||||||
|
// Keep the Handshake messege, ignore any additional following record types
|
||||||
|
hs := segment[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)]
|
||||||
|
|
||||||
|
err := j.parseHandshake(hs)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHandshake body
|
||||||
|
func (j *ClientHello) parseHandshake(hs []byte) error {
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 3}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have "Handshake Type: Client Hello (1)"
|
||||||
|
handshType := uint8(hs[0])
|
||||||
|
if handshType != handshakeType {
|
||||||
|
return &ParseError{errType: HandshakeTypeErr}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if actual length of handshake matches (this is a great exclusion criterion for false positives,
|
||||||
|
// as these fields have to match the actual length of the rest of the segment)
|
||||||
|
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
|
||||||
|
if len(hs[4:]) != int(handshakeLen) {
|
||||||
|
return &ParseError{LengthErr, 4}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Client Hello version is supported
|
||||||
|
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
|
||||||
|
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
|
||||||
|
return &ParseError{VersionErr, 2}
|
||||||
|
}
|
||||||
|
j.Version = tlsVersion
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
sessionIDLen := uint8(hs[38])
|
||||||
|
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
|
||||||
|
return &ParseError{LengthErr, 5}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cipher Suites
|
||||||
|
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(cs) < cipherSuiteHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 6}
|
||||||
|
}
|
||||||
|
|
||||||
|
csLen := uint16(cs[0])<<8 | uint16(cs[1])
|
||||||
|
numCiphers := int(csLen / 2)
|
||||||
|
cipherSuites := make([]uint16, 0, numCiphers)
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 7}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < numCiphers; i++ {
|
||||||
|
cipherSuite := uint16(cs[2+i<<1])<<8 | uint16(cs[3+i<<1])
|
||||||
|
cipherSuites = append(cipherSuites, cipherSuite)
|
||||||
|
}
|
||||||
|
j.CipherSuites = cipherSuites
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
|
||||||
|
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
|
||||||
|
return &ParseError{LengthErr, 8}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
exs := cs[cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen):]
|
||||||
|
|
||||||
|
err := j.parseExtensions(exs)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseExtensions of the handshake
|
||||||
|
func (j *ClientHello) parseExtensions(exs []byte) error {
|
||||||
|
// Check for no extensions, this fields header is nonexistent if no body is used
|
||||||
|
if len(exs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(exs) < extensionsHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 9}
|
||||||
|
}
|
||||||
|
|
||||||
|
exsLen := uint16(exs[0])<<8 | uint16(exs[1])
|
||||||
|
exs = exs[extensionsHeaderLen:]
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(exs) < int(exsLen) {
|
||||||
|
return &ParseError{LengthErr, 10}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sni []byte
|
||||||
|
var extensions, ellipticCurves []uint16
|
||||||
|
var ellipticCurvePF []uint8
|
||||||
|
var versions []uint16
|
||||||
|
var signatureAlgorithms []uint16
|
||||||
|
for len(exs) > 0 {
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(exs) < extensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 11}
|
||||||
|
}
|
||||||
|
|
||||||
|
exType := uint16(exs[0])<<8 | uint16(exs[1])
|
||||||
|
exLen := uint16(exs[2])<<8 | uint16(exs[3])
|
||||||
|
// Ignore any GREASE extensions
|
||||||
|
extensions = append(extensions, exType)
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(exs) < extensionHeaderLen+int(exLen) {
|
||||||
|
return &ParseError{LengthErr, 12}
|
||||||
|
}
|
||||||
|
|
||||||
|
sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)]
|
||||||
|
|
||||||
|
switch exType {
|
||||||
|
case sniExtensionType: // Extensions: server_name
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) < sniExtensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 13}
|
||||||
|
}
|
||||||
|
|
||||||
|
sniType := uint8(sex[2])
|
||||||
|
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
|
||||||
|
sex = sex[sniExtensionHeaderLen:]
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) != int(sniLen) {
|
||||||
|
return &ParseError{LengthErr, 14}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sniType {
|
||||||
|
case sniNameDNSHostnameType:
|
||||||
|
sni = sex
|
||||||
|
default:
|
||||||
|
return &ParseError{errType: SNITypeErr}
|
||||||
|
}
|
||||||
|
case ecExtensionType: // Extensions: supported_groups
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) < ecExtensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 15}
|
||||||
|
}
|
||||||
|
|
||||||
|
ecsLen := uint16(sex[0])<<8 | uint16(sex[1])
|
||||||
|
numCurves := int(ecsLen / 2)
|
||||||
|
ellipticCurves = make([]uint16, 0, numCurves)
|
||||||
|
sex = sex[ecExtensionHeaderLen:]
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) != int(ecsLen) {
|
||||||
|
return &ParseError{LengthErr, 16}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < numCurves; i++ {
|
||||||
|
ecType := uint16(sex[i*2])<<8 | uint16(sex[1+i*2])
|
||||||
|
ellipticCurves = append(ellipticCurves, ecType)
|
||||||
|
}
|
||||||
|
|
||||||
|
case ecpfExtensionType: // Extensions: ec_point_formats
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) < ecpfExtensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 17}
|
||||||
|
}
|
||||||
|
|
||||||
|
ecpfsLen := uint8(sex[0])
|
||||||
|
numPF := int(ecpfsLen)
|
||||||
|
ellipticCurvePF = make([]uint8, numPF)
|
||||||
|
sex = sex[ecpfExtensionHeaderLen:]
|
||||||
|
|
||||||
|
// Check if we can decode the next fields
|
||||||
|
if len(sex) != numPF {
|
||||||
|
return &ParseError{LengthErr, 18}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < numPF; i++ {
|
||||||
|
ellipticCurvePF[i] = uint8(sex[i])
|
||||||
|
}
|
||||||
|
case versionExtensionType:
|
||||||
|
if len(sex) < versionExtensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 19}
|
||||||
|
}
|
||||||
|
versionsLen := int(sex[0])
|
||||||
|
for i := 0; i < versionsLen; i += 2 {
|
||||||
|
versions = append(versions, binary.BigEndian.Uint16(sex[1:][i:]))
|
||||||
|
}
|
||||||
|
case signatureAlgorithmsExtensionType:
|
||||||
|
if len(sex) < signatureAlgorithmsExtensionHeaderLen {
|
||||||
|
return &ParseError{LengthErr, 20}
|
||||||
|
}
|
||||||
|
ssaLen := binary.BigEndian.Uint16(sex)
|
||||||
|
for i := 0; i < int(ssaLen); i += 2 {
|
||||||
|
signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[2:][i:]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exs = exs[4+exLen:]
|
||||||
|
}
|
||||||
|
j.ServerName = string(sni)
|
||||||
|
j.Extensions = extensions
|
||||||
|
j.EllipticCurves = ellipticCurves
|
||||||
|
j.EllipticCurvePF = ellipticCurvePF
|
||||||
|
j.Versions = versions
|
||||||
|
j.SignatureAlgorithms = signatureAlgorithms
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshalJA3 into a byte string
|
||||||
|
func (j *ClientHello) marshalJA3() {
|
||||||
|
// An uint16 can contain numbers with up to 5 digits and an uint8 can contain numbers with up to 3 digits, but we
|
||||||
|
// also need a byte for each separating character, except at the end.
|
||||||
|
byteStringLen := 6*(1+len(j.CipherSuites)+len(j.Extensions)+len(j.EllipticCurves)) + 4*len(j.EllipticCurvePF) - 1
|
||||||
|
byteString := make([]byte, 0, byteStringLen)
|
||||||
|
|
||||||
|
// Version
|
||||||
|
byteString = strconv.AppendUint(byteString, uint64(j.Version), 10)
|
||||||
|
byteString = append(byteString, commaByte)
|
||||||
|
|
||||||
|
// Cipher Suites
|
||||||
|
if len(j.CipherSuites) != 0 {
|
||||||
|
for _, val := range j.CipherSuites {
|
||||||
|
if val&GreaseBitmask != 0x0A0A {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byteString = strconv.AppendUint(byteString, uint64(val), 10)
|
||||||
|
byteString = append(byteString, dashByte)
|
||||||
|
}
|
||||||
|
// Replace last dash with a comma
|
||||||
|
byteString[len(byteString)-1] = commaByte
|
||||||
|
} else {
|
||||||
|
byteString = append(byteString, commaByte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
if len(j.Extensions) != 0 {
|
||||||
|
for _, val := range j.Extensions {
|
||||||
|
if val&GreaseBitmask != 0x0A0A {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byteString = strconv.AppendUint(byteString, uint64(val), 10)
|
||||||
|
byteString = append(byteString, dashByte)
|
||||||
|
}
|
||||||
|
// Replace last dash with a comma
|
||||||
|
byteString[len(byteString)-1] = commaByte
|
||||||
|
} else {
|
||||||
|
byteString = append(byteString, commaByte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elliptic curves
|
||||||
|
if len(j.EllipticCurves) != 0 {
|
||||||
|
for _, val := range j.EllipticCurves {
|
||||||
|
if val&GreaseBitmask != 0x0A0A {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
byteString = strconv.AppendUint(byteString, uint64(val), 10)
|
||||||
|
byteString = append(byteString, dashByte)
|
||||||
|
}
|
||||||
|
// Replace last dash with a comma
|
||||||
|
byteString[len(byteString)-1] = commaByte
|
||||||
|
} else {
|
||||||
|
byteString = append(byteString, commaByte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECPF
|
||||||
|
if len(j.EllipticCurvePF) != 0 {
|
||||||
|
for _, val := range j.EllipticCurvePF {
|
||||||
|
byteString = strconv.AppendUint(byteString, uint64(val), 10)
|
||||||
|
byteString = append(byteString, dashByte)
|
||||||
|
}
|
||||||
|
// Remove last dash
|
||||||
|
byteString = byteString[:len(byteString)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
j.ja3ByteString = byteString
|
||||||
|
}
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package json
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// kanged from v2ray
|
|
||||||
|
|
||||||
type commentFilterState = byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
commentFilterStateContent commentFilterState = iota
|
|
||||||
commentFilterStateEscape
|
|
||||||
commentFilterStateDoubleQuote
|
|
||||||
commentFilterStateDoubleQuoteEscape
|
|
||||||
commentFilterStateSingleQuote
|
|
||||||
commentFilterStateSingleQuoteEscape
|
|
||||||
commentFilterStateComment
|
|
||||||
commentFilterStateSlash
|
|
||||||
commentFilterStateMultilineComment
|
|
||||||
commentFilterStateMultilineCommentStar
|
|
||||||
)
|
|
||||||
|
|
||||||
type CommentFilter struct {
|
|
||||||
br *bufio.Reader
|
|
||||||
state commentFilterState
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCommentFilter(reader io.Reader) io.Reader {
|
|
||||||
return &CommentFilter{br: bufio.NewReader(reader)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *CommentFilter) Read(b []byte) (int, error) {
|
|
||||||
p := b[:0]
|
|
||||||
for len(p) < len(b)-2 {
|
|
||||||
x, err := v.br.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
if len(p) == 0 {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
switch v.state {
|
|
||||||
case commentFilterStateContent:
|
|
||||||
switch x {
|
|
||||||
case '"':
|
|
||||||
v.state = commentFilterStateDoubleQuote
|
|
||||||
p = append(p, x)
|
|
||||||
case '\'':
|
|
||||||
v.state = commentFilterStateSingleQuote
|
|
||||||
p = append(p, x)
|
|
||||||
case '\\':
|
|
||||||
v.state = commentFilterStateEscape
|
|
||||||
case '#':
|
|
||||||
v.state = commentFilterStateComment
|
|
||||||
case '/':
|
|
||||||
v.state = commentFilterStateSlash
|
|
||||||
default:
|
|
||||||
p = append(p, x)
|
|
||||||
}
|
|
||||||
case commentFilterStateEscape:
|
|
||||||
p = append(p, '\\', x)
|
|
||||||
v.state = commentFilterStateContent
|
|
||||||
case commentFilterStateDoubleQuote:
|
|
||||||
switch x {
|
|
||||||
case '"':
|
|
||||||
v.state = commentFilterStateContent
|
|
||||||
p = append(p, x)
|
|
||||||
case '\\':
|
|
||||||
v.state = commentFilterStateDoubleQuoteEscape
|
|
||||||
default:
|
|
||||||
p = append(p, x)
|
|
||||||
}
|
|
||||||
case commentFilterStateDoubleQuoteEscape:
|
|
||||||
p = append(p, '\\', x)
|
|
||||||
v.state = commentFilterStateDoubleQuote
|
|
||||||
case commentFilterStateSingleQuote:
|
|
||||||
switch x {
|
|
||||||
case '\'':
|
|
||||||
v.state = commentFilterStateContent
|
|
||||||
p = append(p, x)
|
|
||||||
case '\\':
|
|
||||||
v.state = commentFilterStateSingleQuoteEscape
|
|
||||||
default:
|
|
||||||
p = append(p, x)
|
|
||||||
}
|
|
||||||
case commentFilterStateSingleQuoteEscape:
|
|
||||||
p = append(p, '\\', x)
|
|
||||||
v.state = commentFilterStateSingleQuote
|
|
||||||
case commentFilterStateComment:
|
|
||||||
if x == '\n' {
|
|
||||||
v.state = commentFilterStateContent
|
|
||||||
p = append(p, '\n')
|
|
||||||
}
|
|
||||||
case commentFilterStateSlash:
|
|
||||||
switch x {
|
|
||||||
case '/':
|
|
||||||
v.state = commentFilterStateComment
|
|
||||||
case '*':
|
|
||||||
v.state = commentFilterStateMultilineComment
|
|
||||||
default:
|
|
||||||
p = append(p, '/', x)
|
|
||||||
}
|
|
||||||
case commentFilterStateMultilineComment:
|
|
||||||
switch x {
|
|
||||||
case '*':
|
|
||||||
v.state = commentFilterStateMultilineCommentStar
|
|
||||||
case '\n':
|
|
||||||
p = append(p, '\n')
|
|
||||||
}
|
|
||||||
case commentFilterStateMultilineCommentStar:
|
|
||||||
switch x {
|
|
||||||
case '/':
|
|
||||||
v.state = commentFilterStateContent
|
|
||||||
case '*':
|
|
||||||
// Stay
|
|
||||||
case '\n':
|
|
||||||
p = append(p, '\n')
|
|
||||||
default:
|
|
||||||
v.state = commentFilterStateMultilineComment
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic("Unknown state.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package json
|
|
||||||
|
|
||||||
import "encoding/json"
|
|
||||||
|
|
||||||
var (
|
|
||||||
Marshal = json.Marshal
|
|
||||||
Unmarshal = json.Unmarshal
|
|
||||||
NewEncoder = json.NewEncoder
|
|
||||||
NewDecoder = json.NewDecoder
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Encoder = json.Encoder
|
|
||||||
Decoder = json.Decoder
|
|
||||||
Token = json.Token
|
|
||||||
Delim = json.Delim
|
|
||||||
SyntaxError = json.SyntaxError
|
|
||||||
)
|
|
||||||
+18
-1
@@ -1,11 +1,16 @@
|
|||||||
package mux
|
package mux
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"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/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-mux"
|
"github.com/sagernet/sing-mux"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +35,7 @@ func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mux.NewClient(mux.Options{
|
return mux.NewClient(mux.Options{
|
||||||
Dialer: dialer,
|
Dialer: &clientDialer{dialer},
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Protocol: options.Protocol,
|
Protocol: options.Protocol,
|
||||||
MaxConnections: options.MaxConnections,
|
MaxConnections: options.MaxConnections,
|
||||||
@@ -40,3 +45,15 @@ func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.
|
|||||||
Brutal: brutalOptions,
|
Brutal: brutalOptions,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type clientDialer struct {
|
||||||
|
N.Dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *clientDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
return d.Dialer.DialContext(adapter.OverrideContext(ctx), network, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *clientDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
return d.Dialer.ListenPacket(adapter.OverrideContext(ctx), destination)
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
|||||||
|
|
||||||
isIPv4 := ip.Is4()
|
isIPv4 := ip.Is4()
|
||||||
|
|
||||||
value, err := syscall.Sysctl(spath)
|
value, err := unix.SysctlRaw(spath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := []byte(value)
|
buf := value
|
||||||
|
|
||||||
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
|
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
|
||||||
// size/offset are round up (aligned) to 8 bytes in darwin
|
// size/offset are round up (aligned) to 8 bytes in darwin
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ func getExecPathFromPID(pid uint32) (string, error) {
|
|||||||
r1, _, err := syscall.SyscallN(
|
r1, _, err := syscall.SyscallN(
|
||||||
procQueryFullProcessImageNameW.Addr(),
|
procQueryFullProcessImageNameW.Addr(),
|
||||||
uintptr(h),
|
uintptr(h),
|
||||||
uintptr(1),
|
uintptr(0),
|
||||||
uintptr(unsafe.Pointer(&buf[0])),
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
uintptr(unsafe.Pointer(&size)),
|
uintptr(unsafe.Pointer(&size)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,30 +16,40 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type LinuxSystemProxy struct {
|
type LinuxSystemProxy struct {
|
||||||
hasGSettings bool
|
hasGSettings bool
|
||||||
hasKWriteConfig5 bool
|
kWriteConfigCmd string
|
||||||
sudoUser string
|
sudoUser string
|
||||||
serverAddr M.Socksaddr
|
serverAddr M.Socksaddr
|
||||||
supportSOCKS bool
|
supportSOCKS bool
|
||||||
isEnabled bool
|
isEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) {
|
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) {
|
||||||
hasGSettings := common.Error(exec.LookPath("gsettings")) == nil
|
hasGSettings := common.Error(exec.LookPath("gsettings")) == nil
|
||||||
hasKWriteConfig5 := common.Error(exec.LookPath("kwriteconfig5")) == nil
|
kWriteConfigCmds := []string{
|
||||||
|
"kwriteconfig5",
|
||||||
|
"kwriteconfig6",
|
||||||
|
}
|
||||||
|
var kWriteConfigCmd string
|
||||||
|
for _, cmd := range kWriteConfigCmds {
|
||||||
|
if common.Error(exec.LookPath(cmd)) == nil {
|
||||||
|
kWriteConfigCmd = cmd
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
var sudoUser string
|
var sudoUser string
|
||||||
if os.Getuid() == 0 {
|
if os.Getuid() == 0 {
|
||||||
sudoUser = os.Getenv("SUDO_USER")
|
sudoUser = os.Getenv("SUDO_USER")
|
||||||
}
|
}
|
||||||
if !hasGSettings && !hasKWriteConfig5 {
|
if !hasGSettings && kWriteConfigCmd == "" {
|
||||||
return nil, E.New("unsupported desktop environment")
|
return nil, E.New("unsupported desktop environment")
|
||||||
}
|
}
|
||||||
return &LinuxSystemProxy{
|
return &LinuxSystemProxy{
|
||||||
hasGSettings: hasGSettings,
|
hasGSettings: hasGSettings,
|
||||||
hasKWriteConfig5: hasKWriteConfig5,
|
kWriteConfigCmd: kWriteConfigCmd,
|
||||||
sudoUser: sudoUser,
|
sudoUser: sudoUser,
|
||||||
serverAddr: serverAddr,
|
serverAddr: serverAddr,
|
||||||
supportSOCKS: supportSOCKS,
|
supportSOCKS: supportSOCKS,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +80,8 @@ func (p *LinuxSystemProxy) Enable() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p.hasKWriteConfig5 {
|
if p.kWriteConfigCmd != "" {
|
||||||
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1")
|
err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -83,7 +93,7 @@ func (p *LinuxSystemProxy) Enable() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0")
|
err = p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -103,8 +113,8 @@ func (p *LinuxSystemProxy) Disable() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p.hasKWriteConfig5 {
|
if p.kWriteConfigCmd != "" {
|
||||||
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0")
|
err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -150,7 +160,7 @@ func (p *LinuxSystemProxy) setKDEProxy(proxyTypes ...string) error {
|
|||||||
proxyUrl = "http://" + p.serverAddr.String()
|
proxyUrl = "http://" + p.serverAddr.String()
|
||||||
}
|
}
|
||||||
err := p.runAsUser(
|
err := p.runAsUser(
|
||||||
"kwriteconfig5",
|
p.kWriteConfigCmd,
|
||||||
"--file",
|
"--file",
|
||||||
"kioslaverc",
|
"kioslaverc",
|
||||||
"--group",
|
"--group",
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package sniff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
trackerConnectFlag = 0
|
||||||
|
trackerProtocolID = 0x41727101980
|
||||||
|
trackerConnectMinSize = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// BitTorrent detects if the stream is a BitTorrent connection.
|
||||||
|
// For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html
|
||||||
|
func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
|
var first byte
|
||||||
|
err := binary.Read(reader, binary.BigEndian, &first)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if first != 19 {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
var protocol [19]byte
|
||||||
|
_, err = reader.Read(protocol[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if string(protocol[:]) != "BitTorrent protocol" {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.Protocol = C.ProtocolBitTorrent
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTP detects if the packet is a uTP connection packet.
|
||||||
|
// For the uTP protocol specification, see
|
||||||
|
// 1. https://www.bittorrent.org/beps/bep_0029.html
|
||||||
|
// 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112
|
||||||
|
func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
|
// A valid uTP packet must be at least 20 bytes long.
|
||||||
|
if len(packet) < 20 {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
version := packet[0] & 0x0F
|
||||||
|
ty := packet[0] >> 4
|
||||||
|
if version != 1 || ty > 4 {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the extensions
|
||||||
|
extension := packet[1]
|
||||||
|
reader := bytes.NewReader(packet[20:])
|
||||||
|
for extension != 0 {
|
||||||
|
err := binary.Read(reader, binary.BigEndian, &extension)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var length byte
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &length)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = reader.Seek(int64(length), io.SeekCurrent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata.Protocol = C.ProtocolBitTorrent
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UDPTracker detects if the packet is a UDP Tracker Protocol packet.
|
||||||
|
// For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html
|
||||||
|
func UDPTracker(_ context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
|
if len(packet) < trackerConnectMinSize {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
if binary.BigEndian.Uint64(packet[:8]) != trackerProtocolID {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
if binary.BigEndian.Uint32(packet[8:12]) != trackerConnectFlag {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
metadata.Protocol = C.ProtocolBitTorrent
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package sniff_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSniffBittorrent(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
packets := []string{
|
||||||
|
"13426974546f7272656e742070726f746f636f6c0000000000100000e21ea9569b69bab33c97851d0298bdfa89bc90922d5554313631302dea812fcd6a3563e3be40c1d1",
|
||||||
|
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452333030302d653369733079647675763638",
|
||||||
|
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452343035302d6f7a316c6e79377931716130",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkt := range packets {
|
||||||
|
pkt, err := hex.DecodeString(pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSniffUTP(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
packets := []string{
|
||||||
|
"010041a282d7ee7b583afb160004000006d8318da776968f92d666f7963f32dae23ba0d2c810d8b8209cc4939f54fde9eeaa521c2c20c9ba7f43f4fb0375f28de06643b5e3ca4685ab7ac76adca99783be72ef05ed59ef4234f5712b75b4c7c0d7bee8fe2ca20ad626ba5bb0ffcc16bf06790896f888048cf72716419a07db1a3dca4550fbcea75b53e97235168a221cf3e553dfbb723961bd719fab038d86e0ecb74747f5a2cd669de1c4b9ad375f3a492d09d98cdfad745435625401315bbba98d35d32086299801377b93495a63a9efddb8d05f5b37a5c5b1c0a25e917f12007bb5e05013ada8aff544fab8cadf61d80ddb0b60f12741e44515a109d144fd53ef845acb4b5ccf0d6fc302d7003d76df3fc3423bb0237301c9e88f900c2d392a8e0fdb36d143cf7527a93fd0a2638b746e72f6699fffcd4fd15348fce780d4caa04382fd9faf1ca0ae377ca805da7536662b84f5ee18dd3ae38fcb095a7543e55f9069ae92c8cf54ae44e97b558d35e2545c66601ed2149cbc32bd6df199a2be7cf0da8b2ff137e0d23e776bc87248425013876d3a3cc31a83b424b752bd0346437f24b532978005d8f5b1b0be1a37a2489c32a18a9ad3118e3f9d30eb299bffae18e1f0677c2a5c185e62519093fe6bc2b7339299ea50a587989f726ca6443a75dd5bb936f6367c6355d80fae53ff529d740b2e5576e3eefdf1fdbfc69c3c8d8ac750512635de63e054bee1d3b689bc1b2bc3d2601e42a00b5c89066d173d4ae7ffedfd2274e5cf6d868fbe640aedb69b8246142f00b32d459974287537ddd5373460dcbc92f5cfdd7a3ed6020822ae922d947893752ca1983d0d32977374c384ac8f5ab566859019b7351526b9f13e932037a55bb052d9deb3b3c23317e0784fdc51a64f2159bfea3b069cf5caf02ee2c3c1a6b6b427bb16165713e8802d95b5c8ed77953690e994bd38c9ae113fedaf6ee7fc2b96c032ceafc2a530ad0422e84546b9c6ad8ef6ea02fa508abddd1805c38a7b42e9b7c971b1b636865ebec06ed754bb404cd6b4e6cc8cb77bd4a0c43410d5cd5ef8fe853a66d49b3b9e06cb141236cdbfdd5761601dc54d1250b86c660e0f898fe62526fdd9acf0eab60a3bbbb2151970461f28f10b31689594bea646c4b03ee197d63bdef4e5a7c22716b3bb9494a83b78ecd81b338b80ac6c09c43485b1b09ba41c74343832c78f0520c1d659ac9eb1502094141e82fb9e5e620970ebc0655514c43c294a7714cbf9a499d277daf089f556398a01589a77494bec8bfb60a108f3813b55368672b88c1af40f6b3c8b513f7c70c3e0efce85228b8b9ec67ba0393f9f7305024d8e2da6a26cf85613d14f249170ce1000089df4c9c260df7f8292aa2ecb5d5bac97656d59aa248caedea2d198e51ce87baece338716d114b458de02d65c9ff808ca5b5b73723b4d1e962d9ac2d98176544dc9984cf8554d07820ef3dd0861cfe57b478328046380de589adad94ee44743ffac73bb7361feca5d56f07cf8ce75080e261282ae30350d7882679b15cab9e7e53ddf93310b33f7390ae5d318bb53f387e6af5d0ef4f947fc9cb8e7e38b52c7f8d772ece6156b38d88796ea19df02c53723b44df7c76315a0de9462f27287e682d2b4cda1a68fe00d7e48c51ee981be44e1ca940fb5190c12655edb4a83c3a4f33e48a015692df4f0b3d61656e362aca657b5ae8c12db5a0db3db1e45135ee918b66918f40e53c4f83e9da0cddfe63f736ae751ab3837a30ae3220d8e8e311487093a7b90c7e7e40dd54ca750e19452f9193aa892aa6a6229ab493dadae988b1724f7898ee69c36d3eb7364c4adbeca811cfe2065873e78c2b6dfdf1595f7a7831c07e03cda82e4f86f76438dfb2b07c13638ce7b509cfa71b88b5102b39a203b423202088e1c2103319cb32c13c1e546ff8612fa194c95a7808ab767c265a1bd5fa0efed5c8ec1701876a00ec8",
|
||||||
|
"01001ecb68176f215d04326300100000dbcf30292d14b54e9ee2d115ee5b8ebc7fad3e882d4fcdd0c14c6b917c11cb4c6a9f410b52a33ae97c2ac77c7a2b122b8955e09af3c5c595f1b2e79ca57cfe44c44e069610773b9bc9ba223d7f6b383e3adddd03fb88a8476028e30979c2ef321ffc97c5c132bcf9ac5b410bbb5ec6cefca3c7209202a14c5ae922b6b157b0a80249d13ffe5b996af0bc8e54ba576d148372494303e7ead0602b05b9c8fc97d48508a028a04d63a1fd28b0edfcd5c51715f63188b53eefede98a76912dca98518551a8856567307a56a702cbfcc115ea0c755b418bc2c7b57721239b82f09fb24328a4b0ce0f109bcb2a64e04b8aadb1f8487585425acdf8fc4ec8ea93cfcec5ac098bb29d42ddef6e46b03f34a5de28316726699b7cb5195c33e5c48abe87d591d63f9991c84c30819d186d6e0e95fd83c8dff07aa669c4430989bcaccfeacb9bcadbdb4d8f1964dbeb9687745656edd30b21c66cc0a1d742a78717d134a19a7f02d285a4973b1a198c00cfdff4676608dc4f3e817e3463c3b4e2c80d3e8d4fbac541a58a2fb7ad6939f607f8144eff6c8b0adc28ee5609ea158987519892fb",
|
||||||
|
"21001ecb6817f2805d044fd700100000dbd03029",
|
||||||
|
"410277ef0b1fb1f60000000000040000c233000000080000000000000000",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkt := range packets {
|
||||||
|
pkt, err := hex.DecodeString(pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.UTP(context.TODO(), &metadata, pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSniffUDPTracker(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
connectPackets := []string{
|
||||||
|
"00000417271019800000000078e90560",
|
||||||
|
"00000417271019800000000022c5d64d",
|
||||||
|
"000004172710198000000000b3863541",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkt := range connectPackets {
|
||||||
|
pkt, err := hex.DecodeString(pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.UDPTracker(context.TODO(), &metadata, pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-10
@@ -17,18 +17,17 @@ import (
|
|||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
|
func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
var length uint16
|
var length uint16
|
||||||
err := binary.Read(reader, binary.BigEndian, &length)
|
err := binary.Read(reader, binary.BigEndian, &length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
buffer := buf.NewSize(int(length))
|
buffer := buf.NewSize(int(length))
|
||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
|
|
||||||
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
|
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
|
||||||
var readTask task.Group
|
var readTask task.Group
|
||||||
readTask.Append0(func(ctx context.Context) error {
|
readTask.Append0(func(ctx context.Context) error {
|
||||||
@@ -37,19 +36,20 @@ func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.
|
|||||||
err = readTask.Run(readCtx)
|
err = readTask.Run(readCtx)
|
||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
return DomainNameQuery(readCtx, buffer.Bytes())
|
return DomainNameQuery(readCtx, metadata, buffer.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func DomainNameQuery(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
|
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
var msg mDNS.Msg
|
var msg mDNS.Msg
|
||||||
err := msg.Unpack(packet)
|
err := msg.Unpack(packet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
|
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolDNS}, nil
|
metadata.Protocol = C.ProtocolDNS
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package sniff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DTLSRecord(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
|
const fixedHeaderSize = 13
|
||||||
|
if len(packet) < fixedHeaderSize {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
contentType := packet[0]
|
||||||
|
switch contentType {
|
||||||
|
case 20, 21, 22, 23, 25:
|
||||||
|
default:
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
versionMajor := packet[1]
|
||||||
|
if versionMajor != 0xfe {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
versionMinor := packet[2]
|
||||||
|
if versionMinor != 0xff && versionMinor != 0xfd {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
metadata.Protocol = C.ProtocolDTLS
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package sniff_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSniffDTLSClientHello(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
packet, err := hex.DecodeString("16fefd0000000000000000007e010000720000000000000072fefd668a43523798e064bd806d0c87660de9c611a59bbdfc3892c4e072d94f2cafc40000000cc02bc02fc00ac014c02cc0300100003c000d0010000e0403050306030401050106010807ff01000100000a00080006001d00170018000b00020100000e000900060008000700010000170000")
|
||||||
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.DTLSRecord(context.Background(), &metadata, packet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSniffDTLSClientApplicationData(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
packet, err := hex.DecodeString("17fefd000100000000000100440001000000000001a4f682b77ecadd10f3f3a2f78d90566212366ff8209fd77314f5a49352f9bb9bd12f4daba0b4736ae29e46b9714d3b424b3e6d0234736619b5aa0d3f")
|
||||||
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.DTLSRecord(context.Background(), &metadata, packet)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, metadata.Protocol, C.ProtocolDTLS)
|
||||||
|
}
|
||||||
@@ -11,10 +11,12 @@ import (
|
|||||||
"github.com/sagernet/sing/protocol/http"
|
"github.com/sagernet/sing/protocol/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HTTPHost(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
|
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
request, err := http.ReadRequest(std_bufio.NewReader(reader))
|
request, err := http.ReadRequest(std_bufio.NewReader(reader))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: M.ParseSocksaddr(request.Host).AddrString()}, nil
|
metadata.Protocol = C.ProtocolHTTP
|
||||||
|
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/sniff"
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -13,7 +14,8 @@ import (
|
|||||||
func TestSniffHTTP1(t *testing.T) {
|
func TestSniffHTTP1(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n"
|
pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n"
|
||||||
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
|
var metadata adapter.InboundContext
|
||||||
|
err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Domain, "www.google.com")
|
require.Equal(t, metadata.Domain, "www.google.com")
|
||||||
}
|
}
|
||||||
@@ -21,7 +23,8 @@ func TestSniffHTTP1(t *testing.T) {
|
|||||||
func TestSniffHTTP1WithPort(t *testing.T) {
|
func TestSniffHTTP1WithPort(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n"
|
pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n"
|
||||||
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
|
var metadata adapter.InboundContext
|
||||||
|
err := sniff.HTTPHost(context.Background(), &metadata, strings.NewReader(pkt))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Domain, "www.gov.cn")
|
require.Equal(t, metadata.Domain, "www.gov.cn")
|
||||||
}
|
}
|
||||||
|
|||||||
+146
-72
@@ -5,95 +5,99 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/ja3"
|
||||||
"github.com/sagernet/sing-box/common/sniff/internal/qtls"
|
"github.com/sagernet/sing-box/common/sniff/internal/qtls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
"golang.org/x/crypto/hkdf"
|
"golang.org/x/crypto/hkdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
|
var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection")
|
||||||
reader := bytes.NewReader(packet)
|
|
||||||
|
|
||||||
|
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
|
reader := bytes.NewReader(packet)
|
||||||
typeByte, err := reader.ReadByte()
|
typeByte, err := reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if typeByte&0x40 == 0 {
|
if typeByte&0x40 == 0 {
|
||||||
return nil, E.New("bad type byte")
|
return E.New("bad type byte")
|
||||||
}
|
}
|
||||||
var versionNumber uint32
|
var versionNumber uint32
|
||||||
err = binary.Read(reader, binary.BigEndian, &versionNumber)
|
err = binary.Read(reader, binary.BigEndian, &versionNumber)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 {
|
if versionNumber != qtls.VersionDraft29 && versionNumber != qtls.Version1 && versionNumber != qtls.Version2 {
|
||||||
return nil, E.New("bad version")
|
return E.New("bad version")
|
||||||
}
|
}
|
||||||
packetType := (typeByte & 0x30) >> 4
|
packetType := (typeByte & 0x30) >> 4
|
||||||
if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 {
|
if packetType == 0 && versionNumber == qtls.Version2 || packetType == 2 && versionNumber != qtls.Version2 || packetType > 2 {
|
||||||
return nil, E.New("bad packet type")
|
return E.New("bad packet type")
|
||||||
}
|
}
|
||||||
|
|
||||||
destConnIDLen, err := reader.ReadByte()
|
destConnIDLen, err := reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if destConnIDLen == 0 || destConnIDLen > 20 {
|
if destConnIDLen == 0 || destConnIDLen > 20 {
|
||||||
return nil, E.New("bad destination connection id length")
|
return E.New("bad destination connection id length")
|
||||||
}
|
}
|
||||||
|
|
||||||
destConnID := make([]byte, destConnIDLen)
|
destConnID := make([]byte, destConnIDLen)
|
||||||
_, err = io.ReadFull(reader, destConnID)
|
_, err = io.ReadFull(reader, destConnID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
srcConnIDLen, err := reader.ReadByte()
|
srcConnIDLen, err := reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen))
|
_, err = io.CopyN(io.Discard, reader, int64(srcConnIDLen))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenLen, err := qtls.ReadUvarint(reader)
|
tokenLen, err := qtls.ReadUvarint(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.CopyN(io.Discard, reader, int64(tokenLen))
|
_, err = io.CopyN(io.Discard, reader, int64(tokenLen))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
packetLen, err := qtls.ReadUvarint(reader)
|
packetLen, err := qtls.ReadUvarint(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hdrLen := int(reader.Size()) - reader.Len()
|
hdrLen := int(reader.Size()) - reader.Len()
|
||||||
if hdrLen+int(packetLen) > len(packet) {
|
if hdrLen+int(packetLen) > len(packet) {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.CopyN(io.Discard, reader, 4)
|
_, err = io.CopyN(io.Discard, reader, 4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pnBytes := make([]byte, aes.BlockSize)
|
pnBytes := make([]byte, aes.BlockSize)
|
||||||
_, err = io.ReadFull(reader, pnBytes)
|
_, err = io.ReadFull(reader, pnBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var salt []byte
|
var salt []byte
|
||||||
@@ -117,7 +121,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
|||||||
hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16)
|
hpKey := qtls.HKDFExpandLabel(crypto.SHA256, secret, []byte{}, hkdfHeaderProtectionLabel, 16)
|
||||||
block, err := aes.NewCipher(hpKey)
|
block, err := aes.NewCipher(hpKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
mask := make([]byte, aes.BlockSize)
|
mask := make([]byte, aes.BlockSize)
|
||||||
block.Encrypt(mask, pnBytes)
|
block.Encrypt(mask, pnBytes)
|
||||||
@@ -129,7 +133,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
|||||||
}
|
}
|
||||||
packetNumberLength := newPacket[0]&0x3 + 1
|
packetNumberLength := newPacket[0]&0x3 + 1
|
||||||
if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen {
|
if hdrLen+int(packetNumberLength) > int(packetLen)+hdrLen {
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
var packetNumber uint32
|
var packetNumber uint32
|
||||||
switch packetNumberLength {
|
switch packetNumberLength {
|
||||||
@@ -142,7 +146,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
|||||||
case 4:
|
case 4:
|
||||||
packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:])
|
packetNumber = binary.BigEndian.Uint32(newPacket[hdrLen:])
|
||||||
default:
|
default:
|
||||||
return nil, E.New("bad packet number length")
|
return E.New("bad packet number length")
|
||||||
}
|
}
|
||||||
extHdrLen := hdrLen + int(packetNumberLength)
|
extHdrLen := hdrLen + int(packetNumberLength)
|
||||||
copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:])
|
copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:])
|
||||||
@@ -166,138 +170,208 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
|||||||
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber))
|
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber))
|
||||||
decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen])
|
decrypted, err := cipher.Open(newPacket[extHdrLen:extHdrLen], nonce, data, newPacket[:extHdrLen])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
var frameType byte
|
var frameType byte
|
||||||
var frameLen uint64
|
var fragments []qCryptoFragment
|
||||||
var fragments []struct {
|
|
||||||
offset uint64
|
|
||||||
length uint64
|
|
||||||
payload []byte
|
|
||||||
}
|
|
||||||
decryptedReader := bytes.NewReader(decrypted)
|
decryptedReader := bytes.NewReader(decrypted)
|
||||||
|
const (
|
||||||
|
frameTypePadding = 0x00
|
||||||
|
frameTypePing = 0x01
|
||||||
|
frameTypeAck = 0x02
|
||||||
|
frameTypeAck2 = 0x03
|
||||||
|
frameTypeCrypto = 0x06
|
||||||
|
frameTypeConnectionClose = 0x1c
|
||||||
|
)
|
||||||
|
var frameTypeList []uint8
|
||||||
for {
|
for {
|
||||||
frameType, err = decryptedReader.ReadByte()
|
frameType, err = decryptedReader.ReadByte()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
frameTypeList = append(frameTypeList, frameType)
|
||||||
switch frameType {
|
switch frameType {
|
||||||
case 0x00: // PADDING
|
case frameTypePadding:
|
||||||
continue
|
continue
|
||||||
case 0x01: // PING
|
case frameTypePing:
|
||||||
continue
|
continue
|
||||||
case 0x02, 0x03: // ACK
|
case frameTypeAck, frameTypeAck2:
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged
|
_, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ACK Delay
|
_, err = qtls.ReadUvarint(decryptedReader) // ACK Delay
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count
|
ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // First ACK Range
|
_, err = qtls.ReadUvarint(decryptedReader) // First ACK Range
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
for i := 0; i < int(ackRangeCount); i++ {
|
for i := 0; i < int(ackRangeCount); i++ {
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Gap
|
_, err = qtls.ReadUvarint(decryptedReader) // Gap
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length
|
_, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if frameType == 0x03 {
|
if frameType == 0x03 {
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count
|
_, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count
|
_, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count
|
_, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 0x06: // CRYPTO
|
case frameTypeCrypto:
|
||||||
var offset uint64
|
var offset uint64
|
||||||
offset, err = qtls.ReadUvarint(decryptedReader)
|
offset, err = qtls.ReadUvarint(decryptedReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
|
return err
|
||||||
}
|
}
|
||||||
var length uint64
|
var length uint64
|
||||||
length, err = qtls.ReadUvarint(decryptedReader)
|
length, err = qtls.ReadUvarint(decryptedReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
|
return err
|
||||||
}
|
}
|
||||||
index := len(decrypted) - decryptedReader.Len()
|
index := len(decrypted) - decryptedReader.Len()
|
||||||
fragments = append(fragments, struct {
|
fragments = append(fragments, qCryptoFragment{offset, length, decrypted[index : index+int(length)]})
|
||||||
offset uint64
|
|
||||||
length uint64
|
|
||||||
payload []byte
|
|
||||||
}{offset, length, decrypted[index : index+int(length)]})
|
|
||||||
frameLen += length
|
|
||||||
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent)
|
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
case 0x1c: // CONNECTION_CLOSE
|
case frameTypeConnectionClose:
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Error Code
|
_, err = qtls.ReadUvarint(decryptedReader) // Error Code
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Frame Type
|
_, err = qtls.ReadUvarint(decryptedReader) // Frame Type
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
var length uint64
|
var length uint64
|
||||||
length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length
|
length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase
|
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil, os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tlsHdr := make([]byte, 5)
|
if metadata.SniffContext != nil {
|
||||||
tlsHdr[0] = 0x16
|
fragments = append(fragments, metadata.SniffContext.([]qCryptoFragment)...)
|
||||||
binary.BigEndian.PutUint16(tlsHdr[1:], uint16(0x0303))
|
metadata.SniffContext = nil
|
||||||
binary.BigEndian.PutUint16(tlsHdr[3:], uint16(frameLen))
|
}
|
||||||
|
var frameLen uint64
|
||||||
|
for _, fragment := range fragments {
|
||||||
|
frameLen += fragment.length
|
||||||
|
}
|
||||||
|
buffer := buf.NewSize(5 + int(frameLen))
|
||||||
|
defer buffer.Release()
|
||||||
|
buffer.WriteByte(0x16)
|
||||||
|
binary.Write(buffer, binary.BigEndian, uint16(0x0303))
|
||||||
|
binary.Write(buffer, binary.BigEndian, uint16(frameLen))
|
||||||
var index uint64
|
var index uint64
|
||||||
var length int
|
var length int
|
||||||
var readers []io.Reader
|
|
||||||
readers = append(readers, bytes.NewReader(tlsHdr))
|
|
||||||
find:
|
find:
|
||||||
for {
|
for {
|
||||||
for _, fragment := range fragments {
|
for _, fragment := range fragments {
|
||||||
if fragment.offset == index {
|
if fragment.offset == index {
|
||||||
readers = append(readers, bytes.NewReader(fragment.payload))
|
buffer.Write(fragment.payload)
|
||||||
index = fragment.offset + fragment.length
|
index = fragment.offset + fragment.length
|
||||||
length++
|
length++
|
||||||
continue find
|
continue find
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if length == len(fragments) {
|
break
|
||||||
break
|
|
||||||
}
|
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, E.New("bad fragments")
|
|
||||||
}
|
|
||||||
metadata, err := TLSClientHello(ctx, io.MultiReader(readers...))
|
|
||||||
if err != nil {
|
|
||||||
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
|
|
||||||
}
|
}
|
||||||
metadata.Protocol = C.ProtocolQUIC
|
metadata.Protocol = C.ProtocolQUIC
|
||||||
return metadata, nil
|
fingerprint, err := ja3.Compute(buffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
metadata.Protocol = C.ProtocolQUIC
|
||||||
|
metadata.Client = C.ClientChromium
|
||||||
|
metadata.SniffContext = fragments
|
||||||
|
return ErrClientHelloFragmented
|
||||||
|
}
|
||||||
|
metadata.Domain = fingerprint.ServerName
|
||||||
|
for metadata.Client == "" {
|
||||||
|
if len(frameTypeList) == 1 {
|
||||||
|
metadata.Client = C.ClientFirefox
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if frameTypeList[0] == frameTypeCrypto && isZero(frameTypeList[1:]) {
|
||||||
|
if len(fingerprint.Versions) == 2 && fingerprint.Versions[0]&ja3.GreaseBitmask == 0x0A0A &&
|
||||||
|
len(fingerprint.EllipticCurves) == 5 && fingerprint.EllipticCurves[0]&ja3.GreaseBitmask == 0x0A0A {
|
||||||
|
metadata.Client = C.ClientSafari
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if len(fingerprint.CipherSuites) == 1 && fingerprint.CipherSuites[0] == tls.TLS_AES_256_GCM_SHA384 &&
|
||||||
|
len(fingerprint.EllipticCurves) == 1 && fingerprint.EllipticCurves[0] == uint16(tls.X25519) &&
|
||||||
|
len(fingerprint.SignatureAlgorithms) == 1 && fingerprint.SignatureAlgorithms[0] == uint16(tls.ECDSAWithP256AndSHA256) {
|
||||||
|
metadata.Client = C.ClientSafari
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if frameTypeList[len(frameTypeList)-1] == frameTypeCrypto && isZero(frameTypeList[:len(frameTypeList)-1]) {
|
||||||
|
metadata.Client = C.ClientQUICGo
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
|
||||||
|
if maybeUQUIC(fingerprint) {
|
||||||
|
metadata.Client = C.ClientQUICGo
|
||||||
|
} else {
|
||||||
|
metadata.Client = C.ClientChromium
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.Client = C.ClientUnknown
|
||||||
|
//nolint:staticcheck
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isZero(slices []uint8) bool {
|
||||||
|
for _, slice := range slices {
|
||||||
|
if slice != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func count(slices []uint8, value uint8) int {
|
||||||
|
var times int
|
||||||
|
for _, slice := range slices {
|
||||||
|
if slice == value {
|
||||||
|
times++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return times
|
||||||
|
}
|
||||||
|
|
||||||
|
type qCryptoFragment struct {
|
||||||
|
offset uint64
|
||||||
|
length uint64
|
||||||
|
payload []byte
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package sniff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/ja3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
|
||||||
|
// The cronet without this behavior does not have version 115
|
||||||
|
var uQUICChrome115 = &ja3.ClientHello{
|
||||||
|
Version: tls.VersionTLS12,
|
||||||
|
CipherSuites: []uint16{4865, 4866, 4867},
|
||||||
|
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 {
|
||||||
|
if uQUICChrome115.Equals(fingerprint, true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -5,31 +5,69 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/sniff"
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSniffQUICv1(t *testing.T) {
|
func TestSniffQUICChromium(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
pkt, err := hex.DecodeString("cc0000000108d2dc7bad02241f5003796e71004215a71bfcb05159416c724be418537389acdd9a4047306283dcb4d7a9cad5cc06322042d204da67a8dbaa328ab476bb428b48fd001501863afd203f8d4ef085629d664f1a734a65969a47e4a63d4e01a21f18c1d90db0c027180906dc135f9ae421bb8617314c8d54c175fef3d3383d310d0916ebcbd6eed9329befbbb109d8fd4af1d2cf9d6adce8e6c1260a7f8256e273e326da0aa7cc148d76e7a08489dc9d52ade89c027cbc3491ada46417c2c04e2ca768e9a7dd6aa00c594e48b678927325da796817693499bb727050cb3baf3d3291a397c3a8d868e8ec7b8f7295e347455c9dadbe2252ae917ac793d958c7fb8a3d2cdb34e3891eb4286f18617556ff7216dd60256aa5b1d11ff4753459fc5f9dedf11d483a26a0835dc6cd50e1c1f54f86e8f1e502821183cd874f6447a74e818bf3445c7795acf4559d1c1fac474911d2ead5c8d23e4aa4f67afb66efe305a30a0b5d825679b31ddc186cbea936535795c7e8c378c87b8c5adc065154d15bae8f85ac8fec2da40c3aa623b682a065440831555011d7647cde44446a0fb4cf5892f2c088ae1920643094be72e3c499fe8d265caf939e8ab607a5b9317917d2a32a812e8a0e6a2f84721bbb5984ffd242838f705d13f4cfb249bc6a5c80d58ac2595edf56648ec3fe21d787573c253a79805252d6d81e26d367d4ff29ef66b5fe8992086af7bada8cad10b82a7c0dc406c5b6d0c5ec3c583e767f759ce08cad6c3c8f91e5a8")
|
pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metadata, err := sniff.QUICClientHello(context.Background(), pkt)
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
|
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||||
|
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
|
||||||
|
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Domain, "cloudflare-quic.com")
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, metadata.Domain, "google.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSniffQUICFragment(t *testing.T) {
|
func TestSniffUQUICChrome115(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
pkt, err := hex.DecodeString("cc00000001082e3d5d1b64040c55000044d0ccea69e773f6631c1d18b04ae9ee75fcfc34ef74fa62533c93534338a86f101a05d70e0697fb483063fa85db1c59ccfbda5c35234931d8524d8aac37eaaad649470a67794cd754b23c98695238b8363452333bc8c4858376b4166e001da2006e35cf98a91e11a56419b2786775284942d0f7163982f7c248867d12dd374957481dbc564013ff785e1916195eef671f725908f761099d992d69231336ba81d9e25fe2fa3a6eff4318a6ccf10176fc841a1b315f7b35c5b292266fc869d76ca533e7d14e86d82db2e22eacd350977e47d2e012d8a5891c5aaf2a0f4c2b2dae897c161e5b68cbb4dee952472bdc1e21504b8f02534ec4366ce3f8bf86efc78e0232778fbd554457567112abdcafcf6d4d8fcf35083c25d9495679614aba21696e338c62b585046cc55ba8c09c844361d889a47c3ea703b4e23545a9ab2c0bb369693a9ddfb5daffa85cf80fdd6ad66738664e5b0a551729b4955cff7255afcb04dee88c2f072c9de7400947a1bd9327ac5d012a33000ada021d4c03d249fb017d6ac9200b2f9436beab8183ddfbe2d8aee31ffb7df9e1cc181c1af80c39a89965d18ed12da8e3ebe2ae1fbe4b348f83ba19e3e3d1c9b22bcf03ab6ad9b30fe180623faa291ebad83bcd71d7b57f2f5e2f3b8e81d24fb70b2f2159239e8f21ffafef2747aba47d97ab4081e603c018b10678cf99cab1fb42156a14486fa435153979d7279fd22cd40af7088bfc7eff41af2f4b3c0c8864d0040d74dff427f7bffdb8c278474ea00311326cf4925471a8cf596cb92119f19e0f789490ba9cb77b98015a987d93e0324cf1a38b55109f00c3e6ddc5180fb107bf468323afec9bb49fd6a86418569789d66cafe3b8253c2aebb3af3782c1c54dd560487d031d28e6a6e23e159581bb1d47efc4da3fe1d169f9ffb0ca9ba61af0a38a92fde5bc5e6ec026e8378a6315a7b95abf1d2da790a391306ce74d0baf8e2ce648ca74c487f2c0a76a28a80cdf5bd34316eb607684fe7e6d9e83824a00e07660d0b90e3cddd61ebf10748263474afa88c300549e64ce2e90560bb1a12dee7e9484f729a8a4ee7c5651adb5194b3b3ae38e501567c7dbf36e7bb37a2c20b74655f47f2d9af18e52e9d4c9c9eee8e63745779b8f0b06f3a09d846ba62eb978ad77c85de1ee2fee3fbb4c2d283c73e1ccba56a4658e48a2665d200f7f9342f8e84c2ba490094a4f94feec89e42d2f654f564c2beb2997bafa1fc2c68ad8e160b63587d49abc31b834878d52acfb05fb73d0e059b206162e3c90b40c4bc08407ffcb3c08431895b691a3fea923f1f3b48db75d3e6b91fd319ffe4d486e0e14bd5c6affc838dee63d9e0b80f169b5e6c02c7321dcb20deb2b8e707b60e345a308d505bbf26a93d8f18b39d62632e9a77cbe48b3b32eb8819d6311a49820d40f5acbf0273c91c36b2269a03e72ee64df3dfb10ddefe73c64ef60870b2b77bd99dea655f5fe791b538a929a14d99f6d69685d72431ea5f0f4b27a044f2f575ab474fcc3857895934de1ca2581798eaef2c17fe5aaf2e6add97fa32997c7026f15c1b1ad0e6043ae506027a7c0242546fdc851cca39a204e56879f2cef838be8ec66e0f2292f8c862e06f810eb9b80c7a467ce6e90155206352c7f82b1173ba3b98d35bb72c259a60db20dd1a43fe6d7aef0265e6eaa5caafd9b64b448ff745a2046acbdb65cf2a5007809808a4828dc99097feedc734c236260c584")
|
pkt, err := hex.DecodeString("cb0000000108181e17c387120abc000044d0705b6a3ef9ee37a8d3949a7d393ed078243c2ee2c3627fad1c3f107c117f4f071131ad61848068fcbbe5c65803c147f7f8ec5e2cd77b77beea23ba779d936dccac540f8396400e3190ea35cc2942af4171a04cb14272491920f90124959f44e80143678c0b52f5d31af319aaa589db2f940f004562724d0af40f737e1bb0002a071e6a1dbc9f52c64f070806a5010abed0298053634d9c9126bd7949ae5087998ade762c0ad06691d99c0875a38c601fc1ee77bfc3b8c11381829f2c9bdd022f4499c43ff1d6aee1a0d296861461dda217d22c568b276016ef3929e59d2f7d7ddf7809920fb7dc805641608949f3f8466ab3d37149aac501f0b107d808f3add4acfc657e4a82e2b88e97a6c74a00c419548760ab3414ba13915c78a1ca79dceee8d59fbe299f20b671ac44823218368b2a026baa55170cf549519ac21dbb6d31d248bd339438a4e663bcdca1fe3ae3f045a5dc19b122e9db9d7af9757076666dda4e9ace1c67def77fa14786f0cab3ebf7a270ea6e2b37838318c95779f80c3b8471948d0046c3614b3a13477c939a39a7855d85d13522a45ae0765739cd5eedef87237e824a929983ace27640c6495dbf5a72fa0b96893dc5d28f3988249a57bdb458d460b4a57043de3da750a76b6e5d2259247ca27cd864ea18f0d09aa62ab6eb7c014fb43179b2a1963d170b756cce83eeaebff78a828d025c811848e16ff862a8080d093478cd2208c8ab0803178325bc0d9d6bb25e62fa50c4ad15cf80916da6578796932036c72e43eb480d1e423ed812ac75a97722f8416529b82ba8ee2219c535012282bb17066bd53e78b87a71abdb7ebdb2a7c2766ff8397962e87d0f85485b64b4ee81cc84f99c47f33f2b0872716441992773f59186e38d32dbf5609a6fda94cb928cd25f5a7a3ab736b5a4236b6d5409ab18892c6a4d3480fc2350abfdf0bab1cedb55bdf0760fdb703e6688f4de596254eed4ed3e67eb03d0717b8e15b31e735214e588c87ae36bc6c310e1894b4c15143e4ccf287b2dbc707a946bf9671ae3c574f9486b2c82eec784bba4cbc76113cbe0f97ac8c13cfa38f2925ab9d06887a612ce48280a91d7e074e6caf898d88e2bbf71360899abf48a03f9a70cf2891199f2d63b116f4871af0ebb4f4906792f66cc21d1609f189138532875c129a68c73e7bcd3b5d8100beac1d8ac4b20d94a59ac8df5a5af58a9acb20413eadf97189f5f19ff889155f0c4d37514ec184eb6903967ff38a41fc087abb0f2cad3761d6e3f95f92a09a72f5c065b16e188088b87460241f27ecdb1bc6ece92c8d36b2d68b58d0fb4d4b3c928c579ade8ae5a995833aadd297c30a37f7bc35440fc97070e1b198e0fac00157452177d16d2803b4239997452b4ad3a951173bdec47a033fd7f8a7942accaa9aaa905b3c5a2175e7c3e07c48bf25331727fd69cd1e64d74d8c9d4a6f8f4491adb7bc911505cb19877083d8f21a12475e313fccf57877ff3556318e81ed9145dd9427f2b65275440893035f417481f721c69215af8ae103530cd0a1d35bf2cb5a27628f8d44d7c6f5ec12ce79d0a8333e0eb48771115d0a191304e46b8db19bbe5c40f1c346dde98e76ff5e21ff38d2c34e60cb07766ed529dd6d2cbacd7fbf1ed8a0e6e40decad0ca5021e91552be87c156d3ae2fffef41c65b14ba6d488f2c3227a1ab11ffce0e2dc47723a69da27a67a7f26e1cb13a7103af9b87a8db8e18ea")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
metadata, err := sniff.QUICClientHello(context.Background(), pkt)
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Domain, "cloudflare-quic.com")
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
|
require.Equal(t, metadata.Client, C.ClientQUICGo)
|
||||||
|
require.Equal(t, metadata.Domain, "www.google.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSniffQUICFirefox(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
pkt, err := hex.DecodeString("c8000000010867f174d7ebfe1b0803cd9c20004286de068f7963cf1736349ee6ebe0ddcd3e4cd0041a51ced3f7ce9eea1fb595458e74bdb4b792b16449bd8cae71419862c4fcbe766eaec7d1af65cd298e1dd46f8bd94a77ab4ca28c54b8e9773de3f02d7cb2463c9f7dcacfb311f024b0266ec6ab7bfb615b4148333fb4d4ece7c4cd90029ca30c2cbae2216b428499ec873fa125797e71c5a5da85087760ad37ca610020f71b76e82651c47576e20bf33cf676cb2d400b8c09d3c8cb4e21c47d2b21f6b68732bef30c8cefd5c723fc23eb29e6f7f65a5e52aad9055c1fb3d8b1811f0380b38d7e2eee8eb37dd5bd5d4ca4b66540175d916289d88a9df7c161964d713999c5057d27edb298ef5164352568b0d4bac3c15d90456e8fd460e41b81d0ec1b1e94b87d3333cc6908b018e0914ae1f214d73e75398da3d55a0106161d3a75897b4eb66e98c59010fae75f0d367d38be48c3a5c58bc8a30773c3fff50690ac9d487822f85d4f5713d626baa92d36e858dd21259cf814bce0b90d18da88a1ade40113e5a088cdb304a2558879152a8cf15c1839e056378aa41acba6fcb9974dee54bd50b5d4eb2c475654e06c0ec06b7f18f4462c808684843a1071041b9bfb2688324e0120144944416e30e83eedbbbcbc275b1f53762d3db18f0998ce54f0e1c512946b4098f07781d49264fa148f4c8220a3b02e73d7f15554aa370aafeff73cb75c52c494edf90f0261abfdd32a4d670f729de50266162687aa8efe14b8506f313b058b02aaaab5825428f5f4510b8e49451fdcb7b5a4af4b59c831afcb89fb4f64dba78e3b38387e87e9e8cdaa1f3b700a87c7d442388863b8950296e5773b38f308d62f52548c0bbf308e40540747cca5bf99b1345bc0d70b8f0e69a83b85a8d69f795b87f93e2bfccf52b529afea4ff6fd456957000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
|
||||||
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
|
require.Equal(t, metadata.Client, C.ClientFirefox)
|
||||||
|
require.Equal(t, metadata.Domain, "www.google.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSniffQUICSafari(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
pkt, err := hex.DecodeString("c70000000108e4e75af2e223198a0000449ef2d83cb4473a62765eba67424cd4a5817315cbf55a9e8daaca360904b0bae60b1629cfeba11e2dfbbf5ea4c588cb134e31af36fd7a409fb0fcc0187e9b56037ac37964ed20a8c1ca19fd6cfd53398324b3d0c71537294f769db208fa998b6811234a4a7eb3b5eceb457ae92e3a2d98f7c110702db8064b5c29fa3298eb1d0529fd445a84a5fd6ff8709be90f8af4f94998d8a8f2953bb05ad08c80668eca784c6aec959114e68e5b827e7c41c79f2277c716a967e7fcc8d1b77442e6cb18329dbedb34b473516b468cba5fc20659e655fbe37f36408289b9a475fcee091bd82828d3be00367e9e5cec9423bb97854abdada1d7562a3777756eb3bddef826ddc1ef46137cb01bb504a54d410d9bcb74cd5f959050c84edf343fa6a49708c228a758ee7adbbadf260b2f1984911489712e2cb364a3d6520badba4b7e539b9c163eeddfd96c0abb0de151e47496bb9750be76ee17ccdb61d35d2c6795174037d6f9d282c3f36c4d9a90b64f3b6ddd0cf4d9ed8e6f7805e25928fa04b087e63ae02761df30720cc01dfc32b64c575c8a66ef82e9a17400ff80cd8609b93ba16d668f4aa734e71c4a5d145f14ee1151bec970214e0ff83fc3e1e85d8694f2975f9155c57c18b7b69bb6a36832a9435f1f4b346a7be188f3a75f9ad2cc6ad0a3d26d6fa7d4c1179bd49bd5989d15ba43ff602890107db96484695086627356750d7b2b3b714ba65d564654e8f60ac10f5b6d3bfb507e8eaa31bab1da2d676195046d165c7f8b32829c9f9b68d97b2af7ac04a1369357e4b65de2b2f24eaf27cc8d95e05db001adebe726f927a94e43e62ce671e6e306e16f05aafcbe6c49080e80286d7939f375023d110a5ad9069364ae928ca480454a9dcddd61bc48b7efeb716a5bd6c7cd39c486ceb20c738af6abf22ba1ddd8b4a3b781fc2f251173409e1aadccbd7514e97106d0ebfc3af6e59445f74cd733a1ba99b10fce3fb4e9f7c88f5e25b567f5ba2b8dabacd375e7faf7634bfa178cbe51aee63032c5126b196ea47b02385fc3062a000fb7e4b4d0d12e74579f8830ede20d10829496032b2cc56743287f9a9b4d5091877a82fea44deb2cffac8a379f78a151d99e28cbc74d732c083bf06d50584e3f18f254e71a48d6ababaf6fff6f425e9be001510dfbe6a32a27792c00ada036b62ddb90c706d7b882c76a7072f5dd11c69a1f49d4ba183cb0b57545419fa27b9b9706098848935ae9c9e8fbe9fac165d1339128b991a73d20e7795e8d6a8c6adfbf20bf13ada43f2aef3ba78c14697910507132623f721387dce60c4707225b84d9782d469a5d9eaa099f35d6a590ef142ddef766495cf3337815ceef5ff2b3ed352637e72b5c23a2a8ff7d7440236a19b981d47f8e519a0431ebfbc0b78d8a36798b4c060c0c6793499f1e2e818862560a5b501c8d02ba1517be1941da2af5b174e0189c62978d878eb0f9c9db3a9221c28fb94645cf6e85ff2eea8c65ba3083a7382b131b83102dd67aa5453ad7375a4eb8c69fc479fbd29dab8924f801d253f2c997120b705c6e5217fb74702e2f1038917dd5fb0eeb7ae1bf7a668fc7d50c034b4cd5a057a8482e6bc9c921297f44e76967265623a167cd9883eb6e64bc77856dc333bd605d7df3bed0e5cecb5a99fe8b62873d58530f")
|
||||||
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
|
require.Equal(t, metadata.Client, C.ClientSafari)
|
||||||
|
require.Equal(t, metadata.Domain, "www.google.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzSniffQUIC(f *testing.F) {
|
func FuzzSniffQUIC(f *testing.F) {
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
sniff.QUICClientHello(context.Background(), data)
|
var metadata adapter.InboundContext
|
||||||
|
err := sniff.QUICClientHello(context.Background(), &metadata, data)
|
||||||
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-14
@@ -14,11 +14,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
StreamSniffer = func(ctx context.Context, reader io.Reader) (*adapter.InboundContext, error)
|
StreamSniffer = func(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error
|
||||||
PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error)
|
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
|
||||||
)
|
)
|
||||||
|
|
||||||
func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) (*adapter.InboundContext, error) {
|
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
timeout = C.ReadPayloadTimeout
|
timeout = C.ReadPayloadTimeout
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout
|
|||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
err := conn.SetReadDeadline(deadline)
|
err := conn.SetReadDeadline(deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "set read deadline")
|
return E.Cause(err, "set read deadline")
|
||||||
}
|
}
|
||||||
_, err = buffer.ReadOnceFrom(conn)
|
_, err = buffer.ReadOnceFrom(conn)
|
||||||
err = E.Errors(err, conn.SetReadDeadline(time.Time{}))
|
err = E.Errors(err, conn.SetReadDeadline(time.Time{}))
|
||||||
@@ -36,27 +36,27 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout
|
|||||||
if i > 0 {
|
if i > 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return nil, E.Cause(err, "read payload")
|
return E.Cause(err, "read payload")
|
||||||
}
|
}
|
||||||
for _, sniffer := range sniffers {
|
for _, sniffer := range sniffers {
|
||||||
metadata, err := sniffer(ctx, bytes.NewReader(buffer.Bytes()))
|
err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes()))
|
||||||
if metadata != nil {
|
if err == nil {
|
||||||
return metadata, nil
|
return nil
|
||||||
}
|
}
|
||||||
errors = append(errors, err)
|
errors = append(errors, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, E.Errors(errors...)
|
return E.Errors(errors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PeekPacket(ctx context.Context, packet []byte, sniffers ...PacketSniffer) (*adapter.InboundContext, error) {
|
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
|
||||||
var errors []error
|
var errors []error
|
||||||
for _, sniffer := range sniffers {
|
for _, sniffer := range sniffers {
|
||||||
metadata, err := sniffer(ctx, packet)
|
err := sniffer(ctx, metadata, packet)
|
||||||
if metadata != nil {
|
if err == nil {
|
||||||
return metadata, nil
|
return nil
|
||||||
}
|
}
|
||||||
errors = append(errors, err)
|
errors = append(errors, err)
|
||||||
}
|
}
|
||||||
return nil, E.Errors(errors...)
|
return E.Errors(errors...)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user