mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 20:28:32 +10:00
Compare commits
190 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bccaedc3c | ||
|
|
be15ce5054 | ||
|
|
2d0326982b | ||
|
|
98c1dd17a0 | ||
|
|
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 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: nekohasekai
|
||||||
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
@@ -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: 完整性要求
|
||||||
|
|||||||
14
.github/update_clients.sh
vendored
Executable file
14
.github/update_clients.sh
vendored
Executable file
@@ -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"
|
||||||
68
.github/workflows/debug.yml
vendored
68
.github/workflows/debug.yml
vendored
@@ -22,25 +22,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # 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.22
|
||||||
- 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
|
continue-on-error: true
|
||||||
- name: Run Test
|
- name: Run Test
|
||||||
run: |
|
run: |
|
||||||
@@ -50,15 +38,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # 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.18.10
|
go-version: ~1.18
|
||||||
- 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
|
||||||
@@ -70,19 +58,39 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # 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@0ad4b8fadaa221de15dcec353f45205ec38ea70b # 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
|
- name: Run Test
|
||||||
run: make ci_build
|
run: make ci_build
|
||||||
cross:
|
cross:
|
||||||
@@ -188,8 +196,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 +208,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@0ad4b8fadaa221de15dcec353f45205ec38ea70b # 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*
|
|
||||||
39
.github/workflows/docker.yml
vendored
39
.github/workflows/docker.yml
vendored
@@ -1,5 +1,9 @@
|
|||||||
name: Build Docker Images
|
name: Build Docker Images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- released
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
@@ -8,8 +12,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@0ad4b8fadaa221de15dcec353f45205ec38ea70b # 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 +48,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@v5
|
||||||
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
|
||||||
|
|||||||
10
.github/workflows/lint.yml
vendored
10
.github/workflows/lint.yml
vendored
@@ -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@0ad4b8fadaa221de15dcec353f45205ec38ea70b # 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.22
|
||||||
- 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
|
||||||
|
|||||||
39
.github/workflows/linux.yml
vendored
Normal file
39
.github/workflows/linux.yml
vendored
Normal file
@@ -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@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.22
|
||||||
|
- 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 }}
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
/vendor/
|
/vendor/
|
||||||
/*.json
|
/*.json
|
||||||
|
/*.srs
|
||||||
/*.db
|
/*.db
|
||||||
/site/
|
/site/
|
||||||
/bin/
|
/bin/
|
||||||
|
|||||||
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@@ -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
|
||||||
86
.goreleaser.fury.yaml
Normal file
86
.goreleaser.fury.yaml
Normal file
@@ -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 }}"
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
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 -w -buildid=
|
||||||
tags:
|
tags:
|
||||||
@@ -30,65 +27,35 @@ 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
|
||||||
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 +63,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 +73,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 +86,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 +103,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 +127,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 +161,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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
|
FROM --platform=$BUILDPLATFORM golang:1.22-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
|
||||||
|
|||||||
36
Makefile
36
Makefile
@@ -1,8 +1,9 @@
|
|||||||
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_GO118 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api
|
||||||
TAGS_GO120 = with_quic,with_ech
|
TAGS_GO120 = with_quic,with_utls
|
||||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
|
TAGS_GO121 = with_ech
|
||||||
|
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,7 +15,7 @@ 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)
|
||||||
@@ -23,6 +24,10 @@ ci_build_go118:
|
|||||||
go build $(PARAMS) $(MAIN)
|
go build $(PARAMS) $(MAIN)
|
||||||
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
|
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
|
||||||
|
|
||||||
|
ci_build_go120:
|
||||||
|
go build $(PARAMS) $(MAIN)
|
||||||
|
go build $(PARAMS) -tags "$(TAGS_GO118),$(TAGS_GO120)" $(MAIN)
|
||||||
|
|
||||||
ci_build:
|
ci_build:
|
||||||
go build $(PARAMS) $(MAIN)
|
go build $(PARAMS) $(MAIN)
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
@@ -59,25 +64,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,9 +193,8 @@ 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
|
mkdocs serve
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
"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/rw"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClashServer interface {
|
type ClashServer interface {
|
||||||
@@ -13,22 +19,86 @@ 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 = rw.WriteUVariant(&buffer, uint64(len(s.Content)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buffer.Write(s.Content)
|
||||||
|
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = rw.WriteVString(&buffer, 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
|
||||||
|
}
|
||||||
|
contentLen, err := rw.ReadUVariant(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Content = make([]byte, contentLen)
|
||||||
|
_, err = io.ReadFull(reader, 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)
|
||||||
|
s.LastEtag, err = rw.ReadVString(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tracker interface {
|
type Tracker interface {
|
||||||
|
|||||||
@@ -46,11 +46,27 @@ 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
|
||||||
|
SourceAddressMatch bool
|
||||||
|
SourcePortMatch bool
|
||||||
|
DestinationAddressMatch bool
|
||||||
|
DestinationPortMatch bool
|
||||||
|
DidMatch bool
|
||||||
|
IgnoreDestinationIPCIDRMatch bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *InboundContext) ResetRuleCache() {
|
||||||
|
c.IPCIDRMatchSource = false
|
||||||
|
c.SourceAddressMatch = false
|
||||||
|
c.SourcePortMatch = false
|
||||||
|
c.DestinationAddressMatch = false
|
||||||
|
c.DestinationPortMatch = false
|
||||||
|
c.DidMatch = false
|
||||||
}
|
}
|
||||||
|
|
||||||
type inboundContextKey struct{}
|
type inboundContextKey struct{}
|
||||||
@@ -83,3 +99,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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ 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/service"
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
mdns "github.com/miekg/dns"
|
mdns "github.com/miekg/dns"
|
||||||
@@ -15,11 +17,12 @@ import (
|
|||||||
|
|
||||||
type Router interface {
|
type Router interface {
|
||||||
Service
|
Service
|
||||||
|
PreStarter
|
||||||
PostStarter
|
PostStarter
|
||||||
|
|
||||||
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 +31,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)
|
||||||
@@ -62,19 +69,44 @@ 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 {
|
||||||
|
StartContext(ctx context.Context, startContext RuleSetStartContext) error
|
||||||
|
Metadata() RuleSetMetadata
|
||||||
|
Close() error
|
||||||
|
HeadlessRule
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|||||||
104
box.go
104
box.go
@@ -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
|
||||||
}
|
}
|
||||||
@@ -145,8 +153,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 +172,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 +180,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 +189,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
|
||||||
@@ -217,16 +235,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 +278,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,7 +297,6 @@ 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, "]")
|
||||||
@@ -263,7 +307,6 @@ func (s *Box) start() error {
|
|||||||
|
|
||||||
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)
|
||||||
@@ -271,14 +314,13 @@ func (s *Box) postStart() error {
|
|||||||
}
|
}
|
||||||
for _, outbound := range s.outbounds {
|
for _, outbound := range s.outbounds {
|
||||||
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound {
|
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound {
|
||||||
s.logger.Trace("post-starting outbound/", outbound.Tag())
|
|
||||||
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/", outbound.Tag())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.Trace("post-starting router")
|
|
||||||
return s.router.PostStart()
|
return s.router.PostStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,41 +331,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
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -43,8 +46,9 @@ 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.(common.Starter); isStarter {
|
||||||
s.logger.Trace("initializing outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
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, "]")
|
||||||
}
|
}
|
||||||
|
|||||||
1
clients/android
Submodule
1
clients/android
Submodule
Submodule clients/android added at 8622e6a5bc
1
clients/apple
Submodule
1
clients/apple
Submodule
Submodule clients/apple added at 0cbe335cbb
@@ -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")
|
||||||
|
|||||||
@@ -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.FileExists(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.FileExists(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.FileExists(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.FileExists(filepath.Join(goBin, "gobind.exe")) {
|
||||||
|
log.Fatal("missing gomobile installation")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !rw.FileExists(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
|
|
||||||
}
|
|
||||||
|
|||||||
43
cmd/sing-box/cmd_geoip.go
Normal file
43
cmd/sing-box/cmd_geoip.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
98
cmd/sing-box/cmd_geoip_export.go
Normal file
98
cmd/sing-box/cmd_geoip_export.go
Normal file
@@ -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.RuleSetVersion1
|
||||||
|
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: headlessRule,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return encoder.Encode(plainRuleSet)
|
||||||
|
}
|
||||||
31
cmd/sing-box/cmd_geoip_list.go
Normal file
31
cmd/sing-box/cmd_geoip_list.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
47
cmd/sing-box/cmd_geoip_lookup.go
Normal file
47
cmd/sing-box/cmd_geoip_lookup.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
41
cmd/sing-box/cmd_geosite.go
Normal file
41
cmd/sing-box/cmd_geosite.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
81
cmd/sing-box/cmd_geosite_export.go
Normal file
81
cmd/sing-box/cmd_geosite_export.go
Normal file
@@ -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.RuleSetVersion1
|
||||||
|
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: headlessRule,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return encoder.Encode(plainRuleSet)
|
||||||
|
}
|
||||||
50
cmd/sing-box/cmd_geosite_list.go
Normal file
50
cmd/sing-box/cmd_geosite_list.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
97
cmd/sing-box/cmd_geosite_lookup.go
Normal file
97
cmd/sing-box/cmd_geosite_lookup.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
56
cmd/sing-box/cmd_geosite_matcher.go
Normal file
56
cmd/sing-box/cmd_geosite_matcher.go
Normal file
@@ -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 ""
|
||||||
|
}
|
||||||
@@ -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])
|
||||||
@@ -65,50 +65,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
|
||||||
}
|
}
|
||||||
|
|||||||
14
cmd/sing-box/cmd_rule_set.go
Normal file
14
cmd/sing-box/cmd_rule_set.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
84
cmd/sing-box/cmd_rule_set_compile.go
Normal file
84
cmd/sing-box/cmd_rule_set_compile.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ruleSet := plainRuleSet.Upgrade()
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
outputFile.Close()
|
||||||
|
os.Remove(outputPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
outputFile.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
83
cmd/sing-box/cmd_rule_set_format.go
Normal file
83
cmd/sing-box/cmd_rule_set_format.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
86
cmd/sing-box/cmd_rule_set_match.go
Normal file
86
cmd/sing-box/cmd_rule_set_match.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
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"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var flagRuleSetMatchFormat string
|
||||||
|
|
||||||
|
var commandRuleSetMatch = &cobra.Command{
|
||||||
|
Use: "match <rule-set path> <domain>",
|
||||||
|
Short: "Check if 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 = compat.Upgrade()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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(&adapter.InboundContext{
|
||||||
|
Domain: domain,
|
||||||
|
}) {
|
||||||
|
println("match rules.[", i, "]: "+currentRule.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
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,
|
||||||
@@ -193,7 +199,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) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
151
common/badtls/read_wait.go
Normal file
151
common/badtls/read_wait.go
Normal file
@@ -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
|
||||||
31
common/badtls/read_wait_ech.go
Normal file
31
common/badtls/read_wait_ech.go
Normal file
@@ -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
|
||||||
13
common/badtls/read_wait_stub.go
Normal file
13
common/badtls/read_wait_stub.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
31
common/badtls/read_wait_utls.go
Normal file
31
common/badtls/read_wait_utls.go
Normal file
@@ -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())
|
||||||
|
|||||||
@@ -15,28 +15,37 @@ 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)
|
||||||
@@ -44,7 +53,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
if options.RoutingMark != 0 {
|
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 {
|
} 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()))
|
||||||
}
|
}
|
||||||
@@ -60,6 +69,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 +110,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 +131,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
listener,
|
listener,
|
||||||
udpAddr4,
|
udpAddr4,
|
||||||
udpAddr6,
|
udpAddr6,
|
||||||
|
options.IsWireGuardListener,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +164,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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
9
common/dialer/wireguard.go
Normal file
9
common/dialer/wireguard.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WireGuardListener interface {
|
||||||
|
ListenPacketCompat(network, address string) (net.PacketConn, error)
|
||||||
|
}
|
||||||
11
common/dialer/wireguard_control.go
Normal file
11
common/dialer/wireguard_control.go
Normal file
@@ -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
|
||||||
9
common/dialer/wiregurad_stub.go
Normal file
9
common/dialer/wiregurad_stub.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
487
common/srs/binary.go
Normal file
487
common/srs/binary.go
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
package srs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/zlib"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/domain"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
|
||||||
|
"go4.org/netipx"
|
||||||
|
)
|
||||||
|
|
||||||
|
var MagicBytes = [3]byte{0x53, 0x52, 0x53} // SRS
|
||||||
|
|
||||||
|
const (
|
||||||
|
ruleItemQueryType uint8 = iota
|
||||||
|
ruleItemNetwork
|
||||||
|
ruleItemDomain
|
||||||
|
ruleItemDomainKeyword
|
||||||
|
ruleItemDomainRegex
|
||||||
|
ruleItemSourceIPCIDR
|
||||||
|
ruleItemIPCIDR
|
||||||
|
ruleItemSourcePort
|
||||||
|
ruleItemSourcePortRange
|
||||||
|
ruleItemPort
|
||||||
|
ruleItemPortRange
|
||||||
|
ruleItemProcessName
|
||||||
|
ruleItemProcessPath
|
||||||
|
ruleItemPackageName
|
||||||
|
ruleItemWIFISSID
|
||||||
|
ruleItemWIFIBSSID
|
||||||
|
ruleItemFinal uint8 = 0xFF
|
||||||
|
)
|
||||||
|
|
||||||
|
func Read(reader io.Reader, recovery bool) (ruleSet option.PlainRuleSet, err error) {
|
||||||
|
var magicBytes [3]byte
|
||||||
|
_, err = io.ReadFull(reader, magicBytes[:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if magicBytes != MagicBytes {
|
||||||
|
err = E.New("invalid sing-box rule set file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var version uint8
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &version)
|
||||||
|
if err != nil {
|
||||||
|
return ruleSet, err
|
||||||
|
}
|
||||||
|
if version != 1 {
|
||||||
|
return ruleSet, E.New("unsupported version: ", version)
|
||||||
|
}
|
||||||
|
zReader, err := zlib.NewReader(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
length, err := rw.ReadUVariant(zReader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ruleSet.Rules = make([]option.HeadlessRule, length)
|
||||||
|
for i := uint64(0); i < length; i++ {
|
||||||
|
ruleSet.Rules[i], err = readRule(zReader, recovery)
|
||||||
|
if err != nil {
|
||||||
|
err = E.Cause(err, "read rule[", i, "]")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Write(writer io.Writer, ruleSet option.PlainRuleSet) error {
|
||||||
|
_, err := writer.Write(MagicBytes[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Write(writer, binary.BigEndian, uint8(1))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
zWriter, err := zlib.NewWriterLevel(writer, zlib.BestCompression)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = rw.WriteUVariant(zWriter, uint64(len(ruleSet.Rules)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, rule := range ruleSet.Rules {
|
||||||
|
err = writeRule(zWriter, rule)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return zWriter.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRule(reader io.Reader, recovery bool) (rule option.HeadlessRule, err error) {
|
||||||
|
var ruleType uint8
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &ruleType)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch ruleType {
|
||||||
|
case 0:
|
||||||
|
rule.Type = C.RuleTypeDefault
|
||||||
|
rule.DefaultOptions, err = readDefaultRule(reader, recovery)
|
||||||
|
case 1:
|
||||||
|
rule.Type = C.RuleTypeLogical
|
||||||
|
rule.LogicalOptions, err = readLogicalRule(reader, recovery)
|
||||||
|
default:
|
||||||
|
err = E.New("unknown rule type: ", ruleType)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRule(writer io.Writer, rule option.HeadlessRule) error {
|
||||||
|
switch rule.Type {
|
||||||
|
case C.RuleTypeDefault:
|
||||||
|
return writeDefaultRule(writer, rule.DefaultOptions)
|
||||||
|
case C.RuleTypeLogical:
|
||||||
|
return writeLogicalRule(writer, rule.LogicalOptions)
|
||||||
|
default:
|
||||||
|
panic("unknown rule type: " + rule.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDefaultRule(reader io.Reader, recovery bool) (rule option.DefaultHeadlessRule, err error) {
|
||||||
|
var lastItemType uint8
|
||||||
|
for {
|
||||||
|
var itemType uint8
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &itemType)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch itemType {
|
||||||
|
case ruleItemQueryType:
|
||||||
|
var rawQueryType []uint16
|
||||||
|
rawQueryType, err = readRuleItemUint16(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rule.QueryType = common.Map(rawQueryType, func(it uint16) option.DNSQueryType {
|
||||||
|
return option.DNSQueryType(it)
|
||||||
|
})
|
||||||
|
case ruleItemNetwork:
|
||||||
|
rule.Network, err = readRuleItemString(reader)
|
||||||
|
case ruleItemDomain:
|
||||||
|
var matcher *domain.Matcher
|
||||||
|
matcher, err = domain.ReadMatcher(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rule.DomainMatcher = matcher
|
||||||
|
case ruleItemDomainKeyword:
|
||||||
|
rule.DomainKeyword, err = readRuleItemString(reader)
|
||||||
|
case ruleItemDomainRegex:
|
||||||
|
rule.DomainRegex, err = readRuleItemString(reader)
|
||||||
|
case ruleItemSourceIPCIDR:
|
||||||
|
rule.SourceIPSet, err = readIPSet(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if recovery {
|
||||||
|
rule.SourceIPCIDR = common.Map(rule.SourceIPSet.Prefixes(), netip.Prefix.String)
|
||||||
|
}
|
||||||
|
case ruleItemIPCIDR:
|
||||||
|
rule.IPSet, err = readIPSet(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if recovery {
|
||||||
|
rule.IPCIDR = common.Map(rule.IPSet.Prefixes(), netip.Prefix.String)
|
||||||
|
}
|
||||||
|
case ruleItemSourcePort:
|
||||||
|
rule.SourcePort, err = readRuleItemUint16(reader)
|
||||||
|
case ruleItemSourcePortRange:
|
||||||
|
rule.SourcePortRange, err = readRuleItemString(reader)
|
||||||
|
case ruleItemPort:
|
||||||
|
rule.Port, err = readRuleItemUint16(reader)
|
||||||
|
case ruleItemPortRange:
|
||||||
|
rule.PortRange, err = readRuleItemString(reader)
|
||||||
|
case ruleItemProcessName:
|
||||||
|
rule.ProcessName, err = readRuleItemString(reader)
|
||||||
|
case ruleItemProcessPath:
|
||||||
|
rule.ProcessPath, err = readRuleItemString(reader)
|
||||||
|
case ruleItemPackageName:
|
||||||
|
rule.PackageName, err = readRuleItemString(reader)
|
||||||
|
case ruleItemWIFISSID:
|
||||||
|
rule.WIFISSID, err = readRuleItemString(reader)
|
||||||
|
case ruleItemWIFIBSSID:
|
||||||
|
rule.WIFIBSSID, err = readRuleItemString(reader)
|
||||||
|
case ruleItemFinal:
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
err = E.New("unknown rule item type: ", itemType, ", last type: ", lastItemType)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastItemType = itemType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeDefaultRule(writer io.Writer, rule option.DefaultHeadlessRule) error {
|
||||||
|
err := binary.Write(writer, binary.BigEndian, uint8(0))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(rule.QueryType) > 0 {
|
||||||
|
err = writeRuleItemUint16(writer, ruleItemQueryType, common.Map(rule.QueryType, func(it option.DNSQueryType) uint16 {
|
||||||
|
return uint16(it)
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.Network) > 0 {
|
||||||
|
err = writeRuleItemString(writer, ruleItemNetwork, rule.Network)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.Domain) > 0 || len(rule.DomainSuffix) > 0 {
|
||||||
|
err = binary.Write(writer, binary.BigEndian, ruleItemDomain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix).Write(writer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.DomainKeyword) > 0 {
|
||||||
|
err = writeRuleItemString(writer, ruleItemDomainKeyword, rule.DomainKeyword)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.DomainRegex) > 0 {
|
||||||
|
err = writeRuleItemString(writer, ruleItemDomainRegex, rule.DomainRegex)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.SourceIPCIDR) > 0 {
|
||||||
|
err = writeRuleItemCIDR(writer, ruleItemSourceIPCIDR, rule.SourceIPCIDR)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "source_ip_cidr")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.IPCIDR) > 0 {
|
||||||
|
err = writeRuleItemCIDR(writer, ruleItemIPCIDR, rule.IPCIDR)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "ipcidr")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.SourcePort) > 0 {
|
||||||
|
err = writeRuleItemUint16(writer, ruleItemSourcePort, rule.SourcePort)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.SourcePortRange) > 0 {
|
||||||
|
err = writeRuleItemString(writer, ruleItemSourcePortRange, rule.SourcePortRange)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.Port) > 0 {
|
||||||
|
err = writeRuleItemUint16(writer, ruleItemPort, rule.Port)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.PortRange) > 0 {
|
||||||
|
err = writeRuleItemString(writer, ruleItemPortRange, rule.PortRange)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.ProcessName) > 0 {
|
||||||
|
err = writeRuleItemString(writer, ruleItemProcessName, rule.ProcessName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.ProcessPath) > 0 {
|
||||||
|
err = writeRuleItemString(writer, ruleItemProcessPath, rule.ProcessPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.PackageName) > 0 {
|
||||||
|
err = writeRuleItemString(writer, ruleItemPackageName, rule.PackageName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.WIFISSID) > 0 {
|
||||||
|
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.WIFIBSSID) > 0 {
|
||||||
|
err = writeRuleItemString(writer, ruleItemWIFIBSSID, rule.WIFIBSSID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = binary.Write(writer, binary.BigEndian, ruleItemFinal)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Write(writer, binary.BigEndian, rule.Invert)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRuleItemString(reader io.Reader) ([]string, error) {
|
||||||
|
length, err := rw.ReadUVariant(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
value := make([]string, length)
|
||||||
|
for i := uint64(0); i < length; i++ {
|
||||||
|
value[i], err = rw.ReadVString(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRuleItemString(writer io.Writer, itemType uint8, value []string) error {
|
||||||
|
err := binary.Write(writer, binary.BigEndian, itemType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = rw.WriteUVariant(writer, uint64(len(value)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, item := range value {
|
||||||
|
err = rw.WriteVString(writer, item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRuleItemUint16(reader io.Reader) ([]uint16, error) {
|
||||||
|
length, err := rw.ReadUVariant(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
value := make([]uint16, length)
|
||||||
|
for i := uint64(0); i < length; i++ {
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &value[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRuleItemUint16(writer io.Writer, itemType uint8, value []uint16) error {
|
||||||
|
err := binary.Write(writer, binary.BigEndian, itemType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = rw.WriteUVariant(writer, uint64(len(value)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, item := range value {
|
||||||
|
err = binary.Write(writer, binary.BigEndian, item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRuleItemCIDR(writer io.Writer, itemType uint8, value []string) error {
|
||||||
|
var builder netipx.IPSetBuilder
|
||||||
|
for i, prefixString := range value {
|
||||||
|
prefix, err := netip.ParsePrefix(prefixString)
|
||||||
|
if err == nil {
|
||||||
|
builder.AddPrefix(prefix)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, addrErr := netip.ParseAddr(prefixString)
|
||||||
|
if addrErr == nil {
|
||||||
|
builder.Add(addr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return E.Cause(err, "parse [", i, "]")
|
||||||
|
}
|
||||||
|
ipSet, err := builder.IPSet()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Write(writer, binary.BigEndian, itemType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeIPSet(writer, ipSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readLogicalRule(reader io.Reader, recovery bool) (logicalRule option.LogicalHeadlessRule, err error) {
|
||||||
|
var mode uint8
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &mode)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case 0:
|
||||||
|
logicalRule.Mode = C.LogicalTypeAnd
|
||||||
|
case 1:
|
||||||
|
logicalRule.Mode = C.LogicalTypeOr
|
||||||
|
default:
|
||||||
|
err = E.New("unknown logical mode: ", mode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
length, err := rw.ReadUVariant(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logicalRule.Rules = make([]option.HeadlessRule, length)
|
||||||
|
for i := uint64(0); i < length; i++ {
|
||||||
|
logicalRule.Rules[i], err = readRule(reader, recovery)
|
||||||
|
if err != nil {
|
||||||
|
err = E.Cause(err, "read logical rule [", i, "]")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &logicalRule.Invert)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeLogicalRule(writer io.Writer, logicalRule option.LogicalHeadlessRule) error {
|
||||||
|
err := binary.Write(writer, binary.BigEndian, uint8(1))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch logicalRule.Mode {
|
||||||
|
case C.LogicalTypeAnd:
|
||||||
|
err = binary.Write(writer, binary.BigEndian, uint8(0))
|
||||||
|
case C.LogicalTypeOr:
|
||||||
|
err = binary.Write(writer, binary.BigEndian, uint8(1))
|
||||||
|
default:
|
||||||
|
panic("unknown logical mode: " + logicalRule.Mode)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = rw.WriteUVariant(writer, uint64(len(logicalRule.Rules)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, rule := range logicalRule.Rules {
|
||||||
|
err = writeRule(writer, rule)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = binary.Write(writer, binary.BigEndian, logicalRule.Invert)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
116
common/srs/ip_set.go
Normal file
116
common/srs/ip_set.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package srs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
|
||||||
|
"go4.org/netipx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type myIPSet struct {
|
||||||
|
rr []myIPRange
|
||||||
|
}
|
||||||
|
|
||||||
|
type myIPRange struct {
|
||||||
|
from netip.Addr
|
||||||
|
to netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func readIPSet(reader io.Reader) (*netipx.IPSet, error) {
|
||||||
|
var version uint8
|
||||||
|
err := binary.Read(reader, binary.BigEndian, &version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var length uint64
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &length)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mySet := &myIPSet{
|
||||||
|
rr: make([]myIPRange, length),
|
||||||
|
}
|
||||||
|
for i := uint64(0); i < length; i++ {
|
||||||
|
var (
|
||||||
|
fromLen uint64
|
||||||
|
toLen uint64
|
||||||
|
fromAddr netip.Addr
|
||||||
|
toAddr netip.Addr
|
||||||
|
)
|
||||||
|
fromLen, err = rw.ReadUVariant(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fromBytes := make([]byte, fromLen)
|
||||||
|
_, err = io.ReadFull(reader, fromBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = fromAddr.UnmarshalBinary(fromBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
toLen, err = rw.ReadUVariant(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
toBytes := make([]byte, toLen)
|
||||||
|
_, err = io.ReadFull(reader, toBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = toAddr.UnmarshalBinary(toBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mySet.rr[i] = myIPRange{fromAddr, toAddr}
|
||||||
|
}
|
||||||
|
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeIPSet(writer io.Writer, set *netipx.IPSet) error {
|
||||||
|
err := binary.Write(writer, binary.BigEndian, uint8(1))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mySet := (*myIPSet)(unsafe.Pointer(set))
|
||||||
|
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, rr := range mySet.rr {
|
||||||
|
var (
|
||||||
|
fromBinary []byte
|
||||||
|
toBinary []byte
|
||||||
|
)
|
||||||
|
fromBinary, err = rr.from.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = rw.WriteUVariant(writer, uint64(len(fromBinary)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = writer.Write(fromBinary)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
toBinary, err = rr.to.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = rw.WriteUVariant(writer, uint64(len(toBinary)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = writer.Write(toBinary)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
31
common/taskmonitor/monitor.go
Normal file
31
common/taskmonitor/monitor.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package taskmonitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Monitor struct {
|
||||||
|
logger logger.Logger
|
||||||
|
timeout time.Duration
|
||||||
|
timer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(logger logger.Logger, timeout time.Duration) *Monitor {
|
||||||
|
return &Monitor{
|
||||||
|
logger: logger,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Monitor) Start(taskName ...any) {
|
||||||
|
m.timer = time.AfterFunc(m.timeout, func() {
|
||||||
|
m.logger.Warn(F.ToString(taskName...), " take too much time to finish!")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Monitor) Finish() {
|
||||||
|
m.timer.Stop()
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/badtls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
@@ -42,7 +43,17 @@ func NewClient(ctx context.Context, serverAddress string, options option.Outboun
|
|||||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return aTLS.ClientHandshake(ctx, conn, config)
|
tlsConn, err := aTLS.ClientHandshake(ctx, conn, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
readWaitConn, err := badtls.NewReadWaitConn(tlsConn)
|
||||||
|
if err == nil {
|
||||||
|
return readWaitConn, nil
|
||||||
|
} else if err != os.ErrInvalid {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tlsConn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Dialer struct {
|
type Dialer struct {
|
||||||
|
|||||||
@@ -27,11 +27,10 @@ func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, ad
|
|||||||
return quic.DialEarly(ctx, conn, addr, c.config, config)
|
return quic.DialEarly(ctx, conn, addr, c.config, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config, enableDatagrams bool) http.RoundTripper {
|
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper {
|
||||||
return &http3.RoundTripper{
|
return &http3.RoundTripper{
|
||||||
TLSClientConfig: c.config,
|
TLSClientConfig: c.config,
|
||||||
QuicConfig: quicConfig,
|
QUICConfig: quicConfig,
|
||||||
EnableDatagrams: enableDatagrams,
|
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
|
quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
|
"crypto/ecdh"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
@@ -137,12 +138,21 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
|
|||||||
hello.SessionId[2] = 1
|
hello.SessionId[2] = 1
|
||||||
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
|
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
|
||||||
copy(hello.SessionId[8:], e.shortID[:])
|
copy(hello.SessionId[8:], e.shortID[:])
|
||||||
|
|
||||||
if debug.Enabled {
|
if debug.Enabled {
|
||||||
fmt.Printf("REALITY hello.sessionId[:16]: %v\n", hello.SessionId[:16])
|
fmt.Printf("REALITY hello.sessionId[:16]: %v\n", hello.SessionId[:16])
|
||||||
}
|
}
|
||||||
|
publicKey, err := ecdh.X25519().NewPublicKey(e.publicKey)
|
||||||
authKey := uConn.HandshakeState.State13.EcdheParams.SharedKey(e.publicKey)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ecdheKey := uConn.HandshakeState.State13.EcdheKey
|
||||||
|
if ecdheKey == nil {
|
||||||
|
return nil, E.New("nil ecdhe_key")
|
||||||
|
}
|
||||||
|
authKey, err := ecdheKey.ECDH(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if authKey == nil {
|
if authKey == nil {
|
||||||
return nil, E.New("nil auth_key")
|
return nil, E.New("nil auth_key")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package tls
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/badtls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
@@ -26,5 +28,15 @@ func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLS
|
|||||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return aTLS.ServerHandshake(ctx, conn, config)
|
tlsConn, err := aTLS.ServerHandshake(ctx, conn, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
readWaitConn, err := badtls.NewReadWaitConn(tlsConn)
|
||||||
|
if err == nil {
|
||||||
|
return readWaitConn, nil
|
||||||
|
} else if err != os.ErrInvalid {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tlsConn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,16 @@ func uTLSClientHelloID(name string) (utls.ClientHelloID, error) {
|
|||||||
switch name {
|
switch name {
|
||||||
case "chrome", "":
|
case "chrome", "":
|
||||||
return utls.HelloChrome_Auto, nil
|
return utls.HelloChrome_Auto, nil
|
||||||
|
case "chrome_psk":
|
||||||
|
return utls.HelloChrome_100_PSK, nil
|
||||||
|
case "chrome_psk_shuffle":
|
||||||
|
return utls.HelloChrome_112_PSK_Shuf, nil
|
||||||
|
case "chrome_padding_psk_shuffle":
|
||||||
|
return utls.HelloChrome_114_Padding_PSK_Shuf, nil
|
||||||
|
case "chrome_pq":
|
||||||
|
return utls.HelloChrome_115_PQ, nil
|
||||||
|
case "chrome_pq_psk":
|
||||||
|
return utls.HelloChrome_115_PQ_PSK, nil
|
||||||
case "firefox":
|
case "firefox":
|
||||||
return utls.HelloFirefox_Auto, nil
|
return utls.HelloFirefox_Auto, nil
|
||||||
case "edge":
|
case "edge":
|
||||||
|
|||||||
5
constant/quic.go
Normal file
5
constant/quic.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
|
package constant
|
||||||
|
|
||||||
|
const WithQUIC = true
|
||||||
5
constant/quic_stub.go
Normal file
5
constant/quic_stub.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build !with_quic
|
||||||
|
|
||||||
|
package constant
|
||||||
|
|
||||||
|
const WithQUIC = false
|
||||||
@@ -9,3 +9,11 @@ const (
|
|||||||
LogicalTypeAnd = "and"
|
LogicalTypeAnd = "and"
|
||||||
LogicalTypeOr = "or"
|
LogicalTypeOr = "or"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RuleSetTypeLocal = "local"
|
||||||
|
RuleSetTypeRemote = "remote"
|
||||||
|
RuleSetVersion1 = 1
|
||||||
|
RuleSetFormatSource = "source"
|
||||||
|
RuleSetFormatBinary = "binary"
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,11 +3,18 @@ package constant
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TCPTimeout = 5 * time.Second
|
TCPKeepAliveInitial = 10 * time.Minute
|
||||||
ReadPayloadTimeout = 300 * time.Millisecond
|
TCPKeepAliveInterval = 75 * time.Second
|
||||||
DNSTimeout = 10 * time.Second
|
TCPTimeout = 5 * time.Second
|
||||||
QUICTimeout = 30 * time.Second
|
ReadPayloadTimeout = 300 * time.Millisecond
|
||||||
STUNTimeout = 15 * time.Second
|
DNSTimeout = 10 * time.Second
|
||||||
UDPTimeout = 5 * time.Minute
|
QUICTimeout = 30 * time.Second
|
||||||
DefaultURLTestInterval = 1 * time.Minute
|
STUNTimeout = 15 * time.Second
|
||||||
|
UDPTimeout = 5 * time.Minute
|
||||||
|
DefaultURLTestInterval = 3 * time.Minute
|
||||||
|
DefaultURLTestIdleTimeout = 30 * time.Minute
|
||||||
|
StartTimeout = 10 * time.Second
|
||||||
|
StopTimeout = 5 * time.Second
|
||||||
|
FatalStopTimeout = 10 * time.Second
|
||||||
|
FakeIPMetadataSaveInterval = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import (
|
|||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/badjson"
|
|
||||||
"github.com/sagernet/sing-box/common/humanize"
|
"github.com/sagernet/sing-box/common/humanize"
|
||||||
"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"
|
"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/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,406 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
# ChangeLog
|
### 1.9.2
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
### 1.9.1
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
### 1.9.0
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
Important changes since 1.8:
|
||||||
|
|
||||||
|
* `domain_suffix` behavior update **1**
|
||||||
|
* `process_path` format update on Windows **2**
|
||||||
|
* Add address filter DNS rule items **3**
|
||||||
|
* Add support for `client-subnet` DNS options **4**
|
||||||
|
* Add rejected DNS response cache support **5**
|
||||||
|
* Add `bypass_domain` and `search_domain` platform HTTP proxy options **6**
|
||||||
|
* Fix missing `rule_set_ipcidr_match_source` item in DNS rules **7**
|
||||||
|
* Handle Windows power events
|
||||||
|
* Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled
|
||||||
|
* Improve DNS truncate behavior
|
||||||
|
* Update Hysteria protocol
|
||||||
|
* Update quic-go to v0.43.1
|
||||||
|
* Update gVisor to 20240422.0
|
||||||
|
* Mitigating TunnelVision attacks **8**
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [Migration](/migration/#domain_suffix-behavior-update).
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
See [Migration](/migration/#process_path-format-update-on-windows).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
|
||||||
|
if using this method.
|
||||||
|
|
||||||
|
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
|
||||||
|
|
||||||
|
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
||||||
|
|
||||||
|
**4**:
|
||||||
|
|
||||||
|
See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule).
|
||||||
|
|
||||||
|
Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests,
|
||||||
|
the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated.
|
||||||
|
|
||||||
|
**5**:
|
||||||
|
|
||||||
|
The new feature allows you to cache the check results of
|
||||||
|
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
|
||||||
|
|
||||||
|
**6**:
|
||||||
|
|
||||||
|
See [TUN](/configuration/inbound/tun) inbound.
|
||||||
|
|
||||||
|
**7**:
|
||||||
|
|
||||||
|
See [DNS Rule](/configuration/dns/rule/).
|
||||||
|
|
||||||
|
**8**:
|
||||||
|
|
||||||
|
See [TunnelVision](/manual/misc/tunnelvision).
|
||||||
|
|
||||||
|
#### 1.9.0-rc.22
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-rc.20
|
||||||
|
|
||||||
|
* Prioritize `*_route_address` in linux auto-route
|
||||||
|
* Fix `*_route_address` in darwin auto-route
|
||||||
|
|
||||||
|
#### 1.8.14
|
||||||
|
|
||||||
|
* Fix hysteria2 panic
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-rc.18
|
||||||
|
|
||||||
|
* Add custom prefix support in EDNS0 client subnet options
|
||||||
|
* Fix hysteria2 crash
|
||||||
|
* Fix `store_rdrc` corrupted
|
||||||
|
* Update quic-go to v0.43.1
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-rc.16
|
||||||
|
|
||||||
|
* Mitigating TunnelVision attacks **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [TunnelVision](/manual/misc/tunnelvision).
|
||||||
|
|
||||||
|
#### 1.9.0-rc.15
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.13
|
||||||
|
|
||||||
|
* Fix fake-ip mapping
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-rc.14
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-rc.13
|
||||||
|
|
||||||
|
* Update Hysteria protocol
|
||||||
|
* Update quic-go to v0.43.0
|
||||||
|
* Update gVisor to 20240422.0
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.12
|
||||||
|
|
||||||
|
* Now we have official APT and DNF repositories **1**
|
||||||
|
* Fix packet MTU for QUIC protocols
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Including stable and beta versions, see https://sing-box.sagernet.org/installation/package-manager/
|
||||||
|
|
||||||
|
#### 1.9.0-rc.11
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.11
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.10
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-beta.17
|
||||||
|
|
||||||
|
* Update `quic-go` to v0.42.0
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-beta.16
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
_Our Testflight distribution has been temporarily blocked by Apple (possibly due to too many beta versions)
|
||||||
|
and you cannot join the test, install or update the sing-box beta app right now.
|
||||||
|
Please wait patiently for processing._
|
||||||
|
|
||||||
|
#### 1.9.0-beta.14
|
||||||
|
|
||||||
|
* Update gVisor to 20240212.0-65-g71212d503
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.9
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.8
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-beta.7
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-beta.6
|
||||||
|
|
||||||
|
* Fix address filter DNS rule items **1**
|
||||||
|
* Fix DNS outbound responding with wrong data
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Fixed an issue where address filter DNS rule was incorrectly rejected under certain circumstances.
|
||||||
|
If you have enabled `store_rdrc` to save results, consider clearing the cache file.
|
||||||
|
|
||||||
|
#### 1.8.7
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.15
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.14
|
||||||
|
|
||||||
|
* Improve DNS truncate behavior
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.13
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.6
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.12
|
||||||
|
|
||||||
|
* Handle Windows power events
|
||||||
|
* Always disable cache for fake-ip DNS transport if `dns.independent_cache` disabled
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.11
|
||||||
|
|
||||||
|
* Fix missing `rule_set_ipcidr_match_source` item in DNS rules **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [DNS Rule](/configuration/dns/rule/).
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.10
|
||||||
|
|
||||||
|
* Add `bypass_domain` and `search_domain` platform HTTP proxy options **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [TUN](/configuration/inbound/tun) inbound.
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.8
|
||||||
|
|
||||||
|
* Add rejected DNS response cache support **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
The new feature allows you to cache the check results of
|
||||||
|
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.7
|
||||||
|
|
||||||
|
* Update gVisor to 20240206.0
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.6
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.3
|
||||||
|
|
||||||
|
* Update `quic-go` to v0.41.0
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.2
|
||||||
|
|
||||||
|
* Add support for `client-subnet` DNS options **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule).
|
||||||
|
|
||||||
|
Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests,
|
||||||
|
the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated.
|
||||||
|
|
||||||
|
#### 1.9.0-alpha.1
|
||||||
|
|
||||||
|
* `domain_suffix` behavior update **1**
|
||||||
|
* `process_path` format update on Windows **2**
|
||||||
|
* Add address filter DNS rule items **3**
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [Migration](/migration/#domain_suffix-behavior-update).
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
See [Migration](/migration/#process_path-format-update-on-windows).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
|
||||||
|
if using this method.
|
||||||
|
|
||||||
|
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
|
||||||
|
|
||||||
|
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
||||||
|
|
||||||
|
#### 1.8.5
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.4
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.2
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.1
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
### 1.8.0
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
Important changes since 1.7:
|
||||||
|
|
||||||
|
* Migrate cache file from Clash API to independent options **1**
|
||||||
|
* Introducing [Rule Set](/configuration/rule-set/) **2**
|
||||||
|
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
|
||||||
|
* Allow nested logical rules **4**
|
||||||
|
* Independent `source_ip_is_private` and `ip_is_private` rules **5**
|
||||||
|
* Add context to JSON decode error message **6**
|
||||||
|
* Reject internal fake-ip queries **7**
|
||||||
|
* Add GSO support for TUN and WireGuard system interface **8**
|
||||||
|
* Add `idle_timeout` for URLTest outbound **9**
|
||||||
|
* Add simple loopback detect
|
||||||
|
* Optimize memory usage of idle connections
|
||||||
|
* Update uTLS to 1.5.4 **10**
|
||||||
|
* Update dependencies **11**
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [Cache File](/configuration/experimental/cache-file/) and
|
||||||
|
[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options).
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
Rule set is independent collections of rules that can be compiled into binaries to improve performance.
|
||||||
|
Compared to legacy GeoIP and Geosite resources,
|
||||||
|
it can include more types of rules, load faster,
|
||||||
|
use less memory, and update automatically.
|
||||||
|
|
||||||
|
See [Route#rule_set](/configuration/route/#rule_set),
|
||||||
|
[Route Rule](/configuration/route/rule/),
|
||||||
|
[DNS Rule](/configuration/dns/rule/),
|
||||||
|
[Rule Set](/configuration/rule-set/),
|
||||||
|
[Source Format](/configuration/rule-set/source-format/) and
|
||||||
|
[Headless Rule](/configuration/rule-set/headless-rule/).
|
||||||
|
|
||||||
|
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and
|
||||||
|
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets.
|
||||||
|
|
||||||
|
**4**:
|
||||||
|
|
||||||
|
Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules.
|
||||||
|
|
||||||
|
**5**:
|
||||||
|
|
||||||
|
The `private` GeoIP country never existed and was actually implemented inside V2Ray.
|
||||||
|
Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets).
|
||||||
|
|
||||||
|
**6**:
|
||||||
|
|
||||||
|
JSON parse errors will now include the current key path.
|
||||||
|
Only takes effect when compiled with Go 1.21+.
|
||||||
|
|
||||||
|
**7**:
|
||||||
|
|
||||||
|
All internal DNS queries now skip DNS rules with `server` type `fakeip`,
|
||||||
|
and the default DNS server can no longer be `fakeip`.
|
||||||
|
|
||||||
|
This change is intended to break incorrect usage and essentially requires no action.
|
||||||
|
|
||||||
|
**8**:
|
||||||
|
|
||||||
|
See [TUN](/configuration/inbound/tun/) inbound and [WireGuard](/configuration/outbound/wireguard/) outbound.
|
||||||
|
|
||||||
|
**9**:
|
||||||
|
|
||||||
|
When URLTest is idle for a certain period of time, the scheduled delay test will be paused.
|
||||||
|
|
||||||
|
**10**:
|
||||||
|
|
||||||
|
Added some new [fingerprints](/configuration/shared/tls#utls).
|
||||||
|
Also, starting with this release, uTLS requires at least Go 1.20.
|
||||||
|
|
||||||
|
**11**:
|
||||||
|
|
||||||
|
Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, `quic-go` to `0.40.1` and `gvisor`
|
||||||
|
to `20231204.0`
|
||||||
|
|
||||||
|
#### 1.8.0-rc.11
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.7.8
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.0-rc.10
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.7.7
|
#### 1.7.7
|
||||||
|
|
||||||
@@ -13,30 +412,192 @@ icon: material/alert-decagram
|
|||||||
|
|
||||||
See [V2Ray transport](/configuration/shared/v2ray-transport/).
|
See [V2Ray transport](/configuration/shared/v2ray-transport/).
|
||||||
|
|
||||||
|
#### 1.8.0-rc.7
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.0-rc.3
|
||||||
|
|
||||||
|
* Fix V2Ray transport `path` validation behavior **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [V2Ray transport](/configuration/shared/v2ray-transport/).
|
||||||
|
|
||||||
#### 1.7.6
|
#### 1.7.6
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.0-rc.1
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.0-beta.9
|
||||||
|
|
||||||
|
* Add simple loopback detect
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.7.5
|
#### 1.7.5
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.0-alpha.17
|
||||||
|
|
||||||
|
* Add GSO support for TUN and WireGuard system interface **1**
|
||||||
|
* Update uTLS to 1.5.4 **2**
|
||||||
|
* Update dependencies **3**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [TUN](/configuration/inbound/tun/) inbound and [WireGuard](/configuration/outbound/wireguard/) outbound.
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
Added some new [fingerprints](/configuration/shared/tls#utls).
|
||||||
|
Also, starting with this release, uTLS requires at least Go 1.20.
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, and `gvisor` to `20231204.0`
|
||||||
|
|
||||||
|
This may break something, good luck!
|
||||||
|
|
||||||
#### 1.7.4
|
#### 1.7.4
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
_Due to the long waiting time, this version is no longer waiting for approval
|
_Due to the long waiting time, this version is no longer waiting for approval
|
||||||
by the Apple App Store, so updates to Apple Platforms will be delayed._
|
by the Apple App Store, so updates to Apple Platforms will be delayed._
|
||||||
|
|
||||||
|
#### 1.8.0-alpha.16
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.0-alpha.15
|
||||||
|
|
||||||
|
* Some chaotic changes **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Designed to optimize memory usage of idle connections, may take effect on the following protocols:
|
||||||
|
|
||||||
|
| Protocol | TCP | UDP |
|
||||||
|
|------------------------------------------------------|------------------|------------------|
|
||||||
|
| HTTP proxy server | :material-check: | / |
|
||||||
|
| SOCKS5 | :material-close: | :material-check: |
|
||||||
|
| Shadowsocks none/AEAD/AEAD2022 | :material-check: | :material-check: |
|
||||||
|
| Trojan | / | :material-check: |
|
||||||
|
| TUIC/Hysteria/Hysteria2 | :material-close: | :material-check: |
|
||||||
|
| Multiplex | :material-close: | :material-check: |
|
||||||
|
| Plain TLS (Trojan/VLESS without extra sub-protocols) | :material-check: | / |
|
||||||
|
| Other protocols | :material-close: | :material-close: |
|
||||||
|
|
||||||
|
At the same time, everything existing may be broken, please actively report problems with this version.
|
||||||
|
|
||||||
|
#### 1.8.0-alpha.13
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.0-alpha.10
|
||||||
|
|
||||||
|
* Add `idle_timeout` for URLTest outbound **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
When URLTest is idle for a certain period of time, the scheduled delay test will be paused.
|
||||||
|
|
||||||
#### 1.7.2
|
#### 1.7.2
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.8.0-alpha.8
|
||||||
|
|
||||||
|
* Add context to JSON decode error message **1**
|
||||||
|
* Reject internal fake-ip queries **2**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
JSON parse errors will now include the current key path.
|
||||||
|
Only takes effect when compiled with Go 1.21+.
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
All internal DNS queries now skip DNS rules with `server` type `fakeip`,
|
||||||
|
and the default DNS server can no longer be `fakeip`.
|
||||||
|
|
||||||
|
This change is intended to break incorrect usage and essentially requires no action.
|
||||||
|
|
||||||
|
#### 1.8.0-alpha.7
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.7.1
|
#### 1.7.1
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.7.0
|
#### 1.8.0-alpha.6
|
||||||
|
|
||||||
|
* Fix rule-set matching logic **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule sets,
|
||||||
|
rather than completely following the AND logic.
|
||||||
|
|
||||||
|
#### 1.8.0-alpha.5
|
||||||
|
|
||||||
|
* Parallel rule-set initialization
|
||||||
|
* Independent `source_ip_is_private` and `ip_is_private` rules **1**
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
The `private` GeoIP country never existed and was actually implemented inside V2Ray.
|
||||||
|
Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets).
|
||||||
|
|
||||||
|
#### 1.8.0-alpha.1
|
||||||
|
|
||||||
|
* Migrate cache file from Clash API to independent options **1**
|
||||||
|
* Introducing [Rule Set](/configuration/rule-set/) **2**
|
||||||
|
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
|
||||||
|
* Allow nested logical rules **4**
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [Cache File](/configuration/experimental/cache-file/) and
|
||||||
|
[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options).
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
Rule set is independent collections of rules that can be compiled into binaries to improve performance.
|
||||||
|
Compared to legacy GeoIP and Geosite resources,
|
||||||
|
it can include more types of rules, load faster,
|
||||||
|
use less memory, and update automatically.
|
||||||
|
|
||||||
|
See [Route#rule_set](/configuration/route/#rule_set),
|
||||||
|
[Route Rule](/configuration/route/rule/),
|
||||||
|
[DNS Rule](/configuration/dns/rule/),
|
||||||
|
[Rule Set](/configuration/rule-set/),
|
||||||
|
[Source Format](/configuration/rule-set/source-format/) and
|
||||||
|
[Headless Rule](/configuration/rule-set/headless-rule/).
|
||||||
|
|
||||||
|
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and
|
||||||
|
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets.
|
||||||
|
|
||||||
|
**4**:
|
||||||
|
|
||||||
|
Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules.
|
||||||
|
|
||||||
|
### 1.7.0
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -68,7 +629,8 @@ The new HTTPUpgrade transport has better performance than WebSocket and is bette
|
|||||||
**3**:
|
**3**:
|
||||||
|
|
||||||
Starting in 1.7.0, multiplexing support is no longer enabled by default
|
Starting in 1.7.0, multiplexing support is no longer enabled by default
|
||||||
and needs to be turned on explicitly in inbound options.
|
and needs to be turned on explicitly in inbound
|
||||||
|
options.
|
||||||
|
|
||||||
**4**
|
**4**
|
||||||
|
|
||||||
@@ -77,7 +639,7 @@ see [TCP Brutal](/configuration/shared/tcp-brutal/) for details.
|
|||||||
|
|
||||||
**5**:
|
**5**:
|
||||||
|
|
||||||
Only supported in graphical clients on Android and iOS.
|
Only supported in graphical clients on Android and Apple platforms.
|
||||||
|
|
||||||
#### 1.7.0-rc.3
|
#### 1.7.0-rc.3
|
||||||
|
|
||||||
@@ -114,7 +676,7 @@ Only supported in graphical clients on Android and iOS.
|
|||||||
|
|
||||||
**1**:
|
**1**:
|
||||||
|
|
||||||
Only supported in graphical clients on Android and iOS.
|
Only supported in graphical clients on Android and Apple platforms.
|
||||||
|
|
||||||
#### 1.7.0-beta.3
|
#### 1.7.0-beta.3
|
||||||
|
|
||||||
@@ -195,7 +757,7 @@ Introduced in V2Ray 5.10.0.
|
|||||||
|
|
||||||
The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse.
|
The new HTTPUpgrade transport has better performance than WebSocket and is better suited for CDN abuse.
|
||||||
|
|
||||||
#### 1.6.0
|
### 1.6.0
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -250,7 +812,8 @@ When `auto_route` is enabled and `strict_route` is disabled, the device can now
|
|||||||
**2**:
|
**2**:
|
||||||
|
|
||||||
Built using Go 1.20, the last version that will run on
|
Built using Go 1.20, the last version that will run on
|
||||||
Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave.
|
Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High
|
||||||
|
Sierra, 10.14 Mojave.
|
||||||
|
|
||||||
#### 1.6.0-rc.4
|
#### 1.6.0-rc.4
|
||||||
|
|
||||||
@@ -264,7 +827,8 @@ Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave
|
|||||||
**1**:
|
**1**:
|
||||||
|
|
||||||
Built using Go 1.20, the last version that will run on
|
Built using Go 1.20, the last version that will run on
|
||||||
Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High Sierra, 10.14 Mojave.
|
Windows 7, 8, Server 2008, Server 2012 and macOS 10.13 High
|
||||||
|
Sierra, 10.14 Mojave.
|
||||||
|
|
||||||
#### 1.6.0-beta.4
|
#### 1.6.0-beta.4
|
||||||
|
|
||||||
@@ -372,7 +936,7 @@ introduce new issues.
|
|||||||
None of the existing Golang BBR congestion control implementations have been reviewed or unit tested.
|
None of the existing Golang BBR congestion control implementations have been reviewed or unit tested.
|
||||||
This update is intended to address the multi-send defects of the old implementation and may introduce new issues.
|
This update is intended to address the multi-send defects of the old implementation and may introduce new issues.
|
||||||
|
|
||||||
#### 1.5.0
|
### 1.5.0
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -566,7 +1130,7 @@ All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria`, `T
|
|||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.4.0
|
### 1.4.0
|
||||||
|
|
||||||
* Fix bugs and update dependencies
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
@@ -692,7 +1256,8 @@ downloaded through TestFlight.
|
|||||||
|
|
||||||
#### 1.3.1-beta.3
|
#### 1.3.1-beta.3
|
||||||
|
|
||||||
* Introducing our [new iOS](/installation/clients/sfi/) and [macOS](/installation/clients/sfm/) client applications **1**
|
* Introducing our [new iOS](/installation/clients/sfi/) and [macOS](/installation/clients/sfm/) client applications **1
|
||||||
|
**
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
**1**:
|
**1**:
|
||||||
@@ -707,7 +1272,7 @@ The old testflight link and app are no longer valid.
|
|||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.3.0
|
### 1.3.0
|
||||||
|
|
||||||
* Fix bugs and update dependencies
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
@@ -899,7 +1464,7 @@ to `domain` rule.
|
|||||||
* Flush DNS cache for macOS when tun start/close
|
* Flush DNS cache for macOS when tun start/close
|
||||||
* Fix tun's DNS hijacking compatibility with systemd-resolved
|
* Fix tun's DNS hijacking compatibility with systemd-resolved
|
||||||
|
|
||||||
#### 1.2.0
|
### 1.2.0
|
||||||
|
|
||||||
* Fix bugs and update dependencies
|
* Fix bugs and update dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ SFA provides an unprivileged TUN implementation through Android VpnService.
|
|||||||
| `inet4_address` | :material-check: | / |
|
| `inet4_address` | :material-check: | / |
|
||||||
| `inet6_address` | :material-check: | / |
|
| `inet6_address` | :material-check: | / |
|
||||||
| `mtu` | :material-check: | / |
|
| `mtu` | :material-check: | / |
|
||||||
|
| `gso` | :material-close: | No permission |
|
||||||
| `auto_route` | :material-check: | / |
|
| `auto_route` | :material-check: | / |
|
||||||
| `strict_route` | :material-close: | Not implemented |
|
| `strict_route` | :material-close: | Not implemented |
|
||||||
| `inet4_route_address` | :material-check: | / |
|
| `inet4_route_address` | :material-check: | / |
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
|
|||||||
* [Play Store](https://play.google.com/store/apps/details?id=io.nekohasekai.sfa)
|
* [Play Store](https://play.google.com/store/apps/details?id=io.nekohasekai.sfa)
|
||||||
* [Play Store (Beta)](https://play.google.com/apps/testing/io.nekohasekai.sfa)
|
* [Play Store (Beta)](https://play.google.com/apps/testing/io.nekohasekai.sfa)
|
||||||
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases)
|
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases)
|
||||||
|
* [F-Droid](https://f-droid.org/packages/io.nekohasekai.sfa/) (Unified signature via reproducible builds)
|
||||||
|
|
||||||
## :material-source-repository: Source code
|
## :material-source-repository: Source code
|
||||||
|
|
||||||
|
|||||||
@@ -14,28 +14,29 @@ SFI/SFM/SFT allows you to run sing-box through NetworkExtension with Application
|
|||||||
|
|
||||||
SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension.
|
SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension.
|
||||||
|
|
||||||
| TUN inbound option | Available | Note |
|
| TUN inbound option | Available | Note |
|
||||||
|-------------------------------|-----------|-------------------|
|
|-------------------------------|-------------------|-------------------|
|
||||||
| `interface_name` | ✖️ | Managed by Darwin |
|
| `interface_name` | :material-close:️ | Managed by Darwin |
|
||||||
| `inet4_address` | ✔️ | / |
|
| `inet4_address` | :material-check: | / |
|
||||||
| `inet6_address` | ✔️ | / |
|
| `inet6_address` | :material-check: | / |
|
||||||
| `mtu` | ✔️ | / |
|
| `mtu` | :material-check: | / |
|
||||||
| `auto_route` | ✔️ | / |
|
| `gso` | :material-close: | Not implemented |
|
||||||
| `strict_route` | ✖️ | Not implemented |
|
| `auto_route` | :material-check: | / |
|
||||||
| `inet4_route_address` | ✔️ | / |
|
| `strict_route` | :material-close:️ | Not implemented |
|
||||||
| `inet6_route_address` | ✔️ | / |
|
| `inet4_route_address` | :material-check: | / |
|
||||||
| `inet4_route_exclude_address` | ✔️ | / |
|
| `inet6_route_address` | :material-check: | / |
|
||||||
| `inet6_route_exclude_address` | ✔️ | / |
|
| `inet4_route_exclude_address` | :material-check: | / |
|
||||||
| `endpoint_independent_nat` | ✔️ | / |
|
| `inet6_route_exclude_address` | :material-check: | / |
|
||||||
| `stack` | ✔️ | / |
|
| `endpoint_independent_nat` | :material-check: | / |
|
||||||
| `include_interface` | ✖️ | Not implemented |
|
| `stack` | :material-check: | / |
|
||||||
| `exclude_interface` | ✖️ | Not implemented |
|
| `include_interface` | :material-close:️ | Not implemented |
|
||||||
| `include_uid` | ✖️ | Not implemented |
|
| `exclude_interface` | :material-close:️ | Not implemented |
|
||||||
| `exclude_uid` | ✖️ | Not implemented |
|
| `include_uid` | :material-close:️ | Not implemented |
|
||||||
| `include_android_user` | ✖️ | Not implemented |
|
| `exclude_uid` | :material-close:️ | Not implemented |
|
||||||
| `include_package` | ✖️ | Not implemented |
|
| `include_android_user` | :material-close:️ | Not implemented |
|
||||||
| `exclude_package` | ✖️ | Not implemented |
|
| `include_package` | :material-close:️ | Not implemented |
|
||||||
| `platform` | ✔️ | / |
|
| `exclude_package` | :material-close:️ | Not implemented |
|
||||||
|
| `platform` | :material-check: | / |
|
||||||
|
|
||||||
| Route/DNS rule option | Available | Note |
|
| Route/DNS rule option | Available | Note |
|
||||||
|-----------------------|------------------|-----------------------|
|
|-----------------------|------------------|-----------------------|
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ platform-specific function implementation, such as TUN transparent proxy impleme
|
|||||||
## :material-download: Download
|
## :material-download: Download
|
||||||
|
|
||||||
* [App Store](https://apps.apple.com/us/app/sing-box/id6451272673)
|
* [App Store](https://apps.apple.com/us/app/sing-box/id6451272673)
|
||||||
* [TestFlight (Beta)](https://testflight.apple.com/join/AcqO44FH)
|
* ~~TestFlight (Beta)~~
|
||||||
|
|
||||||
|
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
|
||||||
|
(one-time sponsorships are accepted).
|
||||||
|
Once you donate, you can get an invitation by sending us your Apple ID [via email](mailto:contact@sagernet.org),
|
||||||
|
or join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot).
|
||||||
|
|
||||||
## :material-file-download: Download (macOS standalone version)
|
## :material-file-download: Download (macOS standalone version)
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,9 @@ icon: material/security
|
|||||||
|
|
||||||
sing-box and official graphics clients do not collect or share personal data,
|
sing-box and official graphics clients do not collect or share personal data,
|
||||||
and the data generated by the software is always on your device.
|
and the data generated by the software is always on your device.
|
||||||
|
|
||||||
|
## Android
|
||||||
|
|
||||||
|
If your configuration contains `wifi_ssid` or `wifi_bssid` routing rules,
|
||||||
|
sing-box uses the location permission in the background
|
||||||
|
to get information about the connected Wi-Fi network to make them work.
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.9.0"
|
||||||
|
|
||||||
|
:material-plus: [client_subnet](#client_subnet)
|
||||||
|
|
||||||
# DNS
|
# DNS
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
@@ -13,6 +21,7 @@
|
|||||||
"disable_expire": false,
|
"disable_expire": false,
|
||||||
"independent_cache": false,
|
"independent_cache": false,
|
||||||
"reverse_mapping": false,
|
"reverse_mapping": false,
|
||||||
|
"client_subnet": "",
|
||||||
"fakeip": {}
|
"fakeip": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,8 +30,8 @@
|
|||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
|
|
||||||
| Key | Format |
|
| Key | Format |
|
||||||
|----------|--------------------------------|
|
|----------|---------------------------------|
|
||||||
| `server` | List of [DNS Server](./server/) |
|
| `server` | List of [DNS Server](./server/) |
|
||||||
| `rules` | List of [DNS Rule](./rule/) |
|
| `rules` | List of [DNS Rule](./rule/) |
|
||||||
| `fakeip` | [FakeIP](./fakeip/) |
|
| `fakeip` | [FakeIP](./fakeip/) |
|
||||||
@@ -60,6 +69,12 @@ Stores a reverse mapping of IP addresses after responding to a DNS query in orde
|
|||||||
Since this process relies on the act of resolving domain names by an application before making a request, it can be
|
Since this process relies on the act of resolving domain names by an application before making a request, it can be
|
||||||
problematic in environments such as macOS, where DNS is proxied and cached by the system.
|
problematic in environments such as macOS, where DNS is proxied and cached by the system.
|
||||||
|
|
||||||
#### fakeip
|
#### client_subnet
|
||||||
|
|
||||||
[FakeIP](./fakeip/) settings.
|
!!! question "Since sing-box 1.9.0"
|
||||||
|
|
||||||
|
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
|
||||||
|
|
||||||
|
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||||
|
|
||||||
|
Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`.
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.9.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [client_subnet](#client_subnet)
|
||||||
|
|
||||||
# DNS
|
# DNS
|
||||||
|
|
||||||
### 结构
|
### 结构
|
||||||
@@ -13,6 +21,7 @@
|
|||||||
"disable_expire": false,
|
"disable_expire": false,
|
||||||
"independent_cache": false,
|
"independent_cache": false,
|
||||||
"reverse_mapping": false,
|
"reverse_mapping": false,
|
||||||
|
"client_subnet": "",
|
||||||
"fakeip": {}
|
"fakeip": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,6 +67,16 @@
|
|||||||
|
|
||||||
由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
|
由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
|
||||||
|
|
||||||
|
#### client_subnet
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.9.0 起"
|
||||||
|
|
||||||
|
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
|
||||||
|
|
||||||
|
如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。
|
||||||
|
|
||||||
|
可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。
|
||||||
|
|
||||||
#### fakeip
|
#### fakeip
|
||||||
|
|
||||||
[FakeIP](./fakeip/) 设置。
|
[FakeIP](./fakeip/) 设置。
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.9.0"
|
||||||
|
|
||||||
|
:material-plus: [geoip](#geoip)
|
||||||
|
:material-plus: [ip_cidr](#ip_cidr)
|
||||||
|
:material-plus: [ip_is_private](#ip_is_private)
|
||||||
|
:material-plus: [client_subnet](#client_subnet)
|
||||||
|
:material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.8.0"
|
||||||
|
|
||||||
|
:material-plus: [rule_set](#rule_set)
|
||||||
|
:material-plus: [source_ip_is_private](#source_ip_is_private)
|
||||||
|
:material-delete-clock: [geoip](#geoip)
|
||||||
|
:material-delete-clock: [geosite](#geosite)
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -42,10 +61,19 @@
|
|||||||
"source_geoip": [
|
"source_geoip": [
|
||||||
"private"
|
"private"
|
||||||
],
|
],
|
||||||
|
"geoip": [
|
||||||
|
"cn"
|
||||||
|
],
|
||||||
"source_ip_cidr": [
|
"source_ip_cidr": [
|
||||||
"10.0.0.0/24",
|
"10.0.0.0/24",
|
||||||
"192.168.0.1"
|
"192.168.0.1"
|
||||||
],
|
],
|
||||||
|
"source_ip_is_private": false,
|
||||||
|
"ip_cidr": [
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.1"
|
||||||
|
],
|
||||||
|
"ip_is_private": false,
|
||||||
"source_port": [
|
"source_port": [
|
||||||
12345
|
12345
|
||||||
],
|
],
|
||||||
@@ -85,13 +113,19 @@
|
|||||||
"wifi_bssid": [
|
"wifi_bssid": [
|
||||||
"00:00:00:00:00:00"
|
"00:00:00:00:00:00"
|
||||||
],
|
],
|
||||||
|
"rule_set": [
|
||||||
|
"geoip-cn",
|
||||||
|
"geosite-cn"
|
||||||
|
],
|
||||||
|
"rule_set_ipcidr_match_source": false,
|
||||||
"invert": false,
|
"invert": false,
|
||||||
"outbound": [
|
"outbound": [
|
||||||
"direct"
|
"direct"
|
||||||
],
|
],
|
||||||
"server": "local",
|
"server": "local",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
"rewrite_ttl": 100
|
"rewrite_ttl": 100,
|
||||||
|
"client_subnet": "127.0.0.1/24"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "logical",
|
"type": "logical",
|
||||||
@@ -99,7 +133,8 @@
|
|||||||
"rules": [],
|
"rules": [],
|
||||||
"server": "local",
|
"server": "local",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
"rewrite_ttl": 100
|
"rewrite_ttl": 100,
|
||||||
|
"client_subnet": "127.0.0.1/24"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -118,10 +153,12 @@
|
|||||||
The default rule uses the following matching logic:
|
The default rule uses the following matching logic:
|
||||||
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
|
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
|
||||||
(`port` || `port_range`) &&
|
(`port` || `port_range`) &&
|
||||||
(`source_geoip` || `source_ip_cidr`) &&
|
(`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) &&
|
||||||
(`source_port` || `source_port_range`) &&
|
(`source_port` || `source_port_range`) &&
|
||||||
`other fields`
|
`other fields`
|
||||||
|
|
||||||
|
Additionally, included rule sets can be considered merged rather than as a single rule sub-item.
|
||||||
|
|
||||||
#### inbound
|
#### inbound
|
||||||
|
|
||||||
Tags of [Inbound](/configuration/inbound/).
|
Tags of [Inbound](/configuration/inbound/).
|
||||||
@@ -166,15 +203,29 @@ Match domain using regular expression.
|
|||||||
|
|
||||||
#### geosite
|
#### geosite
|
||||||
|
|
||||||
|
!!! failure "Deprecated in sing-box 1.8.0"
|
||||||
|
|
||||||
|
Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets).
|
||||||
|
|
||||||
Match geosite.
|
Match geosite.
|
||||||
|
|
||||||
#### source_geoip
|
#### source_geoip
|
||||||
|
|
||||||
|
!!! failure "Deprecated in sing-box 1.8.0"
|
||||||
|
|
||||||
|
GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets).
|
||||||
|
|
||||||
Match source geoip.
|
Match source geoip.
|
||||||
|
|
||||||
#### source_ip_cidr
|
#### source_ip_cidr
|
||||||
|
|
||||||
Match source ip cidr.
|
Match source IP CIDR.
|
||||||
|
|
||||||
|
#### source_ip_is_private
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.8.0"
|
||||||
|
|
||||||
|
Match non-public source IP.
|
||||||
|
|
||||||
#### source_port
|
#### source_port
|
||||||
|
|
||||||
@@ -234,11 +285,9 @@ Match Clash mode.
|
|||||||
|
|
||||||
#### wifi_ssid
|
#### wifi_ssid
|
||||||
|
|
||||||
<!-- md:version 1.7.0-beta.4 -->
|
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|
||||||
Only supported in graphical clients on Android and iOS.
|
Only supported in graphical clients on Android and Apple platforms.
|
||||||
|
|
||||||
Match WiFi SSID.
|
Match WiFi SSID.
|
||||||
|
|
||||||
@@ -246,10 +295,22 @@ Match WiFi SSID.
|
|||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|
||||||
Only supported in graphical clients on Android and iOS.
|
Only supported in graphical clients on Android and Apple platforms.
|
||||||
|
|
||||||
Match WiFi BSSID.
|
Match WiFi BSSID.
|
||||||
|
|
||||||
|
#### rule_set
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.8.0"
|
||||||
|
|
||||||
|
Match [Rule Set](/configuration/route/#rule_set).
|
||||||
|
|
||||||
|
#### rule_set_ipcidr_match_source
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.9.0"
|
||||||
|
|
||||||
|
Make `ipcidr` in rule sets match the source IP.
|
||||||
|
|
||||||
#### invert
|
#### invert
|
||||||
|
|
||||||
Invert match result.
|
Invert match result.
|
||||||
@@ -274,6 +335,46 @@ Disable cache and save cache in this query.
|
|||||||
|
|
||||||
Rewrite TTL in DNS responses.
|
Rewrite TTL in DNS responses.
|
||||||
|
|
||||||
|
#### client_subnet
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.9.0"
|
||||||
|
|
||||||
|
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
|
||||||
|
|
||||||
|
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||||
|
|
||||||
|
Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
|
||||||
|
|
||||||
|
### Address Filter Fields
|
||||||
|
|
||||||
|
Only takes effect for IP address requests. When the query results do not match the address filtering rule items, the current rule will be skipped.
|
||||||
|
|
||||||
|
!!! info ""
|
||||||
|
|
||||||
|
`ip_cidr` items in included rule sets also takes effect as an address filtering field.
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
Enable `experimental.cache_file.store_rdrc` to cache results.
|
||||||
|
|
||||||
|
#### geoip
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.9.0"
|
||||||
|
|
||||||
|
Match GeoIP with query response.
|
||||||
|
|
||||||
|
#### ip_cidr
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.9.0"
|
||||||
|
|
||||||
|
Match IP CIDR with query response.
|
||||||
|
|
||||||
|
#### ip_is_private
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.9.0"
|
||||||
|
|
||||||
|
Match private IP with query response.
|
||||||
|
|
||||||
### Logical Fields
|
### Logical Fields
|
||||||
|
|
||||||
#### type
|
#### type
|
||||||
@@ -286,4 +387,4 @@ Rewrite TTL in DNS responses.
|
|||||||
|
|
||||||
#### rules
|
#### rules
|
||||||
|
|
||||||
Included default rules.
|
Included rules.
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.9.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [geoip](#geoip)
|
||||||
|
:material-plus: [ip_cidr](#ip_cidr)
|
||||||
|
:material-plus: [ip_is_private](#ip_is_private)
|
||||||
|
:material-plus: [client_subnet](#client_subnet)
|
||||||
|
:material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.8.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [rule_set](#rule_set)
|
||||||
|
:material-plus: [source_ip_is_private](#source_ip_is_private)
|
||||||
|
:material-delete-clock: [geoip](#geoip)
|
||||||
|
:material-delete-clock: [geosite](#geosite)
|
||||||
|
|
||||||
### 结构
|
### 结构
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -42,9 +61,19 @@
|
|||||||
"source_geoip": [
|
"source_geoip": [
|
||||||
"private"
|
"private"
|
||||||
],
|
],
|
||||||
"source_ip_cidr": [
|
"geoip": [
|
||||||
"10.0.0.0/24"
|
"cn"
|
||||||
],
|
],
|
||||||
|
"source_ip_cidr": [
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.1"
|
||||||
|
],
|
||||||
|
"source_ip_is_private": false,
|
||||||
|
"ip_cidr": [
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.1"
|
||||||
|
],
|
||||||
|
"ip_is_private": false,
|
||||||
"source_port": [
|
"source_port": [
|
||||||
12345
|
12345
|
||||||
],
|
],
|
||||||
@@ -84,19 +113,26 @@
|
|||||||
"wifi_bssid": [
|
"wifi_bssid": [
|
||||||
"00:00:00:00:00:00"
|
"00:00:00:00:00:00"
|
||||||
],
|
],
|
||||||
|
"rule_set": [
|
||||||
|
"geoip-cn",
|
||||||
|
"geosite-cn"
|
||||||
|
],
|
||||||
|
"rule_set_ipcidr_match_source": false,
|
||||||
"invert": false,
|
"invert": false,
|
||||||
"outbound": [
|
"outbound": [
|
||||||
"direct"
|
"direct"
|
||||||
],
|
],
|
||||||
"server": "local",
|
"server": "local",
|
||||||
"disable_cache": false
|
"disable_cache": false,
|
||||||
|
"client_subnet": "127.0.0.1/24"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "logical",
|
"type": "logical",
|
||||||
"mode": "and",
|
"mode": "and",
|
||||||
"rules": [],
|
"rules": [],
|
||||||
"server": "local",
|
"server": "local",
|
||||||
"disable_cache": false
|
"disable_cache": false,
|
||||||
|
"client_subnet": "127.0.0.1/24"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -115,10 +151,12 @@
|
|||||||
默认规则使用以下匹配逻辑:
|
默认规则使用以下匹配逻辑:
|
||||||
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
|
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
|
||||||
(`port` || `port_range`) &&
|
(`port` || `port_range`) &&
|
||||||
(`source_geoip` || `source_ip_cidr`) &&
|
(`source_geoip` || `source_ip_cidr` || `source_ip_is_private`) &&
|
||||||
(`source_port` || `source_port_range`) &&
|
(`source_port` || `source_port_range`) &&
|
||||||
`other fields`
|
`other fields`
|
||||||
|
|
||||||
|
另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。
|
||||||
|
|
||||||
#### inbound
|
#### inbound
|
||||||
|
|
||||||
[入站](/zh/configuration/inbound/) 标签.
|
[入站](/zh/configuration/inbound/) 标签.
|
||||||
@@ -163,16 +201,30 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
#### geosite
|
#### geosite
|
||||||
|
|
||||||
匹配 GeoSite。
|
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||||
|
|
||||||
|
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。
|
||||||
|
|
||||||
|
匹配 Geosite。
|
||||||
|
|
||||||
#### source_geoip
|
#### source_geoip
|
||||||
|
|
||||||
|
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||||
|
|
||||||
|
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||||
|
|
||||||
匹配源 GeoIP。
|
匹配源 GeoIP。
|
||||||
|
|
||||||
#### source_ip_cidr
|
#### source_ip_cidr
|
||||||
|
|
||||||
匹配源 IP CIDR。
|
匹配源 IP CIDR。
|
||||||
|
|
||||||
|
#### source_ip_is_private
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.8.0 起"
|
||||||
|
|
||||||
|
匹配非公开源 IP。
|
||||||
|
|
||||||
#### source_port
|
#### source_port
|
||||||
|
|
||||||
匹配源端口。
|
匹配源端口。
|
||||||
@@ -233,7 +285,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|
||||||
仅在 Android 与 iOS 的图形客户端中支持。
|
仅在 Android 与 Apple 平台图形客户端中支持。
|
||||||
|
|
||||||
匹配 WiFi SSID。
|
匹配 WiFi SSID。
|
||||||
|
|
||||||
@@ -241,10 +293,22 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|
||||||
仅在 Android 与 iOS 的图形客户端中支持。
|
仅在 Android 与 Apple 平台图形客户端中支持。
|
||||||
|
|
||||||
匹配 WiFi BSSID。
|
匹配 WiFi BSSID。
|
||||||
|
|
||||||
|
#### rule_set
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.8.0 起"
|
||||||
|
|
||||||
|
匹配[规则集](/zh/configuration/route/#rule_set)。
|
||||||
|
|
||||||
|
#### rule_set_ipcidr_match_source
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.9.0 起"
|
||||||
|
|
||||||
|
使规则集中的 `ipcidr` 规则匹配源 IP。
|
||||||
|
|
||||||
#### invert
|
#### invert
|
||||||
|
|
||||||
反选匹配结果。
|
反选匹配结果。
|
||||||
@@ -269,6 +333,46 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
重写 DNS 回应中的 TTL。
|
重写 DNS 回应中的 TTL。
|
||||||
|
|
||||||
|
#### client_subnet
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.9.0 起"
|
||||||
|
|
||||||
|
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
|
||||||
|
|
||||||
|
如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。
|
||||||
|
|
||||||
|
将覆盖 `dns.client_subnet` 与 `servers.[].client_subnet`。
|
||||||
|
|
||||||
|
### 地址筛选字段
|
||||||
|
|
||||||
|
仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
|
||||||
|
|
||||||
|
!!! info ""
|
||||||
|
|
||||||
|
引用的规则集中的 `ip_cidr` 项也作为地址筛选字段生效。
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
启用 `experimental.cache_file.store_rdrc` 以缓存结果。
|
||||||
|
|
||||||
|
#### geoip
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.9.0 起"
|
||||||
|
|
||||||
|
与查询响应匹配 GeoIP。
|
||||||
|
|
||||||
|
#### ip_cidr
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.9.0 起"
|
||||||
|
|
||||||
|
与查询相应匹配 IP CIDR。
|
||||||
|
|
||||||
|
#### ip_is_private
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.9.0 起"
|
||||||
|
|
||||||
|
与查询响应匹配非公开 IP。
|
||||||
|
|
||||||
### 逻辑字段
|
### 逻辑字段
|
||||||
|
|
||||||
#### type
|
#### type
|
||||||
@@ -281,4 +385,4 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
#### rules
|
#### rules
|
||||||
|
|
||||||
包括的默认规则。
|
包括的规则。
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.9.0"
|
||||||
|
|
||||||
|
:material-plus: [client_subnet](#client_subnet)
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -5,17 +13,17 @@
|
|||||||
"dns": {
|
"dns": {
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"tag": "google",
|
"tag": "",
|
||||||
"address": "tls://dns.google",
|
"address": "",
|
||||||
"address_resolver": "local",
|
"address_resolver": "",
|
||||||
"address_strategy": "prefer_ipv4",
|
"address_strategy": "",
|
||||||
"strategy": "ipv4_only",
|
"strategy": "",
|
||||||
"detour": "direct"
|
"detour": "",
|
||||||
|
"client_subnet": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
@@ -45,20 +53,12 @@ The address of the dns server.
|
|||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
|
|
||||||
To ensure that system DNS is in effect, rather than Go's built-in default resolver, enable CGO at compile time.
|
To ensure that Android system DNS is in effect, rather than Go's built-in default resolver, enable CGO at compile time.
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
QUIC and HTTP3 transport is not included by default, see [Installation](./#installation).
|
|
||||||
|
|
||||||
!!! info ""
|
!!! info ""
|
||||||
|
|
||||||
the RCode transport is often used to block queries. Use with rules and the `disable_cache` rule option.
|
the RCode transport is often used to block queries. Use with rules and the `disable_cache` rule option.
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
DHCP transport is not included by default, see [Installation](./#installation).
|
|
||||||
|
|
||||||
| RCode | Description |
|
| RCode | Description |
|
||||||
|-------------------|-----------------------|
|
|-------------------|-----------------------|
|
||||||
| `success` | `No error` |
|
| `success` | `No error` |
|
||||||
@@ -88,10 +88,22 @@ Default domain strategy for resolving the domain names.
|
|||||||
|
|
||||||
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||||
|
|
||||||
Take no effect if override by other settings.
|
Take no effect if overridden by other settings.
|
||||||
|
|
||||||
#### detour
|
#### detour
|
||||||
|
|
||||||
Tag of an outbound for connecting to the dns server.
|
Tag of an outbound for connecting to the dns server.
|
||||||
|
|
||||||
Default outbound will be used if empty.
|
Default outbound will be used if empty.
|
||||||
|
|
||||||
|
#### client_subnet
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.9.0"
|
||||||
|
|
||||||
|
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
|
||||||
|
|
||||||
|
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||||
|
|
||||||
|
Can be overrides by `rules.[].client_subnet`.
|
||||||
|
|
||||||
|
Will overrides `dns.client_subnet`.
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.9.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [client_subnet](#client_subnet)
|
||||||
|
|
||||||
### 结构
|
### 结构
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -5,17 +13,17 @@
|
|||||||
"dns": {
|
"dns": {
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"tag": "google",
|
"tag": "",
|
||||||
"address": "tls://dns.google",
|
"address": "",
|
||||||
"address_resolver": "local",
|
"address_resolver": "",
|
||||||
"address_strategy": "prefer_ipv4",
|
"address_strategy": "",
|
||||||
"strategy": "ipv4_only",
|
"strategy": "",
|
||||||
"detour": "direct"
|
"detour": "",
|
||||||
|
"client_subnet": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 字段
|
### 字段
|
||||||
@@ -45,20 +53,12 @@ DNS 服务器的地址。
|
|||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
|
|
||||||
为了确保系统 DNS 生效,而不是 Go 的内置默认解析器,请在编译时启用 CGO。
|
为了确保 Android 系统 DNS 生效,而不是 Go 的内置默认解析器,请在编译时启用 CGO。
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
默认安装不包含 QUIC 和 HTTP3 传输层,请参阅 [安装](/zh/#_2)。
|
|
||||||
|
|
||||||
!!! info ""
|
!!! info ""
|
||||||
|
|
||||||
RCode 传输层传输层常用于屏蔽请求. 与 DNS 规则和 `disable_cache` 规则选项一起使用。
|
RCode 传输层传输层常用于屏蔽请求. 与 DNS 规则和 `disable_cache` 规则选项一起使用。
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
默认安装不包含 DHCP 传输层,请参阅 [安装](/zh/#_2)。
|
|
||||||
|
|
||||||
| RCode | 描述 |
|
| RCode | 描述 |
|
||||||
|-------------------|----------|
|
|-------------------|----------|
|
||||||
| `success` | `无错误` |
|
| `success` | `无错误` |
|
||||||
@@ -95,3 +95,15 @@ DNS 服务器的地址。
|
|||||||
用于连接到 DNS 服务器的出站的标签。
|
用于连接到 DNS 服务器的出站的标签。
|
||||||
|
|
||||||
如果为空,将使用默认出站。
|
如果为空,将使用默认出站。
|
||||||
|
|
||||||
|
#### client_subnet
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.9.0 起"
|
||||||
|
|
||||||
|
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
|
||||||
|
|
||||||
|
如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。
|
||||||
|
|
||||||
|
可以被 `rules.[].client_subnet` 覆盖。
|
||||||
|
|
||||||
|
将覆盖 `dns.client_subnet`。
|
||||||
|
|||||||
58
docs/configuration/experimental/cache-file.md
Normal file
58
docs/configuration/experimental/cache-file.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.8.0"
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.9.0"
|
||||||
|
|
||||||
|
:material-plus: [store_rdrc](#store_rdrc)
|
||||||
|
:material-plus: [rdrc_timeout](#rdrc_timeout)
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"path": "",
|
||||||
|
"cache_id": "",
|
||||||
|
"store_fakeip": false,
|
||||||
|
"store_rdrc": false,
|
||||||
|
"rdrc_timeout": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### enabled
|
||||||
|
|
||||||
|
Enable cache file.
|
||||||
|
|
||||||
|
#### path
|
||||||
|
|
||||||
|
Path to the cache file.
|
||||||
|
|
||||||
|
`cache.db` will be used if empty.
|
||||||
|
|
||||||
|
#### cache_id
|
||||||
|
|
||||||
|
Identifier in the cache file
|
||||||
|
|
||||||
|
If not empty, configuration specified data will use a separate store keyed by it.
|
||||||
|
|
||||||
|
#### store_fakeip
|
||||||
|
|
||||||
|
Store fakeip in the cache file
|
||||||
|
|
||||||
|
#### store_rdrc
|
||||||
|
|
||||||
|
Store rejected DNS response cache in the cache file
|
||||||
|
|
||||||
|
The check results of [Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields)
|
||||||
|
will be cached until expiration.
|
||||||
|
|
||||||
|
#### rdrc_timeout
|
||||||
|
|
||||||
|
Timeout of rejected DNS response cache.
|
||||||
|
|
||||||
|
`7d` is used by default.
|
||||||
55
docs/configuration/experimental/cache-file.zh.md
Normal file
55
docs/configuration/experimental/cache-file.zh.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.8.0 起"
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.9.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [store_rdrc](#store_rdrc)
|
||||||
|
:material-plus: [rdrc_timeout](#rdrc_timeout)
|
||||||
|
|
||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"path": "",
|
||||||
|
"cache_id": "",
|
||||||
|
"store_fakeip": false,
|
||||||
|
"store_rdrc": false,
|
||||||
|
"rdrc_timeout": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
#### enabled
|
||||||
|
|
||||||
|
启用缓存文件。
|
||||||
|
|
||||||
|
#### path
|
||||||
|
|
||||||
|
缓存文件路径,默认使用`cache.db`。
|
||||||
|
|
||||||
|
#### cache_id
|
||||||
|
|
||||||
|
缓存文件中的标识符。
|
||||||
|
|
||||||
|
如果不为空,配置特定的数据将使用由其键控的单独存储。
|
||||||
|
|
||||||
|
#### store_fakeip
|
||||||
|
|
||||||
|
将 fakeip 存储在缓存文件中。
|
||||||
|
|
||||||
|
#### store_rdrc
|
||||||
|
|
||||||
|
将拒绝的 DNS 响应缓存存储在缓存文件中。
|
||||||
|
|
||||||
|
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。
|
||||||
|
|
||||||
|
#### rdrc_timeout
|
||||||
|
|
||||||
|
拒绝的 DNS 响应缓存超时。
|
||||||
|
|
||||||
|
默认使用 `7d`。
|
||||||
110
docs/configuration/experimental/clash-api.md
Normal file
110
docs/configuration/experimental/clash-api.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
!!! quote "Changes in sing-box 1.8.0"
|
||||||
|
|
||||||
|
:material-delete-alert: [store_mode](#store_mode)
|
||||||
|
:material-delete-alert: [store_selected](#store_selected)
|
||||||
|
:material-delete-alert: [store_fakeip](#store_fakeip)
|
||||||
|
:material-delete-alert: [cache_file](#cache_file)
|
||||||
|
:material-delete-alert: [cache_id](#cache_id)
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"external_controller": "127.0.0.1:9090",
|
||||||
|
"external_ui": "",
|
||||||
|
"external_ui_download_url": "",
|
||||||
|
"external_ui_download_detour": "",
|
||||||
|
"secret": "",
|
||||||
|
"default_mode": "",
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
|
|
||||||
|
"store_mode": false,
|
||||||
|
"store_selected": false,
|
||||||
|
"store_fakeip": false,
|
||||||
|
"cache_file": "",
|
||||||
|
"cache_id": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### external_controller
|
||||||
|
|
||||||
|
RESTful web API listening address. Clash API will be disabled if empty.
|
||||||
|
|
||||||
|
#### external_ui
|
||||||
|
|
||||||
|
A relative path to the configuration directory or an absolute path to a
|
||||||
|
directory in which you put some static web resource. sing-box will then
|
||||||
|
serve it at `http://{{external-controller}}/ui`.
|
||||||
|
|
||||||
|
#### external_ui_download_url
|
||||||
|
|
||||||
|
ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty.
|
||||||
|
|
||||||
|
`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty.
|
||||||
|
|
||||||
|
#### external_ui_download_detour
|
||||||
|
|
||||||
|
The tag of the outbound to download the external UI.
|
||||||
|
|
||||||
|
Default outbound will be used if empty.
|
||||||
|
|
||||||
|
#### secret
|
||||||
|
|
||||||
|
Secret for the RESTful API (optional)
|
||||||
|
Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
|
||||||
|
ALWAYS set a secret if RESTful API is listening on 0.0.0.0
|
||||||
|
|
||||||
|
#### default_mode
|
||||||
|
|
||||||
|
Default mode in clash, `Rule` will be used if empty.
|
||||||
|
|
||||||
|
This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item.
|
||||||
|
|
||||||
|
#### store_mode
|
||||||
|
|
||||||
|
!!! failure "Deprecated in sing-box 1.8.0"
|
||||||
|
|
||||||
|
`store_mode` is deprecated in Clash API and enabled by default if `cache_file.enabled`.
|
||||||
|
|
||||||
|
Store Clash mode in cache file.
|
||||||
|
|
||||||
|
#### store_selected
|
||||||
|
|
||||||
|
!!! failure "Deprecated in sing-box 1.8.0"
|
||||||
|
|
||||||
|
`store_selected` is deprecated in Clash API and enabled by default if `cache_file.enabled`.
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
The tag must be set for target outbounds.
|
||||||
|
|
||||||
|
Store selected outbound for the `Selector` outbound in cache file.
|
||||||
|
|
||||||
|
#### store_fakeip
|
||||||
|
|
||||||
|
!!! failure "Deprecated in sing-box 1.8.0"
|
||||||
|
|
||||||
|
`store_selected` is deprecated in Clash API and migrated to `cache_file.store_fakeip`.
|
||||||
|
|
||||||
|
Store fakeip in cache file.
|
||||||
|
|
||||||
|
#### cache_file
|
||||||
|
|
||||||
|
!!! failure "Deprecated in sing-box 1.8.0"
|
||||||
|
|
||||||
|
`cache_file` is deprecated in Clash API and migrated to `cache_file.enabled` and `cache_file.path`.
|
||||||
|
|
||||||
|
Cache file path, `cache.db` will be used if empty.
|
||||||
|
|
||||||
|
#### cache_id
|
||||||
|
|
||||||
|
!!! failure "Deprecated in sing-box 1.8.0"
|
||||||
|
|
||||||
|
`cache_id` is deprecated in Clash API and migrated to `cache_file.cache_id`.
|
||||||
|
|
||||||
|
Identifier in cache file.
|
||||||
|
|
||||||
|
If not empty, configuration specified data will use a separate store keyed by it.
|
||||||
108
docs/configuration/experimental/clash-api.zh.md
Normal file
108
docs/configuration/experimental/clash-api.zh.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
!!! quote "sing-box 1.8.0 中的更改"
|
||||||
|
|
||||||
|
:material-delete-alert: [store_mode](#store_mode)
|
||||||
|
:material-delete-alert: [store_selected](#store_selected)
|
||||||
|
:material-delete-alert: [store_fakeip](#store_fakeip)
|
||||||
|
:material-delete-alert: [cache_file](#cache_file)
|
||||||
|
:material-delete-alert: [cache_id](#cache_id)
|
||||||
|
|
||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"external_controller": "127.0.0.1:9090",
|
||||||
|
"external_ui": "",
|
||||||
|
"external_ui_download_url": "",
|
||||||
|
"external_ui_download_detour": "",
|
||||||
|
"secret": "",
|
||||||
|
"default_mode": "",
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
|
|
||||||
|
"store_mode": false,
|
||||||
|
"store_selected": false,
|
||||||
|
"store_fakeip": false,
|
||||||
|
"cache_file": "",
|
||||||
|
"cache_id": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### external_controller
|
||||||
|
|
||||||
|
RESTful web API 监听地址。如果为空,则禁用 Clash API。
|
||||||
|
|
||||||
|
#### external_ui
|
||||||
|
|
||||||
|
到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。
|
||||||
|
|
||||||
|
#### external_ui_download_url
|
||||||
|
|
||||||
|
静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。
|
||||||
|
|
||||||
|
默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。
|
||||||
|
|
||||||
|
#### external_ui_download_detour
|
||||||
|
|
||||||
|
用于下载静态网页资源的出站的标签。
|
||||||
|
|
||||||
|
如果为空,将使用默认出站。
|
||||||
|
|
||||||
|
#### secret
|
||||||
|
|
||||||
|
RESTful API 的密钥(可选)
|
||||||
|
通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证
|
||||||
|
如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。
|
||||||
|
|
||||||
|
#### default_mode
|
||||||
|
|
||||||
|
Clash 中的默认模式,默认使用 `Rule`。
|
||||||
|
|
||||||
|
此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。
|
||||||
|
|
||||||
|
#### store_mode
|
||||||
|
|
||||||
|
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||||
|
|
||||||
|
`store_mode` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。
|
||||||
|
|
||||||
|
将 Clash 模式存储在缓存文件中。
|
||||||
|
|
||||||
|
#### store_selected
|
||||||
|
|
||||||
|
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||||
|
|
||||||
|
`store_selected` 已在 Clash API 中废弃,且默认启用当 `cache_file.enabled`。
|
||||||
|
|
||||||
|
!!! note ""
|
||||||
|
|
||||||
|
必须为目标出站设置标签。
|
||||||
|
|
||||||
|
将 `Selector` 中出站的选定的目标出站存储在缓存文件中。
|
||||||
|
|
||||||
|
#### store_fakeip
|
||||||
|
|
||||||
|
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||||
|
|
||||||
|
`store_selected` 已在 Clash API 中废弃,且已迁移到 `cache_file.store_fakeip`。
|
||||||
|
|
||||||
|
将 fakeip 存储在缓存文件中。
|
||||||
|
|
||||||
|
#### cache_file
|
||||||
|
|
||||||
|
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||||
|
|
||||||
|
`cache_file` 已在 Clash API 中废弃,且已迁移到 `cache_file.enabled` 和 `cache_file.path`。
|
||||||
|
|
||||||
|
缓存文件路径,默认使用`cache.db`。
|
||||||
|
|
||||||
|
#### cache_id
|
||||||
|
|
||||||
|
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||||
|
|
||||||
|
`cache_id` 已在 Clash API 中废弃,且已迁移到 `cache_file.cache_id`。
|
||||||
|
|
||||||
|
缓存 ID。
|
||||||
|
|
||||||
|
如果不为空,配置特定的数据将使用由其键控的单独存储。
|
||||||
@@ -1,139 +1,26 @@
|
|||||||
# Experimental
|
# Experimental
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.8.0"
|
||||||
|
|
||||||
|
:material-plus: [cache_file](#cache_file)
|
||||||
|
:material-alert-decagram: [clash_api](#clash_api)
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"clash_api": {
|
"cache_file": {},
|
||||||
"external_controller": "127.0.0.1:9090",
|
"clash_api": {},
|
||||||
"external_ui": "",
|
"v2ray_api": {}
|
||||||
"external_ui_download_url": "",
|
|
||||||
"external_ui_download_detour": "",
|
|
||||||
"secret": "",
|
|
||||||
"default_mode": "",
|
|
||||||
"store_mode": false,
|
|
||||||
"store_selected": false,
|
|
||||||
"store_fakeip": false,
|
|
||||||
"cache_file": "",
|
|
||||||
"cache_id": ""
|
|
||||||
},
|
|
||||||
"v2ray_api": {
|
|
||||||
"listen": "127.0.0.1:8080",
|
|
||||||
"stats": {
|
|
||||||
"enabled": true,
|
|
||||||
"inbounds": [
|
|
||||||
"socks-in"
|
|
||||||
],
|
|
||||||
"outbounds": [
|
|
||||||
"proxy",
|
|
||||||
"direct"
|
|
||||||
],
|
|
||||||
"users": [
|
|
||||||
"sekai"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note ""
|
### Fields
|
||||||
|
|
||||||
Traffic statistics and connection management can degrade performance.
|
| Key | Format |
|
||||||
|
|--------------|----------------------------|
|
||||||
### Clash API Fields
|
| `cache_file` | [Cache File](./cache-file/) |
|
||||||
|
| `clash_api` | [Clash API](./clash-api/) |
|
||||||
!!! quote ""
|
| `v2ray_api` | [V2Ray API](./v2ray-api/) |
|
||||||
|
|
||||||
Clash API is not included by default, see [Installation](./#installation).
|
|
||||||
|
|
||||||
#### external_controller
|
|
||||||
|
|
||||||
RESTful web API listening address. Clash API will be disabled if empty.
|
|
||||||
|
|
||||||
#### external_ui
|
|
||||||
|
|
||||||
A relative path to the configuration directory or an absolute path to a
|
|
||||||
directory in which you put some static web resource. sing-box will then
|
|
||||||
serve it at `http://{{external-controller}}/ui`.
|
|
||||||
|
|
||||||
#### external_ui_download_url
|
|
||||||
|
|
||||||
ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty.
|
|
||||||
|
|
||||||
`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty.
|
|
||||||
|
|
||||||
#### external_ui_download_detour
|
|
||||||
|
|
||||||
The tag of the outbound to download the external UI.
|
|
||||||
|
|
||||||
Default outbound will be used if empty.
|
|
||||||
|
|
||||||
#### secret
|
|
||||||
|
|
||||||
Secret for the RESTful API (optional)
|
|
||||||
Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
|
|
||||||
ALWAYS set a secret if RESTful API is listening on 0.0.0.0
|
|
||||||
|
|
||||||
#### default_mode
|
|
||||||
|
|
||||||
Default mode in clash, `Rule` will be used if empty.
|
|
||||||
|
|
||||||
This setting has no direct effect, but can be used in routing and DNS rules via the `clash_mode` rule item.
|
|
||||||
|
|
||||||
#### store_mode
|
|
||||||
|
|
||||||
Store Clash mode in cache file.
|
|
||||||
|
|
||||||
#### store_selected
|
|
||||||
|
|
||||||
!!! note ""
|
|
||||||
|
|
||||||
The tag must be set for target outbounds.
|
|
||||||
|
|
||||||
Store selected outbound for the `Selector` outbound in cache file.
|
|
||||||
|
|
||||||
#### store_fakeip
|
|
||||||
|
|
||||||
Store fakeip in cache file.
|
|
||||||
|
|
||||||
#### cache_file
|
|
||||||
|
|
||||||
Cache file path, `cache.db` will be used if empty.
|
|
||||||
|
|
||||||
#### cache_id
|
|
||||||
|
|
||||||
Cache ID.
|
|
||||||
|
|
||||||
If not empty, `store_selected` will use a separate store keyed by it.
|
|
||||||
|
|
||||||
### V2Ray API Fields
|
|
||||||
|
|
||||||
!!! quote ""
|
|
||||||
|
|
||||||
V2Ray API is not included by default, see [Installation](./#installation).
|
|
||||||
|
|
||||||
#### listen
|
|
||||||
|
|
||||||
gRPC API listening address. V2Ray API will be disabled if empty.
|
|
||||||
|
|
||||||
#### stats
|
|
||||||
|
|
||||||
Traffic statistics service settings.
|
|
||||||
|
|
||||||
#### stats.enabled
|
|
||||||
|
|
||||||
Enable statistics service.
|
|
||||||
|
|
||||||
#### stats.inbounds
|
|
||||||
|
|
||||||
Inbound list to count traffic.
|
|
||||||
|
|
||||||
#### stats.outbounds
|
|
||||||
|
|
||||||
Outbound list to count traffic.
|
|
||||||
|
|
||||||
#### stats.users
|
|
||||||
|
|
||||||
User list to count traffic.
|
|
||||||
@@ -1,137 +1,26 @@
|
|||||||
# 实验性
|
# 实验性
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.8.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [cache_file](#cache_file)
|
||||||
|
:material-alert-decagram: [clash_api](#clash_api)
|
||||||
|
|
||||||
### 结构
|
### 结构
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"clash_api": {
|
"cache_file": {},
|
||||||
"external_controller": "127.0.0.1:9090",
|
"clash_api": {},
|
||||||
"external_ui": "",
|
"v2ray_api": {}
|
||||||
"external_ui_download_url": "",
|
|
||||||
"external_ui_download_detour": "",
|
|
||||||
"secret": "",
|
|
||||||
"default_mode": "",
|
|
||||||
"store_mode": false,
|
|
||||||
"store_selected": false,
|
|
||||||
"store_fakeip": false,
|
|
||||||
"cache_file": "",
|
|
||||||
"cache_id": ""
|
|
||||||
},
|
|
||||||
"v2ray_api": {
|
|
||||||
"listen": "127.0.0.1:8080",
|
|
||||||
"stats": {
|
|
||||||
"enabled": true,
|
|
||||||
"inbounds": [
|
|
||||||
"socks-in"
|
|
||||||
],
|
|
||||||
"outbounds": [
|
|
||||||
"proxy",
|
|
||||||
"direct"
|
|
||||||
],
|
|
||||||
"users": [
|
|
||||||
"sekai"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note ""
|
### 字段
|
||||||
|
|
||||||
流量统计和连接管理会降低性能。
|
| 键 | 格式 |
|
||||||
|
|--------------|--------------------------|
|
||||||
### Clash API 字段
|
| `cache_file` | [缓存文件](./cache-file/) |
|
||||||
|
| `clash_api` | [Clash API](./clash-api/) |
|
||||||
!!! quote ""
|
| `v2ray_api` | [V2Ray API](./v2ray-api/) |
|
||||||
|
|
||||||
默认安装不包含 Clash API,参阅 [安装](/zh/#_2)。
|
|
||||||
|
|
||||||
#### external_controller
|
|
||||||
|
|
||||||
RESTful web API 监听地址。如果为空,则禁用 Clash API。
|
|
||||||
|
|
||||||
#### external_ui
|
|
||||||
|
|
||||||
到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。
|
|
||||||
|
|
||||||
#### external_ui_download_url
|
|
||||||
|
|
||||||
静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。
|
|
||||||
|
|
||||||
默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`。
|
|
||||||
|
|
||||||
#### external_ui_download_detour
|
|
||||||
|
|
||||||
用于下载静态网页资源的出站的标签。
|
|
||||||
|
|
||||||
如果为空,将使用默认出站。
|
|
||||||
|
|
||||||
#### secret
|
|
||||||
|
|
||||||
RESTful API 的密钥(可选)
|
|
||||||
通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证
|
|
||||||
如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。
|
|
||||||
|
|
||||||
#### default_mode
|
|
||||||
|
|
||||||
Clash 中的默认模式,默认使用 `Rule`。
|
|
||||||
|
|
||||||
此设置没有直接影响,但可以通过 `clash_mode` 规则项在路由和 DNS 规则中使用。
|
|
||||||
|
|
||||||
#### store_mode
|
|
||||||
|
|
||||||
将 Clash 模式存储在缓存文件中。
|
|
||||||
|
|
||||||
#### store_selected
|
|
||||||
|
|
||||||
!!! note ""
|
|
||||||
|
|
||||||
必须为目标出站设置标签。
|
|
||||||
|
|
||||||
将 `Selector` 中出站的选定的目标出站存储在缓存文件中。
|
|
||||||
|
|
||||||
#### store_fakeip
|
|
||||||
|
|
||||||
将 fakeip 存储在缓存文件中。
|
|
||||||
|
|
||||||
#### cache_file
|
|
||||||
|
|
||||||
缓存文件路径,默认使用`cache.db`。
|
|
||||||
|
|
||||||
#### cache_id
|
|
||||||
|
|
||||||
缓存 ID。
|
|
||||||
|
|
||||||
如果不为空,`store_selected` 将会使用以此为键的独立存储。
|
|
||||||
|
|
||||||
### V2Ray API 字段
|
|
||||||
|
|
||||||
!!! quote ""
|
|
||||||
|
|
||||||
默认安装不包含 V2Ray API,参阅 [安装](/zh/#_2)。
|
|
||||||
|
|
||||||
#### listen
|
|
||||||
|
|
||||||
gRPC API 监听地址。如果为空,则禁用 V2Ray API。
|
|
||||||
|
|
||||||
#### stats
|
|
||||||
|
|
||||||
流量统计服务设置。
|
|
||||||
|
|
||||||
#### stats.enabled
|
|
||||||
|
|
||||||
启用统计服务。
|
|
||||||
|
|
||||||
#### stats.inbounds
|
|
||||||
|
|
||||||
统计流量的入站列表。
|
|
||||||
|
|
||||||
#### stats.outbounds
|
|
||||||
|
|
||||||
统计流量的出站列表。
|
|
||||||
|
|
||||||
#### stats.users
|
|
||||||
|
|
||||||
统计流量的用户列表。
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user