mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-14 04:38:28 +10:00
Compare commits
1 Commits
v1.9.0-bet
...
dev-sentry
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05b4d98852 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
|||||||
github: nekohasekai
|
|
||||||
31
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
31
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -44,7 +44,12 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Version
|
||||||
description: If you are using the original command line program, please provide the output of the `sing-box version` command.
|
description: If you are using the original command line program, please provide the output of the `sing-box version` command.
|
||||||
render: shell
|
value: |-
|
||||||
|
<details>
|
||||||
|
```console
|
||||||
|
# Replace this line with the output
|
||||||
|
```
|
||||||
|
</details>
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Description
|
label: Description
|
||||||
@@ -61,22 +66,12 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Logs
|
label: Logs
|
||||||
description: |-
|
description: |-
|
||||||
In addition, if you encounter a crash with the graphical client, please also provide crash logs.
|
If you encounter a crash with the graphical client, please provide crash logs.
|
||||||
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
|
value: |-
|
||||||
- type: checkboxes
|
<details>
|
||||||
attributes:
|
```console
|
||||||
label: Integrity requirements
|
# Replace this line with logs
|
||||||
description: |-
|
```
|
||||||
Please check all of the following options to prove that you have read and understood the requirements, otherwise this issue will be closed.
|
</details>
|
||||||
Sing-box is not a project aimed to please users who can't make any meaningful contributions and gain unethical influence. If you deceive here to deliberately waste the time of the developers, you will be permanently blocked.
|
|
||||||
options:
|
|
||||||
- label: I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values.
|
|
||||||
required: true
|
|
||||||
- label: I confirm that I have provided the server and client configuration files and process that can be reproduced locally, instead of a complicated client configuration file that has been stripped of sensitive data.
|
|
||||||
required: true
|
|
||||||
- label: I confirm that I have provided the simplest configuration that can be used to reproduce the error I reported, instead of depending on remote servers, TUN, graphical interface clients, or other closed-source software.
|
|
||||||
required: true
|
|
||||||
- label: I confirm that I have provided the complete configuration files and logs, rather than just providing parts I think are useful out of confidence in my own intelligence.
|
|
||||||
required: true
|
|
||||||
31
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
31
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
@@ -44,7 +44,12 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 版本
|
label: 版本
|
||||||
description: 如果您使用原始命令行程序,请提供 `sing-box version` 命令的输出。
|
description: 如果您使用原始命令行程序,请提供 `sing-box version` 命令的输出。
|
||||||
render: shell
|
value: |-
|
||||||
|
<details>
|
||||||
|
```console
|
||||||
|
# 使用输出内容覆盖此行
|
||||||
|
```
|
||||||
|
</details>
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 描述
|
label: 描述
|
||||||
@@ -61,22 +66,12 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 日志
|
label: 日志
|
||||||
description: |-
|
description: |-
|
||||||
此外,如果您遭遇图形界面应用程序崩溃,请附加提供崩溃日志。
|
如果您遭遇图形界面应用程序崩溃,请提供崩溃日志。
|
||||||
对于 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
|
value: |-
|
||||||
- type: checkboxes
|
<details>
|
||||||
attributes:
|
```console
|
||||||
label: 完整性要求
|
# 使用日志内容覆盖此行
|
||||||
description: |-
|
```
|
||||||
请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
|
</details>
|
||||||
sing-box 不是讨好无法作出任何意义上的贡献的最终用户并获取非道德影响力的项目,如果您在此处欺骗以故意浪费开发者的时间,您将被永久封锁。
|
|
||||||
options:
|
|
||||||
- label: 我保证阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
|
|
||||||
required: true
|
|
||||||
- label: 我保证提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。
|
|
||||||
required: true
|
|
||||||
- label: 我保证提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器、TUN、图形界面客户端或者其他闭源软件。
|
|
||||||
required: true
|
|
||||||
- label: 我保证提供了完整的配置文件与日志,而不是出于对自身智力的自信而仅提供了部分认为有用的部分。
|
|
||||||
required: true
|
|
||||||
14
.github/update_clients.sh
vendored
14
.github/update_clients.sh
vendored
@@ -1,14 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
74
.github/workflows/debug.yml
vendored
74
.github/workflows/debug.yml
vendored
@@ -22,13 +22,25 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # 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@v4
|
||||||
with:
|
with:
|
||||||
go-version: ^1.22
|
go-version: ${{ steps.version.outputs.go_version }}
|
||||||
|
- 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: |
|
||||||
@@ -38,15 +50,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ~1.18
|
go-version: 1.18.10
|
||||||
- name: Cache go module
|
- name: Cache go module
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@@ -58,39 +70,19 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ~1.20
|
go-version: 1.20.7
|
||||||
- name: Cache go module
|
- name: Cache go module
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: go120-${{ hashFiles('**/go.sum') }}
|
key: go118-${{ 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@b4ffde65f46336ab88eb53be808477a3936bae11 # 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:
|
||||||
@@ -196,7 +188,8 @@ 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 }}
|
||||||
@@ -208,13 +201,22 @@ 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@3df4ab11eba7bda6032a0b82a6bb43b11571feac # 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@v4
|
||||||
with:
|
with:
|
||||||
go-version: ^1.21
|
go-version: ${{ steps.version.outputs.go_version }}
|
||||||
- name: Build
|
- name: Build
|
||||||
id: build
|
id: build
|
||||||
run: make
|
run: make
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: sing-box-${{ matrix.name }}
|
||||||
|
path: sing-box*
|
||||||
|
|||||||
26
.github/workflows/docker.yml
vendored
26
.github/workflows/docker.yml
vendored
@@ -1,16 +1,15 @@
|
|||||||
name: Build Docker Images
|
name: Build Docker Images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
workflow_dispatch:
|
||||||
types:
|
inputs:
|
||||||
- released
|
tag:
|
||||||
|
description: "The tag version you want to build"
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
- 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
|
||||||
@@ -26,14 +25,21 @@ 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
|
||||||
target: dist
|
target: dist
|
||||||
build-args: |
|
|
||||||
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/sagernet/sing-box:latest
|
${{ steps.tag.outputs.latest }}
|
||||||
ghcr.io/sagernet/sing-box:${{ github.ref_name }}
|
${{ steps.tag.outputs.versioned }}
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
14
.github/workflows/lint.yml
vendored
14
.github/workflows/lint.yml
vendored
@@ -22,19 +22,19 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # 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@v4
|
||||||
with:
|
|
||||||
go-version: ^1.22
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
with:
|
||||||
go-version: ${{ steps.version.outputs.go_version }}
|
go-version: ${{ steps.version.outputs.go_version }}
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v4
|
uses: golangci/golangci-lint-action@v3
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout=30m
|
args: --timeout=30m
|
||||||
|
|||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,7 +1,6 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
/vendor/
|
/vendor/
|
||||||
/*.json
|
/*.json
|
||||||
/*.srs
|
|
||||||
/*.db
|
/*.db
|
||||||
/site/
|
/site/
|
||||||
/bin/
|
/bin/
|
||||||
|
|||||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,6 +0,0 @@
|
|||||||
[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
|
|
||||||
@@ -5,6 +5,10 @@ builds:
|
|||||||
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:
|
||||||
@@ -15,18 +19,15 @@ builds:
|
|||||||
- with_ech
|
- with_ech
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_acme
|
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
targets:
|
targets:
|
||||||
- linux_386
|
|
||||||
- linux_amd64_v1
|
- linux_amd64_v1
|
||||||
- linux_amd64_v3
|
- linux_amd64_v3
|
||||||
- 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
|
||||||
@@ -40,31 +41,10 @@ builds:
|
|||||||
flags:
|
flags:
|
||||||
- -v
|
- -v
|
||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
asmflags:
|
||||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
- all=-trimpath={{.Env.GOPATH}}
|
||||||
tags:
|
gcflags:
|
||||||
- with_gvisor
|
- all=-trimpath={{.Env.GOPATH}}
|
||||||
- with_quic
|
|
||||||
- with_dhcp
|
|
||||||
- with_wireguard
|
|
||||||
- with_utls
|
|
||||||
- with_reality_server
|
|
||||||
- with_acme
|
|
||||||
- with_clash_api
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
- GOROOT=/nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/share/go
|
|
||||||
gobinary: /nix/store/kg6i737jjqs923jcijnm003h68c1dghj-go-1.20.11/bin/go
|
|
||||||
targets:
|
|
||||||
- windows_amd64_v1
|
|
||||||
- windows_386
|
|
||||||
- darwin_amd64_v1
|
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
- id: android
|
|
||||||
main: ./cmd/sing-box
|
|
||||||
flags:
|
|
||||||
- -v
|
|
||||||
- -trimpath
|
|
||||||
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:
|
||||||
@@ -75,7 +55,34 @@ builds:
|
|||||||
- with_ech
|
- with_ech
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_acme
|
- with_clash_api
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
- GOROOT=/nix/store/5h8gjl89zx8qxgc572wa3k81zplv8v4z-go-1.20.10/share/go
|
||||||
|
gobinary: /nix/store/5h8gjl89zx8qxgc572wa3k81zplv8v4z-go-1.20.10/bin/go
|
||||||
|
targets:
|
||||||
|
- windows_amd64_v1
|
||||||
|
- windows_386
|
||||||
|
- darwin_amd64_v1
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
- id: android
|
||||||
|
main: ./cmd/sing-box
|
||||||
|
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_clash_api
|
- with_clash_api
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1
|
- CGO_ENABLED=1
|
||||||
@@ -84,8 +91,8 @@ builds:
|
|||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: 7
|
goarm: 7
|
||||||
env:
|
env:
|
||||||
- CC=armv7a-linux-androideabi21-clang
|
- CC=armv7a-linux-androideabi19-clang
|
||||||
- CXX=armv7a-linux-androideabi21-clang++
|
- CXX=armv7a-linux-androideabi19-clang++
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
env:
|
env:
|
||||||
@@ -94,8 +101,8 @@ builds:
|
|||||||
- goos: android
|
- goos: android
|
||||||
goarch: 386
|
goarch: 386
|
||||||
env:
|
env:
|
||||||
- CC=i686-linux-android21-clang
|
- CC=i686-linux-android19-clang
|
||||||
- CXX=i686-linux-android21-clang++
|
- CXX=i686-linux-android19-clang++
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
goamd64: v1
|
goamd64: v1
|
||||||
@@ -146,7 +153,6 @@ nfpms:
|
|||||||
formats:
|
formats:
|
||||||
- deb
|
- deb
|
||||||
- rpm
|
- rpm
|
||||||
- archlinux
|
|
||||||
priority: extra
|
priority: extra
|
||||||
contents:
|
contents:
|
||||||
- src: release/config/config.json
|
- src: release/config/config.json
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -1,23 +1,19 @@
|
|||||||
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
|
FROM golang:1.21-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
|
||||||
ARG TARGETOS TARGETARCH
|
|
||||||
ARG GOPROXY=""
|
ARG GOPROXY=""
|
||||||
ENV GOPROXY ${GOPROXY}
|
ENV GOPROXY ${GOPROXY}
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
ENV GOOS=$TARGETOS
|
|
||||||
ENV GOARCH=$TARGETARCH
|
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& apk add git build-base \
|
&& apk add git build-base \
|
||||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||||
&& go build -v -trimpath -tags \
|
&& go build -v -trimpath -tags with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_clash_api,with_acme \
|
||||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
|
|
||||||
-o /go/bin/sing-box \
|
-o /go/bin/sing-box \
|
||||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
FROM alpine AS dist
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& apk upgrade \
|
&& apk upgrade \
|
||||||
|
|||||||
49
Makefile
49
Makefile
@@ -1,10 +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_reality_server,with_clash_api
|
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api
|
||||||
TAGS_GO120 = with_quic,with_utls
|
TAGS_GO120 = with_quic,with_ech
|
||||||
TAGS_GO121 = with_ech
|
TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
|
||||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
|
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server,with_shadowsocksr
|
||||||
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)
|
||||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||||
@@ -15,7 +14,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
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
@@ -24,10 +23,6 @@ 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)
|
||||||
@@ -66,14 +61,7 @@ proto_install:
|
|||||||
release:
|
release:
|
||||||
go run ./cmd/internal/build goreleaser release --clean --skip-publish || exit 1
|
go run ./cmd/internal/build goreleaser release --clean --skip-publish || exit 1
|
||||||
mkdir dist/release
|
mkdir dist/release
|
||||||
mv dist/*.tar.gz \
|
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/release
|
||||||
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
|
||||||
|
|
||||||
@@ -85,22 +73,18 @@ 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 :app:assembleOtherRelease && ./gradlew --stop
|
cd ../sing-box-for-android && ./gradlew :app:assembleRelease && ./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/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
|
||||||
|
|
||||||
release_android: lib_android update_android_version build_android upload_android
|
release_android: lib_android update_android_version build_android upload_android
|
||||||
|
|
||||||
publish_android:
|
publish_android:
|
||||||
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle
|
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadRelease
|
||||||
|
|
||||||
publish_android_appcenter:
|
|
||||||
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadPlayRelease
|
|
||||||
|
|
||||||
build_ios:
|
build_ios:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
@@ -165,8 +149,10 @@ update_apple_version:
|
|||||||
go run ./cmd/internal/update_apple_version
|
go run ./cmd/internal/update_apple_version
|
||||||
|
|
||||||
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_independent
|
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_independent
|
||||||
|
rm -rf dist
|
||||||
|
|
||||||
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
||||||
|
rm -rf dist
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test -v ./... && \
|
@go test -v ./... && \
|
||||||
@@ -191,17 +177,10 @@ lib:
|
|||||||
go run ./cmd/internal/build_libbox -target ios
|
go run ./cmd/internal/build_libbox -target ios
|
||||||
|
|
||||||
lib_install:
|
lib_install:
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.3
|
go get -v -d
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gobind@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.0.0-20230915142329-c6740b6d2950
|
||||||
|
|
||||||
docs:
|
|
||||||
mkdocs serve
|
|
||||||
|
|
||||||
publish_docs:
|
|
||||||
mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
|
||||||
|
|
||||||
docs_install:
|
|
||||||
pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin dist sing-box
|
rm -rf bin dist sing-box
|
||||||
rm -f $(shell go env GOPATH)/sing-box
|
rm -f $(shell go env GOPATH)/sing-box
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConnectionRouter interface {
|
|
||||||
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
|
||||||
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRouteHandler(
|
|
||||||
metadata InboundContext,
|
|
||||||
router ConnectionRouter,
|
|
||||||
logger logger.ContextLogger,
|
|
||||||
) UpstreamHandlerAdapter {
|
|
||||||
return &routeHandlerWrapper{
|
|
||||||
metadata: metadata,
|
|
||||||
router: router,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRouteContextHandler(
|
|
||||||
router ConnectionRouter,
|
|
||||||
logger logger.ContextLogger,
|
|
||||||
) UpstreamHandlerAdapter {
|
|
||||||
return &routeContextHandlerWrapper{
|
|
||||||
router: router,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
|
|
||||||
|
|
||||||
type routeHandlerWrapper struct {
|
|
||||||
metadata InboundContext
|
|
||||||
router ConnectionRouter
|
|
||||||
logger logger.ContextLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
|
||||||
myMetadata := w.metadata
|
|
||||||
if metadata.Source.IsValid() {
|
|
||||||
myMetadata.Source = metadata.Source
|
|
||||||
}
|
|
||||||
if metadata.Destination.IsValid() {
|
|
||||||
myMetadata.Destination = metadata.Destination
|
|
||||||
}
|
|
||||||
return w.router.RouteConnection(ctx, conn, myMetadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
|
||||||
myMetadata := w.metadata
|
|
||||||
if metadata.Source.IsValid() {
|
|
||||||
myMetadata.Source = metadata.Source
|
|
||||||
}
|
|
||||||
if metadata.Destination.IsValid() {
|
|
||||||
myMetadata.Destination = metadata.Destination
|
|
||||||
}
|
|
||||||
return w.router.RoutePacketConnection(ctx, conn, myMetadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
|
|
||||||
w.logger.ErrorContext(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
|
|
||||||
|
|
||||||
type routeContextHandlerWrapper struct {
|
|
||||||
router ConnectionRouter
|
|
||||||
logger logger.ContextLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
|
||||||
myMetadata := ContextFrom(ctx)
|
|
||||||
if metadata.Source.IsValid() {
|
|
||||||
myMetadata.Source = metadata.Source
|
|
||||||
}
|
|
||||||
if metadata.Destination.IsValid() {
|
|
||||||
myMetadata.Destination = metadata.Destination
|
|
||||||
}
|
|
||||||
return w.router.RouteConnection(ctx, conn, *myMetadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
|
||||||
myMetadata := ContextFrom(ctx)
|
|
||||||
if metadata.Source.IsValid() {
|
|
||||||
myMetadata.Source = metadata.Source
|
|
||||||
}
|
|
||||||
if metadata.Destination.IsValid() {
|
|
||||||
myMetadata.Destination = metadata.Destination
|
|
||||||
}
|
|
||||||
return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
|
||||||
w.logger.ErrorContext(ctx, err)
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
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 {
|
||||||
@@ -19,86 +13,22 @@ 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 CacheFile interface {
|
type ClashCacheFile 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
|
||||||
LoadRuleSet(tag string) *SavedRuleSet
|
FakeIPStorage
|
||||||
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 {
|
||||||
@@ -113,7 +43,7 @@ type OutboundGroup interface {
|
|||||||
|
|
||||||
type URLTestGroup interface {
|
type URLTestGroup interface {
|
||||||
OutboundGroup
|
OutboundGroup
|
||||||
URLTest(ctx context.Context) (map[string]uint16, error)
|
URLTest(ctx context.Context, url string) (map[string]uint16, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OutboundTag(detour Outbound) string {
|
func OutboundTag(detour Outbound) string {
|
||||||
|
|||||||
@@ -46,26 +46,11 @@ type InboundContext struct {
|
|||||||
SourceGeoIPCode string
|
SourceGeoIPCode string
|
||||||
GeoIPCode string
|
GeoIPCode string
|
||||||
ProcessInfo *process.Info
|
ProcessInfo *process.Info
|
||||||
QueryType uint16
|
|
||||||
FakeIP bool
|
FakeIP bool
|
||||||
|
|
||||||
// rule cache
|
// dns cache
|
||||||
|
|
||||||
IPCIDRMatchSource bool
|
QueryType uint16
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type inboundContextKey struct{}
|
type inboundContextKey struct{}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/geoip"
|
"github.com/sagernet/sing-box/common/geoip"
|
||||||
@@ -17,24 +17,19 @@ import (
|
|||||||
|
|
||||||
type Router interface {
|
type Router interface {
|
||||||
Service
|
Service
|
||||||
PreStarter
|
|
||||||
PostStarter
|
|
||||||
|
|
||||||
Outbounds() []Outbound
|
Outbounds() []Outbound
|
||||||
Outbound(tag string) (Outbound, bool)
|
Outbound(tag string) (Outbound, bool)
|
||||||
DefaultOutbound(network string) (Outbound, error)
|
DefaultOutbound(network string) Outbound
|
||||||
|
|
||||||
FakeIPStore() FakeIPStore
|
FakeIPStore() FakeIPStore
|
||||||
|
|
||||||
ConnectionRouter
|
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||||
|
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||||
|
|
||||||
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)
|
||||||
@@ -49,7 +44,6 @@ type Router interface {
|
|||||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||||
PackageManager() tun.PackageManager
|
PackageManager() tun.PackageManager
|
||||||
WIFIState() WIFIState
|
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
|
|
||||||
ClashServer() ClashServer
|
ClashServer() ClashServer
|
||||||
@@ -69,15 +63,11 @@ func RouterFromContext(ctx context.Context) Router {
|
|||||||
return service.FromContext[Router](ctx)
|
return service.FromContext[Router](ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeadlessRule interface {
|
|
||||||
Match(metadata *InboundContext) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
String() string
|
||||||
}
|
}
|
||||||
@@ -86,35 +76,8 @@ type DNSRule interface {
|
|||||||
Rule
|
Rule
|
||||||
DisableCache() bool
|
DisableCache() bool
|
||||||
RewriteTTL() *uint32
|
RewriteTTL() *uint32
|
||||||
ClientSubnet() *netip.Addr
|
|
||||||
WithAddressLimit() bool
|
|
||||||
MatchAddressLimit(metadata *InboundContext) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleSet interface {
|
|
||||||
StartContext(ctx context.Context, startContext RuleSetStartContext) error
|
|
||||||
PostStart() 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 {
|
||||||
InterfaceUpdated()
|
InterfaceUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
type WIFIState struct {
|
|
||||||
SSID string
|
|
||||||
BSSID string
|
|
||||||
}
|
|
||||||
|
|||||||
120
box.go
120
box.go
@@ -9,10 +9,7 @@ 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"
|
||||||
@@ -35,8 +32,7 @@ type Box struct {
|
|||||||
outbounds []adapter.Outbound
|
outbounds []adapter.Outbound
|
||||||
logFactory log.Factory
|
logFactory log.Factory
|
||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
preServices1 map[string]adapter.Service
|
preServices 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,26 +41,21 @@ type Options struct {
|
|||||||
option.Options
|
option.Options
|
||||||
Context context.Context
|
Context context.Context
|
||||||
PlatformInterface platform.Interface
|
PlatformInterface platform.Interface
|
||||||
PlatformLogWriter log.PlatformWriter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.WithDefaultManager(ctx)
|
ctx = pause.ContextWithDefaultManager(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 {
|
if experimentalOptions.ClashAPI != nil || options.PlatformInterface != nil {
|
||||||
needCacheFile = true
|
|
||||||
}
|
|
||||||
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
|
|
||||||
needClashAPI = true
|
needClashAPI = true
|
||||||
}
|
}
|
||||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||||
@@ -80,7 +71,7 @@ func New(options Options) (*Box, error) {
|
|||||||
Observable: needClashAPI,
|
Observable: needClashAPI,
|
||||||
DefaultWriter: defaultLogWriter,
|
DefaultWriter: defaultLogWriter,
|
||||||
BaseTime: createdAt,
|
BaseTime: createdAt,
|
||||||
PlatformWriter: options.PlatformLogWriter,
|
PlatformWriter: options.PlatformInterface,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create log factory")
|
return nil, E.Cause(err, "create log factory")
|
||||||
@@ -153,17 +144,8 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize platform interface")
|
return nil, E.Cause(err, "initialize platform interface")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preServices1 := make(map[string]adapter.Service)
|
preServices := 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)
|
||||||
@@ -172,7 +154,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)
|
||||||
preServices2["clash api"] = clashServer
|
preServices["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))
|
||||||
@@ -180,7 +162,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)
|
||||||
preServices2["v2ray api"] = v2rayServer
|
preServices["v2ray api"] = v2rayServer
|
||||||
}
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
router: router,
|
router: router,
|
||||||
@@ -189,8 +171,7 @@ func New(options Options) (*Box, error) {
|
|||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
logFactory: logFactory,
|
logFactory: logFactory,
|
||||||
logger: logFactory.Logger(),
|
logger: logFactory.Logger(),
|
||||||
preServices1: preServices1,
|
preServices: preServices,
|
||||||
preServices2: preServices2,
|
|
||||||
postServices: postServices,
|
postServices: postServices,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -235,38 +216,16 @@ func (s *Box) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) preStart() error {
|
func (s *Box) preStart() error {
|
||||||
monitor := taskmonitor.New(s.logger, C.DefaultStartTimeout)
|
for serviceName, service := range s.preServices {
|
||||||
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 {
|
||||||
monitor.Start("pre-start ", serviceName)
|
s.logger.Trace("pre-start ", serviceName)
|
||||||
err := preService.PreStart()
|
err := preService.PreStart()
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "pre-start ", serviceName)
|
return E.Cause(err, "pre-starting ", serviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for serviceName, service := range s.preServices2 {
|
err := s.startOutbounds()
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -278,13 +237,8 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for serviceName, service := range s.preServices1 {
|
for serviceName, service := range s.preServices {
|
||||||
err = service.Start()
|
s.logger.Trace("starting ", serviceName)
|
||||||
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)
|
||||||
@@ -297,31 +251,33 @@ func (s *Box) start() error {
|
|||||||
} else {
|
} else {
|
||||||
tag = in.Tag()
|
tag = in.Tag()
|
||||||
}
|
}
|
||||||
|
s.logger.Trace("initializing inbound/", in.Type(), "[", tag, "]")
|
||||||
err = in.Start()
|
err = in.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s.postStart()
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) postStart() error {
|
func (s *Box) postStart() error {
|
||||||
for serviceName, service := range s.postServices {
|
for serviceName, service := range s.postServices {
|
||||||
|
s.logger.Trace("starting ", service)
|
||||||
err := service.Start()
|
err := service.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "start ", serviceName)
|
return E.Cause(err, "start ", serviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, outbound := range s.outbounds {
|
for serviceName, service := range s.outbounds {
|
||||||
if lateOutbound, isLateOutbound := outbound.(adapter.PostStarter); isLateOutbound {
|
if lateService, isLateService := service.(adapter.PostStarter); isLateService {
|
||||||
err := lateOutbound.PostStart()
|
s.logger.Trace("post-starting ", service)
|
||||||
|
err := lateService.PostStart()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "post-start outbound/", outbound.Tag())
|
return E.Cause(err, "post-start ", serviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return s.router.PostStart()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) Close() error {
|
func (s *Box) Close() error {
|
||||||
@@ -331,53 +287,41 @@ func (s *Box) Close() error {
|
|||||||
default:
|
default:
|
||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
monitor := taskmonitor.New(s.logger, C.DefaultStopTimeout)
|
|
||||||
var errors error
|
var errors error
|
||||||
for serviceName, service := range s.postServices {
|
for serviceName, service := range s.postServices {
|
||||||
monitor.Start("close ", serviceName)
|
s.logger.Trace("closing ", 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 {
|
||||||
monitor.Start("close inbound/", in.Type(), "[", i, "]")
|
s.logger.Trace("closing 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 {
|
||||||
monitor.Start("close outbound/", out.Type(), "[", i, "]")
|
s.logger.Trace("closing 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()
|
|
||||||
}
|
}
|
||||||
monitor.Start("close router")
|
s.logger.Trace("closing 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")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
monitor.Finish()
|
for serviceName, service := range s.preServices {
|
||||||
for serviceName, service := range s.preServices1 {
|
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 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 logger")
|
return E.Cause(err, "close log factory")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return errors
|
return errors
|
||||||
|
|||||||
@@ -4,15 +4,12 @@ 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.DefaultStartTimeout)
|
|
||||||
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 {
|
||||||
@@ -46,9 +43,8 @@ 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 {
|
||||||
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
s.logger.Trace("initializing outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||||
err := starter.Start()
|
err := starter.Start()
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule clients/android deleted from f0acb0999b
Submodule clients/apple deleted from 45b4e58b3f
@@ -12,7 +12,7 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
build_shared.FindSDK()
|
build_shared.FindSDK()
|
||||||
|
|
||||||
if os.Getenv("GOPATH") == "" {
|
if os.Getenv("build.Default.GOPATH") == "" {
|
||||||
os.Setenv("GOPATH", build.Default.GOPATH)
|
os.Setenv("GOPATH", build.Default.GOPATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
_ "github.com/sagernet/gomobile"
|
_ "github.com/sagernet/gomobile/event/key"
|
||||||
"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, "-buildvcs=false")
|
sharedFlags = append(sharedFlags, "-ldflags")
|
||||||
currentTag, err := build_shared.ReadTag()
|
currentTag, err := build_shared.ReadTag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
currentTag = "unknown"
|
currentTag = "unknown"
|
||||||
}
|
}
|
||||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
sharedFlags = append(sharedFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
debugFlags = append(debugFlags, "-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,9 +11,7 @@ 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 (
|
||||||
@@ -30,7 +28,7 @@ func FindSDK() {
|
|||||||
}
|
}
|
||||||
for _, path := range searchPath {
|
for _, path := range searchPath {
|
||||||
path = os.ExpandEnv(path)
|
path = os.ExpandEnv(path)
|
||||||
if rw.FileExists(filepath.Join(path, "licenses", "android-sdk-license")) {
|
if rw.FileExists(path + "/licenses/android-sdk-license") {
|
||||||
androidSDKPath = path
|
androidSDKPath = path
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -42,14 +40,6 @@ 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)
|
||||||
@@ -58,13 +48,11 @@ func FindSDK() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findNDK() bool {
|
func findNDK() bool {
|
||||||
const fixedVersion = "26.2.11394342"
|
if rw.FileExists(androidSDKPath + "/ndk/25.1.8937393") {
|
||||||
const versionFile = "source.properties"
|
androidNDKPath = androidSDKPath + "/ndk/25.1.8937393"
|
||||||
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.FileExists(filepath.Join(fixedPath, versionFile)) {
|
|
||||||
androidNDKPath = fixedPath
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
ndkVersions, err := os.ReadDir(filepath.Join(androidSDKPath, "ndk"))
|
ndkVersions, err := os.ReadDir(androidSDKPath + "/ndk")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -85,10 +73,8 @@ func findNDK() bool {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
for _, versionName := range versionNames {
|
for _, versionName := range versionNames {
|
||||||
currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName)
|
if rw.FileExists(androidSDKPath + "/ndk/" + versionName) {
|
||||||
if rw.FileExists(filepath.Join(androidSDKPath, versionFile)) {
|
androidNDKPath = androidSDKPath + "/ndk/" + versionName
|
||||||
androidNDKPath = currentNDKPath
|
|
||||||
log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,14 +85,8 @@ var GoBinPath string
|
|||||||
|
|
||||||
func FindMobile() {
|
func FindMobile() {
|
||||||
goBin := filepath.Join(build.Default.GOPATH, "bin")
|
goBin := filepath.Join(build.Default.GOPATH, "bin")
|
||||||
if runtime.GOOS == "windows" {
|
if !rw.FileExists(goBin + "/" + "gobind") {
|
||||||
if !rw.FileExists(filepath.Join(goBin, "gobind.exe")) {
|
|
||||||
log.Fatal("missing gomobile installation")
|
log.Fatal("missing gomobile installation")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if !rw.FileExists(filepath.Join(goBin, "gobind")) {
|
|
||||||
log.Fatal("missing gomobile installation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GoBinPath = goBin
|
GoBinPath = goBin
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ func ReadTag() (string, error) {
|
|||||||
}
|
}
|
||||||
shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput()
|
shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput()
|
||||||
version := badversion.Parse(currentTagRev[1:])
|
version := badversion.Parse(currentTagRev[1:])
|
||||||
|
if version.PreReleaseIdentifier == "" {
|
||||||
|
version.Patch++
|
||||||
|
}
|
||||||
return version.String() + "-" + shortCommit, nil
|
return version.String() + "-" + shortCommit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -19,34 +18,20 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
common.Must(os.Chdir(androidPath))
|
common.Must(os.Chdir(androidPath))
|
||||||
localProps := common.Must1(os.ReadFile("version.properties"))
|
localProps := common.Must1(os.ReadFile("local.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 {
|
||||||
switch propPair[0] {
|
if propPair[0] == "VERSION_NAME" {
|
||||||
case "VERSION_NAME":
|
if propPair[1] == newVersion.String() {
|
||||||
if propPair[1] != newVersion.String() {
|
log.Info("version not changed")
|
||||||
versionUpdated = true
|
return
|
||||||
|
}
|
||||||
propPair[1] = newVersion.String()
|
propPair[1] = newVersion.String()
|
||||||
log.Info("updated version to ", 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !(versionUpdated || goVersionUpdated) {
|
|
||||||
log.Info("version not changed")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
for _, propPair := range propsList {
|
for _, propPair := range propsList {
|
||||||
switch propPair[0] {
|
switch propPair[0] {
|
||||||
@@ -54,11 +39,13 @@ func main() {
|
|||||||
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("version.properties", []byte(strings.Join(newProps, "\n")), 0o644))
|
common.Must(os.WriteFile("local.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,10 +38,6 @@ 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("", " ")
|
||||||
@@ -73,3 +69,41 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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,26 +65,50 @@ 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 {
|
||||||
rawOptions, err := inbound.RawOptions()
|
switch inbound.Type {
|
||||||
if err != nil {
|
case C.TypeHTTP:
|
||||||
return err
|
inbound.HTTPOptions.TLS = mergeTLSInboundOptions(inbound.HTTPOptions.TLS)
|
||||||
}
|
case C.TypeMixed:
|
||||||
if tlsOptions, containsTLSOptions := rawOptions.(option.InboundTLSOptionsWrapper); containsTLSOptions {
|
inbound.MixedOptions.TLS = mergeTLSInboundOptions(inbound.MixedOptions.TLS)
|
||||||
tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions()))
|
case C.TypeVMess:
|
||||||
|
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:
|
||||||
if tlsOptions, containsTLSOptions := rawOptions.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
|
outbound.VLESSOptions.TLS = mergeTLSOutboundOptions(outbound.VLESSOptions.TLS)
|
||||||
tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions()))
|
case C.TypeTUIC:
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandRuleSet = &cobra.Command{
|
|
||||||
Use: "rule-set",
|
|
||||||
Short: "Manage rule sets",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
mainCommand.AddCommand(commandRuleSet)
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -13,12 +13,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box"
|
"github.com/sagernet/sing-box"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
"github.com/sagernet/sing-box/common/badjsonmerge"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -57,7 +55,8 @@ 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)
|
||||||
}
|
}
|
||||||
options, err := json.UnmarshalExtended[option.Options](configContent)
|
var options option.Options
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -107,18 +106,13 @@ func readConfigAndMerge() (option.Options, error) {
|
|||||||
if len(optionsList) == 1 {
|
if len(optionsList) == 1 {
|
||||||
return optionsList[0].options, nil
|
return optionsList[0].options, nil
|
||||||
}
|
}
|
||||||
var mergedMessage json.RawMessage
|
var mergedOptions option.Options
|
||||||
for _, options := range optionsList {
|
for _, options := range optionsList {
|
||||||
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage)
|
mergedOptions, err = badjsonmerge.MergeOptions(options.options, mergedOptions)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +127,7 @@ func create() (*box.Box, context.CancelFunc, error) {
|
|||||||
}
|
}
|
||||||
options.Log.DisableColor = true
|
options.Log.DisableColor = true
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(globalCtx)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
instance, err := box.New(box.Options{
|
instance, err := box.New(box.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Options: options,
|
Options: options,
|
||||||
@@ -199,7 +193,7 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func closeMonitor(ctx context.Context) {
|
func closeMonitor(ctx context.Context) {
|
||||||
time.Sleep(C.DefaultStopFatalTimeout)
|
time.Sleep(3 * time.Second)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ 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 == "" {
|
||||||
return instance.Router().DefaultOutbound(N.NetworkName(network))
|
outbound := 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,25 +1,22 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"github.com/getsentry/sentry-go"
|
||||||
"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
|
||||||
disableColor bool
|
disableColor bool
|
||||||
|
enableDebug bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var mainCommand = &cobra.Command{
|
var mainCommand = &cobra.Command{
|
||||||
@@ -32,39 +29,37 @@ func init() {
|
|||||||
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
|
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
|
||||||
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
||||||
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
||||||
|
mainCommand.PersistentFlags().BoolVarP(&enableDebug, "debug", "", false, "enable sentry debug mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := mainCommand.Execute(); err != nil {
|
if err := mainCommand.Execute(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if enableDebug {
|
||||||
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("sentry.Init: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer sentry.Flush(2 * time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
|
log.SetStdLogger(log.NewFactory(log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, nil).Logger())
|
||||||
}
|
}
|
||||||
if workingDir != "" {
|
if workingDir != "" {
|
||||||
_, err := os.Stat(workingDir)
|
_, err := os.Stat(workingDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
filemanager.MkdirAll(globalCtx, workingDir, 0o777)
|
os.MkdirAll(workingDir, 0o777)
|
||||||
}
|
}
|
||||||
err = os.Chdir(workingDir)
|
if err := os.Chdir(workingDir); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
common/badjson/array.go
Normal file
46
common/badjson/array.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
54
common/badjson/json.go
Normal file
54
common/badjson/json.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
79
common/badjson/object.go
Normal file
79
common/badjson/object.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
80
common/badjsonmerge/merge.go
Normal file
80
common/badjsonmerge/merge.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
59
common/badjsonmerge/merge_test.go
Normal file
59
common/badjsonmerge/merge_test.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
233
common/badtls/badtls.go
Normal file
233
common/badtls/badtls.go
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
//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()
|
||||||
|
}
|
||||||
14
common/badtls/badtls_stub.go
Normal file
14
common/badtls/badtls_stub.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//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
|
||||||
|
}
|
||||||
22
common/badtls/link.go
Normal file
22
common/badtls/link.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//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
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
//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
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
//go:build go1.21 && !without_badtls && with_ech
|
|
||||||
|
|
||||||
package badtls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
_ "unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/cloudflare-tls"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
|
||||||
tlsConn, loaded := common.Cast[*tls.Conn](conn)
|
|
||||||
if !loaded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return true, func() error {
|
|
||||||
return echReadRecord(tlsConn)
|
|
||||||
}, func() error {
|
|
||||||
return echHandlePostHandshakeMessage(tlsConn)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
|
|
||||||
func echReadRecord(c *tls.Conn) error
|
|
||||||
|
|
||||||
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
|
|
||||||
func echHandlePostHandshakeMessage(c *tls.Conn) error
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
//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
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
//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/common/json"
|
import "github.com/sagernet/sing-box/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,8 +15,6 @@ 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
|
||||||
@@ -25,7 +23,6 @@ type DefaultDialer struct {
|
|||||||
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) {
|
||||||
@@ -101,11 +98,6 @@ 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
|
||||||
@@ -122,7 +114,6 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
|||||||
listener,
|
listener,
|
||||||
udpAddr4,
|
udpAddr4,
|
||||||
udpAddr6,
|
udpAddr6,
|
||||||
options.IsWireGuardListener,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,10 +146,6 @@ 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,13 +6,15 @@ 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)
|
|
||||||
}
|
|
||||||
var (
|
var (
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
err error
|
err error
|
||||||
|
|||||||
@@ -18,19 +18,11 @@ 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) {
|
||||||
dialer, err := d.router.DefaultOutbound(network)
|
return d.router.DefaultOutbound(network).DialContext(ctx, network, destination)
|
||||||
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) {
|
||||||
dialer, err := d.router.DefaultOutbound(N.NetworkUDP)
|
return d.router.DefaultOutbound(N.NetworkUDP).ListenPacket(ctx, destination)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dialer.ListenPacket(ctx, destination)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *RouterDialer) Upstream() any {
|
func (d *RouterDialer) Upstream() any {
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
type WireGuardListener interface {
|
|
||||||
ListenPacketCompat(network, address string) (net.PacketConn, error)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
//go:build with_wireguard
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/wireguard-go/conn"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ WireGuardListener = (conn.Listener)(nil)
|
|
||||||
|
|
||||||
var wgControlFns = conn.ControlFns
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
//go:build !with_wireguard
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing/common/control"
|
|
||||||
)
|
|
||||||
|
|
||||||
var wgControlFns []control.Func
|
|
||||||
@@ -32,7 +32,3 @@ func (r *Reader) Lookup(addr netip.Addr) string {
|
|||||||
}
|
}
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) Close() error {
|
|
||||||
return r.reader.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
128
common/json/comment.go
Normal file
128
common/json/comment.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
18
common/json/std.go
Normal file
18
common/json/std.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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,42 +1,21 @@
|
|||||||
package mux
|
package mux
|
||||||
|
|
||||||
import (
|
import (
|
||||||
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"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client = mux.Client
|
func NewClientWithOptions(dialer N.Dialer, options option.MultiplexOptions) (*Client, error) {
|
||||||
|
|
||||||
func NewClientWithOptions(dialer N.Dialer, logger logger.Logger, options option.OutboundMultiplexOptions) (*Client, error) {
|
|
||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var brutalOptions mux.BrutalOptions
|
|
||||||
if options.Brutal != nil && options.Brutal.Enabled {
|
|
||||||
brutalOptions = mux.BrutalOptions{
|
|
||||||
Enabled: true,
|
|
||||||
SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps),
|
|
||||||
ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps),
|
|
||||||
}
|
|
||||||
if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS {
|
|
||||||
return nil, E.New("brutal: invalid upload speed")
|
|
||||||
}
|
|
||||||
if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS {
|
|
||||||
return nil, E.New("brutal: invalid download speed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mux.NewClient(mux.Options{
|
return mux.NewClient(mux.Options{
|
||||||
Dialer: dialer,
|
Dialer: dialer,
|
||||||
Logger: logger,
|
|
||||||
Protocol: options.Protocol,
|
Protocol: options.Protocol,
|
||||||
MaxConnections: options.MaxConnections,
|
MaxConnections: options.MaxConnections,
|
||||||
MinStreams: options.MinStreams,
|
MinStreams: options.MinStreams,
|
||||||
MaxStreams: options.MaxStreams,
|
MaxStreams: options.MaxStreams,
|
||||||
Padding: options.Padding,
|
Padding: options.Padding,
|
||||||
Brutal: brutalOptions,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
14
common/mux/protocol.go
Normal file
14
common/mux/protocol.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package mux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Client = mux.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Destination = mux.Destination
|
||||||
|
HandleConnection = mux.HandleConnection
|
||||||
|
)
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package mux
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing-mux"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Router struct {
|
|
||||||
router adapter.ConnectionRouter
|
|
||||||
service *mux.Service
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouter, error) {
|
|
||||||
if !options.Enabled {
|
|
||||||
return router, nil
|
|
||||||
}
|
|
||||||
var brutalOptions mux.BrutalOptions
|
|
||||||
if options.Brutal != nil && options.Brutal.Enabled {
|
|
||||||
brutalOptions = mux.BrutalOptions{
|
|
||||||
Enabled: true,
|
|
||||||
SendBPS: uint64(options.Brutal.UpMbps * C.MbpsToBps),
|
|
||||||
ReceiveBPS: uint64(options.Brutal.DownMbps * C.MbpsToBps),
|
|
||||||
}
|
|
||||||
if brutalOptions.SendBPS < mux.BrutalMinSpeedBPS {
|
|
||||||
return nil, E.New("brutal: invalid upload speed")
|
|
||||||
}
|
|
||||||
if brutalOptions.ReceiveBPS < mux.BrutalMinSpeedBPS {
|
|
||||||
return nil, E.New("brutal: invalid download speed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
service, err := mux.NewService(mux.ServiceOptions{
|
|
||||||
NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context {
|
|
||||||
return log.ContextWithNewID(ctx)
|
|
||||||
},
|
|
||||||
Logger: logger,
|
|
||||||
Handler: adapter.NewRouteContextHandler(router, logger),
|
|
||||||
Padding: options.Padding,
|
|
||||||
Brutal: brutalOptions,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Router{router, service}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
|
||||||
if metadata.Destination == mux.Destination {
|
|
||||||
return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata))
|
|
||||||
} else {
|
|
||||||
return r.router.RouteConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
|
||||||
return r.router.RoutePacketConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package mux
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
vmess "github.com/sagernet/sing-vmess"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type V2RayLegacyRouter struct {
|
|
||||||
router adapter.ConnectionRouter
|
|
||||||
logger logger.ContextLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewV2RayLegacyRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) adapter.ConnectionRouter {
|
|
||||||
return &V2RayLegacyRouter{router, logger}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *V2RayLegacyRouter) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
|
||||||
if metadata.Destination.Fqdn == vmess.MuxDestination.Fqdn {
|
|
||||||
r.logger.InfoContext(ctx, "inbound legacy multiplex connection")
|
|
||||||
return vmess.HandleMuxConnection(ctx, conn, adapter.NewRouteHandler(metadata, r.router, r.logger))
|
|
||||||
}
|
|
||||||
return r.router.RouteConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *V2RayLegacyRouter) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
|
||||||
return r.router.RoutePacketConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
@@ -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(0),
|
uintptr(1),
|
||||||
uintptr(unsafe.Pointer(&buf[0])),
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
uintptr(unsafe.Pointer(&size)),
|
uintptr(unsafe.Pointer(&size)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -182,52 +182,11 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
switch frameType {
|
switch frameType {
|
||||||
case 0x00: // PADDING
|
case 0x0:
|
||||||
continue
|
continue
|
||||||
case 0x01: // PING
|
case 0x1:
|
||||||
continue
|
continue
|
||||||
case 0x02, 0x03: // ACK
|
case 0x6:
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Largest Acknowledged
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ACK Delay
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ackRangeCount, err := qtls.ReadUvarint(decryptedReader) // ACK Range Count
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // First ACK Range
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for i := 0; i < int(ackRangeCount); i++ {
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Gap
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ACK Range Length
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if frameType == 0x03 {
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECT0 Count
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECT1 Count
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // ECN-CE Count
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 0x06: // CRYPTO
|
|
||||||
var offset uint64
|
var offset uint64
|
||||||
offset, err = qtls.ReadUvarint(decryptedReader)
|
offset, err = qtls.ReadUvarint(decryptedReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -249,26 +208,8 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
case 0x1c: // CONNECTION_CLOSE
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Error Code
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = qtls.ReadUvarint(decryptedReader) // Frame Type
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var length uint64
|
|
||||||
length, err = qtls.ReadUvarint(decryptedReader) // Reason Phrase Length
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent) // Reason Phrase
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return nil, os.ErrInvalid
|
// ignore unknown frame type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tlsHdr := make([]byte, 5)
|
tlsHdr := make([]byte, 5)
|
||||||
|
|||||||
@@ -1,487 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -105,16 +105,5 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
config = certmagic.New(cache, *config)
|
config = certmagic.New(cache, *config)
|
||||||
var tlsConfig *tls.Config
|
return config.TLSConfig(), &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
|
||||||
if acmeConfig.DisableTLSALPNChallenge || acmeConfig.DNS01Solver != nil {
|
|
||||||
tlsConfig = &tls.Config{
|
|
||||||
GetCertificate: config.GetCertificate,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tlsConfig = &tls.Config{
|
|
||||||
GetCertificate: config.GetCertificate,
|
|
||||||
NextProtos: []string{ACMETLS1Protocol},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
package tls
|
|
||||||
|
|
||||||
const ACMETLS1Protocol = "acme-tls/1"
|
|
||||||
@@ -6,7 +6,6 @@ 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"
|
||||||
@@ -43,17 +42,7 @@ 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()
|
||||||
tlsConn, err := aTLS.ClientHandshake(ctx, conn, config)
|
return 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 {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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"
|
||||||
@@ -138,21 +137,12 @@ 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)
|
|
||||||
if err != nil {
|
authKey := uConn.HandshakeState.State13.EcdheParams.SharedKey(e.publicKey)
|
||||||
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,9 +3,7 @@ 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"
|
||||||
@@ -28,15 +26,5 @@ 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()
|
||||||
tlsConn, err := aTLS.ServerHandshake(ctx, conn, config)
|
return 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,19 +39,11 @@ func (c *STDServerConfig) SetServerName(serverName string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) NextProtos() []string {
|
func (c *STDServerConfig) NextProtos() []string {
|
||||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
|
||||||
return c.config.NextProtos[1:]
|
|
||||||
} else {
|
|
||||||
return c.config.NextProtos
|
return c.config.NextProtos
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
|
||||||
c.config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
|
||||||
} else {
|
|
||||||
c.config.NextProtos = nextProto
|
c.config.NextProtos = nextProto
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) Config() (*STDConfig, error) {
|
func (c *STDServerConfig) Config() (*STDConfig, error) {
|
||||||
|
|||||||
@@ -219,16 +219,6 @@ 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":
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package uot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/common/uot"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.ConnectionRouter = (*Router)(nil)
|
|
||||||
|
|
||||||
type Router struct {
|
|
||||||
router adapter.ConnectionRouter
|
|
||||||
logger logger.ContextLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) *Router {
|
|
||||||
return &Router{router, logger}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
|
||||||
switch metadata.Destination.Fqdn {
|
|
||||||
case uot.MagicAddress:
|
|
||||||
request, err := uot.ReadRequest(conn)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read UoT request")
|
|
||||||
}
|
|
||||||
if request.IsConnect {
|
|
||||||
r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination)
|
|
||||||
} else {
|
|
||||||
r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination)
|
|
||||||
}
|
|
||||||
metadata.Domain = metadata.Destination.Fqdn
|
|
||||||
metadata.Destination = request.Destination
|
|
||||||
return r.router.RoutePacketConnection(ctx, uot.NewConn(conn, *request), metadata)
|
|
||||||
case uot.LegacyMagicAddress:
|
|
||||||
r.logger.InfoContext(ctx, "inbound legacy UoT connection")
|
|
||||||
metadata.Domain = metadata.Destination.Fqdn
|
|
||||||
metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()}
|
|
||||||
return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata)
|
|
||||||
}
|
|
||||||
return r.router.RouteConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
|
||||||
return r.router.RoutePacketConnection(ctx, conn, metadata)
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
//go:build with_quic
|
|
||||||
|
|
||||||
package constant
|
|
||||||
|
|
||||||
const WithQUIC = true
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
//go:build !with_quic
|
|
||||||
|
|
||||||
package constant
|
|
||||||
|
|
||||||
const WithQUIC = false
|
|
||||||
@@ -9,11 +9,3 @@ const (
|
|||||||
LogicalTypeAnd = "and"
|
LogicalTypeAnd = "and"
|
||||||
LogicalTypeOr = "or"
|
LogicalTypeOr = "or"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
RuleSetTypeLocal = "local"
|
|
||||||
RuleSetTypeRemote = "remote"
|
|
||||||
RuleSetVersion1 = 1
|
|
||||||
RuleSetFormatSource = "source"
|
|
||||||
RuleSetFormatBinary = "binary"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
package constant
|
|
||||||
|
|
||||||
const MbpsToBps = 125000
|
|
||||||
@@ -9,9 +9,5 @@ const (
|
|||||||
QUICTimeout = 30 * time.Second
|
QUICTimeout = 30 * time.Second
|
||||||
STUNTimeout = 15 * time.Second
|
STUNTimeout = 15 * time.Second
|
||||||
UDPTimeout = 5 * time.Minute
|
UDPTimeout = 5 * time.Minute
|
||||||
DefaultURLTestInterval = 3 * time.Minute
|
DefaultURLTestInterval = 1 * time.Minute
|
||||||
DefaultURLTestIdleTimeout = 30 * time.Minute
|
|
||||||
DefaultStartTimeout = 10 * time.Second
|
|
||||||
DefaultStopTimeout = 5 * time.Second
|
|
||||||
DefaultStopFatalTimeout = 10 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,5 +5,4 @@ const (
|
|||||||
V2RayTransportTypeWebsocket = "ws"
|
V2RayTransportTypeWebsocket = "ws"
|
||||||
V2RayTransportTypeQUIC = "quic"
|
V2RayTransportTypeQUIC = "quic"
|
||||||
V2RayTransportTypeGRPC = "grpc"
|
V2RayTransportTypeGRPC = "grpc"
|
||||||
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ import (
|
|||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -48,20 +47,12 @@ func applyDebugListenOption(options option.DebugOptions) {
|
|||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
encoder.Encode(memObject)
|
encoder.Encode(memObject)
|
||||||
})
|
})
|
||||||
r.Route("/pprof", func(r chi.Router) {
|
r.HandleFunc("/pprof", pprof.Index)
|
||||||
r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
|
r.HandleFunc("/pprof/*", pprof.Index)
|
||||||
if !strings.HasSuffix(request.URL.Path, "/") {
|
r.HandleFunc("/pprof/cmdline", pprof.Cmdline)
|
||||||
http.Redirect(writer, request, request.URL.Path+"/", http.StatusMovedPermanently)
|
r.HandleFunc("/pprof/profile", pprof.Profile)
|
||||||
} else {
|
r.HandleFunc("/pprof/symbol", pprof.Symbol)
|
||||||
pprof.Index(writer, request)
|
r.HandleFunc("/pprof/trace", pprof.Trace)
|
||||||
}
|
|
||||||
})
|
|
||||||
r.HandleFunc("/*", pprof.Index)
|
|
||||||
r.HandleFunc("/cmdline", pprof.Cmdline)
|
|
||||||
r.HandleFunc("/profile", pprof.Profile)
|
|
||||||
r.HandleFunc("/symbol", pprof.Symbol)
|
|
||||||
r.HandleFunc("/trace", pprof.Trace)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
debugHTTPServer = &http.Server{
|
debugHTTPServer = &http.Server{
|
||||||
Addr: options.Listen,
|
Addr: options.Listen,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build linux || darwin
|
|
||||||
|
|
||||||
package box
|
package box
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !(linux || darwin)
|
//go:build !linux
|
||||||
|
|
||||||
package box
|
package box
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
|||||||
# :material-decagram: Features
|
|
||||||
|
|
||||||
#### UI options
|
|
||||||
|
|
||||||
* Display realtime network speed in the notification
|
|
||||||
|
|
||||||
#### Service
|
|
||||||
|
|
||||||
SFA allows you to run sing-box through ForegroundService or VpnService (when TUN is required).
|
|
||||||
|
|
||||||
#### TUN
|
|
||||||
|
|
||||||
SFA provides an unprivileged TUN implementation through Android VpnService.
|
|
||||||
|
|
||||||
| TUN inbound option | Available | Note |
|
|
||||||
|-------------------------------|------------------|--------------------|
|
|
||||||
| `interface_name` | :material-close: | Managed by Android |
|
|
||||||
| `inet4_address` | :material-check: | / |
|
|
||||||
| `inet6_address` | :material-check: | / |
|
|
||||||
| `mtu` | :material-check: | / |
|
|
||||||
| `gso` | :material-close: | No permission |
|
|
||||||
| `auto_route` | :material-check: | / |
|
|
||||||
| `strict_route` | :material-close: | Not implemented |
|
|
||||||
| `inet4_route_address` | :material-check: | / |
|
|
||||||
| `inet6_route_address` | :material-check: | / |
|
|
||||||
| `inet4_route_exclude_address` | :material-check: | / |
|
|
||||||
| `inet6_route_exclude_address` | :material-check: | / |
|
|
||||||
| `endpoint_independent_nat` | :material-check: | / |
|
|
||||||
| `stack` | :material-check: | / |
|
|
||||||
| `include_interface` | :material-close: | No permission |
|
|
||||||
| `exclude_interface` | :material-close: | No permission |
|
|
||||||
| `include_uid` | :material-close: | No permission |
|
|
||||||
| `exclude_uid` | :material-close: | No permission |
|
|
||||||
| `include_android_user` | :material-close: | No permission |
|
|
||||||
| `include_package` | :material-check: | / |
|
|
||||||
| `exclude_package` | :material-check: | / |
|
|
||||||
| `platform` | :material-check: | / |
|
|
||||||
|
|
||||||
| Route/DNS rule option | Available | Note |
|
|
||||||
|-----------------------|------------------|-----------------------------------|
|
|
||||||
| `process_name` | :material-close: | No permission |
|
|
||||||
| `process_path` | :material-close: | No permission |
|
|
||||||
| `package_name` | :material-check: | / |
|
|
||||||
| `user` | :material-close: | Use `package_name` instead |
|
|
||||||
| `user_id` | :material-close: | Use `package_name` instead |
|
|
||||||
| `wifi_ssid` | :material-check: | Fine location permission required |
|
|
||||||
| `wifi_bssid` | :material-check: | Fine location permission required |
|
|
||||||
|
|
||||||
### Override
|
|
||||||
|
|
||||||
Overrides profile configuration items with platform-specific values.
|
|
||||||
|
|
||||||
#### Per-app proxy
|
|
||||||
|
|
||||||
SFA allows you to select a list of Android apps that require proxying or bypassing in the graphical interface to
|
|
||||||
override the `include_package` and `exclude_package` configuration items.
|
|
||||||
|
|
||||||
In particular, the selector also provides the “China apps” scanning feature, providing Chinese users with an excellent
|
|
||||||
experience to bypass apps that do not require a proxy. Specifically, by scanning China application or SDK
|
|
||||||
characteristics through dex class path and other means, there will be almost no missed reports.
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
|
|
||||||
* The working directory is located at `/sdcard/Android/data/io.nekohasekai.sfa/files` (External files directory)
|
|
||||||
* Crash logs is located in `$working_directory/stderr.log`
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
icon: material/android
|
|
||||||
---
|
|
||||||
|
|
||||||
# sing-box for Android
|
|
||||||
|
|
||||||
SFA allows users to manage and run local or remote sing-box configuration files, and provides
|
|
||||||
platform-specific function implementation, such as TUN transparent proxy implementation.
|
|
||||||
|
|
||||||
## :material-graph: Requirements
|
|
||||||
|
|
||||||
* Android 5.0+
|
|
||||||
|
|
||||||
## :material-download: Download
|
|
||||||
|
|
||||||
* [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)
|
|
||||||
* [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
|
|
||||||
|
|
||||||
* [GitHub](https://github.com/SagerNet/sing-box-for-android)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# :material-decagram: Features
|
|
||||||
|
|
||||||
#### UI options
|
|
||||||
|
|
||||||
* Always On
|
|
||||||
* Include All Networks (Proxy traffic for LAN and cellular services)
|
|
||||||
* (Apple tvOS) Import profile from iPhone/iPad
|
|
||||||
|
|
||||||
#### Service
|
|
||||||
|
|
||||||
SFI/SFM/SFT allows you to run sing-box through NetworkExtension with Application Extension or System Extension.
|
|
||||||
|
|
||||||
#### TUN
|
|
||||||
|
|
||||||
SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension.
|
|
||||||
|
|
||||||
| TUN inbound option | Available | Note |
|
|
||||||
|-------------------------------|-------------------|-------------------|
|
|
||||||
| `interface_name` | :material-close:️ | Managed by Darwin |
|
|
||||||
| `inet4_address` | :material-check: | / |
|
|
||||||
| `inet6_address` | :material-check: | / |
|
|
||||||
| `mtu` | :material-check: | / |
|
|
||||||
| `gso` | :material-close: | Not implemented |
|
|
||||||
| `auto_route` | :material-check: | / |
|
|
||||||
| `strict_route` | :material-close:️ | Not implemented |
|
|
||||||
| `inet4_route_address` | :material-check: | / |
|
|
||||||
| `inet6_route_address` | :material-check: | / |
|
|
||||||
| `inet4_route_exclude_address` | :material-check: | / |
|
|
||||||
| `inet6_route_exclude_address` | :material-check: | / |
|
|
||||||
| `endpoint_independent_nat` | :material-check: | / |
|
|
||||||
| `stack` | :material-check: | / |
|
|
||||||
| `include_interface` | :material-close:️ | Not implemented |
|
|
||||||
| `exclude_interface` | :material-close:️ | Not implemented |
|
|
||||||
| `include_uid` | :material-close:️ | Not implemented |
|
|
||||||
| `exclude_uid` | :material-close:️ | Not implemented |
|
|
||||||
| `include_android_user` | :material-close:️ | Not implemented |
|
|
||||||
| `include_package` | :material-close:️ | Not implemented |
|
|
||||||
| `exclude_package` | :material-close:️ | Not implemented |
|
|
||||||
| `platform` | :material-check: | / |
|
|
||||||
|
|
||||||
| Route/DNS rule option | Available | Note |
|
|
||||||
|-----------------------|------------------|-----------------------|
|
|
||||||
| `process_name` | :material-close: | No permission |
|
|
||||||
| `process_path` | :material-close: | No permission |
|
|
||||||
| `package_name` | :material-close: | / |
|
|
||||||
| `user` | :material-close: | No permission |
|
|
||||||
| `user_id` | :material-close: | No permission |
|
|
||||||
| `wifi_ssid` | :material-alert: | Only supported on iOS |
|
|
||||||
| `wifi_bssid` | :material-alert: | Only supported on iOS |
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
|
|
||||||
* Crash logs is located in `Settings` -> `View Service Log`
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
icon: material/apple
|
|
||||||
---
|
|
||||||
|
|
||||||
# sing-box for Apple platforms
|
|
||||||
|
|
||||||
SFI/SFM/SFT allows users to manage and run local or remote sing-box configuration files, and provides
|
|
||||||
platform-specific function implementation, such as TUN transparent proxy implementation.
|
|
||||||
|
|
||||||
## :material-graph: Requirements
|
|
||||||
|
|
||||||
* iOS 15.0+ / macOS 13.0+ / Apple tvOS 17.0+
|
|
||||||
* An Apple account outside of mainland China
|
|
||||||
|
|
||||||
## :material-download: Download
|
|
||||||
|
|
||||||
* [App Store](https://apps.apple.com/us/app/sing-box/id6451272673)
|
|
||||||
* [TestFlight (Beta)](https://testflight.apple.com/join/AcqO44FH)
|
|
||||||
|
|
||||||
## :material-file-download: Download (macOS standalone version)
|
|
||||||
|
|
||||||
* [Homebrew Cask](https://formulae.brew.sh/cask/sfm)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install sfm
|
|
||||||
```
|
|
||||||
|
|
||||||
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases)
|
|
||||||
|
|
||||||
## :material-source-repository: Source code
|
|
||||||
|
|
||||||
* [GitHub](https://github.com/SagerNet/sing-box-for-apple)
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
---
|
|
||||||
icon: material/pencil-ruler
|
|
||||||
---
|
|
||||||
|
|
||||||
# General
|
|
||||||
|
|
||||||
Describes and explains the functions implemented uniformly by sing-box graphical clients.
|
|
||||||
|
|
||||||
### Profile
|
|
||||||
|
|
||||||
Profile describes a sing-box configuration file and its state.
|
|
||||||
|
|
||||||
#### Local
|
|
||||||
|
|
||||||
* Local Profile represents a local sing-box configuration with minimal state
|
|
||||||
* The graphical client must provide an editor to modify configuration content
|
|
||||||
|
|
||||||
#### iCloud (on iOS and macOS)
|
|
||||||
|
|
||||||
* iCloud Profile represents a remote sing-box configuration with iCloud as the update source
|
|
||||||
* The configuration file is stored in the sing-box folder under iCloud
|
|
||||||
* The graphical client must provide an editor to modify configuration content
|
|
||||||
|
|
||||||
#### Remote
|
|
||||||
|
|
||||||
* Remote Profile represents a remote sing-box configuration with a URL as the update source.
|
|
||||||
* The graphical client should provide a configuration content viewer
|
|
||||||
* The graphical client must implement automatic profile update (default interval is 60 minutes) and HTTP Basic
|
|
||||||
authorization.
|
|
||||||
|
|
||||||
At the same time, the graphical client must provide support for importing remote profiles
|
|
||||||
through a specific URL Scheme. The URL is defined as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
sing-box://import-remote-profile?url=urlEncodedURL#urlEncodedName
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dashboard
|
|
||||||
|
|
||||||
While the sing-box service is running, the graphical client should provide a Dashboard interface to manage the service.
|
|
||||||
|
|
||||||
#### Status
|
|
||||||
|
|
||||||
Dashboard should display status information such as memory, connection, and traffic.
|
|
||||||
|
|
||||||
#### Mode
|
|
||||||
|
|
||||||
Dashboard should provide a Mode selector for switching when the configuration uses at least two `clash_mode` values.
|
|
||||||
|
|
||||||
#### Groups
|
|
||||||
|
|
||||||
When the configuration includes group outbounds (specifically, Selector or URLTest),
|
|
||||||
the dashboard should provide a Group selector for status display or switching.
|
|
||||||
|
|
||||||
### Chore
|
|
||||||
|
|
||||||
#### Core
|
|
||||||
|
|
||||||
Graphical clients should provide a Core region:
|
|
||||||
|
|
||||||
* Display the current sing-box version
|
|
||||||
* Provides a button to clean the working directory
|
|
||||||
* Provides a memory limiter switch
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# :material-cellphone-link: Graphical Clients
|
|
||||||
|
|
||||||
Maintained by Project S to provide a unified experience and platform-specific functionality.
|
|
||||||
|
|
||||||
| Platform | Client |
|
|
||||||
|---------------------------------------|------------------------------------------|
|
|
||||||
| :material-android: Android | [sing-box for Android](./android/) |
|
|
||||||
| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple/) |
|
|
||||||
| :material-laptop: Desktop | Working in progress |
|
|
||||||
|
|
||||||
Some third-party projects that claim to use sing-box or use sing-box as a selling point are not listed here. The core
|
|
||||||
motivation of the maintainers of such projects is to acquire more users, and even though they provide friendly VPN
|
|
||||||
client features, the code is usually of poor quality and contains ads.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user