diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
deleted file mode 100644
index 70af790132..0000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ /dev/null
@@ -1,82 +0,0 @@
-name: Bug report
-description: "Report sing-box bug"
-body:
- - type: dropdown
- attributes:
- label: Operating system
- description: Operating system type
- options:
- - iOS
- - macOS
- - Apple tvOS
- - Android
- - Windows
- - Linux
- - Others
- validations:
- required: true
- - type: input
- attributes:
- label: System version
- description: Please provide the operating system version
- validations:
- required: true
- - type: dropdown
- attributes:
- label: Installation type
- description: Please provide the sing-box installation type
- options:
- - Original sing-box Command Line
- - sing-box for iOS Graphical Client
- - sing-box for macOS Graphical Client
- - sing-box for Apple tvOS Graphical Client
- - sing-box for Android Graphical Client
- - Third-party graphical clients that advertise themselves as using sing-box (Windows)
- - Third-party graphical clients that advertise themselves as using sing-box (Android)
- - Others
- validations:
- required: true
- - type: input
- attributes:
- description: Graphical client version
- label: If you are using a graphical client, please provide the version of the client.
- - type: textarea
- attributes:
- label: Version
- description: If you are using the original command line program, please provide the output of the `sing-box version` command.
- render: shell
- - type: textarea
- attributes:
- label: Description
- description: Please provide a detailed description of the error.
- validations:
- required: true
- - type: textarea
- attributes:
- label: Reproduction
- description: Please provide the steps to reproduce the error, including the configuration files and procedures that can locally (not dependent on the remote server) reproduce the error using the original command line program of sing-box.
- validations:
- required: true
- - type: textarea
- attributes:
- label: Logs
- description: |-
- In addition, if you encounter a crash with the graphical client, please also provide 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.
- render: shell
- - type: checkboxes
- attributes:
- label: Integrity requirements
- description: |-
- Please check all of the following options to prove that you have read and understood the requirements, otherwise this issue will be closed.
- 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
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report_zh.yml b/.github/ISSUE_TEMPLATE/bug_report_zh.yml
deleted file mode 100644
index 98af4adf24..0000000000
--- a/.github/ISSUE_TEMPLATE/bug_report_zh.yml
+++ /dev/null
@@ -1,82 +0,0 @@
-name: 错误反馈
-description: "提交 sing-box 漏洞"
-body:
- - type: dropdown
- attributes:
- label: 操作系统
- description: 请提供操作系统类型
- options:
- - iOS
- - macOS
- - Apple tvOS
- - Android
- - Windows
- - Linux
- - 其他
- validations:
- required: true
- - type: input
- attributes:
- label: 系统版本
- description: 请提供操作系统版本
- validations:
- required: true
- - type: dropdown
- attributes:
- label: 安装类型
- description: 请提供该 sing-box 安装类型
- options:
- - sing-box 原始命令行程序
- - sing-box for iOS 图形客户端程序
- - sing-box for macOS 图形客户端程序
- - sing-box for Apple tvOS 图形客户端程序
- - sing-box for Android 图形客户端程序
- - 宣传使用 sing-box 的第三方图形客户端程序 (Windows)
- - 宣传使用 sing-box 的第三方图形客户端程序 (Android)
- - 其他
- validations:
- required: true
- - type: input
- attributes:
- description: 图形客户端版本
- label: 如果您使用图形客户端程序,请提供该程序版本。
- - type: textarea
- attributes:
- label: 版本
- description: 如果您使用原始命令行程序,请提供 `sing-box version` 命令的输出。
- render: shell
- - type: textarea
- attributes:
- label: 描述
- description: 请提供错误的详细描述。
- validations:
- required: true
- - type: textarea
- attributes:
- label: 重现方式
- description: 请提供重现错误的步骤,必须包括可以在本地(不依赖与远程服务器)使用 sing-box 原始命令行程序重现错误的配置文件与流程。
- validations:
- required: true
- - type: textarea
- attributes:
- label: 日志
- description: |-
- 此外,如果您遭遇图形界面应用程序崩溃,请附加提供崩溃日志。
- 对于 Apple 平台图形客户端程序,请检查 `Settings - View Service Log` 以导出崩溃日志。
- 对于 Android 图形客户端程序,请检查 `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` 文件以导出崩溃日志。
- render: shell
- - type: checkboxes
- attributes:
- label: 完整性要求
- description: |-
- 请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
- sing-box 不是讨好无法作出任何意义上的贡献的最终用户并获取非道德影响力的项目,如果您在此处欺骗以故意浪费开发者的时间,您将被永久封锁。
- options:
- - label: 我保证阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
- required: true
- - label: 我保证提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。
- required: true
- - label: 我保证提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器、TUN、图形界面客户端或者其他闭源软件。
- required: true
- - label: 我保证提供了完整的配置文件与日志,而不是出于对自身智力的自信而仅提供了部分认为有用的部分。
- required: true
diff --git a/.github/renovate.json b/.github/renovate.json
deleted file mode 100644
index 78d9c96144..0000000000
--- a/.github/renovate.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "$schema": "https://docs.renovatebot.com/renovate-schema.json",
- "commitMessagePrefix": "[dependencies]",
- "extends": [
- "config:base",
- ":disableRateLimiting"
- ],
- "baseBranches": [
- "dev-next"
- ],
- "golang": {
- "enabled": false
- },
- "packageRules": [
- {
- "matchManagers": [
- "github-actions"
- ],
- "groupName": "github-actions"
- },
- {
- "matchManagers": [
- "dockerfile"
- ],
- "groupName": "Dockerfile"
- }
- ]
-}
\ No newline at end of file
diff --git a/.github/update_dependencies.sh b/.github/update_dependencies.sh
deleted file mode 100755
index 4702ddfe01..0000000000
--- a/.github/update_dependencies.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-PROJECTS=$(dirname "$0")/../..
-go get -x github.com/sagernet/$1@$(git -C $PROJECTS/$1 rev-parse HEAD)
-go mod tidy
diff --git a/.github/workflows/go.yml b/.github/workflows/core.yml
similarity index 96%
rename from .github/workflows/go.yml
rename to .github/workflows/core.yml
index b3cb274a7e..78338330a2 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/core.yml
@@ -11,7 +11,7 @@ jobs:
build:
runs-on: ubuntu-latest
env:
- TAGS: with_quic,with_wireguard,with_gvisor,with_utls,with_ech,with_clash_api,with_outbound_provider
+ TAGS: with_quic,with_wireguard,with_gvisor,with_utls,with_ech,with_clash_api,with_provider
steps:
- uses: actions/checkout@v4
@@ -49,4 +49,4 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: sing-box_core
- path: sing-box*
+ path: sing-box*
\ No newline at end of file
diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yml
deleted file mode 100644
index 93b9cb08a3..0000000000
--- a/.github/workflows/debug.yml
+++ /dev/null
@@ -1,222 +0,0 @@
-name: Debug build
-
-on:
- push:
- branches:
- - stable-next
- - main-next
- - dev-next
- paths-ignore:
- - '**.md'
- - '.github/**'
- - '!.github/workflows/debug.yml'
- pull_request:
- branches:
- - stable-next
- - main-next
- - dev-next
-
-jobs:
- build:
- name: Debug build
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- with:
- 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
- uses: actions/setup-go@v5
- with:
- 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
- - name: Run Test
- run: |
- go test -v ./...
- build_go118:
- name: Debug build (Go 1.18)
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- with:
- fetch-depth: 0
- - name: Setup Go
- uses: actions/setup-go@v5
- with:
- go-version: 1.18.10
- - name: Cache go module
- uses: actions/cache@v3
- with:
- path: |
- ~/go/pkg/mod
- key: go118-${{ hashFiles('**/go.sum') }}
- - name: Run Test
- run: make ci_build_go118
- build_go120:
- name: Debug build (Go 1.20)
- 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.20.7
- - name: Cache go module
- uses: actions/cache@v3
- with:
- path: |
- ~/go/pkg/mod
- key: go118-${{ hashFiles('**/go.sum') }}
- - name: Run Test
- run: make ci_build
- cross:
- strategy:
- matrix:
- include:
- # windows
- - name: windows-amd64
- goos: windows
- goarch: amd64
- goamd64: v1
- - name: windows-amd64-v3
- goos: windows
- goarch: amd64
- goamd64: v3
- - name: windows-386
- goos: windows
- goarch: 386
- - name: windows-arm64
- goos: windows
- goarch: arm64
- - name: windows-arm32v7
- goos: windows
- goarch: arm
- goarm: 7
-
- # linux
- - name: linux-amd64
- goos: linux
- goarch: amd64
- goamd64: v1
- - name: linux-amd64-v3
- goos: linux
- goarch: amd64
- goamd64: v3
- - name: linux-386
- goos: linux
- goarch: 386
- - name: linux-arm64
- goos: linux
- goarch: arm64
- - name: linux-armv5
- goos: linux
- goarch: arm
- goarm: 5
- - name: linux-armv6
- goos: linux
- goarch: arm
- goarm: 6
- - name: linux-armv7
- goos: linux
- goarch: arm
- goarm: 7
- - name: linux-mips-softfloat
- goos: linux
- goarch: mips
- gomips: softfloat
- - name: linux-mips-hardfloat
- goos: linux
- goarch: mips
- gomips: hardfloat
- - name: linux-mipsel-softfloat
- goos: linux
- goarch: mipsle
- gomips: softfloat
- - name: linux-mipsel-hardfloat
- goos: linux
- goarch: mipsle
- gomips: hardfloat
- - name: linux-mips64
- goos: linux
- goarch: mips64
- - name: linux-mips64el
- goos: linux
- goarch: mips64le
- - name: linux-s390x
- goos: linux
- goarch: s390x
- # darwin
- - name: darwin-amd64
- goos: darwin
- goarch: amd64
- goamd64: v1
- - name: darwin-amd64-v3
- goos: darwin
- goarch: amd64
- goamd64: v3
- - name: darwin-arm64
- goos: darwin
- goarch: arm64
- # freebsd
- - name: freebsd-amd64
- goos: freebsd
- goarch: amd64
- goamd64: v1
- - name: freebsd-amd64-v3
- goos: freebsd
- goarch: amd64
- goamd64: v3
- - name: freebsd-386
- goos: freebsd
- goarch: 386
- - name: freebsd-arm64
- goos: freebsd
- goarch: arm64
-
- fail-fast: false
- runs-on: ubuntu-latest
- env:
- GOOS: ${{ matrix.goos }}
- GOARCH: ${{ matrix.goarch }}
- GOAMD64: ${{ matrix.goamd64 }}
- GOARM: ${{ matrix.goarm }}
- GOMIPS: ${{ matrix.gomips }}
- CGO_ENABLED: 0
- TAGS: with_clash_api,with_quic
- steps:
- - name: Checkout
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- with:
- 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
- uses: actions/setup-go@v5
- with:
- go-version: ${{ steps.version.outputs.go_version }}
- - name: Build
- id: build
- run: make
- - name: Upload artifact
- uses: actions/upload-artifact@v4
- with:
- name: sing-box-${{ matrix.name }}
- path: sing-box*
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
deleted file mode 100644
index a106e593a5..0000000000
--- a/.github/workflows/docker.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-name: Build Docker Images
-on:
- workflow_dispatch:
- inputs:
- tag:
- description: "The tag version you want to build"
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- - name: Setup Docker Buildx
- uses: docker/setup-buildx-action@v3
- - name: Setup QEMU for Docker Buildx
- uses: docker/setup-qemu-action@v3
- - name: Login to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.repository_owner }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Docker metadata
- id: metadata
- uses: docker/metadata-action@v5
- with:
- 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
- uses: docker/build-push-action@v5
- with:
- platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
- target: dist
- build-args: |
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
- tags: |
- ${{ steps.tag.outputs.latest }}
- ${{ steps.tag.outputs.versioned }}
- push: true
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
deleted file mode 100644
index 56d21b721d..0000000000
--- a/.github/workflows/lint.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-name: Lint
-
-on:
- push:
- branches:
- - stable-next
- - main-next
- - dev-next
- paths-ignore:
- - '**.md'
- - '.github/**'
- - '!.github/workflows/lint.yml'
- pull_request:
- branches:
- - stable-next
- - main-next
- - dev-next
-
-jobs:
- build:
- name: Build
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
- with:
- 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
- uses: actions/setup-go@v5
- with:
- go-version: ${{ steps.version.outputs.go_version }}
- - name: golangci-lint
- uses: golangci/golangci-lint-action@v3
- with:
- version: latest
- args: --timeout=30m
- install-mode: binary
\ No newline at end of file
diff --git a/.github/workflows/sfa.yml b/.github/workflows/sfa.yml
new file mode 100644
index 0000000000..1d0585cef1
--- /dev/null
+++ b/.github/workflows/sfa.yml
@@ -0,0 +1,69 @@
+name: Build SFA
+
+on:
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ env:
+ TAGS: with_quic,with_wireguard,with_gvisor,with_utls,with_ech,with_clash_api,with_provider
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Get latest go version
+ id: go_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
+ uses: actions/setup-go@v4.1.0
+ with:
+ go-version: 1.21.5
+
+ - name: Checkout SFA Repository
+ uses: actions/checkout@v3
+ with:
+ repository: SagerNet/sing-box-for-android
+ path: SFA
+ submodules: recursive
+
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'oracle'
+ java-version: 20
+
+ - name: Setup NDK
+ uses: nttld/setup-ndk@v1
+ id: setup-ndk
+ with:
+ ndk-version: r26b
+ add-to-path: false
+ local-cache: false
+
+ - name: Build SFA
+ env:
+ ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
+ run: |
+ mkdir -p SFA/app/libs/
+ make lib_install
+ version=$(CGO_ENABLED=0 go run ./cmd/internal/read_tag)
+ CC=${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android34-clang
+ CGO_ENABLED=1 CC=${CC} gomobile bind -v -androidapi 21 -javapkg=io.nekohasekai -libname=box -tags ${TAGS} -ldflags "-X github.com/sagernet/sing-box/constant.Version=${version} -buildid=" ./experimental/libbox
+ cp ./libbox.aar SFA/app/libs/
+ cd SFA
+ echo "VERSION_NAME=${version}" > local.properties
+ echo "VERSION_CODE=$(date +%Y%m%d%H)" >> local.properties
+ sed -i '/signingConfigs\.release/d' app/build.gradle
+ chmod +x ./gradlew
+ ./gradlew assembleRelease
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: SFA
+ path: SFA/app/build/outputs/apk/
\ No newline at end of file
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
deleted file mode 100644
index b6307da29f..0000000000
--- a/.github/workflows/stale.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-name: Mark stale issues and pull requests
-
-on:
- schedule:
- - cron: "30 1 * * *"
-
-jobs:
- stale:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/stale@v9
- 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'
- days-before-stale: 60
- days-before-close: 5
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 55bdab3a0c..ca27a5835d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/.idea/
+/.vscode/
/vendor/
/*.json
/*.srs
@@ -14,3 +15,4 @@
/*.xcframework/
.DS_Store
/config.d/
+/temp/
diff --git a/Makefile b/Makefile
index d2aa65d988..adc68d43f9 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api
TAGS_GO120 = with_quic,with_ech,with_utls
-TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
+TAGS ?= $(TAGS_GO118),$(TAGS_GO120),with_proxyprovider,with_clash_dashboard
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
GOHOSTOS = $(shell go env GOHOSTOS)
@@ -196,4 +196,18 @@ clean:
update:
git fetch
git reset FETCH_HEAD --hard
- git clean -fdx
\ No newline at end of file
+ git clean -fdx
+
+init_yacd:
+ rm -rf experimental/clashapi/clash_dashboard
+ mkdir -p experimental/clashapi/clash_dashboard
+ git clone https://github.com/Metacubex/Yacd-Meta -b gh-pages experimental/clashapi/clash_dashboard
+
+init_metacubexd:
+ rm -rf experimental/clashapi/clash_dashboard
+ mkdir -p experimental/clashapi/clash_dashboard
+ git clone https://github.com/Metacubex/metacubexd -b gh-pages experimental/clashapi/clash_dashboard
+
+clean_clash_dashboard:
+ rm -rf experimental/clashapi/clash_dashboard
+ mkdir -p experimental/clashapi/clash_dashboard
diff --git a/README.md b/README.md
index 0dc4f4525a..e4e767708e 100644
--- a/README.md
+++ b/README.md
@@ -32,4 +32,339 @@ along with this program. If not, see .
In addition, no derivative work may use the name or imply association
with this application without prior consent.
-```
\ No newline at end of file
+```
+
+### ProxyProvider 支持
+
+- 编译时需要使用 `with_proxyprovider` tag
+
+##### 配置详解
+```json5
+{
+ "proxyproviders": [
+ {
+ "tag": "proxy-provider-x", // 标签,必填,用于区别不同的 proxy-provider,不可重复,设置后outbounds会暴露一个同名的selector出站
+ "url": "", // 订阅链接,必填,支持Clash订阅链接,支持普通分享链接,支持Sing-box订阅链接
+ "cache_file": "/tmp/proxy-provider-x.cache", // 缓存文件,选填,强烈建议填写,可以加快启动速度
+ "update_interval": "4h", // 更新间隔,选填,仅填写 cache_file 有效,若当前缓存文件已经超过该时间,将会进行后台自动更新
+ "request_timeout": "10s", // 请求超时时间
+ "use_h3": false, // 使用 HTTP/3 请求订阅
+ "dns": "tls://223.5.5.5", // 使用自定义 DNS 请求订阅域名
+ "tag_format": "proxy-provider - %s", // 如果有多个订阅并且订阅间存在重名节点,可以尝试使用,其中 %s 为占位符,会被替换为原节点名。比如:原节点名:"HongKong 01",tag_format设置为 "PP - %s",替换后新节点名会更变为 "PP - HongKong 01",以解决节点名冲突的问题
+ "global_filter": {
+ "white_mode": true, // 白名单模式,匹配的节点会被保留,不匹配的节点会被删除
+ "rules": [], // 规则,详情见下文
+ },
+ // 规则
+ // 1. Golang 正则表达式 (example: Node) ==> 匹配 Tag (匹配 Node)
+ // 2. tag:Golang 正则表达式 (example: tag:Node) ==> 匹配 Tag (匹配 Node)
+ // 3. type:Golang 正则表达式 (example: type:vmess) ==> 匹配 Type (节点类型) (匹配 vmess)
+ // 4. server:Golang 正则表达式 (example: server:1.1.1.1) ==> 匹配 Server (节点服务器地址,不含端口) (匹配 1.1.1.1)
+ // 5. 若设置 tag_format 则匹配的是替换前的节点名
+ //
+ "lookup_ip": false, // 是否查询 IP 地址,覆盖节点地址,需要设置 dns 字段
+ "download_ua": "clash.meta", // 更新订阅时使用的 User-Agent
+ "dialer": {}, // 附加在节点 outbound 配置的 Dial 字段
+ "request_dialer": {}, // 请求时使用的 Dial 字段配置,detour 字段无效
+ "running_detour": "", // 运行时后台自动更新所使用的 outbound
+ "groups": [ // 自定义分组
+ {
+ "tag": "", // outbound tag,必填
+ "type": "selector", // outbound 类型,必填,仅支持selector, urltest
+ "filter": {}, // 节点过滤规则,选填,详见上global_filter字段
+ ... Selector 或 URLTest 其他字段配置
+ }
+ ]
+ }
+ ]
+}
+```
+
+##### DNS 支持格式
+```
+tcp://1.1.1.1
+tcp://1.1.1.1:53
+tcp://[2606:4700:4700::1111]
+tcp://[2606:4700:4700::1111]:53
+udp://1.1.1.1
+udp://1.1.1.1:53
+udp://[2606:4700:4700::1111]
+udp://[2606:4700:4700::1111]:53
+tls://1.1.1.1
+tls://1.1.1.1:853
+tls://[2606:4700:4700::1111]
+tls://[2606:4700:4700::1111]:853
+tls://1.1.1.1/?sni=cloudflare-dns.com
+tls://1.1.1.1:853/?sni=cloudflare-dns.com
+tls://[2606:4700:4700::1111]/?sni=cloudflare-dns.com
+tls://[2606:4700:4700::1111]:853/?sni=cloudflare-dns.com
+https://1.1.1.1
+https://1.1.1.1:443/dns-query
+https://[2606:4700:4700::1111]
+https://[2606:4700:4700::1111]:443
+https://1.1.1.1/dns-query?sni=cloudflare-dns.com
+https://1.1.1.1:443/dns-query?sni=cloudflare-dns.com
+https://[2606:4700:4700::1111]/dns-query?sni=cloudflare-dns.com
+https://[2606:4700:4700::1111]:443/dns-query?sni=cloudflare-dns.com
+1.1.1.1 => udp://1.1.1.1:53
+1.1.1.1:53 => udp://1.1.1.1:53
+[2606:4700:4700::1111] => udp://[2606:4700:4700::1111]:53
+[2606:4700:4700::1111]:53 => udp://[2606:4700:4700::1111]:53
+```
+
+##### 简易配置示例
+```json5
+{
+ "proxyproviders": [
+ {
+ "tag": "proxy-provider",
+ "url": "你的订阅链接",
+ "cache_file": "缓存文件路径",
+ "dns": "tcp://223.5.5.5",
+ "update_interval": "4h", // 自动更新缓存
+ "request_timeout": "10s" // 请求超时时间
+ }
+ ]
+}
+```
+
+
+### RuleProvider 支持
+
+- 编译时需要使用 `with_ruleprovider` tag
+
+##### 配置详解
+```json5
+{
+ "ruleproviders": [
+ {
+ "tag": "rule-provider-x", // 标签,必填,用于区别不同的 rule-provider,不可重复
+ "url": "", // 规则订阅链接,必填,仅支持Clash订阅规则
+ "behavior": "", // 规则类型,必填,可选 domain / ipcidr / classical
+ "format": "", // 规则格式,选填,可选 yaml / text,默认 yaml
+ "use_h3": false, // 使用 HTTP/3 请求规则订阅
+ "cache_file": "/tmp/rule-provider-x.cache", // 缓存文件,选填,强烈建议填写,可以加快启动速度
+ "update_interval": "4h", // 更新间隔,选填,仅填写 cache_file 有效,若当前缓存文件已经超过该时间,将会进行后台自动更新
+ "request_timeout": "10s", // 请求超时时间
+ "dns": "tls://223.5.5.5", // 使用自定义 DNS 请求订阅域名,格式与 proxyprovider 相同
+ "request_dialer": {}, // 请求时使用的 Dial 字段配置,detour 字段无效
+ "running_detour": "" // 运行时后台自动更新所使用的 outbound
+ }
+ ]
+}
+```
+
+##### 用法
+
+用于 Route Rule 或者 DNS Rule
+
+假设规则有以下内容:
+```yaml
+payload:
+ - '+.google.com'
+ - '+.github.com'
+```
+
+```json5
+{
+ "dns": {
+ "rules": [
+ {
+ "@rule_provider": "rule-provider-x",
+ "server": "proxy-dns"
+ }
+ ]
+ },
+ "route": {
+ "rules": [
+ {
+ "@rule_provider": "rule-provider-x",
+ "outbound": "proxy-out"
+ }
+ ]
+ }
+}
+```
+等效于
+```json5
+{
+ "dns": {
+ "rules": [
+ {
+ "domain_suffix": [
+ ".google.com",
+ ".github.com"
+ ],
+ "server": "proxy-dns"
+ }
+ ]
+ },
+ "route": {
+ "rules": [
+ {
+ "domain_suffix": [
+ ".google.com",
+ ".github.com"
+ ],
+ "outbound": "proxy-out"
+ }
+ ]
+ }
+}
+```
+
+##### 注意
+
+- 由于 sing-box 规则支持与 Clash 可能不同,某些无法在 sing-box 上使用的规则会被**自动忽略**,请注意
+- 不支持 **logical** 规则,由于规则数目可能非常庞大,设置多个 @rule_provider 靶点可能会导致内存飙升和性能问题(笛卡儿积)
+- DNS Rule 不支持某些类型,如:GeoIP IP-CIDR IP-CIDR6,这是因为 sing-box 程序逻辑所决定的
+- 目前支持的 Clash 规则类型:
+
+```
+Clash 类型 ==> 对于的 sing-box 配置
+
+DOMAIN ==> domain
+DOMAIN-SUFFIX ==> domain_suffix
+DOMAIN-KEYWORD ==> domain_keyword
+GEOSITE ==> geosite
+GEOIP ==> geoip
+IP-CIDR ==> ip_cidr
+IP-CIDR6 ==> ip_cidr
+SRC-IP-CIDR ==> source_ip_cidr
+SRC-PORT ==> source_port
+DST-PORT ==> port
+PROCESS-NAME ==> process_name
+PROCESS-PATH ==> process_path
+NETWORK ==> network
+```
+
+### Tor No Fatal 启动
+
+```json
+{
+ "outbounds": [
+ {
+ "tag": "tor-out",
+ "type": "tor",
+ "no_fatal": true // 启动时将 tor outbound 启动置于后台,加快启动速度,但启动失败会导致无法使用
+ }
+ ]
+}
+```
+
+### Clash Dashboard 内置支持
+
+- 编译时需要使用 `with_clash_dashboard` tag
+- 编译前需要先初始化 web 文件
+
+```
+使用 yacd 作为 Clash Dashboard:make init_yacd
+使用 metacubexd 作为 Clash Dashboard:make init_metacubexd
+清除 web 文件:make clean_clash_dashboard
+```
+
+##### 用法
+
+```json5
+{
+ "experimental": {
+ "clash_api": {
+ "external_controller": "0.0.0.0:9090",
+ //"external_ui": "" // 无需填写
+ "external_ui_buildin": true // 启用内置 Clash Dashboard
+ }
+ }
+}
+```
+
+### Geo Resource 自动更新支持
+
+##### 用法
+```json5
+{
+ "route": {
+ "geosite": {
+ "path": "/temp/geosite.db",
+ "auto_update_interval": "12h" // 更新间隔,在程序运行时会间隔时间自动更新
+ },
+ "geoip": {
+ "path": "/temp/geoip.db",
+ "auto_update_interval": "12h"
+ }
+ }
+}
+```
+
+- 支持在 Clash API 中调用 API 更新 Geo Resource
+
+
+### JSTest 出站支持(*** 实验性 ***)
+
+JSTest 出站允许用户根据 JS 脚本代码选择出站,依附 JS 脚本,用户可以自定义强大的出站选择逻辑,比如:送中节点规避,流媒体节点选择,等等。
+
+你可以在 jstest/javascript/ 目录下找到一些示例脚本。
+
+- 编译时需要使用 `with_jstest` tag
+- JS 脚本请自行测试,慎而又慎,不要随意使用不明脚本,可能会导致安全问题或预期外的问题
+- JS 脚本运行需要依赖 JS 虚拟机,内存占用可能会比较大(10-20M 左右,视脚本而定),建议使用时注意内存占用情况
+
+- 专门告知使用送中节点的脚本的用户:请**确保 Google 定位已经正常关闭**,否则运行该脚本可能会**导致上游节点全部送中**,~~尤其是机场用户~~,运行所造成的一切后果概不负责
+
+##### 用法
+```json5
+{
+ "outbounds": [
+ {
+ "tag": "google-cn-auto-switch",
+ "type": "jstest",
+ "js_path": "/etc/sing-box/google_cn.js", // JS 脚本路径
+ "js_base64": "", // JS 脚本 Base64 编码,若遇到某些存储脚本文件困难的情况,如:使用了移动客户端,可以使用该字段
+ "interval": "60s", // 脚本执行间隔
+ "interrupt_exist_connections": false // 切换时是否中断已有连接
+ }
+ ]
+}
+```
+
+
+### Script 脚本支持
+
+Script 脚本允许用户在程序运行时执行脚本,可以用于自定义一些功能。
+
+- 编译时需要使用 `with_script` tag
+
+##### 用法
+```json5
+{
+ "scripts": [
+ {
+ "tag": "script-x", // 标签,必填,用于区别不同的 script,不可重复
+ "command": "/path/to/script", // 脚本命令,必填,绝对路径
+ "args": [], // 脚本参数,选填
+ "directory": "/path/to/directory", // 脚本工作目录,选填,绝对路径
+ "mode": "pre-start", // 运行模式,必填,可选列表如下
+ "no_fatal": false, // 忽略脚本是否运行失败,若是运行在整个程序生命周期的脚本,则会在启动失败时退出,会在运行异常退出时程序不强制退出
+ "env": { // 环境变量,选填
+ "foo": "bar"
+ },
+ "log": {
+ "enabled": false, // 是否启用日志,选填,默认 false
+ "stdout_log_level": "info", // stdout 日志等级,选填,可选:trace,debug,info,warn,error,fatal,panic,默认 info
+ "stderr_log_level": "error", // stderr 日志等级,选填,可选:trace,debug,info,warn,error,fatal,panic,默认 error
+ }
+ }
+ ]
+}
+```
+
+##### 运行模式
+```
+1. pre-start // 在启动其他服务前运行脚本
+2. pre-start-service-pre-close // 运行的脚本会持续整个程序的生命周期,在启动其他服务前运行,且在关闭其他服务前停止
+3. pre-start-service-post-close // 运行的脚本会持续整个程序的生命周期,在启动其他服务前运行,且在关闭其他服务后停止
+4. post-start // 在启动其他服务后运行脚本
+5. post-start-service-pre-close // 运行的脚本会持续整个程序的生命周期,在启动其他服务后运行,且在关闭其他服务前停止
+6. post-start-service-post-close // 运行的脚本会持续整个程序的生命周期,在启动其他服务后运行,且在关闭其他服务后停止
+7. pre-close // 在关闭其他服务前运行脚本
+8. post-close // 在关闭其他服务后运行脚本
+```
diff --git a/adapter/proxyprovider.go b/adapter/proxyprovider.go
new file mode 100644
index 0000000000..77d46a09c1
--- /dev/null
+++ b/adapter/proxyprovider.go
@@ -0,0 +1,18 @@
+package adapter
+
+import (
+ "time"
+
+ "github.com/sagernet/sing-box/option"
+)
+
+type ProxyProvider interface {
+ Service
+ Tag() string
+ StartGetOutbounds() ([]option.Outbound, error)
+ GetOutboundOptions() ([]option.Outbound, error)
+ GetFullOutboundOptions() ([]option.Outbound, error)
+ GetClashInfo() (uint64, uint64, uint64, time.Time, error) // download, upload, total, expire, error
+ LastUpdateTime() time.Time
+ Update()
+}
diff --git a/adapter/router.go b/adapter/router.go
index 5828ab3530..609c38b81a 100644
--- a/adapter/router.go
+++ b/adapter/router.go
@@ -6,8 +6,8 @@ import (
"net/netip"
"github.com/sagernet/sing-box/common/geoip"
- "github.com/sagernet/sing-dns"
- "github.com/sagernet/sing-tun"
+ dns "github.com/sagernet/sing-dns"
+ tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
@@ -17,19 +17,22 @@ import (
type Router interface {
Service
- PreStarter
PostStarter
Outbounds() []Outbound
Outbound(tag string) (Outbound, bool)
DefaultOutbound(network string) (Outbound, error)
+ ProxyProviders() []ProxyProvider
+ ProxyProvider(tag string) (ProxyProvider, bool)
+
FakeIPStore() FakeIPStore
ConnectionRouter
GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error)
+ UpdateGeoDatabase()
RuleSet(tag string) (RuleSet, bool)
@@ -57,6 +60,8 @@ type Router interface {
SetV2RayServer(server V2RayServer)
ResetNetwork() error
+
+ Reload()
}
func ContextWithRouter(ctx context.Context, router Router) context.Context {
diff --git a/box.go b/box.go
index fe1c1b8dea..b905664be8 100644
--- a/box.go
+++ b/box.go
@@ -18,7 +18,9 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/outbound"
+ "github.com/sagernet/sing-box/proxyprovider"
"github.com/sagernet/sing-box/route"
+ "github.com/sagernet/sing-box/script"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
@@ -29,16 +31,19 @@ import (
var _ adapter.Service = (*Box)(nil)
type Box struct {
- createdAt time.Time
- router adapter.Router
- inbounds []adapter.Inbound
- outbounds []adapter.Outbound
- logFactory log.Factory
- logger log.ContextLogger
- preServices1 map[string]adapter.Service
- preServices2 map[string]adapter.Service
- postServices map[string]adapter.Service
- done chan struct{}
+ createdAt time.Time
+ router adapter.Router
+ inbounds []adapter.Inbound
+ outbounds []adapter.Outbound
+ proxyProviders []adapter.ProxyProvider
+ scripts []*script.Script
+ logFactory log.Factory
+ logger log.ContextLogger
+ preServices1 map[string]adapter.Service
+ preServices2 map[string]adapter.Service
+ postServices map[string]adapter.Service
+ reloadChan chan struct{}
+ done chan struct{}
}
type Options struct {
@@ -55,7 +60,8 @@ func New(options Options) (*Box, error) {
ctx = context.Background()
}
ctx = service.ContextWithDefaultRegistry(ctx)
- ctx = pause.WithDefaultManager(ctx)
+ ctx = pause.ContextWithDefaultManager(ctx)
+ reloadChan := make(chan struct{}, 1)
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
var needCacheFile bool
@@ -85,14 +91,36 @@ func New(options Options) (*Box, error) {
if err != nil {
return nil, E.Cause(err, "create log factory")
}
+ routeOptions := common.PtrValueOrDefault(options.Route)
+ dnsOptions := common.PtrValueOrDefault(options.DNS)
+ var scripts []*script.Script
+ for i, scriptOptions := range options.Scripts {
+ var tag string
+ if scriptOptions.Tag != "" {
+ tag = scriptOptions.Tag
+ } else {
+ tag = F.ToString(i)
+ }
+ s, err := script.NewScript(
+ ctx,
+ logFactory.NewLogger(F.ToString("script", "[", tag, "]")),
+ tag,
+ scriptOptions,
+ )
+ if err != nil {
+ return nil, E.Cause(err, "parse script[", i, "]")
+ }
+ scripts = append(scripts, s)
+ }
router, err := route.NewRouter(
ctx,
logFactory,
- common.PtrValueOrDefault(options.Route),
- common.PtrValueOrDefault(options.DNS),
+ routeOptions,
+ dnsOptions,
common.PtrValueOrDefault(options.NTP),
options.Inbounds,
options.PlatformInterface,
+ reloadChan,
)
if err != nil {
return nil, E.Cause(err, "parse route options")
@@ -138,12 +166,49 @@ func New(options Options) (*Box, error) {
}
outbounds = append(outbounds, out)
}
+ var proxyProviders []adapter.ProxyProvider
+ if len(options.ProxyProviders) > 0 {
+ proxyProviders = make([]adapter.ProxyProvider, 0, len(options.ProxyProviders))
+ for i, proxyProviderOptions := range options.ProxyProviders {
+ var pp adapter.ProxyProvider
+ var tag string
+ if proxyProviderOptions.Tag != "" {
+ tag = proxyProviderOptions.Tag
+ } else {
+ tag = F.ToString(i)
+ proxyProviderOptions.Tag = tag
+ }
+ pp, err = proxyprovider.NewProxyProvider(ctx, router, logFactory.NewLogger(F.ToString("proxyprovider[", tag, "]")), tag, proxyProviderOptions)
+ if err != nil {
+ return nil, E.Cause(err, "parse proxyprovider[", i, "]")
+ }
+ outboundOptions, err := pp.StartGetOutbounds()
+ if err != nil {
+ return nil, E.Cause(err, "get outbounds from proxyprovider[", i, "]")
+ }
+ for i, outboundOptions := range outboundOptions {
+ var out adapter.Outbound
+ tag := outboundOptions.Tag
+ out, err = outbound.New(
+ ctx,
+ router,
+ logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
+ tag,
+ outboundOptions)
+ if err != nil {
+ return nil, E.Cause(err, "parse proxyprovider ["+pp.Tag()+"] outbound[", i, "]")
+ }
+ outbounds = append(outbounds, out)
+ }
+ proxyProviders = append(proxyProviders, pp)
+ }
+ }
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"})
common.Must(oErr)
outbounds = append(outbounds, out)
return out
- })
+ }, proxyProviders)
if err != nil {
return nil, err
}
@@ -157,12 +222,9 @@ func New(options Options) (*Box, error) {
preServices2 := 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)
- }
+ cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
preServices1["cache file"] = cacheFile
+ service.MustRegister[adapter.CacheFile](ctx, cacheFile)
}
if needClashAPI {
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
@@ -183,16 +245,19 @@ func New(options Options) (*Box, error) {
preServices2["v2ray api"] = v2rayServer
}
return &Box{
- router: router,
- inbounds: inbounds,
- outbounds: outbounds,
- createdAt: createdAt,
- logFactory: logFactory,
- logger: logFactory.Logger(),
- preServices1: preServices1,
- preServices2: preServices2,
- postServices: postServices,
- done: make(chan struct{}),
+ router: router,
+ inbounds: inbounds,
+ outbounds: outbounds,
+ proxyProviders: proxyProviders,
+ scripts: scripts,
+ createdAt: createdAt,
+ logFactory: logFactory,
+ logger: logFactory.Logger(),
+ preServices1: preServices1,
+ preServices2: preServices2,
+ postServices: postServices,
+ done: make(chan struct{}),
+ reloadChan: reloadChan,
}, nil
}
@@ -242,6 +307,12 @@ func (s *Box) preStart() error {
if err != nil {
return E.Cause(err, "start logger")
}
+ for _, script := range s.scripts {
+ err := script.PreStart()
+ if err != nil {
+ return E.Cause(err, "pre-start script[", script.Tag(), "]")
+ }
+ }
for serviceName, service := range s.preServices1 {
if preService, isPreService := service.(adapter.PreStarter); isPreService {
monitor.Start("pre-start ", serviceName)
@@ -262,10 +333,6 @@ func (s *Box) preStart() error {
}
}
}
- err = s.router.PreStart()
- if err != nil {
- return E.Cause(err, "pre-start router")
- }
err = s.startOutbounds()
if err != nil {
return err
@@ -290,6 +357,20 @@ func (s *Box) start() error {
return E.Cause(err, "start ", serviceName)
}
}
+ for serviceName, service := range s.preServices2 {
+ s.logger.Trace("starting ", serviceName)
+ err = service.Start()
+ if err != nil {
+ return E.Cause(err, "start ", serviceName)
+ }
+ }
+ for _, proxyProvider := range s.proxyProviders {
+ s.logger.Trace("starting proxyprovider ", proxyProvider.Tag())
+ err = proxyProvider.Start()
+ if err != nil {
+ return E.Cause(err, "start proxyprovider ", proxyProvider.Tag())
+ }
+ }
for i, in := range s.inbounds {
var tag string
if in.Tag() == "" {
@@ -320,8 +401,17 @@ func (s *Box) postStart() error {
}
}
}
-
- return s.router.PostStart()
+ err := s.router.PostStart()
+ if err != nil {
+ return E.Cause(err, "post-start router")
+ }
+ for _, script := range s.scripts {
+ err := script.PostStart()
+ if err != nil {
+ return E.Cause(err, "post-start script[", script.Tag(), "]")
+ }
+ }
+ return nil
}
func (s *Box) Close() error {
@@ -333,6 +423,11 @@ func (s *Box) Close() error {
}
monitor := taskmonitor.New(s.logger, C.DefaultStopTimeout)
var errors error
+ for _, script := range s.scripts {
+ errors = E.Append(errors, script.PreClose(), func(err error) error {
+ return E.Cause(err, "pre-close script[", script.Tag(), "]")
+ })
+ }
for serviceName, service := range s.postServices {
monitor.Start("close ", serviceName)
errors = E.Append(errors, service.Close(), func(err error) error {
@@ -340,6 +435,12 @@ func (s *Box) Close() error {
})
monitor.Finish()
}
+ for _, proxyProvider := range s.proxyProviders {
+ s.logger.Trace("closing proxyprovider ", proxyProvider.Tag())
+ errors = E.Append(errors, proxyProvider.Close(), func(err error) error {
+ return E.Cause(err, "close proxyprovider ", proxyProvider.Tag())
+ })
+ }
for i, in := range s.inbounds {
monitor.Start("close inbound/", in.Type(), "[", i, "]")
errors = E.Append(errors, in.Close(), func(err error) error {
@@ -375,6 +476,12 @@ func (s *Box) Close() error {
})
monitor.Finish()
}
+ for _, script := range s.scripts {
+ errors = E.Append(errors, script.PostClose(), func(err error) error {
+ return E.Cause(err, "post-close script[", script.Tag(), "]")
+ })
+ }
+ s.logger.Trace("closing log factory")
if err := common.Close(s.logFactory); err != nil {
errors = E.Append(errors, err, func(err error) error {
return E.Cause(err, "close logger")
@@ -386,3 +493,7 @@ func (s *Box) Close() error {
func (s *Box) Router() adapter.Router {
return s.router
}
+
+func (s *Box) ReloadChan() <-chan struct{} {
+ return s.reloadChan
+}
diff --git a/cmd/sing-box/cmd_geosite_export.go b/cmd/sing-box/cmd_geosite_export.go
index 2a6c27a0ed..71f1018d8f 100644
--- a/cmd/sing-box/cmd_geosite_export.go
+++ b/cmd/sing-box/cmd_geosite_export.go
@@ -1,6 +1,7 @@
package main
import (
+ "encoding/json"
"io"
"os"
@@ -8,7 +9,6 @@ import (
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"
)
diff --git a/cmd/sing-box/cmd_parselink.go b/cmd/sing-box/cmd_parselink.go
new file mode 100644
index 0000000000..9998f9be4c
--- /dev/null
+++ b/cmd/sing-box/cmd_parselink.go
@@ -0,0 +1,80 @@
+//go:build with_proxyprovider
+
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/sagernet/sing-box/proxyprovider"
+
+ "github.com/spf13/cobra"
+)
+
+var commandParseLink = &cobra.Command{
+ Use: "parselink",
+ Short: "Parse Subscribe Link. Support Clash/Sing-box/Raw",
+ Run: func(cmd *cobra.Command, args []string) {
+ parseLinkDo()
+ },
+}
+
+var parseLink string
+
+func init() {
+ commandParseLink.PersistentFlags().StringVarP(&parseLink, "link", "l", "", "Subscribe Link. Support Clash/Sing-box/Raw")
+ mainCommand.AddCommand(commandParseLink)
+}
+
+func parseLinkDo() {
+ if parseLink == "" {
+ fmt.Println("link is empty")
+ os.Exit(1)
+ return
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ go func() {
+ signalChan := make(chan os.Signal, 1)
+ signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
+ select {
+ case <-signalChan:
+ cancel()
+ case <-ctx.Done():
+ return
+ }
+ }()
+
+ outbounds, err := proxyprovider.ParseLink(ctx, parseLink)
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ return
+ }
+
+ var data any
+ if len(outbounds) == 1 {
+ data = outbounds[0]
+ } else {
+ data = outbounds
+ }
+
+ buffer := bytes.NewBuffer(nil)
+ encoder := json.NewEncoder(buffer)
+ encoder.SetIndent("", " ")
+ err = encoder.Encode(data)
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ return
+ }
+
+ fmt.Println(buffer.String())
+}
diff --git a/cmd/sing-box/cmd_rule_set_compile.go b/cmd/sing-box/cmd_rule_set_compile.go
index 6e065101a8..e34f5d2b67 100644
--- a/cmd/sing-box/cmd_rule_set_compile.go
+++ b/cmd/sing-box/cmd_rule_set_compile.go
@@ -47,14 +47,10 @@ func compileRuleSet(sourcePath string) error {
return err
}
}
- content, err := io.ReadAll(reader)
- if err != nil {
- return err
- }
- plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
- if err != nil {
- return err
- }
+ decoder := json.NewDecoder(json.NewCommentFilter(reader))
+ decoder.DisallowUnknownFields()
+ var plainRuleSet option.PlainRuleSetCompat
+ err = decoder.Decode(&plainRuleSet)
if err != nil {
return err
}
diff --git a/cmd/sing-box/cmd_rule_set_format.go b/cmd/sing-box/cmd_rule_set_format.go
index 6276204c46..a3f98bd26d 100644
--- a/cmd/sing-box/cmd_rule_set_format.go
+++ b/cmd/sing-box/cmd_rule_set_format.go
@@ -50,14 +50,18 @@ func formatRuleSet(sourcePath string) error {
if err != nil {
return err
}
- plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
+ decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content)))
+ decoder.DisallowUnknownFields()
+ var plainRuleSet option.PlainRuleSetCompat
+ err = decoder.Decode(&plainRuleSet)
if err != nil {
return err
}
+ ruleSet := plainRuleSet.Upgrade()
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent("", " ")
- err = encoder.Encode(plainRuleSet)
+ err = encoder.Encode(ruleSet)
if err != nil {
return E.Cause(err, "encode config")
}
diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go
index bde811f78a..a8534bd777 100644
--- a/cmd/sing-box/cmd_run.go
+++ b/cmd/sing-box/cmd_run.go
@@ -133,7 +133,7 @@ func create() (*box.Box, context.CancelFunc, error) {
}
options.Log.DisableColor = true
}
- ctx, cancel := context.WithCancel(globalCtx)
+ ctx, cancel := context.WithCancel(context.Background())
instance, err := box.New(box.Options{
Context: ctx,
Options: options,
@@ -177,20 +177,31 @@ func run() error {
}
runtimeDebug.FreeOSMemory()
for {
- osSignal := <-osSignals
- if osSignal == syscall.SIGHUP {
+ reloadTag := false
+ select {
+ case osSignal := <-osSignals:
+ if osSignal == syscall.SIGHUP {
+ err = check()
+ if err != nil {
+ log.Error(E.Cause(err, "reload service"))
+ continue
+ }
+ reloadTag = true
+ }
+ case <-instance.ReloadChan():
err = check()
if err != nil {
log.Error(E.Cause(err, "reload service"))
continue
}
+ reloadTag = true
}
cancel()
closeCtx, closed := context.WithCancel(context.Background())
go closeMonitor(closeCtx)
instance.Close()
closed()
- if osSignal != syscall.SIGHUP {
+ if !reloadTag {
return nil
}
break
diff --git a/cmd/sing-box/main.go b/cmd/sing-box/main.go
index 66b7daa1d9..1880d1cbff 100644
--- a/cmd/sing-box/main.go
+++ b/cmd/sing-box/main.go
@@ -3,19 +3,15 @@ package main
import (
"context"
"os"
- "os/user"
- "strconv"
"time"
_ "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
- "github.com/sagernet/sing/service/filemanager"
"github.com/spf13/cobra"
)
var (
- globalCtx context.Context
configPaths []string
configDirectories []string
workingDir string
@@ -41,30 +37,15 @@ func main() {
}
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 {
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
}
if workingDir != "" {
_, err := os.Stat(workingDir)
if err != nil {
- filemanager.MkdirAll(globalCtx, workingDir, 0o777)
+ os.MkdirAll(workingDir, 0o777)
}
- err = os.Chdir(workingDir)
- if err != nil {
+ if err := os.Chdir(workingDir); err != nil {
log.Fatal(err)
}
}
diff --git a/common/badtls/read_wait.go b/common/badtls/read_wait.go
index 1a0998bf32..4657bc5b0f 100644
--- a/common/badtls/read_wait.go
+++ b/common/badtls/read_wait.go
@@ -4,8 +4,6 @@ package badtls
import (
"bytes"
- "context"
- "net"
"os"
"reflect"
"sync"
@@ -20,32 +18,20 @@ import (
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
+ *tls.STDConn
+ halfAccess *sync.Mutex
+ rawInput *bytes.Buffer
+ input *bytes.Reader
+ hand *bytes.Buffer
+ readWaitOptions N.ReadWaitOptions
}
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 {
+ stdConn, isSTDConn := conn.(*tls.STDConn)
+ if !isSTDConn {
return nil, os.ErrInvalid
}
- rawConn := reflect.Indirect(reflect.ValueOf(conn))
+ rawConn := reflect.Indirect(reflect.ValueOf(stdConn))
rawHalfConn := rawConn.FieldByName("in")
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid half conn")
@@ -71,13 +57,11 @@ func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
}
hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
return &ReadWaitConn{
- Conn: conn,
- halfAccess: halfAccess,
- rawInput: rawInput,
- input: input,
- hand: hand,
- tlsReadRecord: tlsReadRecord,
- tlsHandlePostHandshakeMessage: tlsHandlePostHandshakeMessage,
+ STDConn: stdConn,
+ halfAccess: halfAccess,
+ rawInput: rawInput,
+ input: input,
+ hand: hand,
}, nil
}
@@ -87,19 +71,19 @@ func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy
}
func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
- err = c.HandshakeContext(context.Background())
+ err = c.Handshake()
if err != nil {
return
}
c.halfAccess.Lock()
defer c.halfAccess.Unlock()
for c.input.Len() == 0 {
- err = c.tlsReadRecord()
+ err = tlsReadRecord(c.STDConn)
if err != nil {
return
}
for c.hand.Len() > 0 {
- err = c.tlsHandlePostHandshakeMessage()
+ err = tlsHandlePostHandshakeMessage(c.STDConn)
if err != nil {
return
}
@@ -116,7 +100,7 @@ func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 &&
// recordType(c.rawInput.Bytes()[0]) == recordTypeAlert {
c.rawInput.Bytes()[0] == 21 {
- _ = c.tlsReadRecord()
+ _ = tlsReadRecord(c.STDConn)
// return n, err // will be io.EOF on closeNotify
}
@@ -124,24 +108,8 @@ func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
return
}
-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 tlsReadRecord crypto/tls.(*Conn).readRecord
+func tlsReadRecord(c *tls.STDConn) error
-//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
-func stdTLSHandlePostHandshakeMessage(c *tls.STDConn) error
+//go:linkname tlsHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
+func tlsHandlePostHandshakeMessage(c *tls.STDConn) error
diff --git a/common/dialer/default.go b/common/dialer/default.go
index 0234b1b97f..9fbd1d8ef2 100644
--- a/common/dialer/default.go
+++ b/common/dialer/default.go
@@ -15,17 +15,14 @@ import (
N "github.com/sagernet/sing/common/network"
)
-var _ WireGuardListener = (*DefaultDialer)(nil)
-
type DefaultDialer struct {
- dialer4 tcpDialer
- dialer6 tcpDialer
- udpDialer4 net.Dialer
- udpDialer6 net.Dialer
- udpListener net.ListenConfig
- udpAddr4 string
- udpAddr6 string
- isWireGuardListener bool
+ dialer4 tcpDialer
+ dialer6 tcpDialer
+ udpDialer4 net.Dialer
+ udpDialer6 net.Dialer
+ udpListener net.ListenConfig
+ udpAddr4 string
+ udpAddr6 string
}
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)
}
- if options.IsWireGuardListener {
- for _, controlFn := range wgControlFns {
- listener.Control = control.Append(listener.Control, controlFn)
- }
- }
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
if err != nil {
return nil, err
@@ -122,7 +114,6 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
listener,
udpAddr4,
udpAddr6,
- options.IsWireGuardListener,
}, 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) {
if !conntrack.Enabled || err != nil {
return conn, err
diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go
index bbb4b3a92e..b059e38e34 100644
--- a/common/dialer/dialer.go
+++ b/common/dialer/dialer.go
@@ -6,13 +6,15 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-dns"
+ "github.com/sagernet/sing/common"
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) {
- if options.IsWireGuardListener {
- return NewDefault(router, options)
- }
var (
dialer N.Dialer
err error
diff --git a/common/dialer/simple.go b/common/dialer/simple.go
new file mode 100644
index 0000000000..ae289fd8c1
--- /dev/null
+++ b/common/dialer/simple.go
@@ -0,0 +1,93 @@
+package dialer
+
+import (
+ "net"
+ "time"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+ "github.com/sagernet/sing/common/control"
+ E "github.com/sagernet/sing/common/exceptions"
+ M "github.com/sagernet/sing/common/metadata"
+)
+
+func NewSimple(options option.DialerOptions) (*DefaultDialer, error) {
+ var dialer net.Dialer
+ var listener net.ListenConfig
+ if options.BindInterface != "" {
+ bindFunc := control.BindToInterface(control.DefaultInterfaceFinder(), options.BindInterface, -1)
+ dialer.Control = control.Append(dialer.Control, bindFunc)
+ listener.Control = control.Append(listener.Control, bindFunc)
+ }
+ if options.RoutingMark != 0 {
+ dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
+ listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
+ }
+ if options.ReuseAddr {
+ listener.Control = control.Append(listener.Control, control.ReuseAddr())
+ }
+ if options.ProtectPath != "" {
+ dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath))
+ listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath))
+ }
+ if options.ConnectTimeout != 0 {
+ dialer.Timeout = time.Duration(options.ConnectTimeout)
+ } else {
+ dialer.Timeout = C.TCPTimeout
+ }
+ var udpFragment bool
+ if options.UDPFragment != nil {
+ udpFragment = *options.UDPFragment
+ } else {
+ udpFragment = options.UDPFragmentDefault
+ }
+ if !udpFragment {
+ dialer.Control = control.Append(dialer.Control, control.DisableUDPFragment())
+ listener.Control = control.Append(listener.Control, control.DisableUDPFragment())
+ }
+ var (
+ dialer4 = dialer
+ udpDialer4 = dialer
+ udpAddr4 string
+ )
+ if options.Inet4BindAddress != nil {
+ bindAddr := options.Inet4BindAddress.Build()
+ dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
+ udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
+ udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String()
+ }
+ var (
+ dialer6 = dialer
+ udpDialer6 = dialer
+ udpAddr6 string
+ )
+ if options.Inet6BindAddress != nil {
+ bindAddr := options.Inet6BindAddress.Build()
+ dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
+ udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
+ udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
+ }
+ if options.TCPMultiPath {
+ if !go121Available {
+ return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
+ }
+ setMultiPathTCP(&dialer4)
+ }
+ tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
+ if err != nil {
+ return nil, err
+ }
+ tcpDialer6, err := newTCPDialer(dialer6, options.TCPFastOpen)
+ if err != nil {
+ return nil, err
+ }
+ return &DefaultDialer{
+ tcpDialer4,
+ tcpDialer6,
+ udpDialer4,
+ udpDialer6,
+ listener,
+ udpAddr4,
+ udpAddr6,
+ }, nil
+}
diff --git a/common/geoip/reader.go b/common/geoip/reader.go
index 9e225f7530..c6028bb2d2 100644
--- a/common/geoip/reader.go
+++ b/common/geoip/reader.go
@@ -32,7 +32,3 @@ func (r *Reader) Lookup(addr netip.Addr) string {
}
return "unknown"
}
-
-func (r *Reader) Close() error {
- return r.reader.Close()
-}
diff --git a/common/process/searcher_windows.go b/common/process/searcher_windows.go
index 5b3d59b5ab..f13b440e28 100644
--- a/common/process/searcher_windows.go
+++ b/common/process/searcher_windows.go
@@ -223,7 +223,7 @@ func getExecPathFromPID(pid uint32) (string, error) {
r1, _, err := syscall.SyscallN(
procQueryFullProcessImageNameW.Addr(),
uintptr(h),
- uintptr(0),
+ uintptr(1),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
)
diff --git a/common/simpledns/dns.go b/common/simpledns/dns.go
new file mode 100644
index 0000000000..ed4d479526
--- /dev/null
+++ b/common/simpledns/dns.go
@@ -0,0 +1,299 @@
+package simpledns
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/netip"
+ "net/url"
+ "strings"
+
+ M "github.com/sagernet/sing/common/metadata"
+ N "github.com/sagernet/sing/common/network"
+
+ "github.com/miekg/dns"
+)
+
+// tcp://1.1.1.1
+// tcp://1.1.1.1:53
+// tcp://[2606:4700:4700::1111]
+// tcp://[2606:4700:4700::1111]:53
+// udp://1.1.1.1
+// udp://1.1.1.1:53
+// udp://[2606:4700:4700::1111]
+// udp://[2606:4700:4700::1111]:53
+// tls://1.1.1.1
+// tls://1.1.1.1:853
+// tls://[2606:4700:4700::1111]
+// tls://[2606:4700:4700::1111]:853
+// tls://1.1.1.1/?sni=cloudflare-dns.com
+// tls://1.1.1.1:853/?sni=cloudflare-dns.com
+// tls://[2606:4700:4700::1111]/?sni=cloudflare-dns.com
+// tls://[2606:4700:4700::1111]:853/?sni=cloudflare-dns.com
+// https://1.1.1.1
+// https://1.1.1.1:443/dns-query
+// https://[2606:4700:4700::1111]
+// https://[2606:4700:4700::1111]:443
+// https://1.1.1.1/dns-query?sni=cloudflare-dns.com
+// https://1.1.1.1:443/dns-query?sni=cloudflare-dns.com
+// https://[2606:4700:4700::1111]/dns-query?sni=cloudflare-dns.com
+// https://[2606:4700:4700::1111]:443/dns-query?sni=cloudflare-dns.com
+// 1.1.1.1 => udp://1.1.1.1:53
+// 1.1.1.1:53 => udp://1.1.1.1:53
+// [2606:4700:4700::1111] => udp://[2606:4700:4700::1111]:53
+// [2606:4700:4700::1111]:53 => udp://[2606:4700:4700::1111]:53
+
+func DNSLookup(ctx context.Context, dialer N.Dialer, addr string, queryDomain string, a, aaaa bool) ([]netip.Addr, error) {
+ var f func(...*dns.Msg) ([][]netip.Addr, error)
+ switch {
+ case strings.HasPrefix(addr, "tcp://"):
+ ipPort, err := netip.ParseAddrPort(addr[6:])
+ if err != nil {
+ ip, err := netip.ParseAddr(addr[6:])
+ if err != nil {
+ return nil, fmt.Errorf("invalid addr: %s", addr)
+ }
+ ipPort = netip.AddrPortFrom(ip, 53)
+ }
+ f = func(msgs ...*dns.Msg) ([][]netip.Addr, error) {
+ return dnsLookupTCPOrUDPOrTLS(ctx, dialer, "tcp", nil, ipPort.String(), msgs...)
+ }
+ case strings.HasPrefix(addr, "udp://"):
+ ipPort, err := netip.ParseAddrPort(addr[6:])
+ if err != nil {
+ ip, err := netip.ParseAddr(addr[6:])
+ if err != nil {
+ return nil, fmt.Errorf("invalid addr: %s", addr)
+ }
+ ipPort = netip.AddrPortFrom(ip, 53)
+ }
+ f = func(msgs ...*dns.Msg) ([][]netip.Addr, error) {
+ return dnsLookupTCPOrUDPOrTLS(ctx, dialer, "udp", nil, ipPort.String(), msgs...)
+ }
+ case strings.HasPrefix(addr, "tls://"):
+ u, err := url.Parse(addr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid addr: %s", addr)
+ }
+ ipPort, err := netip.ParseAddrPort(u.Host)
+ if err != nil {
+ ip, err := netip.ParseAddr(u.Host)
+ if err != nil {
+ return nil, fmt.Errorf("invalid addr: %s", addr)
+ }
+ ipPort = netip.AddrPortFrom(ip, 853)
+ }
+ tlsConfig := &tls.Config{}
+ query := u.Query()
+ sni := query.Get("sni")
+ if sni != "" {
+ tlsConfig.ServerName = sni
+ } else {
+ tlsConfig.ServerName = u.Hostname()
+ }
+ u.RawQuery = ""
+ f = func(msgs ...*dns.Msg) ([][]netip.Addr, error) {
+ return dnsLookupTCPOrUDPOrTLS(ctx, dialer, "tcp", tlsConfig, ipPort.String(), msgs...)
+ }
+ case strings.HasPrefix(addr, "https://"):
+ u, err := url.Parse(addr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid addr: %s", addr)
+ }
+ ipPort, err := netip.ParseAddrPort(u.Host)
+ if err != nil {
+ ip, err := netip.ParseAddr(u.Host)
+ if err != nil {
+ return nil, fmt.Errorf("invalid addr: %s", addr)
+ }
+ ipPort = netip.AddrPortFrom(ip, 443)
+ }
+ if u.Path == "" {
+ u.Path = "/dns-query"
+ }
+ tlsConfig := &tls.Config{}
+ query := u.Query()
+ sni := query.Get("sni")
+ if sni != "" {
+ tlsConfig.ServerName = sni
+ } else {
+ tlsConfig.ServerName = u.Hostname()
+ }
+ u.RawQuery = query.Encode()
+ f = func(msgs ...*dns.Msg) ([][]netip.Addr, error) {
+ return dnsLookupHTTPS(ctx, dialer, tlsConfig, ipPort.String(), u.String(), msgs...)
+ }
+ default:
+ ipPort, err := netip.ParseAddrPort(addr)
+ if err != nil {
+ ip, err := netip.ParseAddr(addr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid addr: %s", addr)
+ }
+ ipPort = netip.AddrPortFrom(ip, 53)
+ }
+ f = func(msgs ...*dns.Msg) ([][]netip.Addr, error) {
+ return dnsLookupTCPOrUDPOrTLS(ctx, dialer, "udp", nil, ipPort.String(), msgs...)
+ }
+ }
+ var msgs []*dns.Msg
+ if a {
+ msg := &dns.Msg{}
+ msg.SetQuestion(dns.Fqdn(queryDomain), dns.TypeA)
+ msgs = append(msgs, msg)
+ }
+ if aaaa {
+ msg := &dns.Msg{}
+ msg.SetQuestion(dns.Fqdn(queryDomain), dns.TypeAAAA)
+ msgs = append(msgs, msg)
+ }
+ if len(msgs) == 0 {
+ return nil, fmt.Errorf("no query")
+ }
+
+ ipss, err := f(msgs...)
+ if err != nil {
+ return nil, err
+ }
+
+ var ips []netip.Addr
+ for _, s := range ipss {
+ ips = append(ips, s...)
+ }
+
+ return ips, nil
+}
+
+func dnsLookupTCPOrUDPOrTLS(ctx context.Context, dialer N.Dialer, network string, tlsConfig *tls.Config, addr string, msgs ...*dns.Msg) ([][]netip.Addr, error) {
+ conn, err := dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
+ if err != nil {
+ return nil, err
+ }
+
+ if tlsConfig != nil {
+ tlsConn := tls.Client(conn, tlsConfig)
+ err = tlsConn.HandshakeContext(ctx)
+ if err != nil {
+ return nil, err
+ }
+ conn = tlsConn
+ }
+
+ dnsConn := &dns.Conn{Conn: conn}
+ defer dnsConn.Close()
+
+ var ipss [][]netip.Addr
+
+ for _, msg := range msgs {
+ err = dnsConn.WriteMsg(msg)
+ if err != nil {
+ return nil, err
+ }
+
+ respMsg, err := dnsConn.ReadMsg()
+ if err != nil {
+ return nil, err
+ }
+
+ var ips []netip.Addr
+ for _, answer := range respMsg.Answer {
+ switch answer.Header().Rrtype {
+ case dns.TypeA:
+ a := answer.(*dns.A)
+ ip, ok := netip.AddrFromSlice(a.A)
+ if ok {
+ ips = append(ips, ip)
+ }
+ case dns.TypeAAAA:
+ a := answer.(*dns.AAAA)
+ ip, ok := netip.AddrFromSlice(a.AAAA)
+ if ok {
+ ips = append(ips, ip)
+ }
+ }
+ }
+
+ ipss = append(ipss, ips)
+ }
+
+ return ipss, nil
+}
+
+func dnsLookupHTTPS(ctx context.Context, dialer N.Dialer, tlsConfig *tls.Config, addr, url string, msgs ...*dns.Msg) ([][]netip.Addr, error) {
+ client := &http.Client{
+ Transport: &http.Transport{
+ ForceAttemptHTTP2: true,
+ TLSClientConfig: tlsConfig,
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
+ },
+ },
+ }
+
+ buffer := bytes.NewBuffer(nil)
+
+ var ipss [][]netip.Addr
+
+ for _, msg := range msgs {
+ rawMsg, err := msg.Pack()
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(rawMsg))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", "application/dns-message")
+ req.Header.Set("Accept", "application/dns-message")
+
+ req = req.WithContext(ctx)
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = io.Copy(buffer, resp.Body)
+ if err != nil {
+ resp.Body.Close()
+ return nil, err
+ }
+
+ var respMsg dns.Msg
+ err = respMsg.Unpack(buffer.Bytes())
+ if err != nil {
+ resp.Body.Close()
+ return nil, err
+ }
+
+ resp.Body.Close()
+ buffer.Reset()
+
+ var ips []netip.Addr
+ for _, answer := range respMsg.Answer {
+ switch answer.Header().Rrtype {
+ case dns.TypeA:
+ a := answer.(*dns.A)
+ ip, ok := netip.AddrFromSlice(a.A)
+ if ok {
+ ips = append(ips, ip)
+ }
+ case dns.TypeAAAA:
+ a := answer.(*dns.AAAA)
+ ip, ok := netip.AddrFromSlice(a.AAAA)
+ if ok {
+ ips = append(ips, ip)
+ }
+ }
+ }
+
+ ipss = append(ipss, ips)
+ }
+
+ return ipss, nil
+}
diff --git a/common/tls/acme.go b/common/tls/acme.go
index 08b24ed22c..d311c27931 100644
--- a/common/tls/acme.go
+++ b/common/tls/acme.go
@@ -105,16 +105,5 @@ func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Con
},
})
config = certmagic.New(cache, *config)
- var tlsConfig *tls.Config
- 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
+ return config.TLSConfig(), &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
}
diff --git a/common/tls/std_server.go b/common/tls/std_server.go
index 7184bdb36b..28a94cf15f 100644
--- a/common/tls/std_server.go
+++ b/common/tls/std_server.go
@@ -39,19 +39,11 @@ func (c *STDServerConfig) SetServerName(serverName 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) {
- 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) {
diff --git a/constant/proxy.go b/constant/proxy.go
index 1e9baee298..d2ce12c6ff 100644
--- a/constant/proxy.go
+++ b/constant/proxy.go
@@ -23,6 +23,7 @@ const (
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
+ TypeRandomAddr = "randomaddr"
)
const (
@@ -30,6 +31,8 @@ const (
TypeURLTest = "urltest"
)
+const TypeJSTest = "jstest"
+
func ProxyDisplayName(proxyType string) string {
switch proxyType {
case TypeDirect:
@@ -68,10 +71,14 @@ func ProxyDisplayName(proxyType string) string {
return "TUIC"
case TypeHysteria2:
return "Hysteria2"
+ case TypeRandomAddr:
+ return "RandomAddr"
case TypeSelector:
return "Selector"
case TypeURLTest:
return "URLTest"
+ case TypeJSTest:
+ return "JSTest"
default:
return "Unknown"
}
diff --git a/debug_http.go b/debug_http.go
index df356c2ed4..09f015e7a0 100644
--- a/debug_http.go
+++ b/debug_http.go
@@ -5,7 +5,6 @@ import (
"net/http/pprof"
"runtime"
"runtime/debug"
- "strings"
"github.com/sagernet/sing-box/common/humanize"
"github.com/sagernet/sing-box/log"
@@ -48,20 +47,12 @@ func applyDebugListenOption(options option.DebugOptions) {
encoder.SetIndent("", " ")
encoder.Encode(memObject)
})
- r.Route("/pprof", func(r chi.Router) {
- r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
- if !strings.HasSuffix(request.URL.Path, "/") {
- http.Redirect(writer, request, request.URL.Path+"/", http.StatusMovedPermanently)
- } else {
- pprof.Index(writer, request)
- }
- })
- r.HandleFunc("/*", pprof.Index)
- r.HandleFunc("/cmdline", pprof.Cmdline)
- r.HandleFunc("/profile", pprof.Profile)
- r.HandleFunc("/symbol", pprof.Symbol)
- r.HandleFunc("/trace", pprof.Trace)
- })
+ r.HandleFunc("/pprof", pprof.Index)
+ r.HandleFunc("/pprof/*", pprof.Index)
+ r.HandleFunc("/pprof/cmdline", pprof.Cmdline)
+ r.HandleFunc("/pprof/profile", pprof.Profile)
+ r.HandleFunc("/pprof/symbol", pprof.Symbol)
+ r.HandleFunc("/pprof/trace", pprof.Trace)
})
debugHTTPServer = &http.Server{
Addr: options.Listen,
diff --git a/debug_linux.go b/debug_linux.go
new file mode 100644
index 0000000000..3296bdec68
--- /dev/null
+++ b/debug_linux.go
@@ -0,0 +1,23 @@
+package box
+
+import (
+ "runtime"
+ "syscall"
+)
+
+func rusageMaxRSS() float64 {
+ ru := syscall.Rusage{}
+ err := syscall.Getrusage(syscall.RUSAGE_SELF, &ru)
+ if err != nil {
+ return 0
+ }
+
+ rss := float64(ru.Maxrss)
+ if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
+ rss /= 1 << 20 // ru_maxrss is bytes on darwin
+ } else {
+ // ru_maxrss is kilobytes elsewhere (linux, openbsd, etc)
+ rss /= 1 << 10
+ }
+ return rss
+}
diff --git a/debug_stub.go b/debug_stub.go
index a8988c2011..ea7e2c0b2b 100644
--- a/debug_stub.go
+++ b/debug_stub.go
@@ -1,4 +1,4 @@
-//go:build !(linux || darwin)
+//go:build !linux
package box
diff --git a/docs/changelog.md b/docs/changelog.md
index 12d94aa052..d0b76e4161 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,136 +2,13 @@
icon: material/alert-decagram
---
-#### 1.8.0
+#### 1.8.0-beta.3
* Fixes and improvements
-Important changes since 1.7:
-
-* Migrate cache file from Clash API to independent options **1**
-* Introducing [Rule Set](/configuration/rule-set/) **2**
-* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
-* Allow nested logical rules **4**
-* Independent `source_ip_is_private` and `ip_is_private` rules **5**
-* Add context to JSON decode error message **6**
-* Reject internal fake-ip queries **7**
-* Add GSO support for TUN and WireGuard system interface **8**
-* Add `idle_timeout` for URLTest outbound **9**
-* Add simple loopback detect
-* Optimize memory usage of idle connections
-* Update uTLS to 1.5.4 **10**
-* Update dependencies **11**
-
-**1**:
-
-See [Cache File](/configuration/experimental/cache-file/) and
-[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options).
-
-**2**:
-
-Rule set is independent collections of rules that can be compiled into binaries to improve performance.
-Compared to legacy GeoIP and Geosite resources,
-it can include more types of rules, load faster,
-use less memory, and update automatically.
-
-See [Route#rule_set](/configuration/route/#rule_set),
-[Route Rule](/configuration/route/rule/),
-[DNS Rule](/configuration/dns/rule/),
-[Rule Set](/configuration/rule-set/),
-[Source Format](/configuration/rule-set/source-format/) and
-[Headless Rule](/configuration/rule-set/headless-rule/).
-
-For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and
-[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets).
-
-**3**:
-
-New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets.
-
-**4**:
-
-Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules.
-
-**5**:
-
-The `private` GeoIP country never existed and was actually implemented inside V2Ray.
-Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets).
-
-**6**:
-
-JSON parse errors will now include the current key path.
-Only takes effect when compiled with Go 1.21+.
-
-**7**:
-
-All internal DNS queries now skip DNS rules with `server` type `fakeip`,
-and the default DNS server can no longer be `fakeip`.
-
-This change is intended to break incorrect usage and essentially requires no action.
-
-**8**:
-
-See [TUN](/configuration/inbound/tun/) inbound and [WireGuard](/configuration/outbound/wireguard/) outbound.
-
-**9**:
-
-When URLTest is idle for a certain period of time, the scheduled delay test will be paused.
-
-**10**:
-
-Added some new [fingerprints](/configuration/shared/tls#utls).
-Also, starting with this release, uTLS requires at least Go 1.20.
-
-**11**:
-
-Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, `quic-go` to `0.40.1` and `gvisor` to `20231204.0`
-
-
-#### 1.8.0-rc.11
+#### 1.8.0-beta.2
-* Fixes and improvements
-
-#### 1.7.8
-
-* Fixes and improvements
-
-#### 1.8.0-rc.10
-
-* Fixes and improvements
-
-#### 1.7.7
-
-* Fix V2Ray transport `path` validation behavior **1**
-* Fixes and improvements
-
-**1**:
-
-See [V2Ray transport](/configuration/shared/v2ray-transport/).
-
-#### 1.8.0-rc.7
-
-* Fixes and improvements
-
-#### 1.8.0-rc.3
-
-* Fix V2Ray transport `path` validation behavior **1**
-* Fixes and improvements
-
-**1**:
-
-See [V2Ray transport](/configuration/shared/v2ray-transport/).
-
-#### 1.7.6
-
-* Fixes and improvements
-
-#### 1.8.0-rc.1
-
-* Fixes and improvements
-
-#### 1.8.0-beta.9
-
-* Add simple loopback detect
+* Fix GSO support
* Fixes and improvements
#### 1.7.5
@@ -147,7 +24,7 @@ See [V2Ray transport](/configuration/shared/v2ray-transport/).
**1**:
-See [TUN](/configuration/inbound/tun/) inbound and [WireGuard](/configuration/outbound/wireguard/) outbound.
+See [TUN](/configuration/inbound/tun) inbound and [WireGuard](/configuration/outbound/wireguard) outbound.
**2**:
@@ -259,13 +136,70 @@ Since GeoIP was deprecated, we made this rule independent, see [Migration](/migr
#### 1.8.0-alpha.1
* Migrate cache file from Clash API to independent options **1**
-* Introducing [Rule Set](/configuration/rule-set/) **2**
+* Introducing [Rule Set](/configuration/rule-set) **2**
+* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
+* Allow nested logical rules **4**
+
+**1**:
+
+See [Cache File](/configuration/experimental/cache-file) and
+[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options).
+
+**2**:
+
+Rule set is independent collections of rules that can be compiled into binaries to improve performance.
+Compared to legacy GeoIP and Geosite resources,
+it can include more types of rules, load faster,
+use less memory, and update automatically.
+
+See [Route#rule_set](/configuration/route/#rule_set),
+[Route Rule](/configuration/route/rule),
+[DNS Rule](/configuration/dns/rule),
+[Rule Set](/configuration/rule-set),
+[Source Format](/configuration/rule-set/source-format) and
+[Headless Rule](/configuration/rule-set/headless-rule).
+
+For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and
+[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets).
+
+**3**:
+
+New commands manage GeoIP, Geosite and rule set resources, and help you migrate GEO resources to rule sets.
+
+**4**:
+
+Logical rules in route rules, DNS rules, and the new headless rule now allow nesting of logical rules.
+
+#### 1.8.0-alpha.6
+
+* Fix rule-set matching logic **1**
+* Fixes and improvements
+
+**1**:
+
+Now the rules in the `rule_set` rule item can be logically considered to be merged into the rule using rule sets,
+rather than completely following the AND logic.
+
+#### 1.8.0-alpha.5
+
+* Parallel rule-set initialization
+* Independent `source_ip_is_private` and `ip_is_private` rules **1**
+
+**1**:
+
+The `private` GeoIP country never existed and was actually implemented inside V2Ray.
+Since GeoIP was deprecated, we made this rule independent, see [Migration](/migration/#migrate-geoip-to-rule-sets).
+
+#### 1.8.0-alpha.1
+
+* Migrate cache file from Clash API to independent options **1**
+* Introducing [Rule Set](/configuration/rule-set) **2**
* Add `sing-box geoip`, `sing-box geosite` and `sing-box rule-set` commands **3**
* Allow nested logical rules **4**
**1**:
-See [Cache File](/configuration/experimental/cache-file/) and
+See [Cache File](/configuration/experimental/cache-file) and
[Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-options).
**2**:
@@ -276,11 +210,11 @@ it can include more types of rules, load faster,
use less memory, and update automatically.
See [Route#rule_set](/configuration/route/#rule_set),
-[Route Rule](/configuration/route/rule/),
-[DNS Rule](/configuration/dns/rule/),
-[Rule Set](/configuration/rule-set/),
-[Source Format](/configuration/rule-set/source-format/) and
-[Headless Rule](/configuration/rule-set/headless-rule/).
+[Route Rule](/configuration/route/rule),
+[DNS Rule](/configuration/dns/rule),
+[Rule Set](/configuration/rule-set),
+[Source Format](/configuration/rule-set/source-format) and
+[Headless Rule](/configuration/rule-set/headless-rule).
For GEO resources migration, see [Migrate GeoIP to rule sets](/migration/#migrate-geoip-to-rule-sets) and
[Migrate Geosite to rule sets](/migration/#migrate-geosite-to-rule-sets).
@@ -299,8 +233,8 @@ Logical rules in route rules, DNS rules, and the new headless rule now allow nes
Important changes since 1.6:
-* Add [exclude route support](/configuration/inbound/tun/) for TUN inbound
-* Add `udp_disable_domain_unmapping` [inbound listen option](/configuration/shared/listen/) **1**
+* Add [exclude route support](/configuration/inbound/tun) for TUN inbound
+* Add `udp_disable_domain_unmapping` [inbound listen option](/configuration/shared/listen) **1**
* Add [HTTPUpgrade V2Ray transport](/configuration/shared/v2ray-transport#HTTPUpgrade) support **2**
* Migrate multiplex and UoT server to inbound **3**
* Add TCP Brutal support for multiplex **4**
@@ -331,7 +265,7 @@ options.
**4**
Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server,
-see [TCP Brutal](/configuration/shared/tcp-brutal/) for details.
+see [TCP Brutal](/configuration/shared/tcp-brutal) for details.
**5**:
@@ -420,7 +354,7 @@ Only supported in graphical clients on Android and iOS.
#### 1.6.1
-* Our [Android client](/installation/clients/sfa/) is now available in the Google Play Store ▶️
+* Our [Android client](/installation/clients/sfa) is now available in the Google Play Store ▶️
* Fixes and improvements
#### 1.7.0-alpha.6
@@ -440,7 +374,7 @@ options.
**2**
Hysteria Brutal Congestion Control Algorithm in TCP. A kernel module needs to be installed on the Linux server,
-see [TCP Brutal](/configuration/shared/tcp-brutal/) for details.
+see [TCP Brutal](/configuration/shared/tcp-brutal) for details.
#### 1.7.0-alpha.3
@@ -459,13 +393,13 @@ The new HTTPUpgrade transport has better performance than WebSocket and is bette
Important changes since 1.5:
-* Our [Apple tvOS client](/installation/clients/sft/) is now available in the App Store 🍎
+* Our [Apple tvOS client](/installation/clients/sft) is now available in the App Store 🍎
* Update BBR congestion control for TUIC and Hysteria2 **1**
* Update brutal congestion control for Hysteria2
* Add `brutal_debug` option for Hysteria2
* Update legacy Hysteria protocol **2**
* Add TLS self sign key pair generate command
-* Remove [Deprecated Features](/deprecated/) by agreement
+* Remove [Deprecated Features](/deprecated) by agreement
**1**:
@@ -483,8 +417,8 @@ the old protocol (Hysteria 1) have been updated to be consistent with Hysteria 2
#### 1.7.0-alpha.1
-* Add [exclude route support](/configuration/inbound/tun/) for TUN inbound
-* Add `udp_disable_domain_unmapping` [inbound listen option](/configuration/shared/listen/) **1**
+* Add [exclude route support](/configuration/inbound/tun) for TUN inbound
+* Add `udp_disable_domain_unmapping` [inbound listen option](/configuration/shared/listen) **1**
* Fixes and improvements
**1**:
@@ -604,7 +538,7 @@ introduce new issues.
#### 1.5.2
-* Our [Apple tvOS client](/installation/clients/sft/) is now available in the App Store 🍎
+* Our [Apple tvOS client](/installation/clients/sft) is now available in the App Store 🍎
* Fixes and improvements
#### 1.6.0-alpha.3
@@ -624,7 +558,7 @@ introduce new issues.
* Update BBR congestion control for TUIC and Hysteria2 **1**
* Update quic-go to v0.39.0
* Update gVisor to 20230814.0
-* Remove [Deprecated Features](/deprecated/) by agreement
+* Remove [Deprecated Features](/deprecated) by agreement
* Fixes and improvements
**1**:
@@ -638,7 +572,7 @@ This update is intended to address the multi-send defects of the old implementat
Important changes since 1.4:
-* Add TLS [ECH server](/configuration/shared/tls/) support
+* Add TLS [ECH server](/configuration/shared/tls) support
* Improve TLS TCH client configuration
* Add TLS ECH key pair generator **1**
* Add TLS ECH support for QUIC based protocols **2**
@@ -647,7 +581,7 @@ Important changes since 1.4:
* Add `interrupt_exist_connections` option for `Selector` and `URLTest` outbounds **4**
* Add DNS01 challenge support for ACME TLS certificate issuer **5**
* Add `merge` command **6**
-* Mark [Deprecated Features](/deprecated/)
+* Mark [Deprecated Features](/deprecated)
**1**:
@@ -659,7 +593,7 @@ All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria[/2]`
**3**:
-See [Hysteria2 inbound](/configuration/inbound/hysteria2/) and [Hysteria2 outbound](/configuration/outbound/hysteria2/)
+See [Hysteria2 inbound](/configuration/inbound/hysteria2) and [Hysteria2 outbound](/configuration/outbound/hysteria2)
For protocol description, please refer to [https://v2.hysteria.network](https://v2.hysteria.network)
@@ -672,7 +606,7 @@ Only inbound connections are affected by this setting, internal connections will
**5**:
Only `Alibaba Cloud DNS` and `Cloudflare` are supported, see [ACME Fields](/configuration/shared/tls#acme-fields)
-and [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/).
+and [DNS01 Challenge Fields](/configuration/shared/dns01_challenge).
**6**:
@@ -754,7 +688,7 @@ Global Flags:
Only `Alibaba Cloud DNS` and `Cloudflare` are supported,
see [ACME Fields](/configuration/shared/tls#acme-fields)
-and [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/).
+and [DNS01 Challenge Fields](/configuration/shared/dns01_challenge).
#### 1.5.0-beta.10
@@ -783,7 +717,7 @@ Only inbound connections are affected by this setting, internal connections will
* Fix compatibility issues with official Hysteria2 server and client
* Fixes and improvements
-* Mark [deprecated features](/deprecated/)
+* Mark [deprecated features](/deprecated)
#### 1.5.0-beta.3
@@ -802,13 +736,13 @@ Hysteria2 server and client when using `fastOpen=false` or UDP MTU >= 1200.
**1**:
-See [Hysteria2 inbound](/configuration/inbound/hysteria2/) and [Hysteria2 outbound](/configuration/outbound/hysteria2/)
+See [Hysteria2 inbound](/configuration/inbound/hysteria2) and [Hysteria2 outbound](/configuration/outbound/hysteria2)
For protocol description, please refer to [https://v2.hysteria.network](https://v2.hysteria.network)
#### 1.5.0-beta.1
-* Add TLS [ECH server](/configuration/shared/tls/) support
+* Add TLS [ECH server](/configuration/shared/tls) support
* Improve TLS TCH client configuration
* Add TLS ECH key pair generator **1**
* Add TLS ECH support for QUIC based protocols **2**
@@ -841,12 +775,12 @@ Important changes since 1.3:
*1*:
-See [TUIC inbound](/configuration/inbound/tuic/)
-and [TUIC outbound](/configuration/outbound/tuic/)
+See [TUIC inbound](/configuration/inbound/tuic)
+and [TUIC outbound](/configuration/outbound/tuic)
**2**:
-This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp/), designed to provide a QUIC
+This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp), designed to provide a QUIC
stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or
another program compatible with the protocol as a server.
@@ -877,7 +811,7 @@ Requires sing-box to be compiled with Go 1.21.
**1**:
-This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp/), designed to provide a QUIC
+This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp), designed to provide a QUIC
stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or
another program compatible with the protocol as a server.
@@ -915,8 +849,8 @@ Requires sing-box to be compiled with Go 1.21.
*1*:
-See [TUIC inbound](/configuration/inbound/tuic/)
-and [TUIC outbound](/configuration/outbound/tuic/)
+See [TUIC inbound](/configuration/inbound/tuic)
+and [TUIC outbound](/configuration/outbound/tuic)
#### 1.3.6
@@ -925,7 +859,7 @@ and [TUIC outbound](/configuration/outbound/tuic/)
#### 1.3.5
* Fixes and improvements
-* Introducing our [Apple tvOS](/installation/clients/sft/) client applications **1**
+* Introducing our [Apple tvOS](/installation/clients/sft) client applications **1**
* Add per app proxy and app installed/updated trigger support for Android client
* Add profile sharing support for Android/iOS/macOS clients
@@ -952,8 +886,7 @@ downloaded through TestFlight.
#### 1.3.1-beta.3
-* Introducing our [new iOS](/installation/clients/sfi/) and [macOS](/installation/clients/sfm/) client applications **1
- **
+* Introducing our [new iOS](/installation/clients/sfi) and [macOS](/installation/clients/sfm) client applications **1**
* Fixes and improvements
**1**:
@@ -974,7 +907,7 @@ The old testflight link and app are no longer valid.
Important changes since 1.2:
-* Add [FakeIP](/configuration/dns/fakeip/) support **1**
+* Add [FakeIP](/configuration/dns/fakeip) support **1**
* Improve multiplex **2**
* Add [DNS reverse mapping](/configuration/dns#reverse_mapping) support
* Add `rewrite_ttl` DNS rule action
@@ -1001,11 +934,11 @@ Important changes since 1.2:
*1*:
-See [FAQ](/faq/fakeip/) for more information.
+See [FAQ](/faq/fakeip) for more information.
*2*:
-Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex/).
+Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex).
#### 1.3-rc2
@@ -1067,7 +1000,7 @@ Improved performance and reduced memory usage.
*1*:
-Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex/).
+Added new `h2mux` multiplex protocol and `padding` multiplex option, see [Multiplex](/configuration/shared/multiplex).
#### 1.2.6
@@ -1119,25 +1052,25 @@ This is an incompatible update for XUDP in VLESS if vision flow is enabled.
#### 1.3-beta1
* Add [DNS reverse mapping](/configuration/dns#reverse_mapping) support
-* Add [L3 routing](/configuration/route/ip-rule/) support **1**
+* Add [L3 routing](/configuration/route/ip-rule) support **1**
* Add `rewrite_ttl` DNS rule action
-* Add [FakeIP](/configuration/dns/fakeip/) support **2**
+* Add [FakeIP](/configuration/dns/fakeip) support **2**
* Add `store_fakeip` Clash API option
* Add multi-peer support for [WireGuard](/configuration/outbound/wireguard#peers) outbound
* Add loopback detect
*1*:
-It can currently be used to [route connections directly to WireGuard](/examples/wireguard-direct/) or block connections
+It can currently be used to [route connections directly to WireGuard](/examples/wireguard-direct) or block connections
at the IP layer.
*2*:
-See [FAQ](/faq/fakeip/) for more information.
+See [FAQ](/faq/fakeip) for more information.
#### 1.2.3
-* Introducing our [new Android client application](/installation/clients/sfa/)
+* Introducing our [new Android client application](/installation/clients/sfa)
* Improve UDP domain destination NAT
* Update reality protocol
* Fix TTL calculation for DNS response
@@ -1166,16 +1099,16 @@ to `domain` rule.
Important changes since 1.1:
-* Introducing our [new iOS client application](/installation/clients/sfi/)
-* Introducing [UDP over TCP protocol version 2](/configuration/shared/udp-over-tcp/)
+* Introducing our [new iOS client application](/installation/clients/sfi)
+* Introducing [UDP over TCP protocol version 2](/configuration/shared/udp-over-tcp)
* Add [platform options](/configuration/inbound/tun#platform) for tun inbound
* Add [ShadowTLS protocol v3](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-v3-en.md)
-* Add [VLESS server](/configuration/inbound/vless/) and [vision](/configuration/outbound/vless#flow) support
-* Add [reality TLS](/configuration/shared/tls/) support
-* Add [NTP service](/configuration/ntp/)
-* Add [DHCP DNS server](/configuration/dns/server/) support
-* Add SSH [host key validation](/configuration/outbound/ssh/) support
-* Add [query_type](/configuration/dns/rule/) DNS rule item
+* Add [VLESS server](/configuration/inbound/vless) and [vision](/configuration/outbound/vless#flow) support
+* Add [reality TLS](/configuration/shared/tls) support
+* Add [NTP service](/configuration/ntp)
+* Add [DHCP DNS server](/configuration/dns/server) support
+* Add SSH [host key validation](/configuration/outbound/ssh) support
+* Add [query_type](/configuration/dns/rule) DNS rule item
* Add fallback support for v2ray transport
* Add custom TLS server support for http based v2ray transports
* Add health check support for http-based v2ray transports
@@ -1206,7 +1139,7 @@ name.
#### 1.2-beta9
-* Introducing the [UDP over TCP protocol version 2](/configuration/shared/udp-over-tcp/)
+* Introducing the [UDP over TCP protocol version 2](/configuration/shared/udp-over-tcp)
* Add health check support for http-based v2ray transports
* Remove length limit on short_id for reality TLS config
* Fix bugs and update dependencies
@@ -1223,7 +1156,7 @@ name.
#### 1.2-beta6
-* Introducing our [new iOS client application](/installation/clients/sfi/)
+* Introducing our [new iOS client application](/installation/clients/sfi)
* Add [platform options](/configuration/inbound/tun#platform) for tun inbound
* Add custom TLS server support for http based v2ray transports
* Add generate commands
@@ -1236,8 +1169,8 @@ name.
#### 1.2-beta5
-* Add [VLESS server](/configuration/inbound/vless/) and [vision](/configuration/outbound/vless#flow) support
-* Add [reality TLS](/configuration/shared/tls/) support
+* Add [VLESS server](/configuration/inbound/vless) and [vision](/configuration/outbound/vless#flow) support
+* Add [reality TLS](/configuration/shared/tls) support
* Fix match private address
#### 1.1.6
@@ -1252,7 +1185,7 @@ name.
#### 1.2-beta4
-* Add [NTP service](/configuration/ntp/)
+* Add [NTP service](/configuration/ntp)
* Add Add multiple server names and multi-user support for shadowtls
* Add strict mode support for shadowtls v3
* Add uTLS support for shadowtls v3
@@ -1272,9 +1205,9 @@ name.
#### 1.2-beta1
-* Add [DHCP DNS server](/configuration/dns/server/) support
-* Add SSH [host key validation](/configuration/outbound/ssh/) support
-* Add [query_type](/configuration/dns/rule/) DNS rule item
+* Add [DHCP DNS server](/configuration/dns/server) support
+* Add SSH [host key validation](/configuration/outbound/ssh) support
+* Add [query_type](/configuration/dns/rule) DNS rule item
* Add v2ray [user stats](/configuration/experimental#statsusers) api
* Add new clash DNS query api
* Improve vmess request
@@ -1503,7 +1436,7 @@ and [ShadowTLS outbound](/configuration/outbound/shadowtls#version)
#### 1.1-beta6
-* Add [URLTest outbound](/configuration/outbound/urltest/)
+* Add [URLTest outbound](/configuration/outbound/urltest)
* Fix bugs in 1.1-beta5
#### 1.1-beta5
@@ -1535,8 +1468,8 @@ The default tun stack is changed to system.
#### 1.1-beta4
* Add internal simple-obfs and v2ray-plugin [Shadowsocks plugins](/configuration/outbound/shadowsocks#plugin)
-* Add [ShadowsocksR outbound](/configuration/outbound/shadowsocksr/)
-* Add [VLESS outbound and XUDP](/configuration/outbound/vless/)
+* Add [ShadowsocksR outbound](/configuration/outbound/shadowsocksr)
+* Add [VLESS outbound and XUDP](/configuration/outbound/vless)
* Skip wait for hysteria tcp handshake response
* Fix socks4 client
* Fix hysteria inbound
@@ -1563,7 +1496,7 @@ The default tun stack is changed to system.
*1*:
Switching modes using the Clash API, and `store-selected` are now supported,
-see [Experimental](/configuration/experimental/).
+see [Experimental](/configuration/experimental).
*2*:
@@ -1644,15 +1577,15 @@ and [Listen Fields](/configuration/shared/listen#udp_fragment).
* Fix write trojan udp
* Fix DNS routing
* Add attribute support for geosite
-* Update documentation for [Dial Fields](/configuration/shared/dial/)
+* Update documentation for [Dial Fields](/configuration/shared/dial)
#### 1.0-beta3
* Add [chained inbound](/configuration/shared/listen#detour) support
* Add process_path rule item
* Add macOS redirect support
-* Add ShadowTLS [Inbound](/configuration/inbound/shadowtls/), [Outbound](/configuration/outbound/shadowtls/)
- and [Examples](/examples/shadowtls/)
+* Add ShadowTLS [Inbound](/configuration/inbound/shadowtls), [Outbound](/configuration/outbound/shadowtls)
+ and [Examples](/examples/shadowtls)
* Fix search android package in non-owner users
* Fix socksaddr type condition
* Fix smux session status
@@ -1696,7 +1629,7 @@ and [Listen Fields](/configuration/shared/listen#udp_fragment).
##### 2022/08/23
-* Add [V2Ray Transport](/configuration/shared/v2ray-transport/) support for VMess and Trojan
+* Add [V2Ray Transport](/configuration/shared/v2ray-transport) support for VMess and Trojan
* Allow plain http request in Naive inbound (It can now be used with nginx)
* Add proxy protocol support
* Free memory after start
@@ -1705,13 +1638,13 @@ and [Listen Fields](/configuration/shared/listen#udp_fragment).
##### 2022/08/22
-* Add strategy setting for each [DNS server](/configuration/dns/server/)
+* Add strategy setting for each [DNS server](/configuration/dns/server)
* Add bind address to outbound options
##### 2022/08/21
-* Add [Tor outbound](/configuration/outbound/tor/)
-* Add [SSH outbound](/configuration/outbound/ssh/)
+* Add [Tor outbound](/configuration/outbound/tor)
+* Add [SSH outbound](/configuration/outbound/ssh)
##### 2022/08/20
@@ -1725,8 +1658,8 @@ and [Listen Fields](/configuration/shared/listen#udp_fragment).
##### 2022/08/19
-* Add Hysteria [Inbound](/configuration/inbound/hysteria/) and [Outbund](/configuration/outbound/hysteria/)
-* Add [ACME TLS certificate issuer](/configuration/shared/tls/)
+* Add Hysteria [Inbound](/configuration/inbound/hysteria) and [Outbund](/configuration/outbound/hysteria)
+* Add [ACME TLS certificate issuer](/configuration/shared/tls)
* Allow read config from stdin (-c stdin)
* Update gVisor to 20220815.0
@@ -1744,11 +1677,11 @@ and [Listen Fields](/configuration/shared/listen#udp_fragment).
##### 2022/08/16
* Add ip_version (route/dns) rule item
-* Add [WireGuard](/configuration/outbound/wireguard/) outbound
+* Add [WireGuard](/configuration/outbound/wireguard) outbound
##### 2022/08/15
-* Add uid, android user and package rules support in [Tun](/configuration/inbound/tun/) routing.
+* Add uid, android user and package rules support in [Tun](/configuration/inbound/tun) routing.
##### 2022/08/13
@@ -1757,15 +1690,15 @@ and [Listen Fields](/configuration/shared/listen#udp_fragment).
##### 2022/08/12
* Performance improvements
-* Add UoT option for [SOCKS](/configuration/outbound/socks/) outbound
+* Add UoT option for [SOCKS](/configuration/outbound/socks) outbound
##### 2022/08/11
-* Add UoT option for [Shadowsocks](/configuration/outbound/shadowsocks/) outbound, UoT support for all inbounds
+* Add UoT option for [Shadowsocks](/configuration/outbound/shadowsocks) outbound, UoT support for all inbounds
##### 2022/08/10
-* Add full-featured [Naive](/configuration/inbound/naive/) inbound
+* Add full-featured [Naive](/configuration/inbound/naive) inbound
* Fix default dns server option [#9] by iKirby
##### 2022/08/09
diff --git a/docs/clients/android/features.md b/docs/clients/android/features.md
index 8fe84add26..346976cc77 100644
--- a/docs/clients/android/features.md
+++ b/docs/clients/android/features.md
@@ -19,6 +19,7 @@ SFA provides an unprivileged TUN implementation through Android VpnService.
| `inet6_address` | :material-check: | / |
| `mtu` | :material-check: | / |
| `gso` | :material-close: | No permission |
+| `gso_max_size` | :material-close: | No permission |
| `auto_route` | :material-check: | / |
| `strict_route` | :material-close: | Not implemented |
| `inet4_route_address` | :material-check: | / |
diff --git a/docs/clients/apple/features.md b/docs/clients/apple/features.md
index 7d419103b5..7c7b8c9b91 100644
--- a/docs/clients/apple/features.md
+++ b/docs/clients/apple/features.md
@@ -21,6 +21,7 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension
| `inet6_address` | :material-check: | / |
| `mtu` | :material-check: | / |
| `gso` | :material-close: | Not implemented |
+| `gso_max_size` | :material-close: | Not implemented |
| `auto_route` | :material-check: | / |
| `strict_route` | :material-close:️ | Not implemented |
| `inet4_route_address` | :material-check: | / |
diff --git a/docs/clients/index.md b/docs/clients/index.md
index 45d2c9a948..b80c161867 100644
--- a/docs/clients/index.md
+++ b/docs/clients/index.md
@@ -2,11 +2,11 @@
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 |
+| 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
diff --git a/docs/clients/index.zh.md b/docs/clients/index.zh.md
index 81e4129183..ef643086ac 100644
--- a/docs/clients/index.zh.md
+++ b/docs/clients/index.zh.md
@@ -4,8 +4,8 @@
| 平台 | 客户端 |
|---------------------------------------|-----------------------------------------|
-| :material-android: Android | [sing-box for Android](./android/) |
-| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple/) |
+| :material-android: Android | [sing-box for Android](./android) |
+| :material-apple: iOS/macOS/Apple tvOS | [sing-box for Apple platforms](./apple) |
| :material-laptop: Desktop | 施工中 |
此处没有列出一些声称使用或以 sing-box 为卖点的第三方项目。此类项目维护者的动机是获得更多用户,即使它们提供友好的商业
diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md
index e2832c4275..5f8b254733 100644
--- a/docs/configuration/dns/index.md
+++ b/docs/configuration/dns/index.md
@@ -23,9 +23,9 @@
| Key | Format |
|----------|--------------------------------|
-| `server` | List of [DNS Server](./server/) |
-| `rules` | List of [DNS Rule](./rule/) |
-| `fakeip` | [FakeIP](./fakeip/) |
+| `server` | List of [DNS Server](./server) |
+| `rules` | List of [DNS Rule](./rule) |
+| `fakeip` | [FakeIP](./fakeip) |
#### final
@@ -62,4 +62,4 @@ problematic in environments such as macOS, where DNS is proxied and cached by th
#### fakeip
-[FakeIP](./fakeip/) settings.
+[FakeIP](./fakeip) settings.
diff --git a/docs/configuration/dns/index.zh.md b/docs/configuration/dns/index.zh.md
index afc6e9311c..b84f95282b 100644
--- a/docs/configuration/dns/index.zh.md
+++ b/docs/configuration/dns/index.zh.md
@@ -21,10 +21,10 @@
### 字段
-| 键 | 格式 |
-|----------|-------------------------|
-| `server` | 一组 [DNS 服务器](./server/) |
-| `rules` | 一组 [DNS 规则](./rule/) |
+| 键 | 格式 |
+|----------|------------------------|
+| `server` | 一组 [DNS 服务器](./server) |
+| `rules` | 一组 [DNS 规则](./rule) |
#### final
@@ -60,4 +60,4 @@
#### fakeip
-[FakeIP](./fakeip/) 设置。
+[FakeIP](./fakeip) 设置。
diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md
index 68cc32cfae..c393a919ff 100644
--- a/docs/configuration/dns/rule.md
+++ b/docs/configuration/dns/rule.md
@@ -142,7 +142,7 @@ icon: material/alert-decagram
#### inbound
-Tags of [Inbound](/configuration/inbound/).
+Tags of [Inbound](/configuration/inbound).
#### ip_version
diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md
index 5b1d75019a..41e90281f0 100644
--- a/docs/configuration/dns/rule.zh.md
+++ b/docs/configuration/dns/rule.zh.md
@@ -139,7 +139,7 @@ icon: material/alert-decagram
#### inbound
-[入站](/zh/configuration/inbound/) 标签.
+[入站](/zh/configuration/inbound) 标签.
#### ip_version
diff --git a/docs/configuration/dns/server.md b/docs/configuration/dns/server.md
index 545810bf9e..8123f4d1c2 100644
--- a/docs/configuration/dns/server.md
+++ b/docs/configuration/dns/server.md
@@ -30,18 +30,18 @@ The tag of the dns server.
The address of the dns server.
-| Protocol | Format |
-|--------------------------------------|-------------------------------|
-| `System` | `local` |
-| `TCP` | `tcp://1.0.0.1` |
-| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
-| `TLS` | `tls://dns.google` |
-| `HTTPS` | `https://1.1.1.1/dns-query` |
-| `QUIC` | `quic://dns.adguard.com` |
-| `HTTP3` | `h3://8.8.8.8/dns-query` |
-| `RCode` | `rcode://refused` |
-| `DHCP` | `dhcp://auto` or `dhcp://en0` |
-| [FakeIP](/configuration/dns/fakeip/) | `fakeip` |
+| Protocol | Format |
+|-------------------------------------|-------------------------------|
+| `System` | `local` |
+| `TCP` | `tcp://1.0.0.1` |
+| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
+| `TLS` | `tls://dns.google` |
+| `HTTPS` | `https://1.1.1.1/dns-query` |
+| `QUIC` | `quic://dns.adguard.com` |
+| `HTTP3` | `h3://8.8.8.8/dns-query` |
+| `RCode` | `rcode://refused` |
+| `DHCP` | `dhcp://auto` or `dhcp://en0` |
+| [FakeIP](/configuration/dns/fakeip) | `fakeip` |
!!! warning ""
diff --git a/docs/configuration/dns/server.zh.md b/docs/configuration/dns/server.zh.md
index 36bcde5d3c..728590de80 100644
--- a/docs/configuration/dns/server.zh.md
+++ b/docs/configuration/dns/server.zh.md
@@ -30,18 +30,18 @@ DNS 服务器的标签。
DNS 服务器的地址。
-| 协议 | 格式 |
-|--------------------------------------|------------------------------|
-| `System` | `local` |
-| `TCP` | `tcp://1.0.0.1` |
-| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
-| `TLS` | `tls://dns.google` |
-| `HTTPS` | `https://1.1.1.1/dns-query` |
-| `QUIC` | `quic://dns.adguard.com` |
-| `HTTP3` | `h3://8.8.8.8/dns-query` |
-| `RCode` | `rcode://refused` |
-| `DHCP` | `dhcp://auto` 或 `dhcp://en0` |
-| [FakeIP](/configuration/dns/fakeip/) | `fakeip` |
+| 协议 | 格式 |
+|-------------------------------------|------------------------------|
+| `System` | `local` |
+| `TCP` | `tcp://1.0.0.1` |
+| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
+| `TLS` | `tls://dns.google` |
+| `HTTPS` | `https://1.1.1.1/dns-query` |
+| `QUIC` | `quic://dns.adguard.com` |
+| `HTTP3` | `h3://8.8.8.8/dns-query` |
+| `RCode` | `rcode://refused` |
+| `DHCP` | `dhcp://auto` 或 `dhcp://en0` |
+| [FakeIP](/configuration/dns/fakeip) | `fakeip` |
!!! warning ""
diff --git a/docs/configuration/experimental/index.md b/docs/configuration/experimental/index.md
index 4ddcc41af7..1057e59b36 100644
--- a/docs/configuration/experimental/index.md
+++ b/docs/configuration/experimental/index.md
@@ -25,6 +25,6 @@ icon: material/alert-decagram
| Key | Format |
|--------------|----------------------------|
-| `cache_file` | [Cache File](./cache-file/) |
-| `clash_api` | [Clash API](./clash-api/) |
-| `v2ray_api` | [V2Ray API](./v2ray-api/) |
\ No newline at end of file
+| `cache_file` | [Cache File](./cache-file) |
+| `clash_api` | [Clash API](./clash-api) |
+| `v2ray_api` | [V2Ray API](./v2ray-api) |
\ No newline at end of file
diff --git a/docs/configuration/inbound/direct.md b/docs/configuration/inbound/direct.md
index 6dc93578a7..706c677525 100644
--- a/docs/configuration/inbound/direct.md
+++ b/docs/configuration/inbound/direct.md
@@ -17,7 +17,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
diff --git a/docs/configuration/inbound/http.md b/docs/configuration/inbound/http.md
index cd2ec35dc8..3b14905015 100644
--- a/docs/configuration/inbound/http.md
+++ b/docs/configuration/inbound/http.md
@@ -20,7 +20,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
diff --git a/docs/configuration/inbound/hysteria.md b/docs/configuration/inbound/hysteria.md
index 4725aafcea..789dffeada 100644
--- a/docs/configuration/inbound/hysteria.md
+++ b/docs/configuration/inbound/hysteria.md
@@ -31,7 +31,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md
index 7c611e6491..7d9d504f39 100644
--- a/docs/configuration/inbound/hysteria2.md
+++ b/docs/configuration/inbound/hysteria2.md
@@ -35,7 +35,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
diff --git a/docs/configuration/inbound/index.md b/docs/configuration/inbound/index.md
index 9591ae866f..830d86a95c 100644
--- a/docs/configuration/inbound/index.md
+++ b/docs/configuration/inbound/index.md
@@ -15,24 +15,24 @@
### Fields
-| Type | Format | Injectable |
-|---------------|-------------------------------|------------|
-| `direct` | [Direct](./direct/) | X |
-| `mixed` | [Mixed](./mixed/) | TCP |
-| `socks` | [SOCKS](./socks/) | TCP |
-| `http` | [HTTP](./http/) | TCP |
-| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP |
-| `vmess` | [VMess](./vmess/) | TCP |
-| `trojan` | [Trojan](./trojan/) | TCP |
-| `naive` | [Naive](./naive/) | X |
-| `hysteria` | [Hysteria](./hysteria/) | X |
-| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP |
-| `tuic` | [TUIC](./tuic/) | X |
-| `hysteria2` | [Hysteria2](./hysteria2/) | X |
-| `vless` | [VLESS](./vless/) | TCP |
-| `tun` | [Tun](./tun/) | X |
-| `redirect` | [Redirect](./redirect/) | X |
-| `tproxy` | [TProxy](./tproxy/) | X |
+| Type | Format | Injectable |
+|---------------|------------------------------|------------|
+| `direct` | [Direct](./direct) | X |
+| `mixed` | [Mixed](./mixed) | TCP |
+| `socks` | [SOCKS](./socks) | TCP |
+| `http` | [HTTP](./http) | TCP |
+| `shadowsocks` | [Shadowsocks](./shadowsocks) | TCP |
+| `vmess` | [VMess](./vmess) | TCP |
+| `trojan` | [Trojan](./trojan) | TCP |
+| `naive` | [Naive](./naive) | X |
+| `hysteria` | [Hysteria](./hysteria) | X |
+| `shadowtls` | [ShadowTLS](./shadowtls) | TCP |
+| `tuic` | [TUIC](./tuic) | X |
+| `hysteria2` | [Hysteria2](./hysteria2) | X |
+| `vless` | [VLESS](./vless) | TCP |
+| `tun` | [Tun](./tun) | X |
+| `redirect` | [Redirect](./redirect) | X |
+| `tproxy` | [TProxy](./tproxy) | X |
#### tag
diff --git a/docs/configuration/inbound/index.zh.md b/docs/configuration/inbound/index.zh.md
index 458cd602d4..5b3592f68f 100644
--- a/docs/configuration/inbound/index.zh.md
+++ b/docs/configuration/inbound/index.zh.md
@@ -17,22 +17,22 @@
| 类型 | 格式 | 注入支持 |
|---------------|------------------------------|------|
-| `direct` | [Direct](./direct/) | X |
-| `mixed` | [Mixed](./mixed/) | TCP |
-| `socks` | [SOCKS](./socks/) | TCP |
-| `http` | [HTTP](./http/) | TCP |
-| `shadowsocks` | [Shadowsocks](./shadowsocks/) | TCP |
-| `vmess` | [VMess](./vmess/) | TCP |
-| `trojan` | [Trojan](./trojan/) | TCP |
-| `naive` | [Naive](./naive/) | X |
-| `hysteria` | [Hysteria](./hysteria/) | X |
-| `shadowtls` | [ShadowTLS](./shadowtls/) | TCP |
-| `tuic` | [TUIC](./tuic/) | X |
-| `hysteria2` | [Hysteria2](./hysteria2/) | X |
-| `vless` | [VLESS](./vless/) | TCP |
-| `tun` | [Tun](./tun/) | X |
-| `redirect` | [Redirect](./redirect/) | X |
-| `tproxy` | [TProxy](./tproxy/) | X |
+| `direct` | [Direct](./direct) | X |
+| `mixed` | [Mixed](./mixed) | TCP |
+| `socks` | [SOCKS](./socks) | TCP |
+| `http` | [HTTP](./http) | TCP |
+| `shadowsocks` | [Shadowsocks](./shadowsocks) | TCP |
+| `vmess` | [VMess](./vmess) | TCP |
+| `trojan` | [Trojan](./trojan) | TCP |
+| `naive` | [Naive](./naive) | X |
+| `hysteria` | [Hysteria](./hysteria) | X |
+| `shadowtls` | [ShadowTLS](./shadowtls) | TCP |
+| `tuic` | [TUIC](./tuic) | X |
+| `hysteria2` | [Hysteria2](./hysteria2) | X |
+| `vless` | [VLESS](./vless) | TCP |
+| `tun` | [Tun](./tun) | X |
+| `redirect` | [Redirect](./redirect) | X |
+| `tproxy` | [TProxy](./tproxy) | X |
#### tag
diff --git a/docs/configuration/inbound/mixed.md b/docs/configuration/inbound/mixed.md
index 1f5bf0ac0d..62313f489d 100644
--- a/docs/configuration/inbound/mixed.md
+++ b/docs/configuration/inbound/mixed.md
@@ -21,7 +21,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
diff --git a/docs/configuration/inbound/naive.md b/docs/configuration/inbound/naive.md
index 0b4ff4b776..83f2656682 100644
--- a/docs/configuration/inbound/naive.md
+++ b/docs/configuration/inbound/naive.md
@@ -20,7 +20,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
diff --git a/docs/configuration/inbound/redirect.md b/docs/configuration/inbound/redirect.md
index 50a5bacd28..97736e286b 100644
--- a/docs/configuration/inbound/redirect.md
+++ b/docs/configuration/inbound/redirect.md
@@ -15,4 +15,4 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
diff --git a/docs/configuration/inbound/shadowsocks.md b/docs/configuration/inbound/shadowsocks.md
index 4072782bc4..415a59138e 100644
--- a/docs/configuration/inbound/shadowsocks.md
+++ b/docs/configuration/inbound/shadowsocks.md
@@ -50,7 +50,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
diff --git a/docs/configuration/inbound/shadowsocks.zh.md b/docs/configuration/inbound/shadowsocks.zh.md
index cdfd80806c..36a292bb0c 100644
--- a/docs/configuration/inbound/shadowsocks.zh.md
+++ b/docs/configuration/inbound/shadowsocks.zh.md
@@ -50,7 +50,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### 字段
diff --git a/docs/configuration/inbound/shadowtls.md b/docs/configuration/inbound/shadowtls.md
index db010b7908..e8b7ba23f7 100644
--- a/docs/configuration/inbound/shadowtls.md
+++ b/docs/configuration/inbound/shadowtls.md
@@ -35,7 +35,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
@@ -66,11 +66,11 @@ Only available in the ShadowTLS protocol 3.
==Required==
-Handshake server address and [Dial options](/configuration/shared/dial/).
+Handshake server address and [Dial options](/configuration/shared/dial).
#### handshake_for_server_name
-Handshake server address and [Dial options](/configuration/shared/dial/) for specific server name.
+Handshake server address and [Dial options](/configuration/shared/dial) for specific server name.
Only available in the ShadowTLS protocol 2/3.
diff --git a/docs/configuration/inbound/socks.md b/docs/configuration/inbound/socks.md
index 4937f39054..59f7d96f83 100644
--- a/docs/configuration/inbound/socks.md
+++ b/docs/configuration/inbound/socks.md
@@ -20,7 +20,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
diff --git a/docs/configuration/inbound/tproxy.md b/docs/configuration/inbound/tproxy.md
index 422885374f..4975931e89 100644
--- a/docs/configuration/inbound/tproxy.md
+++ b/docs/configuration/inbound/tproxy.md
@@ -17,7 +17,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
diff --git a/docs/configuration/inbound/trojan.md b/docs/configuration/inbound/trojan.md
index bd6c73b311..cd45539ddc 100644
--- a/docs/configuration/inbound/trojan.md
+++ b/docs/configuration/inbound/trojan.md
@@ -31,7 +31,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
@@ -65,4 +65,4 @@ See [Multiplex](/configuration/shared/multiplex#inbound) for details.
#### transport
-V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/).
+V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport).
diff --git a/docs/configuration/inbound/trojan.zh.md b/docs/configuration/inbound/trojan.zh.md
index d8b30cae68..54144ae680 100644
--- a/docs/configuration/inbound/trojan.zh.md
+++ b/docs/configuration/inbound/trojan.zh.md
@@ -67,4 +67,4 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### transport
-V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。
\ No newline at end of file
+V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。
\ No newline at end of file
diff --git a/docs/configuration/inbound/tuic.md b/docs/configuration/inbound/tuic.md
index 8a2d8c7e06..04624a895d 100644
--- a/docs/configuration/inbound/tuic.md
+++ b/docs/configuration/inbound/tuic.md
@@ -24,7 +24,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md
index 002c690a4b..d899bccdec 100644
--- a/docs/configuration/inbound/tun.md
+++ b/docs/configuration/inbound/tun.md
@@ -5,6 +5,7 @@ icon: material/alert-decagram
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [gso](#gso)
+ :material-plus: [gso_max_size](#gso_max_size)
:material-alert-decagram: [stack](#stack)
!!! quote ""
@@ -22,6 +23,7 @@ icon: material/alert-decagram
"inet6_address": "fdfe:dcba:9876::1/126",
"mtu": 9000,
"gso": false,
+ "gso_max_size": 65536,
"auto_route": true,
"strict_route": true,
"inet4_route_address": [
@@ -39,7 +41,6 @@ icon: material/alert-decagram
"fc00::/7"
],
"endpoint_independent_nat": false,
- "udp_timeout": "5m",
"stack": "system",
"include_interface": [
"lan0"
@@ -119,6 +120,18 @@ The maximum transmission unit.
Enable generic segmentation offload.
+#### gso_max_size
+
+!!! question "Since sing-box 1.8.0"
+
+!!! quote ""
+
+ Only supported on Linux.
+
+Maximum GSO packet size.
+
+`65536` is used by default.
+
#### auto_route
Set the default route to the Tun.
@@ -262,4 +275,4 @@ System HTTP proxy settings.
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md
index 6a80063487..e030587e6e 100644
--- a/docs/configuration/inbound/tun.zh.md
+++ b/docs/configuration/inbound/tun.zh.md
@@ -5,6 +5,7 @@ icon: material/alert-decagram
!!! quote "sing-box 1.8.0 中的更改"
:material-plus: [gso](#gso)
+ :material-plus: [gso_max_size](#gso_max_size)
:material-alert-decagram: [stack](#stack)
!!! quote ""
@@ -22,6 +23,7 @@ icon: material/alert-decagram
"inet6_address": "fdfe:dcba:9876::1/126",
"mtu": 9000,
"gso": false,
+ "gso_max_size": 65536,
"auto_route": true,
"strict_route": true,
"inet4_route_address": [
@@ -39,7 +41,6 @@ icon: material/alert-decagram
"fc00::/7"
],
"endpoint_independent_nat": false,
- "udp_timeout": "5m",
"stack": "system",
"include_interface": [
"lan0"
@@ -119,6 +120,18 @@ tun 接口的 IPv6 前缀。
启用通用分段卸载。
+#### gso_max_size
+
+!!! question "自 sing-box 1.8.0 起"
+
+!!! quote ""
+
+ 仅支持 Linux。
+
+通用分段卸载包的最大大小。
+
+默认使用 `65536`。
+
#### auto_route
设置到 Tun 的默认路由。
diff --git a/docs/configuration/inbound/vless.md b/docs/configuration/inbound/vless.md
index 93faf716a2..f78ae01c19 100644
--- a/docs/configuration/inbound/vless.md
+++ b/docs/configuration/inbound/vless.md
@@ -22,7 +22,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
@@ -56,4 +56,4 @@ See [Multiplex](/configuration/shared/multiplex#inbound) for details.
#### transport
-V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/).
+V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport).
diff --git a/docs/configuration/inbound/vless.zh.md b/docs/configuration/inbound/vless.zh.md
index 30b151dad3..4beecd6faa 100644
--- a/docs/configuration/inbound/vless.zh.md
+++ b/docs/configuration/inbound/vless.zh.md
@@ -56,4 +56,4 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### transport
-V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。
+V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。
diff --git a/docs/configuration/inbound/vmess.md b/docs/configuration/inbound/vmess.md
index f38a6cae8e..0e559d1ef6 100644
--- a/docs/configuration/inbound/vmess.md
+++ b/docs/configuration/inbound/vmess.md
@@ -22,7 +22,7 @@
### Listen Fields
-See [Listen Fields](/configuration/shared/listen/) for details.
+See [Listen Fields](/configuration/shared/listen) for details.
### Fields
@@ -51,4 +51,4 @@ See [Multiplex](/configuration/shared/multiplex#inbound) for details.
#### transport
-V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/).
+V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport).
diff --git a/docs/configuration/inbound/vmess.zh.md b/docs/configuration/inbound/vmess.zh.md
index 9aef44df51..9554ab79e0 100644
--- a/docs/configuration/inbound/vmess.zh.md
+++ b/docs/configuration/inbound/vmess.zh.md
@@ -51,4 +51,4 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### transport
-V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。
+V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。
diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index 0c22fc25ab..a4ade70726 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -18,15 +18,15 @@ sing-box uses JSON for configuration files.
### Fields
-| Key | Format |
-|----------------|---------------------------------|
-| `log` | [Log](./log/) |
-| `dns` | [DNS](./dns/) |
-| `ntp` | [NTP](./ntp/) |
-| `inbounds` | [Inbound](./inbound/) |
-| `outbounds` | [Outbound](./outbound/) |
-| `route` | [Route](./route/) |
-| `experimental` | [Experimental](./experimental/) |
+| Key | Format |
+|----------------|--------------------------------|
+| `log` | [Log](./log) |
+| `dns` | [DNS](./dns) |
+| `ntp` | [NTP](./ntp) |
+| `inbounds` | [Inbound](./inbound) |
+| `outbounds` | [Outbound](./outbound) |
+| `route` | [Route](./route) |
+| `experimental` | [Experimental](./experimental) |
### Check
diff --git a/docs/configuration/index.zh.md b/docs/configuration/index.zh.md
index 0d24a7ca7a..80b2ebd385 100644
--- a/docs/configuration/index.zh.md
+++ b/docs/configuration/index.zh.md
@@ -17,14 +17,14 @@ sing-box 使用 JSON 作为配置文件格式。
### 字段
-| Key | Format |
-|----------------|------------------------|
-| `log` | [日志](./log/) |
-| `dns` | [DNS](./dns/) |
-| `inbounds` | [入站](./inbound/) |
-| `outbounds` | [出站](./outbound/) |
-| `route` | [路由](./route/) |
-| `experimental` | [实验性](./experimental/) |
+| Key | Format |
+|----------------|-----------------------|
+| `log` | [日志](./log) |
+| `dns` | [DNS](./dns) |
+| `inbounds` | [入站](./inbound) |
+| `outbounds` | [出站](./outbound) |
+| `route` | [路由](./route) |
+| `experimental` | [实验性](./experimental) |
### 检查
diff --git a/docs/configuration/ntp/index.md b/docs/configuration/ntp/index.md
index b95b9b1f8b..8b3570a3d4 100644
--- a/docs/configuration/ntp/index.md
+++ b/docs/configuration/ntp/index.md
@@ -47,4 +47,4 @@ Time synchronization interval.
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
\ No newline at end of file
+See [Dial Fields](/configuration/shared/dial) for details.
\ No newline at end of file
diff --git a/docs/configuration/outbound/direct.md b/docs/configuration/outbound/direct.md
index c2f5671a6b..68eb0daf19 100644
--- a/docs/configuration/outbound/direct.md
+++ b/docs/configuration/outbound/direct.md
@@ -33,4 +33,4 @@ Protocol value can be `1` or `2`.
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/http.md b/docs/configuration/outbound/http.md
index 0b9dfa2369..c69422231f 100644
--- a/docs/configuration/outbound/http.md
+++ b/docs/configuration/outbound/http.md
@@ -55,4 +55,4 @@ TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/hysteria.md b/docs/configuration/outbound/hysteria.md
index 90190e051f..8caa9248c2 100644
--- a/docs/configuration/outbound/hysteria.md
+++ b/docs/configuration/outbound/hysteria.md
@@ -109,4 +109,4 @@ TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md
index ae0b96edda..26d5b72800 100644
--- a/docs/configuration/outbound/hysteria2.md
+++ b/docs/configuration/outbound/hysteria2.md
@@ -84,4 +84,4 @@ Enable debug information logging for Hysteria Brutal CC.
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/index.md b/docs/configuration/outbound/index.md
index c5dc291769..2fa8f09a9f 100644
--- a/docs/configuration/outbound/index.md
+++ b/docs/configuration/outbound/index.md
@@ -17,24 +17,24 @@
| Type | Format |
|----------------|--------------------------------|
-| `direct` | [Direct](./direct/) |
-| `block` | [Block](./block/) |
-| `socks` | [SOCKS](./socks/) |
-| `http` | [HTTP](./http/) |
-| `shadowsocks` | [Shadowsocks](./shadowsocks/) |
-| `vmess` | [VMess](./vmess/) |
-| `trojan` | [Trojan](./trojan/) |
-| `wireguard` | [Wireguard](./wireguard/) |
-| `hysteria` | [Hysteria](./hysteria/) |
-| `vless` | [VLESS](./vless/) |
-| `shadowtls` | [ShadowTLS](./shadowtls/) |
-| `tuic` | [TUIC](./tuic/) |
-| `hysteria2` | [Hysteria2](./hysteria2/) |
-| `tor` | [Tor](./tor/) |
-| `ssh` | [SSH](./ssh/) |
-| `dns` | [DNS](./dns/) |
-| `selector` | [Selector](./selector/) |
-| `urltest` | [URLTest](./urltest/) |
+| `direct` | [Direct](./direct) |
+| `block` | [Block](./block) |
+| `socks` | [SOCKS](./socks) |
+| `http` | [HTTP](./http) |
+| `shadowsocks` | [Shadowsocks](./shadowsocks) |
+| `vmess` | [VMess](./vmess) |
+| `trojan` | [Trojan](./trojan) |
+| `wireguard` | [Wireguard](./wireguard) |
+| `hysteria` | [Hysteria](./hysteria) |
+| `vless` | [VLESS](./vless) |
+| `shadowtls` | [ShadowTLS](./shadowtls) |
+| `tuic` | [TUIC](./tuic) |
+| `hysteria2` | [Hysteria2](./hysteria2) |
+| `tor` | [Tor](./tor) |
+| `ssh` | [SSH](./ssh) |
+| `dns` | [DNS](./dns) |
+| `selector` | [Selector](./selector) |
+| `urltest` | [URLTest](./urltest) |
#### tag
diff --git a/docs/configuration/outbound/index.zh.md b/docs/configuration/outbound/index.zh.md
index c7ee59e91d..2f1406f639 100644
--- a/docs/configuration/outbound/index.zh.md
+++ b/docs/configuration/outbound/index.zh.md
@@ -17,24 +17,24 @@
| 类型 | 格式 |
|----------------|--------------------------------|
-| `direct` | [Direct](./direct/) |
-| `block` | [Block](./block/) |
-| `socks` | [SOCKS](./socks/) |
-| `http` | [HTTP](./http/) |
-| `shadowsocks` | [Shadowsocks](./shadowsocks/) |
-| `vmess` | [VMess](./vmess/) |
-| `trojan` | [Trojan](./trojan/) |
-| `wireguard` | [Wireguard](./wireguard/) |
-| `hysteria` | [Hysteria](./hysteria/) |
-| `vless` | [VLESS](./vless/) |
-| `shadowtls` | [ShadowTLS](./shadowtls/) |
-| `tuic` | [TUIC](./tuic/) |
-| `hysteria2` | [Hysteria2](./hysteria2/) |
-| `tor` | [Tor](./tor/) |
-| `ssh` | [SSH](./ssh/) |
-| `dns` | [DNS](./dns/) |
-| `selector` | [Selector](./selector/) |
-| `urltest` | [URLTest](./urltest/) |
+| `direct` | [Direct](./direct) |
+| `block` | [Block](./block) |
+| `socks` | [SOCKS](./socks) |
+| `http` | [HTTP](./http) |
+| `shadowsocks` | [Shadowsocks](./shadowsocks) |
+| `vmess` | [VMess](./vmess) |
+| `trojan` | [Trojan](./trojan) |
+| `wireguard` | [Wireguard](./wireguard) |
+| `hysteria` | [Hysteria](./hysteria) |
+| `vless` | [VLESS](./vless) |
+| `shadowtls` | [ShadowTLS](./shadowtls) |
+| `tuic` | [TUIC](./tuic) |
+| `hysteria2` | [Hysteria2](./hysteria2) |
+| `tor` | [Tor](./tor) |
+| `ssh` | [SSH](./ssh) |
+| `dns` | [DNS](./dns) |
+| `selector` | [Selector](./selector) |
+| `urltest` | [URLTest](./urltest) |
#### tag
diff --git a/docs/configuration/outbound/shadowsocks.md b/docs/configuration/outbound/shadowsocks.md
index a088d2718e..6fc9348408 100644
--- a/docs/configuration/outbound/shadowsocks.md
+++ b/docs/configuration/outbound/shadowsocks.md
@@ -89,7 +89,7 @@ Both is enabled by default.
UDP over TCP configuration.
-See [UDP Over TCP](/configuration/shared/udp-over-tcp/) for details.
+See [UDP Over TCP](/configuration/shared/udp-over-tcp) for details.
Conflict with `multiplex`.
@@ -99,4 +99,4 @@ See [Multiplex](/configuration/shared/multiplex#outbound) for details.
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/shadowsocks.zh.md b/docs/configuration/outbound/shadowsocks.zh.md
index 818a4fa988..6d9b7a5c85 100644
--- a/docs/configuration/outbound/shadowsocks.zh.md
+++ b/docs/configuration/outbound/shadowsocks.zh.md
@@ -89,7 +89,7 @@ Shadowsocks SIP003 插件参数。
UDP over TCP 配置。
-参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp/)。
+参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp)。
与 `multiplex` 冲突。
diff --git a/docs/configuration/outbound/shadowtls.md b/docs/configuration/outbound/shadowtls.md
index a54391b500..1fa08c774f 100644
--- a/docs/configuration/outbound/shadowtls.md
+++ b/docs/configuration/outbound/shadowtls.md
@@ -53,4 +53,4 @@ TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/socks.md b/docs/configuration/outbound/socks.md
index b04e67b653..94d83fe56d 100644
--- a/docs/configuration/outbound/socks.md
+++ b/docs/configuration/outbound/socks.md
@@ -59,8 +59,8 @@ Both is enabled by default.
UDP over TCP protocol settings.
-See [UDP Over TCP](/configuration/shared/udp-over-tcp/) for details.
+See [UDP Over TCP](/configuration/shared/udp-over-tcp) for details.
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/socks.zh.md b/docs/configuration/outbound/socks.zh.md
index dd9a1ac9fe..75548da734 100644
--- a/docs/configuration/outbound/socks.zh.md
+++ b/docs/configuration/outbound/socks.zh.md
@@ -59,7 +59,7 @@ SOCKS5 密码。
UDP over TCP 配置。
-参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp/)。
+参阅 [UDP Over TCP](/zh/configuration/shared/udp-over-tcp)。
### 拨号字段
diff --git a/docs/configuration/outbound/ssh.md b/docs/configuration/outbound/ssh.md
index 45ec72b2b5..1384f8ddf4 100644
--- a/docs/configuration/outbound/ssh.md
+++ b/docs/configuration/outbound/ssh.md
@@ -68,4 +68,4 @@ Client version. Random version will be used if empty.
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/tor.md b/docs/configuration/outbound/tor.md
index ac77833509..e3188ea330 100644
--- a/docs/configuration/outbound/tor.md
+++ b/docs/configuration/outbound/tor.md
@@ -48,4 +48,4 @@ See [tor(1)](https://linux.die.net/man/1/tor) for details.
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/trojan.md b/docs/configuration/outbound/trojan.md
index 6a45fd02cc..34b16c7d38 100644
--- a/docs/configuration/outbound/trojan.md
+++ b/docs/configuration/outbound/trojan.md
@@ -55,8 +55,8 @@ See [Multiplex](/configuration/shared/multiplex#outbound) for details.
#### transport
-V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/).
+V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport).
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/trojan.zh.md b/docs/configuration/outbound/trojan.zh.md
index 2248c739bc..55bb970911 100644
--- a/docs/configuration/outbound/trojan.zh.md
+++ b/docs/configuration/outbound/trojan.zh.md
@@ -55,7 +55,7 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
#### transport
-V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。
+V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。
### 拨号字段
diff --git a/docs/configuration/outbound/tuic.md b/docs/configuration/outbound/tuic.md
index 4f4ef4850d..69a1a6d63c 100644
--- a/docs/configuration/outbound/tuic.md
+++ b/docs/configuration/outbound/tuic.md
@@ -68,7 +68,7 @@ Conflict with `udp_over_stream`.
#### udp_over_stream
-This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp/), designed to provide a QUIC
+This is the TUIC port of the [UDP over TCP protocol](/configuration/shared/udp-over-tcp), designed to provide a QUIC
stream based UDP relay mode that TUIC does not provide. Since it is an add-on protocol, you will need to use sing-box or
another program compatible with the protocol as a server.
@@ -93,4 +93,4 @@ TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/tuic.zh.md b/docs/configuration/outbound/tuic.zh.md
index ee8fd15d3f..aca0274543 100644
--- a/docs/configuration/outbound/tuic.zh.md
+++ b/docs/configuration/outbound/tuic.zh.md
@@ -66,7 +66,7 @@ UDP 包中继模式
#### udp_over_stream
-这是 TUIC 的 [UDP over TCP 协议](/configuration/shared/udp-over-tcp/) 移植, 旨在提供 TUIC 不提供的 基于 QUIC 流的 UDP 中继模式。 由于它是一个附加协议,因此您需要使用 sing-box 或其他兼容的程序作为服务器。
+这是 TUIC 的 [UDP over TCP 协议](/configuration/shared/udp-over-tcp) 移植, 旨在提供 TUIC 不提供的 基于 QUIC 流的 UDP 中继模式。 由于它是一个附加协议,因此您需要使用 sing-box 或其他兼容的程序作为服务器。
此模式在正确的 UDP 代理场景中没有任何积极作用,仅适用于中继流式 UDP 流量(基本上是 QUIC 流)。
diff --git a/docs/configuration/outbound/vless.md b/docs/configuration/outbound/vless.md
index 28134e55ce..0d2fadc646 100644
--- a/docs/configuration/outbound/vless.md
+++ b/docs/configuration/outbound/vless.md
@@ -75,8 +75,8 @@ See [Multiplex](/configuration/shared/multiplex#outbound) for details.
#### transport
-V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/).
+V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport).
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/vless.zh.md b/docs/configuration/outbound/vless.zh.md
index 8978a6ac6b..3d3f24ee65 100644
--- a/docs/configuration/outbound/vless.zh.md
+++ b/docs/configuration/outbound/vless.zh.md
@@ -75,7 +75,7 @@ UDP 包编码,默认使用 xudp。
#### transport
-V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。
+V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。
### 拨号字段
diff --git a/docs/configuration/outbound/vmess.md b/docs/configuration/outbound/vmess.md
index 536601afd7..ac29559e72 100644
--- a/docs/configuration/outbound/vmess.md
+++ b/docs/configuration/outbound/vmess.md
@@ -100,8 +100,8 @@ See [Multiplex](/configuration/shared/multiplex#outbound) for details.
#### transport
-V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport/).
+V2Ray Transport configuration, see [V2Ray Transport](/configuration/shared/v2ray-transport).
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/vmess.zh.md b/docs/configuration/outbound/vmess.zh.md
index 295b8ddef0..dbf1612e51 100644
--- a/docs/configuration/outbound/vmess.zh.md
+++ b/docs/configuration/outbound/vmess.zh.md
@@ -100,7 +100,7 @@ UDP 包编码。
#### transport
-V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport/)。
+V2Ray 传输配置,参阅 [V2Ray 传输层](/zh/configuration/shared/v2ray-transport)。
### 拨号字段
diff --git a/docs/configuration/outbound/wireguard.md b/docs/configuration/outbound/wireguard.md
index 4cd91d2225..53c39c2c6c 100644
--- a/docs/configuration/outbound/wireguard.md
+++ b/docs/configuration/outbound/wireguard.md
@@ -5,6 +5,7 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [gso](#gso)
+ :material-plus: [gso_max_size](#gso_max_size)
### Structure
@@ -17,6 +18,7 @@ icon: material/new-box
"server_port": 1080,
"system_interface": false,
"gso": false,
+ "gso_max_size": 65536,
"interface_name": "wg0",
"local_address": [
"10.0.0.2/32"
@@ -79,7 +81,19 @@ Custom interface name for system interface.
Only supported on Linux.
-Try to enable generic segmentation offload.
+Enable generic segmentation offload for system interface.
+
+#### gso_max_size
+
+!!! question "Since sing-box 1.8.0"
+
+!!! quote ""
+
+ Only supported on Linux.
+
+Maximum GSO packet size.
+
+`65536` is used by default.
#### local_address
@@ -150,4 +164,4 @@ Both is enabled by default.
### Dial Fields
-See [Dial Fields](/configuration/shared/dial/) for details.
+See [Dial Fields](/configuration/shared/dial) for details.
diff --git a/docs/configuration/outbound/wireguard.zh.md b/docs/configuration/outbound/wireguard.zh.md
index e853d72e85..b416e93224 100644
--- a/docs/configuration/outbound/wireguard.zh.md
+++ b/docs/configuration/outbound/wireguard.zh.md
@@ -5,6 +5,7 @@ icon: material/new-box
!!! quote "sing-box 1.8.0 中的更改"
:material-plus: [gso](#gso)
+ :material-plus: [gso_max_size](#gso_max_size)
### 结构
@@ -17,6 +18,7 @@ icon: material/new-box
"server_port": 1080,
"system_interface": false,
"gso": false,
+ "gso_max_size": 65536,
"interface_name": "wg0",
"local_address": [
"10.0.0.2/32"
@@ -67,7 +69,19 @@ icon: material/new-box
仅支持 Linux。
-尝试启用通用分段卸载。
+为系统接口启用通用分段卸载。
+
+#### gso_max_size
+
+!!! question "自 sing-box 1.8.0 起"
+
+!!! quote ""
+
+ 仅支持 Linux。
+
+通用分段卸载包的最大大小。
+
+默认使用 `65536`。
#### local_address
diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md
index 5deb44f5b7..7c1787eaea 100644
--- a/docs/configuration/route/index.md
+++ b/docs/configuration/route/index.md
@@ -32,18 +32,18 @@ icon: material/alert-decagram
| Key | Format |
|-----------|----------------------|
-| `geoip` | [GeoIP](./geoip/) |
-| `geosite` | [Geosite](./geosite/) |
+| `geoip` | [GeoIP](./geoip) |
+| `geosite` | [Geosite](./geosite) |
#### rules
-List of [Route Rule](./rule/)
+List of [Route Rule](./rule)
#### rule_set
!!! question "Since sing-box 1.8.0"
-List of [Rule Set](/configuration/rule-set/)
+List of [Rule Set](/configuration/rule-set)
#### final
diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md
index 290268f4a7..b5302727a3 100644
--- a/docs/configuration/route/index.zh.md
+++ b/docs/configuration/route/index.zh.md
@@ -30,20 +30,21 @@ icon: material/alert-decagram
### 字段
-| 键 | 格式 |
-|-----------|-----------------------|
-| `geoip` | [GeoIP](./geoip/) |
-| `geosite` | [Geosite](./geosite/) |
+| 键 | 格式 |
+|------------|-----------------------------------|
+| `geoip` | [GeoIP](./geoip) |
+| `geosite` | [Geosite](./geosite) |
+
#### rule
-一组 [路由规则](./rule/) 。
+一组 [路由规则](./rule)。
#### rule_set
!!! question "自 sing-box 1.8.0 起"
-一组 [规则集](/configuration/rule-set/)。
+一组 [规则集](/configuration/rule-set)。
#### final
diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md
index 9bedef8675..ec19d0efb7 100644
--- a/docs/configuration/route/rule.md
+++ b/docs/configuration/route/rule.md
@@ -144,7 +144,7 @@ icon: material/alert-decagram
#### inbound
-Tags of [Inbound](/configuration/inbound/).
+Tags of [Inbound](/configuration/inbound).
#### ip_version
diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md
index 0e6f989604..53cad72333 100644
--- a/docs/configuration/route/rule.zh.md
+++ b/docs/configuration/route/rule.zh.md
@@ -142,7 +142,7 @@ icon: material/alert-decagram
#### inbound
-[入站](/zh/configuration/inbound/) 标签。
+[入站](/zh/configuration/inbound) 标签。
#### ip_version
diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md
index 8e1934aec4..116c1ee66f 100644
--- a/docs/configuration/rule-set/source-format.md
+++ b/docs/configuration/rule-set/source-format.md
@@ -31,4 +31,4 @@ Version of Rule Set, must be `1`.
==Required==
-List of [Headless Rule](./headless-rule.md/).
+List of [Headless Rule](./headless-rule.md).
diff --git a/docs/configuration/shared/listen.md b/docs/configuration/shared/listen.md
index ae3ed6a403..c1b1ed3382 100644
--- a/docs/configuration/shared/listen.md
+++ b/docs/configuration/shared/listen.md
@@ -7,7 +7,7 @@
"tcp_fast_open": false,
"tcp_multi_path": false,
"udp_fragment": false,
- "udp_timeout": "5m",
+ "udp_timeout": 300,
"detour": "another-in",
"sniff": false,
"sniff_override_destination": false,
@@ -19,14 +19,14 @@
### Fields
-| Field | Available Context |
-|--------------------------------|---------------------------------------------------------|
-| `listen` | Needs to listen on TCP or UDP. |
-| `listen_port` | Needs to listen on TCP or UDP. |
-| `tcp_fast_open` | Needs to listen on TCP. |
-| `tcp_multi_path` | Needs to listen on TCP. |
-| `udp_timeout` | Needs to assemble UDP connections. |
-| `udp_disable_domain_unmapping` | Needs to listen on UDP and accept domain UDP addresses. |
+| Field | Available Context |
+|--------------------------------|-------------------------------------------------------------------|
+| `listen` | Needs to listen on TCP or UDP. |
+| `listen_port` | Needs to listen on TCP or UDP. |
+| `tcp_fast_open` | Needs to listen on TCP. |
+| `tcp_multi_path` | Needs to listen on TCP. |
+| `udp_timeout` | Needs to assemble UDP connections, currently Tun and Shadowsocks. |
+| `udp_disable_domain_unmapping` | Needs to listen on UDP and accept domain UDP addresses. |
#### listen
@@ -56,9 +56,7 @@ Enable UDP fragmentation.
#### udp_timeout
-UDP NAT expiration time in seconds.
-
-`5m` is used by default.
+UDP NAT expiration time in seconds, default is 300 (5 minutes).
#### detour
diff --git a/docs/configuration/shared/listen.zh.md b/docs/configuration/shared/listen.zh.md
index 398c98c53d..b7fd74870a 100644
--- a/docs/configuration/shared/listen.zh.md
+++ b/docs/configuration/shared/listen.zh.md
@@ -7,7 +7,7 @@
"tcp_fast_open": false,
"tcp_multi_path": false,
"udp_fragment": false,
- "udp_timeout": "5m",
+ "udp_timeout": 300,
"detour": "another-in",
"sniff": false,
"sniff_override_destination": false,
@@ -18,13 +18,13 @@
```
-| 字段 | 可用上下文 |
-|------------------|-----------------|
-| `listen` | 需要监听 TCP 或 UDP。 |
-| `listen_port` | 需要监听 TCP 或 UDP。 |
-| `tcp_fast_open` | 需要监听 TCP。 |
-| `tcp_multi_path` | 需要监听 TCP。 |
-| `udp_timeout` | 需要组装 UDP 连接。 |
+| 字段 | 可用上下文 |
+|-----------------------------------|-------------------------------------|
+| `listen` | 需要监听 TCP 或 UDP。 |
+| `listen_port` | 需要监听 TCP 或 UDP。 |
+| `tcp_fast_open` | 需要监听 TCP。 |
+| `tcp_multi_path` | 需要监听 TCP。 |
+| `udp_timeout` | 需要组装 UDP 连接, 当前为 Tun 和 Shadowsocks。 |
|
### 字段
@@ -57,9 +57,7 @@
#### udp_timeout
-UDP NAT 过期时间,以秒为单位。
-
-默认使用 `5m`。
+UDP NAT 过期时间,以秒为单位,默认为 300(5 分钟)。
#### detour
diff --git a/docs/configuration/shared/multiplex.md b/docs/configuration/shared/multiplex.md
index bf722127c9..eab2116381 100644
--- a/docs/configuration/shared/multiplex.md
+++ b/docs/configuration/shared/multiplex.md
@@ -35,7 +35,7 @@ If enabled, non-padded connections will be rejected.
#### brutal
-See [TCP Brutal](/configuration/shared/tcp-brutal/) for details.
+See [TCP Brutal](/configuration/shared/tcp-brutal) for details.
### Outbound Fields
@@ -83,4 +83,4 @@ Enable padding.
#### brutal
-See [TCP Brutal](/configuration/shared/tcp-brutal/) for details.
+See [TCP Brutal](/configuration/shared/tcp-brutal) for details.
diff --git a/docs/configuration/shared/multiplex.zh.md b/docs/configuration/shared/multiplex.zh.md
index 124fe49b00..ae1cad64e7 100644
--- a/docs/configuration/shared/multiplex.zh.md
+++ b/docs/configuration/shared/multiplex.zh.md
@@ -34,7 +34,7 @@
#### brutal
-参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal/)。
+参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal)。
### 出站字段
@@ -82,4 +82,4 @@
#### brutal
-参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal/)。
\ No newline at end of file
+参阅 [TCP Brutal](/zh/configuration/shared/tcp-brutal)。
\ No newline at end of file
diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md
index a5c7bec4c2..8dead243bb 100644
--- a/docs/configuration/shared/tls.md
+++ b/docs/configuration/shared/tls.md
@@ -173,9 +173,10 @@ By default, the maximum version is currently TLS 1.3.
#### cipher_suites
-A list of enabled TLS 1.0–1.2 cipher suites. The order of the list is ignored. Note that TLS 1.3 cipher suites are not configurable.
+The elliptic curves that will be used in an ECDHE handshake, in preference order.
-If empty, a safe default list is used. The default cipher suites might change over time.
+If empty, the default will be used. The client will use the first preference as the type for its key share in TLS 1.3.
+This may change in the future.
#### certificate
@@ -362,7 +363,7 @@ The MAC key.
ACME DNS01 challenge field. If configured, other challenge methods will be disabled.
-See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/) for details.
+See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge) for details.
### Reality Fields
@@ -372,7 +373,7 @@ See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/) for details
==Required==
-Handshake server address and [Dial options](/configuration/shared/dial/).
+Handshake server address and [Dial options](/configuration/shared/dial).
#### private_key
diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md
index 5a75945d15..1e00b93fbe 100644
--- a/docs/configuration/shared/tls.zh.md
+++ b/docs/configuration/shared/tls.zh.md
@@ -170,9 +170,12 @@ TLS 版本值:
#### cipher_suites
-启用的 TLS 1.0-1.2密码套件的列表。列表的顺序被忽略。请注意,TLS 1.3 的密码套件是不可配置的。
+将在 ECDHE 握手中使用的椭圆曲线,按优先顺序排列。
-如果为空,则使用安全的默认列表。默认密码套件可能会随着时间的推移而改变。
+如果为空,将使用默认值。
+
+客户端将使用第一个首选项作为其在 TLS 1.3 中的密钥共享类型。
+这在未来可能会改变。
#### certificate
@@ -349,7 +352,7 @@ MAC 密钥。
ACME DNS01 验证字段。如果配置,将禁用其他验证方法。
-参阅 [DNS01 验证字段](/configuration/shared/dns01_challenge/)。
+参阅 [DNS01 验证字段](/configuration/shared/dns01_challenge)。
### Reality 字段
diff --git a/docs/configuration/shared/v2ray-transport.md b/docs/configuration/shared/v2ray-transport.md
index afc332414b..d29572f6e1 100644
--- a/docs/configuration/shared/v2ray-transport.md
+++ b/docs/configuration/shared/v2ray-transport.md
@@ -53,15 +53,9 @@ The client will choose randomly and the server will verify if not empty.
#### path
-!!! warning
-
- V2Ray's documentation says that the path between the server and the client must be consistent,
- but the actual code allows the client to add any suffix to the path.
- sing-box uses the same behavior as V2Ray, but note that the behavior does not exist in `WebSocket` and `HTTPUpgrade` transport.
-
Path of HTTP request.
-The server will verify.
+The server will verify if not empty.
#### method
@@ -83,10 +77,7 @@ Specifies the time until idle clients should be closed with a GOAWAY frame. PING
In HTTP2 client:
-Specifies the period of time after which a health check will be performed using a ping frame if no frames have been
-received on the connection.Please note that a ping response is considered a received frame, so if there is no other
-traffic on the connection, the health check will be executed every interval. If the value is zero, no health check will
-be performed.
+Specifies the period of time after which a health check will be performed using a ping frame if no frames have been received on the connection. Please note that a ping response is considered a received frame, so if there is no other traffic on the connection, the health check will be executed every interval. If the value is zero, no health check will be performed.
Zero is used by default.
@@ -94,9 +85,7 @@ Zero is used by default.
In HTTP2 client:
-Specifies the timeout duration after sending a PING frame, within which a response must be received.
-If a response to the PING frame is not received within the specified timeout duration, the connection will be closed.
-The default timeout duration is 15 seconds.
+Specifies the timeout duration after sending a PING frame, within which a response must be received. If a response to the PING frame is not received within the specified timeout duration, the connection will be closed. The default timeout duration is 15 seconds.
### WebSocket
@@ -114,14 +103,12 @@ The default timeout duration is 15 seconds.
Path of HTTP request.
-The server will verify.
+The server will verify if not empty.
#### headers
Extra headers of HTTP request.
-The server will write in response if not empty.
-
#### max_early_data
Allowed payload size is in the request. Enabled if not zero.
@@ -171,8 +158,7 @@ Service name of gRPC.
In standard gRPC server/client:
-If the transport doesn't see any activity after a duration of this time,
-it pings the client to check if the connection is still active.
+If the transport doesn't see any activity after a duration of this time, it pings the client to check if the connection is still active.
In default gRPC server/client:
@@ -182,8 +168,7 @@ It has the same behavior as the corresponding setting in HTTP transport.
In standard gRPC server/client:
-The timeout that after performing a keepalive check, the client will wait for activity.
-If no activity is detected, the connection will be closed.
+The timeout that after performing a keepalive check, the client will wait for activity. If no activity is detected, the connection will be closed.
In default gRPC server/client:
@@ -193,9 +178,7 @@ It has the same behavior as the corresponding setting in HTTP transport.
In standard gRPC client:
-If enabled, the client transport sends keepalive pings even with no active connections.
-If disabled, when there are no active connections, `idle_timeout` and `ping_timeout` will be ignored and no keepalive
-pings will be sent.
+If enabled, the client transport sends keepalive pings even with no active connections. If disabled, when there are no active connections, `idle_timeout` and `ping_timeout` will be ignored and no keepalive pings will be sent.
Disabled by default.
@@ -220,7 +203,7 @@ The server will verify if not empty.
Path of HTTP request.
-The server will verify.
+The server will verify if not empty.
#### headers
diff --git a/docs/configuration/shared/v2ray-transport.zh.md b/docs/configuration/shared/v2ray-transport.zh.md
index e5bd7de72e..d0b4775da1 100644
--- a/docs/configuration/shared/v2ray-transport.zh.md
+++ b/docs/configuration/shared/v2ray-transport.zh.md
@@ -48,30 +48,25 @@ V2Ray Transport 是 v2ray 发明的一组私有协议,并污染了其他协议
主机域名列表。
-如果设置,客户端将随机选择,服务器将验证。
+客户端将随机选择,默认服务器将验证。
#### path
-!!! warning
-
- V2Ray 文档称服务端和客户端的路径必须一致,但实际代码允许客户端向路径添加任何后缀。
- sing-box 使用与 V2Ray 相同的行为,但请注意,该行为在 `WebSocket` 和 `HTTPUpgrade` 传输层中不存在。
-
HTTP 请求路径
-服务器将验证。
+默认服务器将验证。
#### method
HTTP 请求方法
-如果设置,服务器将验证。
+默认服务器将验证。
#### headers
HTTP 请求的额外标头
-如果设置,服务器将写入响应。
+默认服务器将写入响应。
#### idle_timeout
@@ -107,13 +102,11 @@ HTTP 请求的额外标头
HTTP 请求路径
-服务器将验证。
+默认服务器将验证。
#### headers
-HTTP 请求的额外标头
-
-如果设置,服务器将写入响应。
+HTTP 请求的额外标头。
#### max_early_data
@@ -203,16 +196,16 @@ gRPC 服务名称。
主机域名。
-服务器将验证。
+默认服务器将验证。
#### path
HTTP 请求路径
-服务器将验证。
+默认服务器将验证。
#### headers
HTTP 请求的额外标头。
-如果设置,服务器将写入响应。
+默认服务器将写入响应。
diff --git a/docs/deprecated.md b/docs/deprecated.md
index 0613122e86..2243acd351 100644
--- a/docs/deprecated.md
+++ b/docs/deprecated.md
@@ -19,7 +19,7 @@ The maxmind GeoIP National Database, as an IP classification database,
is not entirely suitable for traffic bypassing,
and all existing implementations suffer from high memory usage and difficult management.
-sing-box 1.8.0 introduces [Rule Set](/configuration/rule-set/), which can completely replace GeoIP,
+sing-box 1.8.0 introduces [Rule Set](/configuration/rule_set), which can completely replace GeoIP,
check [Migration](/migration/#migrate-geoip-to-rule-sets).
#### Geosite
@@ -29,7 +29,7 @@ Geosite is deprecated and may be removed in the future.
Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution,
suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management.
-sing-box 1.8.0 introduces [Rule Set](/configuration/rule-set/), which can completely replace Geosite,
+sing-box 1.8.0 introduces [Rule Set](/configuration/rule_set), which can completely replace Geosite,
check [Migration](/migration/#migrate-geosite-to-rule-sets).
Geosite,即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案,存在着大量问题,包括缺少维护、规则不准确、管理困难。
diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md
index 69ec4bdd7f..c8a61d049d 100644
--- a/docs/deprecated.zh.md
+++ b/docs/deprecated.zh.md
@@ -18,7 +18,7 @@ GeoIP 已废弃且可能在不久的将来移除。
maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过,
且现有的实现均存在内存使用大与管理困难的问题。
-sing-box 1.8.0 引入了[规则集](/configuration/rule-set/),
+sing-box 1.8.0 引入了[规则集](/configuration/rule_set),
可以完全替代 GeoIP, 参阅 [迁移指南](/zh/migration/#geoip)。
#### Geosite
@@ -28,7 +28,7 @@ Geosite 已废弃且可能在不久的将来移除。
Geosite,即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案,
存在着包括缺少维护、规则不准确和管理困难内的大量问题。
-sing-box 1.8.0 引入了[规则集](/configuration/rule-set/),
+sing-box 1.8.0 引入了[规则集](/configuration/rule_set),
可以完全替代 Geosite,参阅 [迁移指南](/zh/migration/#geosite)。
## 1.6.0
diff --git a/docs/installation/build-from-source.md b/docs/installation/build-from-source.md
index fc78d87f87..5d4c3f7912 100644
--- a/docs/installation/build-from-source.md
+++ b/docs/installation/build-from-source.md
@@ -53,19 +53,19 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
## :material-folder-settings: Build Tags
-| Build Tag | Enabled by default | Description |
-|------------------------------------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server/), [Naive inbound](/configuration/inbound/naive/), [Hysteria Inbound](/configuration/inbound/hysteria/), [Hysteria Outbound](/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). |
-| `with_grpc` | :material-close:️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). |
-| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). |
-| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). |
-| `with_ech` | :material-check: | Build with TLS ECH extension support for TLS outbound, see [TLS](/configuration/shared/tls#ech). |
-| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). |
-| `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls/). |
-| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). |
-| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). |
-| `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
-| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
-| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
+| Build Tag | Enabled by default | Description |
+|------------------------------------|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server), [Naive inbound](/configuration/inbound/naive), [Hysteria Inbound](/configuration/inbound/hysteria), [Hysteria Outbound](/configuration/outbound/hysteria) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). |
+| `with_grpc` | :material-close:️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). |
+| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server). |
+| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard). |
+| `with_ech` | :material-check: | Build with TLS ECH extension support for TLS outbound, see [TLS](/configuration/shared/tls#ech). |
+| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). |
+| `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls). |
+| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls). |
+| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). |
+| `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
+| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
+| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor). |
It is not recommended to change the default build tag list unless you really know what you are adding.
diff --git a/docs/installation/build-from-source.zh.md b/docs/installation/build-from-source.zh.md
index a1a09b9312..4cac68ba1d 100644
--- a/docs/installation/build-from-source.zh.md
+++ b/docs/installation/build-from-source.zh.md
@@ -55,17 +55,17 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
| 构建标记 | 默认启动 | 说明 |
|------------------------------------|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server/), [Naive inbound](/configuration/inbound/naive/), [Hysteria Inbound](/configuration/inbound/hysteria/), [Hysteria Outbound](/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). |
+| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server), [Naive inbound](/configuration/inbound/naive), [Hysteria Inbound](/configuration/inbound/hysteria), [Hysteria Outbound](/configuration/outbound/hysteria) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). |
| `with_grpc` | :material-close:️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). |
-| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). |
-| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). |
+| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server). |
+| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard). |
| `with_ech` | :material-check: | Build with TLS ECH extension support for TLS outbound, see [TLS](/configuration/shared/tls#ech). |
| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). |
-| `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls/). |
-| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). |
+| `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls). |
+| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls). |
| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). |
| `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
-| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
+| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor). |
除非您确实知道您正在启用什么,否则不建议更改默认构建标签列表。
diff --git a/docs/installation/package-manager.md b/docs/installation/package-manager.md
index c605ffb0c2..6ce1669d02 100644
--- a/docs/installation/package-manager.md
+++ b/docs/installation/package-manager.md
@@ -28,18 +28,18 @@ icon: material/package
=== ":material-linux: Linux"
- | Type | Platform | Link | Command | Actively maintained |
- |----------|---------------|-------------------------|------------------------------|---------------------|
- | APK | Alpine | [sing-box][alpine] | `apk add sing-box` | :material-check: |
- | AUR | Arch Linux | [sing-box][aur] ᴬᵁᴿ | `? -S sing-box` | :material-check: |
- | nixpkgs | NixOS | [sing-box][nixpkgs] | `nix-env -iA nixos.sing-box` | :material-check: |
- | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: |
+ | Type | Platform | Link | Command | Actively maintained |
+ |----------|--------------------|---------------------|------------------------------|---------------------|
+ | AUR | (Linux) Arch Linux | [sing-box][aur] ᴬᵁᴿ | `? -S sing-box` | :material-check: |
+ | nixpkgs | (Linux) NixOS | [sing-box][nixpkgs] | `nix-env -iA nixos.sing-box` | :material-check: |
+ | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: |
+ | Alpine | (Linux) Alpine | [sing-box][alpine] | `apk add sing-box` | :material-alert: |
=== ":material-apple: macOS"
- | Type | Platform | Link | Command | Actively maintained |
- |----------|----------|------------------|-------------------------|---------------------|
- | Homebrew | macOS | [sing-box][brew] | `brew install sing-box` | :material-check: |
+ | Type | Platform | Link | Command | Actively maintained |
+ |----------|---------------|------------------|-------------------------|---------------------|
+ | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: |
=== ":material-microsoft-windows: Windows"
@@ -55,12 +55,6 @@ icon: material/package
|------------|--------------------|---------------------|------------------------------|---------------------|
| Termux | Android | [sing-box][termux] | `pkg add sing-box` | :material-check: |
-=== ":material-freebsd: FreeBSD"
-
- | Type | Platform | Link | Command | Actively maintained |
- |------------|----------|-------------------|------------------------|---------------------|
- | FreshPorts | FreeBSD | [sing-box][ports] | `pkg install sing-box` | :material-alert: |
-
## :material-book-multiple: Service Management
For Linux systems with [systemd][systemd], usually the installation already includes a sing-box service,
@@ -83,11 +77,9 @@ you can manage the service using the following command:
[nixpkgs]: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/tools/networking/sing-box/default.nix
-[brew]: https://formulae.brew.sh/formula/sing-box
-
-[openwrt]: https://github.com/openwrt/packages/tree/master/net/sing-box
+[termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box
-[immortalwrt]: https://github.com/immortalwrt/packages/tree/master/net/sing-box
+[brew]: https://formulae.brew.sh/formula/sing-box
[choco]: https://chocolatey.org/packages/sing-box
@@ -95,8 +87,4 @@ you can manage the service using the following command:
[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/SagerNet/sing-box
-[termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box
-
-[ports]: https://www.freshports.org/net/sing-box
-
-[systemd]: https://systemd.io/
+[systemd]: https://systemd.io/
\ No newline at end of file
diff --git a/docs/installation/package-manager.zh.md b/docs/installation/package-manager.zh.md
index e3c406302d..3d2aee08e6 100644
--- a/docs/installation/package-manager.zh.md
+++ b/docs/installation/package-manager.zh.md
@@ -28,18 +28,18 @@ icon: material/package
=== ":material-linux: Linux"
- | 类型 | 平台 | 链接 | 命令 | 活跃维护 |
- |----------|------------|---------------------|------------------------------|------------------|
- | Alpine | Alpine | [sing-box][alpine] | `apk add sing-box` | :material-check: |
- | AUR | Arch Linux | [sing-box][aur] ᴬᵁᴿ | `? -S sing-box` | :material-check: |
- | nixpkgs | NixOS | [sing-box][nixpkgs] | `nix-env -iA nixos.sing-box` | :material-check: |
- | Homebrew | Linux | [sing-box][brew] | `brew install sing-box` | :material-check: |
+ | 类型 | 平台 | 链接 | 命令 | 活跃维护 |
+ |----------|--------------------|---------------------|------------------------------|------------------|
+ | AUR | (Linux) Arch Linux | [sing-box][aur] ᴬᵁᴿ | `? -S sing-box` | :material-check: |
+ | nixpkgs | (Linux) NixOS | [sing-box][nixpkgs] | `nix-env -iA nixos.sing-box` | :material-check: |
+ | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: |
+ | Alpine | (Linux) Alpine | [sing-box][alpine] | `apk add sing-box` | :material-alert: |
=== ":material-apple: macOS"
- | 类型 | 平台 | 链接 | 命令 | 活跃维护 |
- |----------|-------|------------------|-------------------------|------------------|
- | Homebrew | macOS | [sing-box][brew] | `brew install sing-box` | :material-check: |
+ | 类型 | 平台 | 链接 | 命令 | 活跃维护 |
+ |----------|---------------|------------------|-------------------------|------------------|
+ | Homebrew | macOS / Linux | [sing-box][brew] | `brew install sing-box` | :material-check: |
=== ":material-microsoft-windows: Windows"
@@ -55,12 +55,6 @@ icon: material/package
|--------|---------|--------------------|--------------------|------------------|
| Termux | Android | [sing-box][termux] | `pkg add sing-box` | :material-check: |
-=== ":material-freebsd: FreeBSD"
-
- | 类型 | 平台 | 链接 | 命令 | 活跃维护 |
- |------------|---------|-------------------|------------------------|------------------|
- | FreshPorts | FreeBSD | [sing-box][ports] | `pkg install sing-box` | :material-alert: |
-
## :material-book-multiple: 服务管理
对于带有 [systemd][systemd] 的 Linux 系统,通常安装已经包含 sing-box 服务,
@@ -83,6 +77,8 @@ icon: material/package
[nixpkgs]: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/tools/networking/sing-box/default.nix
+[termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box
+
[brew]: https://formulae.brew.sh/formula/sing-box
[choco]: https://chocolatey.org/packages/sing-box
@@ -91,8 +87,4 @@ icon: material/package
[winget]: https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/SagerNet/sing-box
-[termux]: https://github.com/termux/termux-packages/tree/master/packages/sing-box
-
-[ports]: https://www.freshports.org/net/sing-box
-
-[systemd]: https://systemd.io/
+[systemd]: https://systemd.io/
\ No newline at end of file
diff --git a/docs/manual/proxy-protocol/shadowsocks.md b/docs/manual/proxy-protocol/shadowsocks.md
index 94821d83d1..be0bc20800 100644
--- a/docs/manual/proxy-protocol/shadowsocks.md
+++ b/docs/manual/proxy-protocol/shadowsocks.md
@@ -35,6 +35,10 @@ but only AEAD 2022 ciphers TCP with multiplexing is recommended.
## :material-server: Server Example
+!!! info ""
+
+ Password of cipher `2022-blake3-aes-128-gcm` can be generated by command `sing-box generate rand 16 --base64`
+
=== ":material-account: Single-user"
```json
diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md
index b01fa71c8a..11bc40ceb4 100644
--- a/docs/manual/proxy/client.md
+++ b/docs/manual/proxy/client.md
@@ -330,7 +330,10 @@ flowchart TB
"invert": true
},
{
- "geosite": "cn",
+ "geosite": [
+ "cn",
+ "category-companies@cn"
+ ],
}
],
"server": "local"
@@ -382,7 +385,10 @@ flowchart TB
"invert": true
},
{
- "rule_set": "geosite-cn",
+ "rule_set": [
+ "geosite-cn",
+ "geosite-category-companies@cn"
+ ]
}
],
"server": "local"
@@ -402,6 +408,12 @@ flowchart TB
"tag": "geosite-geolocation-!cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs"
+ },
+ {
+ "type": "remote",
+ "tag": "geosite-category-companies@cn",
+ "format": "binary",
+ "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs"
}
]
}
@@ -475,7 +487,10 @@ flowchart TB
"invert": true
},
{
- "geosite": "cn",
+ "geosite": [
+ "cn",
+ "category-companies@cn"
+ ],
"geoip": "cn"
}
],
@@ -555,7 +570,8 @@ flowchart TB
{
"rule_set": [
"geoip-cn",
- "geosite-cn"
+ "geosite-cn",
+ "geosite-category-companies@cn"
]
}
],
@@ -580,6 +596,12 @@ flowchart TB
"tag": "geosite-geolocation-!cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs"
+ },
+ {
+ "type": "remote",
+ "tag": "geosite-category-companies@cn",
+ "format": "binary",
+ "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-companies@cn.srs"
}
]
}
diff --git a/docs/manual/proxy/tun.md b/docs/manual/proxy/tun.md
new file mode 100644
index 0000000000..65cfb1cfd1
--- /dev/null
+++ b/docs/manual/proxy/tun.md
@@ -0,0 +1,66 @@
+# :material-expansion-card: TUN
+
+## :material-text-box: Definition
+
+Refers to TUNnel, a virtual network device supported by the kernel.
+It’s also used in sing-box to denote the extensive functionality surrounding TUN inbound:
+including traffic assembly, automatic routing, and network and default interface monitoring.
+
+The following flow chart describes the minimal TUN-based transparent proxy process in sing-box:
+
+``` mermaid
+flowchart LR
+ subgraph inbound [Inbound]
+ direction TB
+ packet[IP Packet]
+ packet --> windows[Windows / macOS]
+ packet --> linux[Linux]
+ tun[TUN interface]
+ windows -. route .-> tun
+ linux -. iproute2 route/rule .-> tun
+ tun --> gvisor[gVisor TUN stack]
+ tun --> system[system TUN stack]
+ assemble([L3 to L4 assemble])
+ gvisor --> assemble
+ system --> assemble
+ assemble --> conn[TCP and UDP connections]
+ conn --> router[sing-box Router]
+ end
+
+ subgraph outbound [Outbound]
+ direction TB
+ direct[Direct outbound]
+ proxy[Proxy outbounds]
+ direct --> adi([auto detect interface])
+ proxy --> adi
+ adi --> default[Default network interface in the system]
+ default --> destination[Destination server]
+ default --> proxy_server[Proxy server]
+ proxy_server --> destination
+ end
+
+ inbound --> outbound
+```
+
+## :material-help-box: How to
+
+A basic TUN-based transparent proxy configuration file includes: an TUN inbound, `route.auto_detect_interface`, like:
+
+```json
+{
+ "inbounds": [
+ {
+ "type": "tun",
+ "inet4_address": "172.19.0.1/30",
+ "inet6_address": "fdfe:dcba:9876::1/126",
+ "auto_route": true,
+ "strict_route": true
+ }
+ ],
+ "route": {
+ "auto_detect_interface": true
+ }
+}
+```
+
+TODO: finish this wiki
\ No newline at end of file
diff --git a/docs/migration.md b/docs/migration.md
index b282a90fc7..ea2875265a 100644
--- a/docs/migration.md
+++ b/docs/migration.md
@@ -2,36 +2,18 @@
icon: material/arrange-bring-forward
---
-## 1.9.0
+## 1.8.0
!!! warning "Unstable"
This version is still under development, and the following migration guide may be changed in the future.
-### `domain_suffix` behavior update
-
-For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects.
-
-sing-box 1.9.0 modifies the behavior of `domain_suffix`: If the rule value is prefixed with `.`,
-the behavior is unchanged, otherwise it matches `(domain|.+\.domain)` instead.
-
-### `process_path` format update on Windows
-
-The `process_path` rule of sing-box is inherited from Clash,
-the original code uses the local system's path format (e.g. `\Device\HarddiskVolume1\folder\program.exe`),
-but when the device has multiple disks, the HarddiskVolume serial number is not stable.
-
-sing-box 1.9.0 make QueryFullProcessImageNameW output a Win32 path (such as `C:\folder\program.exe`),
-which will disrupt the existing `process_path` use cases in Windows.
-
-## 1.8.0
-
### :material-close-box: Migrate cache file from Clash API to independent options
!!! info "References"
- [Clash API](/configuration/experimental/clash-api/) /
- [Cache File](/configuration/experimental/cache-file/)
+ [Clash API](/configuration/experimental/clash-api) /
+ [Cache File](/configuration/experimental/cache-file)
=== ":material-card-remove: Deprecated"
@@ -68,11 +50,11 @@ which will disrupt the existing `process_path` use cases in Windows.
!!! info "References"
- [GeoIP](/configuration/route/geoip/) /
- [Route](/configuration/route/) /
- [Route Rule](/configuration/route/rule/) /
- [DNS Rule](/configuration/dns/rule/) /
- [Rule Set](/configuration/rule-set/)
+ [GeoIP](/configuration/route/geoip) /
+ [Route](/configuration/route) /
+ [Route Rule](/configuration/route/rule) /
+ [DNS Rule](/configuration/dns/rule) /
+ [Rule Set](/configuration/rule-set)
!!! tip
@@ -153,11 +135,11 @@ which will disrupt the existing `process_path` use cases in Windows.
!!! info "References"
- [Geosite](/configuration/route/geosite/) /
- [Route](/configuration/route/) /
- [Route Rule](/configuration/route/rule/) /
- [DNS Rule](/configuration/dns/rule/) /
- [Rule Set](/configuration/rule-set/)
+ [Geosite](/configuration/route/geosite) /
+ [Route](/configuration/route) /
+ [Route Rule](/configuration/route/rule) /
+ [DNS Rule](/configuration/dns/rule) /
+ [Rule Set](/configuration/rule-set)
!!! tip
diff --git a/docs/migration.zh.md b/docs/migration.zh.md
index bd63bf1767..cdde2a6377 100644
--- a/docs/migration.zh.md
+++ b/docs/migration.zh.md
@@ -2,35 +2,18 @@
icon: material/arrange-bring-forward
---
-## 1.9.0
+## 1.8.0
!!! warning "不稳定的"
该版本仍在开发中,迁移指南可能将在未来更改。
-### `domain_suffix` 行为更新
-
-由于历史原因,sing-box 的 `domain_suffix` 规则匹配字面前缀,而不与其他项目相同。
-
-sing-box 1.9.0 修改了 `domain_suffix` 的行为:如果规则值以 `.` 为前缀则行为不变,否则改为匹配 `(domain|.+\.domain)`。
-
-### 对 Windows 上 `process_path` 格式的更新
-
-sing-box 的 `process_path` 规则继承自Clash,
-原始代码使用本地系统的路径格式(例如 `\Device\HarddiskVolume1\folder\program.exe`),
-但是当设备有多个硬盘时,该 HarddiskVolume 系列号并不稳定。
-
-sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\folder\program.exe`),
-这将会破坏现有的 Windows `process_path` 用例。
-
-## 1.8.0
-
### :material-close-box: 将缓存文件从 Clash API 迁移到独立选项
!!! info "参考"
- [Clash API](/zh/configuration/experimental/clash-api/) /
- [Cache File](/zh/configuration/experimental/cache-file/)
+ [Clash API](/zh/configuration/experimental/clash-api) /
+ [Cache File](/zh/configuration/experimental/cache-file)
=== ":material-card-remove: 弃用的"
@@ -67,11 +50,11 @@ sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\fold
!!! info "参考"
- [GeoIP](/zh/configuration/route/geoip/) /
- [路由](/zh/configuration/route/) /
- [路由规则](/zh/configuration/route/rule/) /
- [DNS 规则](/zh/configuration/dns/rule/) /
- [规则集](/zh/configuration/rule-set/)
+ [GeoIP](/zh/configuration/route/geoip) /
+ [路由](/zh/configuration/route) /
+ [路由规则](/zh/configuration/route/rule) /
+ [DNS 规则](/zh/configuration/dns/rule) /
+ [规则集](/zh/configuration/rule-set)
!!! tip
@@ -152,11 +135,11 @@ sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\fold
!!! info "参考"
- [Geosite](/zh/configuration/route/geosite/) /
- [路由](/zh/configuration/route/) /
- [路由规则](/zh/configuration/route/rule/) /
- [DNS 规则](/zh/configuration/dns/rule/) /
- [规则集](/zh/configuration/rule-set/)
+ [Geosite](/zh/configuration/route/geosite) /
+ [路由](/zh/configuration/route) /
+ [路由规则](/zh/configuration/route/rule) /
+ [DNS 规则](/zh/configuration/dns/rule) /
+ [规则集](/zh/configuration/rule-set)
!!! tip
diff --git a/experimental/clashapi/clash_dashboard.go b/experimental/clashapi/clash_dashboard.go
new file mode 100644
index 0000000000..d694120262
--- /dev/null
+++ b/experimental/clashapi/clash_dashboard.go
@@ -0,0 +1,38 @@
+//go:build with_clash_dashboard
+
+package clashapi
+
+import (
+ "embed"
+ _ "embed"
+ "io/fs"
+ "net/http"
+ "path"
+
+ "github.com/go-chi/chi/v5"
+)
+
+//go:embed clash_dashboard
+var dashboardFS embed.FS
+
+type fsFunc func(name string) (fs.File, error)
+
+func (f fsFunc) Open(name string) (fs.File, error) {
+ return f(name)
+}
+
+func initDashboard() (func(r chi.Router), error) {
+ handler := fsFunc(func(name string) (fs.File, error) {
+ assetPath := path.Join("clash_dashboard", name)
+ file, err := dashboardFS.Open(assetPath)
+ if err != nil {
+ return nil, err
+ }
+ return file, err
+ })
+
+ return func(r chi.Router) {
+ r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP)
+ r.Get("/ui/*", http.StripPrefix("/ui", http.FileServer(http.FS(handler))).ServeHTTP)
+ }, nil
+}
diff --git a/experimental/clashapi/clash_dashboard_stub.go b/experimental/clashapi/clash_dashboard_stub.go
new file mode 100644
index 0000000000..3276067983
--- /dev/null
+++ b/experimental/clashapi/clash_dashboard_stub.go
@@ -0,0 +1,13 @@
+//go:build !with_clash_dashboard
+
+package clashapi
+
+import (
+ E "github.com/sagernet/sing/common/exceptions"
+
+ "github.com/go-chi/chi/v5"
+)
+
+func initDashboard() (func(r chi.Router), error) {
+ return nil, E.New(`Clash Dashboard is not included in this build, rebuild with -tags with_clash_dashboard`)
+}
diff --git a/experimental/clashapi/configs.go b/experimental/clashapi/configs.go
index 9d1e6109d2..1759a01019 100644
--- a/experimental/clashapi/configs.go
+++ b/experimental/clashapi/configs.go
@@ -12,8 +12,10 @@ import (
func configRouter(server *Server, logFactory log.Factory) http.Handler {
r := chi.NewRouter()
r.Get("/", getConfigs(server, logFactory))
- r.Put("/", updateConfigs)
+ // r.Put("/", updateConfigs)
+ r.Put("/", reload(server))
r.Patch("/", patchConfigs(server))
+ r.Post("/geo", updateGeo(server))
return r
}
@@ -66,3 +68,10 @@ func patchConfigs(server *Server) func(w http.ResponseWriter, r *http.Request) {
func updateConfigs(w http.ResponseWriter, r *http.Request) {
render.NoContent(w, r)
}
+
+func updateGeo(server *Server) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ server.router.UpdateGeoDatabase()
+ render.NoContent(w, r)
+ }
+}
diff --git a/experimental/clashapi/provider.go b/experimental/clashapi/provider.go
index 352b28944e..97398e2a1e 100644
--- a/experimental/clashapi/provider.go
+++ b/experimental/clashapi/provider.go
@@ -3,50 +3,104 @@ package clashapi
import (
"context"
"net/http"
+ "sync"
+ "time"
+
+ "github.com/sagernet/sing-box/adapter"
+ "github.com/sagernet/sing-box/common/urltest"
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/outbound"
+ "github.com/sagernet/sing/common/json/badjson"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
-func proxyProviderRouter() http.Handler {
+func proxyProviderRouter(server *Server, router adapter.Router) http.Handler {
r := chi.NewRouter()
- r.Get("/", getProviders)
+ r.Get("/", getProviders(server, router))
r.Route("/{name}", func(r chi.Router) {
- r.Use(parseProviderName, findProviderByName)
- r.Get("/", getProvider)
+ r.Use(parseProviderName, findProviderByName(router))
+ r.Get("/", getProvider(server, router))
r.Put("/", updateProvider)
- r.Get("/healthcheck", healthCheckProvider)
+ r.Get("/healthcheck", healthCheckProvider(server, router))
})
return r
}
-func getProviders(w http.ResponseWriter, r *http.Request) {
- render.JSON(w, r, render.M{
- "providers": render.M{},
- })
+func getProviders(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ proxyProviders := router.ProxyProviders()
+ if proxyProviders == nil {
+ render.Status(r, http.StatusOK)
+ render.JSON(w, r, render.M{
+ "providers": render.M{},
+ })
+ return
+ }
+ m := render.M{}
+ for _, proxyProvider := range proxyProviders {
+ m[proxyProvider.Tag()] = proxyProviderInfo(server, router, proxyProvider)
+ }
+ render.JSON(w, r, render.M{
+ "providers": m,
+ })
+ }
}
-func getProvider(w http.ResponseWriter, r *http.Request) {
- /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
- render.JSON(w, r, provider)*/
- render.NoContent(w, r)
+func getProvider(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ proxyProvider := r.Context().Value(CtxKeyProvider).(adapter.ProxyProvider)
+ render.JSON(w, r, proxyProviderInfo(server, router, proxyProvider))
+ render.NoContent(w, r)
+ }
}
func updateProvider(w http.ResponseWriter, r *http.Request) {
- /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
- if err := provider.Update(); err != nil {
- render.Status(r, http.StatusServiceUnavailable)
- render.JSON(w, r, newError(err.Error()))
- return
- }*/
+ proxyProvider := r.Context().Value(CtxKeyProvider).(adapter.ProxyProvider)
+ proxyProvider.Update()
render.NoContent(w, r)
}
-func healthCheckProvider(w http.ResponseWriter, r *http.Request) {
- /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
- provider.HealthCheck()*/
- render.NoContent(w, r)
+func healthCheckProvider(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ proxyProvider := r.Context().Value(CtxKeyProvider).(adapter.ProxyProvider)
+ ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
+ defer cancel()
+
+ wg := &sync.WaitGroup{}
+
+ proxyProviderOutbound, loaded := router.Outbound(proxyProvider.Tag())
+ if loaded {
+ proxyProviderGroupOutbound := proxyProviderOutbound.(adapter.OutboundGroup)
+ for _, outboundTag := range proxyProviderGroupOutbound.All() {
+ out, loaded := router.Outbound(outboundTag)
+ if loaded {
+ wg.Add(1)
+ go func(proxy adapter.Outbound) {
+ defer wg.Done()
+ delay, err := urltest.URLTest(ctx, "", proxy)
+ defer func() {
+ realTag := outbound.RealTag(proxy)
+ if err != nil {
+ server.urlTestHistory.DeleteURLTestHistory(realTag)
+ } else {
+ server.urlTestHistory.StoreURLTestHistory(realTag, &urltest.History{
+ Time: time.Now(),
+ Delay: delay,
+ })
+ }
+ }()
+ }(out)
+ }
+ }
+ }
+
+ wg.Wait()
+
+ render.NoContent(w, r)
+ }
}
func parseProviderName(next http.Handler) http.Handler {
@@ -57,18 +111,58 @@ func parseProviderName(next http.Handler) http.Handler {
})
}
-func findProviderByName(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- /*name := r.Context().Value(CtxKeyProviderName).(string)
- providers := tunnel.ProxyProviders()
- provider, exist := providers[name]
- if !exist {*/
- render.Status(r, http.StatusNotFound)
- render.JSON(w, r, ErrNotFound)
- //return
- //}
-
- // ctx := context.WithValue(r.Context(), CtxKeyProvider, provider)
- // next.ServeHTTP(w, r.WithContext(ctx))
- })
+func findProviderByName(router adapter.Router) func(next http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ name := r.Context().Value(CtxKeyProviderName).(string)
+ proxyProvider, loaded := router.ProxyProvider(name)
+ if !loaded {
+ render.Status(r, http.StatusNotFound)
+ render.JSON(w, r, ErrNotFound)
+ return
+ }
+
+ ctx := context.WithValue(r.Context(), CtxKeyProvider, proxyProvider)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+ }
+}
+
+func proxyProviderInfo(server *Server, router adapter.Router, proxyProvider adapter.ProxyProvider) *badjson.JSONObject {
+ var info badjson.JSONObject
+ info.Put("name", proxyProvider.Tag())
+ info.Put("type", "Proxy")
+ info.Put("vehicleType", "HTTP")
+ subscriptionInfo := render.M{}
+ download, upload, total, expire, err := proxyProvider.GetClashInfo()
+ if err == nil {
+ subscriptionInfo["Download"] = download
+ subscriptionInfo["Upload"] = upload
+ subscriptionInfo["Total"] = total
+ subscriptionInfo["Expire"] = expire.Unix()
+ } else {
+ subscriptionInfo["Download"] = 0
+ subscriptionInfo["Upload"] = 0
+ subscriptionInfo["Total"] = 0
+ subscriptionInfo["Expire"] = 0
+ }
+ info.Put("subscriptionInfo", subscriptionInfo)
+ info.Put("updatedAt", proxyProvider.LastUpdateTime())
+ proxyProviderOutbound, loaded := router.Outbound(proxyProvider.Tag())
+ if loaded {
+ proxies := make([]*badjson.JSONObject, 0)
+ proxyProviderGroupOutbound := proxyProviderOutbound.(adapter.OutboundGroup)
+ for _, outboundTag := range proxyProviderGroupOutbound.All() {
+ out, loaded := router.Outbound(outboundTag)
+ if loaded {
+ switch out.Type() {
+ case C.TypeSelector, C.TypeURLTest, C.TypeJSTest:
+ continue
+ }
+ proxies = append(proxies, proxyInfo(server, out))
+ }
+ }
+ info.Put("proxies", proxies)
+ }
+ return &info
}
diff --git a/experimental/clashapi/reload.go b/experimental/clashapi/reload.go
new file mode 100644
index 0000000000..9c87570c59
--- /dev/null
+++ b/experimental/clashapi/reload.go
@@ -0,0 +1,19 @@
+//go:build !android && !ios
+
+package clashapi
+
+import (
+ "net/http"
+
+ "github.com/go-chi/render"
+)
+
+func reload(server *Server) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ defer func() {
+ server.logger.Warn("box restarting...")
+ server.router.Reload()
+ }()
+ render.NoContent(w, r)
+ }
+}
diff --git a/experimental/clashapi/reload_stub.go b/experimental/clashapi/reload_stub.go
new file mode 100644
index 0000000000..55565930d7
--- /dev/null
+++ b/experimental/clashapi/reload_stub.go
@@ -0,0 +1,19 @@
+//go:build android || ios
+
+package clashapi
+
+import (
+ "net/http"
+
+ "github.com/go-chi/render"
+)
+
+var ErrOSNotSupported = &HTTPError{
+ Message: "OS not supported",
+}
+
+func reload(server *Server) func(w http.ResponseWriter, r *http.Request) {
+ return func(w http.ResponseWriter, r *http.Request) {
+ render.JSON(w, r, ErrOSNotSupported)
+ }
+}
diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go
index 1eec8448af..4cc2fd2f63 100644
--- a/experimental/clashapi/server.go
+++ b/experimental/clashapi/server.go
@@ -53,6 +53,7 @@ type Server struct {
externalUI string
externalUIDownloadURL string
externalUIDownloadDetour string
+ externalUIBuildin bool
}
func NewServer(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
@@ -71,6 +72,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
externalController: options.ExternalController != "",
externalUIDownloadURL: options.ExternalUIDownloadURL,
externalUIDownloadDetour: options.ExternalUIDownloadDetour,
+ externalUIBuildin: options.ExternalUIBuildin,
}
server.urlTestHistory = service.PtrFromContext[urltest.HistoryStorage](ctx)
if server.urlTestHistory == nil {
@@ -106,7 +108,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
r.Mount("/proxies", proxyRouter(server, router))
r.Mount("/rules", ruleRouter(router))
r.Mount("/connections", connectionRouter(router, trafficManager))
- r.Mount("/providers/proxies", proxyProviderRouter())
+ r.Mount("/providers/proxies", proxyProviderRouter(server, router))
r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/script", scriptRouter())
r.Mount("/profile", profileRouter())
@@ -124,6 +126,12 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
fs.ServeHTTP(w, r)
})
})
+ } else if options.ExternalUIBuildin {
+ f, err := initDashboard()
+ if err != nil {
+ return nil, err
+ }
+ chiRouter.Group(f)
}
return server, nil
}
@@ -143,7 +151,9 @@ func (s *Server) PreStart() error {
func (s *Server) Start() error {
if s.externalController {
- s.checkAndDownloadExternalUI()
+ if !s.externalUIBuildin {
+ s.checkAndDownloadExternalUI()
+ }
listener, err := net.Listen("tcp", s.httpServer.Addr)
if err != nil {
return E.Cause(err, "external controller listen error")
diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go
index 4e635d1257..b7c20eb075 100644
--- a/experimental/clashapi/trafficontrol/tracker.go
+++ b/experimental/clashapi/trafficontrol/tracker.go
@@ -1,6 +1,7 @@
package trafficontrol
import (
+ "encoding/json"
"net"
"net/netip"
"time"
@@ -9,7 +10,6 @@ import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio"
- "github.com/sagernet/sing/common/json"
N "github.com/sagernet/sing/common/network"
"github.com/gofrs/uuid/v5"
diff --git a/experimental/libbox/command_log.go b/experimental/libbox/command_log.go
index ce72010dd0..da142657e6 100644
--- a/experimental/libbox/command_log.go
+++ b/experimental/libbox/command_log.go
@@ -60,11 +60,11 @@ func (s *CommandServer) handleLogConn(conn net.Conn) error {
for element := s.savedLines.Front(); element != nil; element = element.Next() {
savedLines = append(savedLines, element.Value)
}
- s.access.Unlock()
subscription, done, err := s.observer.Subscribe()
if err != nil {
return err
}
+ s.access.Unlock()
defer s.observer.UnSubscribe(subscription)
for _, line := range savedLines {
err = writeLog(conn, []byte(line))
diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go
index a484c64b23..3375f7502c 100644
--- a/experimental/libbox/service.go
+++ b/experimental/libbox/service.go
@@ -44,6 +44,8 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
urlTestHistoryStorage := urltest.NewHistoryStorage()
ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
+ pauseManager := pause.NewDefaultManager(ctx)
+ ctx = pause.ContextWithManager(ctx, pauseManager)
platformWrapper := &platformInterfaceWrapper{iif: platformInterface, useProcFS: platformInterface.UseProcFS()}
instance, err := box.New(box.Options{
Context: ctx,
@@ -61,7 +63,7 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
cancel: cancel,
instance: instance,
urlTestHistoryStorage: urlTestHistoryStorage,
- pauseManager: service.FromContext[pause.Manager](ctx),
+ pauseManager: pauseManager,
}, nil
}
diff --git a/go.mod b/go.mod
index 78301aec7c..fba3850e88 100644
--- a/go.mod
+++ b/go.mod
@@ -7,12 +7,14 @@ require (
github.com/caddyserver/certmagic v0.20.0
github.com/cloudflare/circl v1.3.6
github.com/cretz/bine v0.2.0
+ github.com/dlclark/regexp2 v1.10.0
github.com/fsnotify/fsnotify v1.7.0
- github.com/go-chi/chi/v5 v5.0.11
+ github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.3
github.com/gofrs/uuid/v5 v5.0.0
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2
+ github.com/ipsn/go-libtor v1.0.380
github.com/libdns/alidns v1.0.3
github.com/libdns/cloudflare v0.1.0
github.com/logrusorgru/aurora v2.0.3+incompatible
@@ -20,39 +22,42 @@ require (
github.com/miekg/dns v1.1.57
github.com/ooni/go-libtor v1.1.8
github.com/oschwald/maxminddb-golang v1.12.0
+ github.com/robertkrimen/otto v0.2.1
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1
github.com/sagernet/gomobile v0.1.1
github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e
- github.com/sagernet/quic-go v0.40.1-beta.2
+ github.com/sagernet/quic-go v0.40.0
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
- github.com/sagernet/sing v0.3.0-rc.7.0.20240105061852-782bc05c5573
- github.com/sagernet/sing-dns v0.1.12
- github.com/sagernet/sing-mux v0.1.8-rc.1
- github.com/sagernet/sing-quic v0.1.7-rc.2
+ github.com/sagernet/sing v0.2.20-0.20231212123824-8836b6754226
+ github.com/sagernet/sing-dns v0.1.11
+ github.com/sagernet/sing-mux v0.1.6-0.20231208180947-9053c29513a2
+ github.com/sagernet/sing-quic v0.1.6-0.20231207143711-eb3cbf9ed054
github.com/sagernet/sing-shadowsocks v0.2.6
- github.com/sagernet/sing-shadowsocks2 v0.1.6-rc.1
+ github.com/sagernet/sing-shadowsocks2 v0.1.6-0.20231207143709-50439739601a
github.com/sagernet/sing-shadowtls v0.1.4
- github.com/sagernet/sing-tun v0.2.0-rc.1
+ github.com/sagernet/sing-tun v0.1.24-0.20231212060935-6a1419aeae11
github.com/sagernet/sing-vmess v0.1.8
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6
github.com/sagernet/utls v1.5.4
- github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8
+ github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
go.uber.org/zap v1.26.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
- golang.org/x/crypto v0.17.0
+ golang.org/x/crypto v0.16.0
golang.org/x/net v0.19.0
golang.org/x/sys v0.15.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
- google.golang.org/grpc v1.60.1
- google.golang.org/protobuf v1.32.0
+ google.golang.org/grpc v1.59.0
+ google.golang.org/protobuf v1.31.0
howett.net/plist v1.0.1
)
+require gopkg.in/sourcemap.v1 v1.0.5 // indirect
+
//replace github.com/sagernet/sing => ../sing
require (
@@ -86,13 +91,13 @@ require (
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect
+ golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.16.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1
lukechampine.com/blake3 v1.2.1 // indirect
)
diff --git a/go.sum b/go.sum
index c6ac1d6f61..07157a564b 100644
--- a/go.sum
+++ b/go.sum
@@ -15,12 +15,14 @@ github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbe
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
+github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
-github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
-github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
+github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
@@ -51,6 +53,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
+github.com/ipsn/go-libtor v1.0.380 h1:hCmALDBe3bPpgwMunonMLArrG41MxzpE91Bk8KQYnYM=
+github.com/ipsn/go-libtor v1.0.380/go.mod h1:6rIeHU7irp8ZH8E/JqaEOKlD6s4vSSUh4ngHelhlSMw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@@ -93,6 +97,8 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
+github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
+github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
@@ -104,27 +110,27 @@ github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e h1:DOkjByVeAR56dks
github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e/go.mod h1:fLxq/gtp0qzkaEwywlRRiGmjOK5ES/xUzyIKIFP2Asw=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
-github.com/sagernet/quic-go v0.40.1-beta.2 h1:USRwm36XuAFdcrmv4vDRD+YUOO08DfvLNruXThrVHZU=
-github.com/sagernet/quic-go v0.40.1-beta.2/go.mod h1:CcKTpzTAISxrM4PA5M20/wYuz9Tj6Tx4DwGbNl9UQrU=
+github.com/sagernet/quic-go v0.40.0 h1:DvQNPb72lzvNQDe9tcUyHTw8eRv6PLtM2mNYmdlzUMo=
+github.com/sagernet/quic-go v0.40.0/go.mod h1:VqtdhlbkeeG5Okhb3eDMb/9o0EoglReHunNT9ukrJAI=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
-github.com/sagernet/sing v0.3.0-rc.7.0.20240105061852-782bc05c5573 h1:aBpX3AIQ6jfIFBj4Gwofd3jZc2s1dC6/ChoyUOglyrc=
-github.com/sagernet/sing v0.3.0-rc.7.0.20240105061852-782bc05c5573/go.mod h1:9pfuAH6mZfgnz/YjP6xu5sxx882rfyjpcrTdUpd6w3g=
-github.com/sagernet/sing-dns v0.1.12 h1:1HqZ+ln+Rezx/aJMStaS0d7oPeX2EobSV1NT537kyj4=
-github.com/sagernet/sing-dns v0.1.12/go.mod h1:rx/DTOisneQpCgNQ4jbFU/JNEtnz0lYcHXenlVzpjEU=
-github.com/sagernet/sing-mux v0.1.8-rc.1 h1:5dsZgWmNr9W6JzQj4fb3xX2pMP0OyJH6kVtlqc2kFKA=
-github.com/sagernet/sing-mux v0.1.8-rc.1/go.mod h1:KK5zCbNujj5kn36G+wLFROOXyJhaaXLyaZWY2w7kBNQ=
-github.com/sagernet/sing-quic v0.1.7-rc.2 h1:rCWhtvzQwgkWbX4sVHYdNwzyPweoUPEgBCBatywHjMs=
-github.com/sagernet/sing-quic v0.1.7-rc.2/go.mod h1:IbKCPWXP13zd3cdu0rirtYjkMlquc5zWtc3avfSUGAw=
+github.com/sagernet/sing v0.2.20-0.20231212123824-8836b6754226 h1:rcII71ho6F/7Nyx7n2kESLcnvNMdcU4i8ZUGF2Fi7yA=
+github.com/sagernet/sing v0.2.20-0.20231212123824-8836b6754226/go.mod h1:Ce5LNojQOgOiWhiD8pPD6E9H7e2KgtOe3Zxx4Ou5u80=
+github.com/sagernet/sing-dns v0.1.11 h1:PPrMCVVrAeR3f5X23I+cmvacXJ+kzuyAsBiWyUKhGSE=
+github.com/sagernet/sing-dns v0.1.11/go.mod h1:zJ/YjnYB61SYE+ubMcMqVdpaSvsyQ2iShQGO3vuLvvE=
+github.com/sagernet/sing-mux v0.1.6-0.20231208180947-9053c29513a2 h1:rRlYQPbMKmzKX+43XC04gEQvxc45/AxfteRWfcl2/rw=
+github.com/sagernet/sing-mux v0.1.6-0.20231208180947-9053c29513a2/go.mod h1:IdSrwwqBeJTrjLZJRFXE+F8mYXNI/rPAjzlgTFuEVmo=
+github.com/sagernet/sing-quic v0.1.6-0.20231207143711-eb3cbf9ed054 h1:Ed7FskwQcep5oQ+QahgVK0F6jPPSV8Nqwjr9MwGatMU=
+github.com/sagernet/sing-quic v0.1.6-0.20231207143711-eb3cbf9ed054/go.mod h1:u758WWv3G1OITG365CYblL0NfAruFL1PpLD9DUVTv1o=
github.com/sagernet/sing-shadowsocks v0.2.6 h1:xr7ylAS/q1cQYS8oxKKajhuQcchd5VJJ4K4UZrrpp0s=
github.com/sagernet/sing-shadowsocks v0.2.6/go.mod h1:j2YZBIpWIuElPFL/5sJAj470bcn/3QQ5lxZUNKLDNAM=
-github.com/sagernet/sing-shadowsocks2 v0.1.6-rc.1 h1:E+8OyyVg0YfFNUmxMx9jYBEhjLYMQSAMzJrUmE934bo=
-github.com/sagernet/sing-shadowsocks2 v0.1.6-rc.1/go.mod h1:wFkU7sKxyZADS/idtJqBhtc+QBf5iwX9nZO7ymcn6MM=
+github.com/sagernet/sing-shadowsocks2 v0.1.6-0.20231207143709-50439739601a h1:uYIKfpE1/EJpa+1Bja7b006VixeRuVduOpeuesMk2lU=
+github.com/sagernet/sing-shadowsocks2 v0.1.6-0.20231207143709-50439739601a/go.mod h1:pjeylQ4ApvpEH7B4PUBrdyJf4xmQkg8BaIzT5fI2fR0=
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
-github.com/sagernet/sing-tun v0.2.0-rc.1 h1:CnlxRgrJKAMKYNuJOcKie6TjRz8wremEq1wndLup7cA=
-github.com/sagernet/sing-tun v0.2.0-rc.1/go.mod h1:hpbL9jNAbYT9G2EHCpCXVIgSrM/2Wgnrm/Hped+8zdY=
+github.com/sagernet/sing-tun v0.1.24-0.20231212060935-6a1419aeae11 h1:crTOVPJGOGWOW+Q2a0FQiiS/G2+W6uCLKtOofFMisQc=
+github.com/sagernet/sing-tun v0.1.24-0.20231212060935-6a1419aeae11/go.mod h1:DgXPnBqtqWrZj37Mun/W61dW0Q56eLqTZYhcuNLaCtY=
github.com/sagernet/sing-vmess v0.1.8 h1:XVWad1RpTy9b5tPxdm5MCU8cGfrTGdR8qCq6HV2aCNc=
github.com/sagernet/sing-vmess v0.1.8/go.mod h1:vhx32UNzTDUkNwOyIjcZQohre1CaytquC5mPplId8uA=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
@@ -133,8 +139,8 @@ github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 h1:z3SJQhVyU63FT26
github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6/go.mod h1:73xRZuxwkFk4aiLw28hG8W6o9cr2UPrGL9pdY2UTbvY=
github.com/sagernet/utls v1.5.4 h1:KmsEGbB2dKUtCNC+44NwAdNAqnqQ6GA4pTO0Yik56co=
github.com/sagernet/utls v1.5.4/go.mod h1:CTGxPWExIloRipK3XFpYv0OVyhO8kk3XCGW/ieyTh1s=
-github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 h1:R0OMYAScomNAVpTfbHFpxqJpvwuhxSRi+g6z7gZhABs=
-github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8/go.mod h1:K4J7/npM+VAMUeUmTa2JaA02JmyheP0GpRBOUvn3ecc=
+github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f h1:Kvo8w8Y9lzFGB/7z09MJ3TR99TFtfI/IuY87Ygcycho=
+github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
@@ -168,10 +174,10 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
-golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4=
-golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
+golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
+golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -204,17 +210,19 @@ golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY=
-google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
-google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
+google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
+google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
-google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
+gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/inbound/direct.go b/inbound/direct.go
index 7079a9f24f..08d0726a25 100644
--- a/inbound/direct.go
+++ b/inbound/direct.go
@@ -4,7 +4,6 @@ import (
"context"
"net"
"net/netip"
- "time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
@@ -48,13 +47,13 @@ func NewDirect(ctx context.Context, router adapter.Router, logger log.ContextLog
inbound.overrideOption = 3
inbound.overrideDestination = M.Socksaddr{Port: options.OverridePort}
}
- var udpTimeout time.Duration
+ var udpTimeout int64
if options.UDPTimeout != 0 {
- udpTimeout = time.Duration(options.UDPTimeout)
+ udpTimeout = options.UDPTimeout
} else {
- udpTimeout = C.UDPTimeout
+ udpTimeout = int64(C.UDPTimeout.Seconds())
}
- inbound.udpNat = udpnat.New[netip.AddrPort](int64(udpTimeout.Seconds()), adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound))
+ inbound.udpNat = udpnat.New[netip.AddrPort](udpTimeout, adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound))
inbound.connHandler = inbound
inbound.packetHandler = inbound
inbound.packetUpstream = inbound.udpNat
diff --git a/inbound/hysteria.go b/inbound/hysteria.go
index 9cb2559d0e..29707f650a 100644
--- a/inbound/hysteria.go
+++ b/inbound/hysteria.go
@@ -5,7 +5,6 @@ package inbound
import (
"context"
"net"
- "time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/humanize"
@@ -67,12 +66,6 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
} else {
receiveBps = uint64(options.DownMbps) * hysteria.MbpsToBps
}
- var udpTimeout time.Duration
- if options.UDPTimeout != 0 {
- udpTimeout = time.Duration(options.UDPTimeout)
- } else {
- udpTimeout = C.UDPTimeout
- }
service, err := hysteria.NewService[int](hysteria.ServiceOptions{
Context: ctx,
Logger: logger,
@@ -80,7 +73,6 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
ReceiveBPS: receiveBps,
XPlusPassword: options.Obfs,
TLSConfig: tlsConfig,
- UDPTimeout: udpTimeout,
Handler: adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil),
// Legacy options
diff --git a/inbound/hysteria2.go b/inbound/hysteria2.go
index 0e49cca296..5db881c3d4 100644
--- a/inbound/hysteria2.go
+++ b/inbound/hysteria2.go
@@ -8,7 +8,6 @@ import (
"net/http"
"net/http/httputil"
"net/url"
- "time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tls"
@@ -88,12 +87,6 @@ func NewHysteria2(ctx context.Context, router adapter.Router, logger log.Context
},
tlsConfig: tlsConfig,
}
- var udpTimeout time.Duration
- if options.UDPTimeout != 0 {
- udpTimeout = time.Duration(options.UDPTimeout)
- } else {
- udpTimeout = C.UDPTimeout
- }
service, err := hysteria2.NewService[int](hysteria2.ServiceOptions{
Context: ctx,
Logger: logger,
@@ -103,7 +96,6 @@ func NewHysteria2(ctx context.Context, router adapter.Router, logger log.Context
SalamanderPassword: salamanderPassword,
TLSConfig: tlsConfig,
IgnoreClientBandwidth: options.IgnoreClientBandwidth,
- UDPTimeout: udpTimeout,
Handler: adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil),
MasqueradeHandler: masqueradeHandler,
})
diff --git a/inbound/shadowsocks.go b/inbound/shadowsocks.go
index ca15b8d8e8..a45b6daf4b 100644
--- a/inbound/shadowsocks.go
+++ b/inbound/shadowsocks.go
@@ -4,7 +4,6 @@ import (
"context"
"net"
"os"
- "time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/mux"
@@ -66,19 +65,19 @@ func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
return nil, err
}
- var udpTimeout time.Duration
+ var udpTimeout int64
if options.UDPTimeout != 0 {
- udpTimeout = time.Duration(options.UDPTimeout)
+ udpTimeout = options.UDPTimeout
} else {
- udpTimeout = C.UDPTimeout
+ udpTimeout = int64(C.UDPTimeout.Seconds())
}
switch {
case options.Method == shadowsocks.MethodNone:
- inbound.service = shadowsocks.NewNoneService(int64(udpTimeout.Seconds()), inbound.upstreamContextHandler())
+ inbound.service = shadowsocks.NewNoneService(options.UDPTimeout, inbound.upstreamContextHandler())
case common.Contains(shadowaead.List, options.Method):
- inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, int64(udpTimeout.Seconds()), inbound.upstreamContextHandler())
+ inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, udpTimeout, inbound.upstreamContextHandler())
case common.Contains(shadowaead_2022.List, options.Method):
- inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, int64(udpTimeout.Seconds()), inbound.upstreamContextHandler(), ntp.TimeFuncFromContext(ctx))
+ inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, udpTimeout, inbound.upstreamContextHandler(), ntp.TimeFuncFromContext(ctx))
default:
err = E.New("unsupported method: ", options.Method)
}
diff --git a/inbound/shadowsocks_multi.go b/inbound/shadowsocks_multi.go
index a291af4acb..c3c7d2abd3 100644
--- a/inbound/shadowsocks_multi.go
+++ b/inbound/shadowsocks_multi.go
@@ -4,7 +4,6 @@ import (
"context"
"net"
"os"
- "time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/mux"
@@ -54,25 +53,25 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
if err != nil {
return nil, err
}
- var udpTimeout time.Duration
+ var udpTimeout int64
if options.UDPTimeout != 0 {
- udpTimeout = time.Duration(options.UDPTimeout)
+ udpTimeout = options.UDPTimeout
} else {
- udpTimeout = C.UDPTimeout
+ udpTimeout = int64(C.UDPTimeout.Seconds())
}
var service shadowsocks.MultiService[int]
if common.Contains(shadowaead_2022.List, options.Method) {
service, err = shadowaead_2022.NewMultiServiceWithPassword[int](
options.Method,
options.Password,
- int64(udpTimeout.Seconds()),
+ udpTimeout,
adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound),
ntp.TimeFuncFromContext(ctx),
)
} else if common.Contains(shadowaead.List, options.Method) {
service, err = shadowaead.NewMultiService[int](
options.Method,
- int64(udpTimeout.Seconds()),
+ udpTimeout,
adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound))
} else {
return nil, E.New("unsupported method: " + options.Method)
diff --git a/inbound/shadowsocks_relay.go b/inbound/shadowsocks_relay.go
index fbc2838ade..44f39c9f6a 100644
--- a/inbound/shadowsocks_relay.go
+++ b/inbound/shadowsocks_relay.go
@@ -4,7 +4,6 @@ import (
"context"
"net"
"os"
- "time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/mux"
@@ -51,16 +50,16 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.
if err != nil {
return nil, err
}
- var udpTimeout time.Duration
+ var udpTimeout int64
if options.UDPTimeout != 0 {
- udpTimeout = time.Duration(options.UDPTimeout)
+ udpTimeout = options.UDPTimeout
} else {
- udpTimeout = C.UDPTimeout
+ udpTimeout = int64(C.UDPTimeout.Seconds())
}
service, err := shadowaead_2022.NewRelayServiceWithPassword[int](
options.Method,
options.Password,
- int64(udpTimeout.Seconds()),
+ udpTimeout,
adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound),
)
if err != nil {
diff --git a/inbound/tproxy.go b/inbound/tproxy.go
index a074eb495b..be421ad322 100644
--- a/inbound/tproxy.go
+++ b/inbound/tproxy.go
@@ -5,7 +5,6 @@ import (
"net"
"net/netip"
"syscall"
- "time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir"
@@ -38,15 +37,15 @@ func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLog
listenOptions: options.ListenOptions,
},
}
- var udpTimeout time.Duration
+ var udpTimeout int64
if options.UDPTimeout != 0 {
- udpTimeout = time.Duration(options.UDPTimeout)
+ udpTimeout = options.UDPTimeout
} else {
- udpTimeout = C.UDPTimeout
+ udpTimeout = int64(C.UDPTimeout.Seconds())
}
tproxy.connHandler = tproxy
tproxy.oobPacketHandler = tproxy
- tproxy.udpNat = udpnat.New[netip.AddrPort](int64(udpTimeout.Seconds()), tproxy.upstreamContextHandler())
+ tproxy.udpNat = udpnat.New[netip.AddrPort](udpTimeout, tproxy.upstreamContextHandler())
tproxy.packetUpstream = tproxy.udpNat
return tproxy
}
diff --git a/inbound/tuic.go b/inbound/tuic.go
index f0e2d8d1b1..ff9b9ce728 100644
--- a/inbound/tuic.go
+++ b/inbound/tuic.go
@@ -52,12 +52,6 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
},
tlsConfig: tlsConfig,
}
- var udpTimeout time.Duration
- if options.UDPTimeout != 0 {
- udpTimeout = time.Duration(options.UDPTimeout)
- } else {
- udpTimeout = C.UDPTimeout
- }
service, err := tuic.NewService[int](tuic.ServiceOptions{
Context: ctx,
Logger: logger,
@@ -66,7 +60,6 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
AuthTimeout: time.Duration(options.AuthTimeout),
ZeroRTTHandshake: options.ZeroRTTHandshake,
Heartbeat: time.Duration(options.Heartbeat),
- UDPTimeout: udpTimeout,
Handler: adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil),
})
if err != nil {
diff --git a/inbound/tun.go b/inbound/tun.go
index 7a08b1ec86..4aa11f6921 100644
--- a/inbound/tun.go
+++ b/inbound/tun.go
@@ -5,7 +5,6 @@ import (
"net"
"strconv"
"strings"
- "time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
@@ -44,11 +43,15 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
if tunMTU == 0 {
tunMTU = 9000
}
- var udpTimeout time.Duration
+ gsoMaxSize := options.GSOMaxSize
+ if gsoMaxSize == 0 {
+ gsoMaxSize = 65536
+ }
+ var udpTimeout int64
if options.UDPTimeout != 0 {
- udpTimeout = time.Duration(options.UDPTimeout)
+ udpTimeout = options.UDPTimeout
} else {
- udpTimeout = C.UDPTimeout
+ udpTimeout = int64(C.UDPTimeout.Seconds())
}
includeUID := uidToRange(options.IncludeUID)
if len(options.IncludeUIDRange) > 0 {
@@ -76,6 +79,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
Name: options.InterfaceName,
MTU: tunMTU,
GSO: options.GSO,
+ GSOMaxSize: gsoMaxSize,
Inet4Address: options.Inet4Address,
Inet6Address: options.Inet6Address,
AutoRoute: options.AutoRoute,
@@ -95,7 +99,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
TableIndex: 2022,
},
endpointIndependentNat: options.EndpointIndependentNat,
- udpTimeout: int64(udpTimeout.Seconds()),
+ udpTimeout: udpTimeout,
stack: options.Stack,
platformInterface: platformInterface,
platformOptions: common.PtrValueOrDefault(options.Platform),
diff --git a/jstest/golang/http_request.go b/jstest/golang/http_request.go
new file mode 100644
index 0000000000..e4eab2cada
--- /dev/null
+++ b/jstest/golang/http_request.go
@@ -0,0 +1,204 @@
+package golang
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/sagernet/sing-box/option"
+ E "github.com/sagernet/sing/common/exceptions"
+
+ "github.com/robertkrimen/otto"
+)
+
+type HTTPRequest struct {
+ Method string `json:"method"`
+ URL string `json:"url"`
+ Headers map[string]string `json:"headers"`
+ Cookies map[string]string `json:"cookies"`
+ Body string `json:"body"`
+ DisableRedirect bool `json:"disable_redirect"`
+ Timeout option.Duration `json:"timeout"`
+ Detour string `json:"detour"`
+}
+
+type HTTPResponse struct {
+ Status int
+ Headers map[string]string
+ Body string
+ Cost time.Duration
+ Error error
+}
+
+var HTTPRequestKey = (*struct{})(nil)
+
+func JSGoHTTPRequests(ctx context.Context, jsVM *otto.Otto, httpClient *http.Client) func(call otto.FunctionCall) otto.Value {
+ return JSDo[otto.Value](jsVM, func(call otto.FunctionCall) (*otto.Value, error) {
+ requestsArg := call.Argument(0)
+ if !requestsArg.IsObject() {
+ return nil, E.New("requests must be object")
+ }
+
+ requestsAny, err := requestsArg.Export()
+ if err != nil {
+ return nil, E.Cause(err, "failed to parse requests")
+ }
+ raw, err := json.Marshal(requestsAny)
+ if err != nil {
+ return nil, E.Cause(err, "failed to parse requests")
+ }
+ var requests []HTTPRequest
+ err = json.Unmarshal(raw, &requests)
+ if err != nil {
+ return nil, E.Cause(err, "failed to parse requests")
+ }
+ for i := range requests {
+ if requests[i].Method == "" {
+ requests[i].Method = http.MethodGet
+ }
+ if requests[i].URL == "" {
+ return nil, E.Cause(err, "url must not be empty")
+ }
+ if requests[i].Detour == "" {
+ return nil, E.Cause(err, "detour must not be empty")
+ }
+ }
+ if len(requests) == 0 {
+ return nil, E.Cause(err, "requests must not be empty")
+ }
+
+ var timeout time.Duration
+ timeoutArg := call.Argument(1)
+ if !timeoutArg.IsUndefined() {
+ if timeoutArg.IsNumber() {
+ n, _ := timeoutArg.ToInteger()
+ timeout = time.Duration(n) * time.Second
+ } else if timeoutArg.IsString() {
+ s, _ := timeoutArg.ToString()
+ if s != "" {
+ d, err := time.ParseDuration(s)
+ if err != nil {
+ return nil, E.Cause(err, "failed to parse timeout")
+ }
+ timeout = d
+ }
+ } else {
+ return nil, E.New("timeout must be number or string")
+ }
+ }
+
+ ctx := ctx
+ var cancel context.CancelFunc
+ if timeout > 0 {
+ ctx, cancel = context.WithTimeout(ctx, timeout)
+ } else {
+ ctx, cancel = context.WithCancel(ctx)
+ }
+ defer cancel()
+
+ responses := make([]HTTPResponse, len(requests))
+ var responseLock sync.Mutex
+ if len(requests) == 1 {
+ request := requests[0]
+ ctx := context.WithValue(ctx, HTTPRequestKey, &request)
+ responses[0] = *HTTPRequestDo(ctx, httpClient, &request)
+ } else {
+ requestDone := make(chan struct{}, len(requests))
+ for i, request := range requests {
+ go func(index int, request HTTPRequest) {
+ defer func() {
+ requestDone <- struct{}{}
+ }()
+ ctx := context.WithValue(ctx, HTTPRequestKey, &request)
+ response := HTTPRequestDo(ctx, httpClient, &request)
+ responseLock.Lock()
+ responses[index] = *response
+ responseLock.Unlock()
+ }(i, request)
+ }
+ for i := 0; i < len(requests); i++ {
+ <-requestDone
+ }
+ }
+
+ responsesJS, _ := jsVM.Object(`(new Array())`)
+ for _, response := range responses {
+ responseJS, _ := jsVM.Object(`({})`)
+ if response.Error != nil {
+ responseJS.Set("error", response.Error.Error())
+ } else {
+ responseJS.Set("cost", response.Cost.Milliseconds())
+ responseJS.Set("status", response.Status)
+ responseJS.Set("headers", response.Headers)
+ responseJS.Set("body", response.Body)
+ }
+ responsesJS.Call("push", responseJS)
+ }
+ responseValue := responsesJS.Value()
+
+ return &responseValue, nil
+ })
+}
+
+func HTTPRequestDo(ctx context.Context, httpClient *http.Client, req *HTTPRequest) (resp *HTTPResponse) {
+ resp = &HTTPResponse{}
+ var body io.Reader
+ if req.Body != "" {
+ body = strings.NewReader(req.Body)
+ }
+ httpReq, err := http.NewRequest(req.Method, req.URL, body)
+ if err != nil {
+ resp.Error = E.Cause(err, "failed to create http request")
+ return
+ }
+ if req.Headers != nil && len(req.Headers) > 0 {
+ for k, v := range req.Headers {
+ httpReq.Header.Set(k, v)
+ }
+ }
+ if req.Cookies != nil && len(req.Cookies) > 0 {
+ for key, value := range req.Cookies {
+ httpReq.AddCookie(&http.Cookie{
+ Name: key,
+ Value: value,
+ })
+ }
+ }
+
+ if req.Timeout > 0 {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, time.Duration(req.Timeout))
+ defer cancel()
+ }
+
+ t := time.Now()
+ httpResp, err := httpClient.Do(httpReq.WithContext(ctx))
+ if err != nil {
+ resp.Error = E.Cause(err, "failed to do http request")
+ return
+ }
+ resp.Cost = time.Since(t)
+
+ resp.Status = httpResp.StatusCode
+ buffer := bytes.NewBuffer(nil)
+ _, err = io.Copy(buffer, httpResp.Body)
+ if err != nil {
+ resp.Error = E.Cause(err, "failed to read http response body")
+ return
+ }
+
+ if buffer.Len() > 0 {
+ resp.Body = buffer.String()
+ }
+ resp.Headers = make(map[string]string)
+ for k, v := range httpResp.Header {
+ resp.Headers[k] = strings.Join(v, ", ")
+ }
+
+ return
+}
diff --git a/jstest/golang/log.go b/jstest/golang/log.go
new file mode 100644
index 0000000000..909390ca29
--- /dev/null
+++ b/jstest/golang/log.go
@@ -0,0 +1,35 @@
+package golang
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/robertkrimen/otto"
+)
+
+func JSGoLog(jsVM *otto.Otto, logFunc func(args ...any)) func(otto.FunctionCall) otto.Value {
+ return func(call otto.FunctionCall) otto.Value {
+ if len(call.ArgumentList) == 0 {
+ return otto.NullValue()
+ }
+ args := make([]any, 0, len(call.ArgumentList))
+ for _, arg := range call.ArgumentList {
+ args = append(args, fmt.Sprint(logValue(arg)))
+ }
+ logFunc(args...)
+ return otto.NullValue()
+ }
+}
+
+func logValue(v otto.Value) any {
+ raw, err := v.MarshalJSON()
+ if err != nil {
+ return "???"
+ }
+ var m any
+ err = json.Unmarshal(raw, &m)
+ if err != nil {
+ return "???"
+ }
+ return m
+}
diff --git a/jstest/golang/result.go b/jstest/golang/result.go
new file mode 100644
index 0000000000..c8f069a629
--- /dev/null
+++ b/jstest/golang/result.go
@@ -0,0 +1,16 @@
+package golang
+
+import "github.com/robertkrimen/otto"
+
+func JSDo[T any](jsVM *otto.Otto, f func(call otto.FunctionCall) (*T, error)) func(call otto.FunctionCall) otto.Value {
+ return func(call otto.FunctionCall) otto.Value {
+ result, err := f(call)
+ obj, _ := jsVM.Object(`({})`)
+ if err != nil {
+ obj.Set("error", err.Error())
+ } else {
+ obj.Set("value", *result)
+ }
+ return obj.Value()
+ }
+}
diff --git a/jstest/golang/urltest.go b/jstest/golang/urltest.go
new file mode 100644
index 0000000000..1a83b8a641
--- /dev/null
+++ b/jstest/golang/urltest.go
@@ -0,0 +1,164 @@
+package golang
+
+import (
+ "context"
+ "encoding/json"
+ "sync"
+ "time"
+
+ "github.com/sagernet/sing-box/adapter"
+ "github.com/sagernet/sing-box/common/urltest"
+ E "github.com/sagernet/sing/common/exceptions"
+
+ "github.com/robertkrimen/otto"
+)
+
+type URLTestRequest struct {
+ URL string `json:"url"`
+ Detour string `json:"detour"`
+}
+
+type URLTestResponse struct {
+ Delay uint16
+ Error error
+}
+
+func JSGoURLTest(ctx context.Context, router adapter.Router, jsVM *otto.Otto) func(otto.FunctionCall) otto.Value {
+ return JSDo[otto.Value](jsVM, func(call otto.FunctionCall) (*otto.Value, error) {
+ requestsArg := call.Argument(0)
+ if !requestsArg.IsObject() {
+ return nil, E.New("requests must be object")
+ }
+
+ requestsAny, err := requestsArg.Export()
+ if err != nil {
+ return nil, E.Cause(err, "failed to parse requests")
+ }
+ raw, err := json.Marshal(requestsAny)
+ if err != nil {
+ return nil, E.Cause(err, "failed to parse requests")
+ }
+ var requests []URLTestRequest
+ err = json.Unmarshal(raw, &requests)
+ if err != nil {
+ return nil, E.Cause(err, "failed to parse requests")
+ }
+ for i := range requests {
+ if requests[i].Detour == "" {
+ return nil, E.Cause(err, "detour must not be empty")
+ }
+ }
+ if len(requests) == 0 {
+ return nil, E.Cause(err, "requests must not be empty")
+ }
+
+ var timeout time.Duration
+ timeoutArg := call.Argument(1)
+ if !timeoutArg.IsUndefined() {
+ if timeoutArg.IsNumber() {
+ n, _ := timeoutArg.ToInteger()
+ timeout = time.Duration(n) * time.Second
+ } else if timeoutArg.IsString() {
+ s, _ := timeoutArg.ToString()
+ if s != "" {
+ d, err := time.ParseDuration(s)
+ if err != nil {
+ return nil, E.Cause(err, "failed to parse timeout")
+ }
+ timeout = d
+ }
+ } else {
+ return nil, E.New("timeout must be number or string")
+ }
+ }
+
+ var historyStorage *urltest.HistoryStorage
+ clashServer := router.ClashServer()
+ if clashServer != nil {
+ historyStorage = clashServer.HistoryStorage()
+ }
+
+ ctx := ctx
+ var cancel context.CancelFunc
+ if timeout > 0 {
+ ctx, cancel = context.WithTimeout(ctx, timeout)
+ } else {
+ ctx, cancel = context.WithCancel(ctx)
+ }
+ defer cancel()
+
+ responses := make([]URLTestResponse, len(requests))
+ var responseLock sync.Mutex
+ if len(requests) == 1 {
+ request := requests[0]
+ dialer, loaded := router.Outbound(request.Detour)
+ if !loaded {
+ return nil, E.New("detour not found")
+ }
+ delay, err := urltest.URLTest(ctx, request.URL, dialer)
+ if err != nil {
+ if historyStorage != nil {
+ historyStorage.DeleteURLTestHistory(request.Detour)
+ }
+ return nil, E.Cause(err, "urltest failed")
+ }
+ historyStorage.StoreURLTestHistory(request.Detour, &urltest.History{
+ Time: time.Now(),
+ Delay: delay,
+ })
+ responses[0] = URLTestResponse{
+ Delay: delay,
+ }
+ } else {
+ requestDone := make(chan struct{}, len(requests))
+ for i, request := range requests {
+ go func(index int, request URLTestRequest) {
+ defer func() {
+ requestDone <- struct{}{}
+ }()
+ var response URLTestResponse
+ dialer, loaded := router.Outbound(request.Detour)
+ if !loaded {
+ response.Error = E.New("detour not found")
+ } else {
+ delay, err := urltest.URLTest(ctx, request.URL, dialer)
+ if err != nil {
+ response.Error = E.Cause(err, "urltest failed")
+ if historyStorage != nil {
+ historyStorage.DeleteURLTestHistory(request.Detour)
+ }
+ } else {
+ response.Delay = delay
+ if historyStorage != nil {
+ historyStorage.StoreURLTestHistory(request.Detour, &urltest.History{
+ Time: time.Now(),
+ Delay: delay,
+ })
+ }
+ }
+ }
+ responseLock.Lock()
+ responses[index] = response
+ responseLock.Unlock()
+ }(i, request)
+ }
+ for i := 0; i < len(requests); i++ {
+ <-requestDone
+ }
+ }
+
+ responsesJS, _ := jsVM.Object(`(new Array())`)
+ for _, response := range responses {
+ responseJS, _ := jsVM.Object(`({})`)
+ if response.Error != nil {
+ responseJS.Set("error", response.Error.Error())
+ } else {
+ responseJS.Set("delay", response.Delay)
+ }
+ responsesJS.Call("push", responseJS)
+ }
+ responseValue := responsesJS.Value()
+
+ return &responseValue, nil
+ })
+}
diff --git a/jstest/javascript/auto_urltest.js b/jstest/javascript/auto_urltest.js
new file mode 100644
index 0000000000..ebce72da0b
--- /dev/null
+++ b/jstest/javascript/auto_urltest.js
@@ -0,0 +1,14 @@
+
+function Test(outbounds, now_selected) {
+ var requests = new Array();
+
+ for (var i = 0; i < outbounds.length; i++) {
+ requests.push({
+ detour: outbounds[i]
+ });
+ }
+
+ urltests(requests);
+
+ return { value: now_selected };
+}
diff --git a/jstest/javascript/chatgpt.js b/jstest/javascript/chatgpt.js
new file mode 100644
index 0000000000..a0bdf01c03
--- /dev/null
+++ b/jstest/javascript/chatgpt.js
@@ -0,0 +1,53 @@
+// From https://github.com/lmc999/RegionRestrictionCheck
+
+function Test(outbounds, now_selected) {
+ var requests = new Array();
+
+ for (var i = 0; i < outbounds.length; i++) {
+ requests.push({
+ method: "GET",
+ url: "https://chat.openai.com",
+ headers: {
+ "Host": "chat.openai.com",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60",
+ "Accept-Language": "en",
+ },
+ detour: outbounds[i]
+ });
+ }
+
+ var result = http_requests(requests);
+
+ if (typeof result.error === 'string' && result.error != "") {
+ return { error: result.error };
+ }
+
+ log_debug("http requests success");
+
+ var responses = result.value;
+ var selected = null;
+ var min_cost = 0;
+
+ for (var i = 0; i < responses.length; i++) {
+ var result = responses[i];
+ if (result.error !== null && typeof result.error == 'string' && result.error !== "") {
+ log_error("detour: [" + outbounds[i] + "], error: [" + result.error + "]");
+ } else {
+ var isAllow = false;
+ if (result.status !== 403 && typeof result.body == 'string' && result.body !== "" && result.body.search("Sorry, you have been blocked") < 0) {
+ isAllow = true;
+ if (min_cost === 0 || result.cost < min_cost) {
+ selected = outbounds[i];
+ min_cost = result.cost;
+ }
+ }
+ log_debug("detour: [" + outbounds[i] + "], status: [" + result.status + "], isAllow: [" + isAllow + "], cost: [" + result.cost + "ms]")
+ }
+ }
+
+ if (selected == null) {
+ return { error: "no outbound is available" };
+ }
+
+ return { value: selected };
+}
diff --git a/jstest/javascript/chatgpt.license b/jstest/javascript/chatgpt.license
new file mode 100644
index 0000000000..29ebfa545f
--- /dev/null
+++ b/jstest/javascript/chatgpt.license
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
\ No newline at end of file
diff --git a/jstest/javascript/chatgpt_js.base64 b/jstest/javascript/chatgpt_js.base64
new file mode 100644
index 0000000000..64437828f9
--- /dev/null
+++ b/jstest/javascript/chatgpt_js.base64
@@ -0,0 +1 @@
+Ly8gRnJvbSBodHRwczovL2dpdGh1Yi5jb20vbG1jOTk5L1JlZ2lvblJlc3RyaWN0aW9uQ2hlY2sNCg0KZnVuY3Rpb24gVGVzdChvdXRib3VuZHMsIG5vd19zZWxlY3RlZCkgew0KICAgIHZhciByZXF1ZXN0cyA9IG5ldyBBcnJheSgpOw0KDQogICAgZm9yICh2YXIgaSA9IDA7IGkgPCBvdXRib3VuZHMubGVuZ3RoOyBpKyspIHsNCiAgICAgICAgcmVxdWVzdHMucHVzaCh7DQogICAgICAgICAgICBtZXRob2Q6ICJHRVQiLA0KICAgICAgICAgICAgdXJsOiAiaHR0cHM6Ly9jaGF0Lm9wZW5haS5jb20iLA0KICAgICAgICAgICAgaGVhZGVyczogew0KICAgICAgICAgICAgICAgICJIb3N0IjogImNoYXQub3BlbmFpLmNvbSIsDQogICAgICAgICAgICAgICAgIlVzZXItQWdlbnQiOiAiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzExNy4wLjAuMCBTYWZhcmkvNTM3LjM2IEVkZy8xMTcuMC4yMDQ1LjYwIiwNCiAgICAgICAgICAgICAgICAiQWNjZXB0LUxhbmd1YWdlIjogImVuIiwNCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICBkZXRvdXI6IG91dGJvdW5kc1tpXQ0KICAgICAgICB9KTsNCiAgICB9DQoNCiAgICB2YXIgcmVzdWx0ID0gaHR0cF9yZXF1ZXN0cyhyZXF1ZXN0cyk7DQoNCiAgICBpZiAodHlwZW9mIHJlc3VsdC5lcnJvciA9PT0gJ3N0cmluZycgJiYgcmVzdWx0LmVycm9yICE9ICIiKSB7DQogICAgICAgIHJldHVybiB7IGVycm9yOiByZXN1bHQuZXJyb3IgfTsNCiAgICB9DQoNCiAgICBsb2dfZGVidWcoImh0dHAgcmVxdWVzdHMgc3VjY2VzcyIpOw0KDQogICAgdmFyIHJlc3BvbnNlcyA9IHJlc3VsdC52YWx1ZTsNCiAgICB2YXIgc2VsZWN0ZWQgPSBudWxsOw0KICAgIHZhciBtaW5fY29zdCA9IDA7DQoNCiAgICBmb3IgKHZhciBpID0gMDsgaSA8IHJlc3BvbnNlcy5sZW5ndGg7IGkrKykgew0KICAgICAgICB2YXIgcmVzdWx0ID0gcmVzcG9uc2VzW2ldOw0KICAgICAgICBpZiAocmVzdWx0LmVycm9yICE9PSBudWxsICYmIHR5cGVvZiByZXN1bHQuZXJyb3IgPT0gJ3N0cmluZycgJiYgcmVzdWx0LmVycm9yICE9PSAiIikgew0KICAgICAgICAgICAgbG9nX2Vycm9yKCJkZXRvdXI6IFsiICsgb3V0Ym91bmRzW2ldICsgIl0sIGVycm9yOiBbIiArIHJlc3VsdC5lcnJvciArICJdIik7DQogICAgICAgIH0gZWxzZSB7DQogICAgICAgICAgICB2YXIgaXNBbGxvdyA9IGZhbHNlOw0KICAgICAgICAgICAgaWYgKHJlc3VsdC5zdGF0dXMgIT09IDQwMyAmJiB0eXBlb2YgcmVzdWx0LmJvZHkgPT0gJ3N0cmluZycgJiYgcmVzdWx0LmJvZHkgIT09ICIiICYmIHJlc3VsdC5ib2R5LnNlYXJjaCgiU29ycnksIHlvdSBoYXZlIGJlZW4gYmxvY2tlZCIpIDwgMCkgew0KICAgICAgICAgICAgICAgIGlzQWxsb3cgPSB0cnVlOw0KICAgICAgICAgICAgICAgIGlmIChtaW5fY29zdCA9PT0gMCB8fCByZXN1bHQuY29zdCA8IG1pbl9jb3N0KSB7DQogICAgICAgICAgICAgICAgICAgIHNlbGVjdGVkID0gb3V0Ym91bmRzW2ldOw0KICAgICAgICAgICAgICAgICAgICBtaW5fY29zdCA9IHJlc3VsdC5jb3N0Ow0KICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0NCiAgICAgICAgICAgIGxvZ19kZWJ1ZygiZGV0b3VyOiBbIiArIG91dGJvdW5kc1tpXSArICJdLCBzdGF0dXM6IFsiICsgcmVzdWx0LnN0YXR1cyArICJdLCBpc0FsbG93OiBbIiArIGlzQWxsb3cgKyAiXSwgY29zdDogWyIgKyByZXN1bHQuY29zdCArICJtc10iKQ0KICAgICAgICB9DQogICAgfQ0KDQogICAgaWYgKHNlbGVjdGVkID09IG51bGwpIHsNCiAgICAgICAgcmV0dXJuIHsgZXJyb3I6ICJubyBvdXRib3VuZCBpcyBhdmFpbGFibGUiIH07DQogICAgfQ0KDQogICAgcmV0dXJuIHsgdmFsdWU6IHNlbGVjdGVkIH07DQp9DQo=
\ No newline at end of file
diff --git a/jstest/javascript/google_cn.js b/jstest/javascript/google_cn.js
new file mode 100644
index 0000000000..42f4a7e1ed
--- /dev/null
+++ b/jstest/javascript/google_cn.js
@@ -0,0 +1,61 @@
+// From https://github.com/lmc999/RegionRestrictionCheck
+
+function Test(outbounds, now_selected) {
+ var requests = new Array();
+
+ for (var i = 0; i < outbounds.length; i++) {
+ requests.push({
+ method: "GET",
+ url: "https://www.youtube.com/premium",
+ headers: {
+ "Host": "www.youtube.com",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60",
+ "Accept-Language": "en",
+ },
+ cookies: {
+ "YSC": "BiCUU3-5Gdk",
+ "CONSENT": "YES+cb.20220301-11-p0.en+FX+700",
+ "GPS": "1",
+ "VISITOR_INFO1_LIVE": "4VwPMkB7W5A",
+ "PREF": "tz=Asia.Shanghai",
+ "_gcl_au": "1.1.1809531354.1646633279",
+ },
+ detour: outbounds[i]
+ });
+ }
+
+ var result = http_requests(requests);
+
+ if (typeof result.error === 'string' && result.error != "") {
+ return { error: result.error };
+ }
+
+ log_debug("http requests success");
+
+ var responses = result.value;
+ var selected = null;
+ var min_cost = 0;
+
+ for (var i = 0; i < responses.length; i++) {
+ var result = responses[i];
+ if (result.error !== null && typeof result.error == 'string' && result.error !== "") {
+ log_error("detour: [" + outbounds[i] + "], error: [" + result.error + "]");
+ } else {
+ var isCN = true;
+ if (result.status === 200 && result.body !== "" && result.body.search("www.google.cn") < 0) {
+ isCN = false;
+ if (min_cost === 0 || result.cost < min_cost) {
+ selected = outbounds[i];
+ min_cost = result.cost;
+ }
+ }
+ log_debug("detour: [" + outbounds[i] + "], status: [" + result.status + "], cn: ["+isCN+"], cost: [" + result.cost + "ms]")
+ }
+ }
+
+ if (selected == null) {
+ return { error: "no outbound is available" };
+ }
+
+ return { value: selected };
+}
diff --git a/jstest/javascript/google_cn.license b/jstest/javascript/google_cn.license
new file mode 100644
index 0000000000..29ebfa545f
--- /dev/null
+++ b/jstest/javascript/google_cn.license
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
\ No newline at end of file
diff --git a/jstest/javascript/google_cn_js.base64 b/jstest/javascript/google_cn_js.base64
new file mode 100644
index 0000000000..26f6aa2cbe
--- /dev/null
+++ b/jstest/javascript/google_cn_js.base64
@@ -0,0 +1 @@
+Ly8gRnJvbSBodHRwczovL2dpdGh1Yi5jb20vbG1jOTk5L1JlZ2lvblJlc3RyaWN0aW9uQ2hlY2sNCg0KZnVuY3Rpb24gVGVzdChvdXRib3VuZHMsIG5vd19zZWxlY3RlZCkgew0KICAgIHZhciByZXF1ZXN0cyA9IG5ldyBBcnJheSgpOw0KDQogICAgZm9yICh2YXIgaSA9IDA7IGkgPCBvdXRib3VuZHMubGVuZ3RoOyBpKyspIHsNCiAgICAgICAgcmVxdWVzdHMucHVzaCh7DQogICAgICAgICAgICBtZXRob2Q6ICJHRVQiLA0KICAgICAgICAgICAgdXJsOiAiaHR0cHM6Ly93d3cueW91dHViZS5jb20vcHJlbWl1bSIsDQogICAgICAgICAgICBoZWFkZXJzOiB7DQogICAgICAgICAgICAgICAgIkhvc3QiOiAid3d3LnlvdXR1YmUuY29tIiwNCiAgICAgICAgICAgICAgICAiVXNlci1BZ2VudCI6ICJNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvMTE3LjAuMC4wIFNhZmFyaS81MzcuMzYgRWRnLzExNy4wLjIwNDUuNjAiLA0KICAgICAgICAgICAgICAgICJBY2NlcHQtTGFuZ3VhZ2UiOiAiZW4iLA0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgIGNvb2tpZXM6IHsNCiAgICAgICAgICAgICAgICAiWVNDIjogIkJpQ1VVMy01R2RrIiwNCiAgICAgICAgICAgICAgICAiQ09OU0VOVCI6ICJZRVMrY2IuMjAyMjAzMDEtMTEtcDAuZW4rRlgrNzAwIiwNCiAgICAgICAgICAgICAgICAiR1BTIjogIjEiLA0KICAgICAgICAgICAgICAgICJWSVNJVE9SX0lORk8xX0xJVkUiOiAiNFZ3UE1rQjdXNUEiLA0KICAgICAgICAgICAgICAgICJQUkVGIjogInR6PUFzaWEuU2hhbmdoYWkiLA0KICAgICAgICAgICAgICAgICJfZ2NsX2F1IjogIjEuMS4xODA5NTMxMzU0LjE2NDY2MzMyNzkiLA0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgIGRldG91cjogb3V0Ym91bmRzW2ldDQogICAgICAgIH0pOw0KICAgIH0NCg0KICAgIHZhciByZXN1bHQgPSBodHRwX3JlcXVlc3RzKHJlcXVlc3RzKTsNCg0KICAgIGlmICh0eXBlb2YgcmVzdWx0LmVycm9yID09PSAnc3RyaW5nJyAmJiByZXN1bHQuZXJyb3IgIT0gIiIpIHsNCiAgICAgICAgcmV0dXJuIHsgZXJyb3I6IHJlc3VsdC5lcnJvciB9Ow0KICAgIH0NCg0KICAgIGxvZ19kZWJ1ZygiaHR0cCByZXF1ZXN0cyBzdWNjZXNzIik7DQoNCiAgICB2YXIgcmVzcG9uc2VzID0gcmVzdWx0LnZhbHVlOw0KICAgIHZhciBzZWxlY3RlZCA9IG51bGw7DQogICAgdmFyIG1pbl9jb3N0ID0gMDsNCg0KICAgIGZvciAodmFyIGkgPSAwOyBpIDwgcmVzcG9uc2VzLmxlbmd0aDsgaSsrKSB7DQogICAgICAgIHZhciByZXN1bHQgPSByZXNwb25zZXNbaV07DQogICAgICAgIGlmIChyZXN1bHQuZXJyb3IgIT09IG51bGwgJiYgdHlwZW9mIHJlc3VsdC5lcnJvciA9PSAnc3RyaW5nJyAmJiByZXN1bHQuZXJyb3IgIT09ICIiKSB7DQogICAgICAgICAgICBsb2dfZXJyb3IoImRldG91cjogWyIgKyBvdXRib3VuZHNbaV0gKyAiXSwgZXJyb3I6IFsiICsgcmVzdWx0LmVycm9yICsgIl0iKTsNCiAgICAgICAgfSBlbHNlIHsNCiAgICAgICAgICAgIHZhciBpc0NOID0gdHJ1ZTsNCiAgICAgICAgICAgIGlmIChyZXN1bHQuc3RhdHVzID09PSAyMDAgJiYgcmVzdWx0LmJvZHkgIT09ICIiICYmIHJlc3VsdC5ib2R5LnNlYXJjaCgid3d3Lmdvb2dsZS5jbiIpIDwgMCkgew0KICAgICAgICAgICAgICAgIGlzQ04gPSBmYWxzZTsNCiAgICAgICAgICAgICAgICBpZiAobWluX2Nvc3QgPT09IDAgfHwgcmVzdWx0LmNvc3QgPCBtaW5fY29zdCkgew0KICAgICAgICAgICAgICAgICAgICBzZWxlY3RlZCA9IG91dGJvdW5kc1tpXTsNCiAgICAgICAgICAgICAgICAgICAgbWluX2Nvc3QgPSByZXN1bHQuY29zdDsNCiAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9DQogICAgICAgICAgICBsb2dfZGVidWcoImRldG91cjogWyIgKyBvdXRib3VuZHNbaV0gKyAiXSwgc3RhdHVzOiBbIiArIHJlc3VsdC5zdGF0dXMgKyAiXSwgY246IFsiK2lzQ04rIl0sIGNvc3Q6IFsiICsgcmVzdWx0LmNvc3QgKyAibXNdIikNCiAgICAgICAgfQ0KICAgIH0NCg0KICAgIGlmIChzZWxlY3RlZCA9PSBudWxsKSB7DQogICAgICAgIHJldHVybiB7IGVycm9yOiAibm8gb3V0Ym91bmQgaXMgYXZhaWxhYmxlIiB9Ow0KICAgIH0NCg0KICAgIHJldHVybiB7IHZhbHVlOiBzZWxlY3RlZCB9Ow0KfQ0K
\ No newline at end of file
diff --git a/jstest/javascript/netflix.js b/jstest/javascript/netflix.js
new file mode 100644
index 0000000000..8d7e1260c3
--- /dev/null
+++ b/jstest/javascript/netflix.js
@@ -0,0 +1,99 @@
+// From https://github.com/lmc999/RegionRestrictionCheck
+
+function Test(outbounds, now_selected) {
+ var requests = new Array();
+
+ for (var i = 0; i < outbounds.length; i++) {
+ requests.push({
+ method: "GET",
+ url: "https://www.netflix.com/title/81280792",
+ headers: {
+ "Host": "www.netflix.com",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60",
+ },
+ detour: outbounds[i]
+ });
+ }
+ for (var i = 0; i < outbounds.length; i++) {
+ requests.push({
+ method: "GET",
+ url: "https://www.netflix.com/title/70143836",
+ headers: {
+ "Host": "www.netflix.com",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60",
+ },
+ detour: outbounds[i]
+ });
+ }
+ for (var i = 0; i < outbounds.length; i++) {
+ requests.push({
+ method: "GET",
+ url: "https://www.netflix.com/title/80018499",
+ headers: {
+ "Host": "www.netflix.com",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.60",
+ },
+ detour: outbounds[i]
+ });
+ }
+
+ var result = http_requests(requests);
+
+ if (typeof result.error === 'string' && result.error != "") {
+ return { error: result.error };
+ }
+
+ log_debug("http requests success");
+
+ var responses = result.value;
+ var selected = null;
+ var min_cost = 0;
+
+ for (var i = 0; i < outbounds.length; i++) {
+ var result1 = responses[i];
+ var result2 = responses[i + outbounds.length];
+ var result3 = responses[i + outbounds.length * 2];
+ if ((result1.error !== null && typeof result1.error == 'string' && result1.error !== "") || (result2.error !== null && typeof result2.error == 'string' && result2.error !== "")) {
+ log_error("detour: [" + outbounds[i] + "], error: [" + result1.error + "] [" + result2.error + "]");
+ } else {
+ var isAllow = false;
+ var type = "Failed";
+ if (result1.status === 404 && result2.status === 404) {
+ type = "Originals Only";
+ }
+ if (result1.status === 403 && result2.status === 403) {
+ type = "Blocked";
+ }
+ if (result1.status === 200 || result2.status === 200) {
+ var tag = false;
+ if (result3.headers != undefined) {
+ var u = result3.headers["X-Originating-Url"].toString();
+ if (u !== "") {
+ var cc = u.split('/')[3];
+ if (cc !== "title") {
+ type = cc.split('-')[0].toUpperCase();
+ tag = true;
+ }
+ }
+ }
+ if (!tag) {
+ type = "Yes";
+ }
+ isAllow = true;
+ }
+ if (isAllow) {
+ if (min_cost === 0 || (result1.cost + result2.cost) / 2 < min_cost) {
+ selected = outbounds[i];
+ min_cost = (result1.cost + result2.cost) / 2;
+ }
+ }
+ log_debug("detour: [" + outbounds[i] + "], status: [" + result1.status + ", " + result2.status + "], type: [" + type + "], cost: [" + (result1.cost + result2.cost) / 2 + "ms]")
+ }
+ }
+
+ if (selected == null) {
+ return { error: "no outbound is available" };
+ }
+
+ return { value: selected };
+}
\ No newline at end of file
diff --git a/jstest/javascript/netflix.license b/jstest/javascript/netflix.license
new file mode 100644
index 0000000000..29ebfa545f
--- /dev/null
+++ b/jstest/javascript/netflix.license
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
\ No newline at end of file
diff --git a/jstest/javascript/netflix_js.base64 b/jstest/javascript/netflix_js.base64
new file mode 100644
index 0000000000..5b3c78e12a
--- /dev/null
+++ b/jstest/javascript/netflix_js.base64
@@ -0,0 +1 @@
+Ly8gRnJvbSBodHRwczovL2dpdGh1Yi5jb20vbG1jOTk5L1JlZ2lvblJlc3RyaWN0aW9uQ2hlY2sNCg0KZnVuY3Rpb24gVGVzdChvdXRib3VuZHMsIG5vd19zZWxlY3RlZCkgew0KICAgIHZhciByZXF1ZXN0cyA9IG5ldyBBcnJheSgpOw0KDQogICAgZm9yICh2YXIgaSA9IDA7IGkgPCBvdXRib3VuZHMubGVuZ3RoOyBpKyspIHsNCiAgICAgICAgcmVxdWVzdHMucHVzaCh7DQogICAgICAgICAgICBtZXRob2Q6ICJHRVQiLA0KICAgICAgICAgICAgdXJsOiAiaHR0cHM6Ly93d3cubmV0ZmxpeC5jb20vdGl0bGUvODEyODA3OTIiLA0KICAgICAgICAgICAgaGVhZGVyczogew0KICAgICAgICAgICAgICAgICJIb3N0IjogInd3dy5uZXRmbGl4LmNvbSIsDQogICAgICAgICAgICAgICAgIlVzZXItQWdlbnQiOiAiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzExNy4wLjAuMCBTYWZhcmkvNTM3LjM2IEVkZy8xMTcuMC4yMDQ1LjYwIiwNCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICBkZXRvdXI6IG91dGJvdW5kc1tpXQ0KICAgICAgICB9KTsNCiAgICB9DQogICAgZm9yICh2YXIgaSA9IDA7IGkgPCBvdXRib3VuZHMubGVuZ3RoOyBpKyspIHsNCiAgICAgICAgcmVxdWVzdHMucHVzaCh7DQogICAgICAgICAgICBtZXRob2Q6ICJHRVQiLA0KICAgICAgICAgICAgdXJsOiAiaHR0cHM6Ly93d3cubmV0ZmxpeC5jb20vdGl0bGUvNzAxNDM4MzYiLA0KICAgICAgICAgICAgaGVhZGVyczogew0KICAgICAgICAgICAgICAgICJIb3N0IjogInd3dy5uZXRmbGl4LmNvbSIsDQogICAgICAgICAgICAgICAgIlVzZXItQWdlbnQiOiAiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzExNy4wLjAuMCBTYWZhcmkvNTM3LjM2IEVkZy8xMTcuMC4yMDQ1LjYwIiwNCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICBkZXRvdXI6IG91dGJvdW5kc1tpXQ0KICAgICAgICB9KTsNCiAgICB9DQogICAgZm9yICh2YXIgaSA9IDA7IGkgPCBvdXRib3VuZHMubGVuZ3RoOyBpKyspIHsNCiAgICAgICAgcmVxdWVzdHMucHVzaCh7DQogICAgICAgICAgICBtZXRob2Q6ICJHRVQiLA0KICAgICAgICAgICAgdXJsOiAiaHR0cHM6Ly93d3cubmV0ZmxpeC5jb20vdGl0bGUvODAwMTg0OTkiLA0KICAgICAgICAgICAgaGVhZGVyczogew0KICAgICAgICAgICAgICAgICJIb3N0IjogInd3dy5uZXRmbGl4LmNvbSIsDQogICAgICAgICAgICAgICAgIlVzZXItQWdlbnQiOiAiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzExNy4wLjAuMCBTYWZhcmkvNTM3LjM2IEVkZy8xMTcuMC4yMDQ1LjYwIiwNCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICBkZXRvdXI6IG91dGJvdW5kc1tpXQ0KICAgICAgICB9KTsNCiAgICB9DQoNCiAgICB2YXIgcmVzdWx0ID0gaHR0cF9yZXF1ZXN0cyhyZXF1ZXN0cyk7DQoNCiAgICBpZiAodHlwZW9mIHJlc3VsdC5lcnJvciA9PT0gJ3N0cmluZycgJiYgcmVzdWx0LmVycm9yICE9ICIiKSB7DQogICAgICAgIHJldHVybiB7IGVycm9yOiByZXN1bHQuZXJyb3IgfTsNCiAgICB9DQoNCiAgICBsb2dfZGVidWcoImh0dHAgcmVxdWVzdHMgc3VjY2VzcyIpOw0KDQogICAgdmFyIHJlc3BvbnNlcyA9IHJlc3VsdC52YWx1ZTsNCiAgICB2YXIgc2VsZWN0ZWQgPSBudWxsOw0KICAgIHZhciBtaW5fY29zdCA9IDA7DQoNCiAgICBmb3IgKHZhciBpID0gMDsgaSA8IG91dGJvdW5kcy5sZW5ndGg7IGkrKykgew0KICAgICAgICB2YXIgcmVzdWx0MSA9IHJlc3BvbnNlc1tpXTsNCiAgICAgICAgdmFyIHJlc3VsdDIgPSByZXNwb25zZXNbaSArIG91dGJvdW5kcy5sZW5ndGhdOw0KICAgICAgICB2YXIgcmVzdWx0MyA9IHJlc3BvbnNlc1tpICsgb3V0Ym91bmRzLmxlbmd0aCAqIDJdOw0KICAgICAgICBpZiAoKHJlc3VsdDEuZXJyb3IgIT09IG51bGwgJiYgdHlwZW9mIHJlc3VsdDEuZXJyb3IgPT0gJ3N0cmluZycgJiYgcmVzdWx0MS5lcnJvciAhPT0gIiIpIHx8IChyZXN1bHQyLmVycm9yICE9PSBudWxsICYmIHR5cGVvZiByZXN1bHQyLmVycm9yID09ICdzdHJpbmcnICYmIHJlc3VsdDIuZXJyb3IgIT09ICIiKSkgew0KICAgICAgICAgICAgbG9nX2Vycm9yKCJkZXRvdXI6IFsiICsgb3V0Ym91bmRzW2ldICsgIl0sIGVycm9yOiBbIiArIHJlc3VsdDEuZXJyb3IgKyAiXSBbIiArIHJlc3VsdDIuZXJyb3IgKyAiXSIpOw0KICAgICAgICB9IGVsc2Ugew0KICAgICAgICAgICAgdmFyIGlzQWxsb3cgPSBmYWxzZTsNCiAgICAgICAgICAgIHZhciB0eXBlID0gIkZhaWxlZCI7DQogICAgICAgICAgICBpZiAocmVzdWx0MS5zdGF0dXMgPT09IDQwNCAmJiByZXN1bHQyLnN0YXR1cyA9PT0gNDA0KSB7DQogICAgICAgICAgICAgICAgdHlwZSA9ICJPcmlnaW5hbHMgT25seSI7DQogICAgICAgICAgICB9DQogICAgICAgICAgICBpZiAocmVzdWx0MS5zdGF0dXMgPT09IDQwMyAmJiByZXN1bHQyLnN0YXR1cyA9PT0gNDAzKSB7DQogICAgICAgICAgICAgICAgdHlwZSA9ICJCbG9ja2VkIjsNCiAgICAgICAgICAgIH0NCiAgICAgICAgICAgIGlmIChyZXN1bHQxLnN0YXR1cyA9PT0gMjAwIHx8IHJlc3VsdDIuc3RhdHVzID09PSAyMDApIHsNCiAgICAgICAgICAgICAgICB2YXIgdGFnID0gZmFsc2U7DQogICAgICAgICAgICAgICAgaWYgKHJlc3VsdDMuaGVhZGVycyAhPSB1bmRlZmluZWQpIHsNCiAgICAgICAgICAgICAgICAgICAgdmFyIHUgPSByZXN1bHQzLmhlYWRlcnNbIlgtT3JpZ2luYXRpbmctVXJsIl0udG9TdHJpbmcoKTsNCiAgICAgICAgICAgICAgICAgICAgaWYgKHUgIT09ICIiKSB7DQogICAgICAgICAgICAgICAgICAgICAgICB2YXIgY2MgPSB1LnNwbGl0KCcvJylbM107DQogICAgICAgICAgICAgICAgICAgICAgICBpZiAoY2MgIT09ICJ0aXRsZSIpIHsNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0eXBlID0gY2Muc3BsaXQoJy0nKVswXS50b1VwcGVyQ2FzZSgpOw0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRhZyA9IHRydWU7DQogICAgICAgICAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICAgICAgaWYgKCF0YWcpIHsNCiAgICAgICAgICAgICAgICAgICAgdHlwZSA9ICJZZXMiOw0KICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgICAgICBpc0FsbG93ID0gdHJ1ZTsNCiAgICAgICAgICAgIH0NCiAgICAgICAgICAgIGlmIChpc0FsbG93KSB7DQogICAgICAgICAgICAgICAgaWYgKG1pbl9jb3N0ID09PSAwIHx8IChyZXN1bHQxLmNvc3QgKyByZXN1bHQyLmNvc3QpIC8gMiA8IG1pbl9jb3N0KSB7DQogICAgICAgICAgICAgICAgICAgIHNlbGVjdGVkID0gb3V0Ym91bmRzW2ldOw0KICAgICAgICAgICAgICAgICAgICBtaW5fY29zdCA9IChyZXN1bHQxLmNvc3QgKyByZXN1bHQyLmNvc3QpIC8gMjsNCiAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9DQogICAgICAgICAgICBsb2dfZGVidWcoImRldG91cjogWyIgKyBvdXRib3VuZHNbaV0gKyAiXSwgc3RhdHVzOiBbIiArIHJlc3VsdDEuc3RhdHVzICsgIiwgIiArIHJlc3VsdDIuc3RhdHVzICsgIl0sIHR5cGU6IFsiICsgdHlwZSArICJdLCBjb3N0OiBbIiArIChyZXN1bHQxLmNvc3QgKyByZXN1bHQyLmNvc3QpIC8gMiArICJtc10iKQ0KICAgICAgICB9DQogICAgfQ0KDQogICAgaWYgKHNlbGVjdGVkID09IG51bGwpIHsNCiAgICAgICAgcmV0dXJuIHsgZXJyb3I6ICJubyBvdXRib3VuZCBpcyBhdmFpbGFibGUiIH07DQogICAgfQ0KDQogICAgcmV0dXJuIHsgdmFsdWU6IHNlbGVjdGVkIH07DQp9
\ No newline at end of file
diff --git a/log/log.go b/log/log.go
index 7b8f284350..7692417225 100644
--- a/log/log.go
+++ b/log/log.go
@@ -47,7 +47,7 @@ func New(options Options) (Factory, error) {
DisableColors: logOptions.DisableColor || logFilePath != "",
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
FullTimestamp: logOptions.Timestamp,
- TimestampFormat: "-0700 2006-01-02 15:04:05",
+ TimestampFormat: "[2006-01-02 15:04:05 UTC-07]",
}
factory := NewDefaultFactory(
options.Context,
diff --git a/option/config.go b/option/config.go
index 3f5d7602c0..007562ee46 100644
--- a/option/config.go
+++ b/option/config.go
@@ -7,15 +7,17 @@ import (
)
type _Options struct {
- RawMessage json.RawMessage `json:"-"`
- Schema string `json:"$schema,omitempty"`
- Log *LogOptions `json:"log,omitempty"`
- DNS *DNSOptions `json:"dns,omitempty"`
- NTP *NTPOptions `json:"ntp,omitempty"`
- Inbounds []Inbound `json:"inbounds,omitempty"`
- Outbounds []Outbound `json:"outbounds,omitempty"`
- Route *RouteOptions `json:"route,omitempty"`
- Experimental *ExperimentalOptions `json:"experimental,omitempty"`
+ RawMessage json.RawMessage `json:"-"`
+ Schema string `json:"$schema,omitempty"`
+ Log *LogOptions `json:"log,omitempty"`
+ DNS *DNSOptions `json:"dns,omitempty"`
+ NTP *NTPOptions `json:"ntp,omitempty"`
+ Inbounds []Inbound `json:"inbounds,omitempty"`
+ Outbounds []Outbound `json:"outbounds,omitempty"`
+ ProxyProviders []ProxyProvider `json:"proxyproviders,omitempty"`
+ Route *RouteOptions `json:"route,omitempty"`
+ Experimental *ExperimentalOptions `json:"experimental,omitempty"`
+ Scripts []ScriptOptions `json:"scripts,omitempty"`
}
type Options _Options
diff --git a/option/debug.go b/option/debug.go
index 0b0b825a82..17fb05d03d 100644
--- a/option/debug.go
+++ b/option/debug.go
@@ -1,8 +1,9 @@
package option
import (
+ "encoding/json"
+
"github.com/sagernet/sing-box/common/humanize"
- "github.com/sagernet/sing/common/json"
)
type DebugOptions struct {
diff --git a/option/experimental.go b/option/experimental.go
index c685f51f54..0be085233a 100644
--- a/option/experimental.go
+++ b/option/experimental.go
@@ -19,6 +19,7 @@ type ClashAPIOptions struct {
ExternalUI string `json:"external_ui,omitempty"`
ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"`
ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"`
+ ExternalUIBuildin bool `json:"external_ui_buildin,omitempty"`
Secret string `json:"secret,omitempty"`
DefaultMode string `json:"default_mode,omitempty"`
ModeList []string `json:"-"`
diff --git a/option/inbound.go b/option/inbound.go
index 54e8bab84a..071d8db402 100644
--- a/option/inbound.go
+++ b/option/inbound.go
@@ -1,8 +1,6 @@
package option
import (
- "time"
-
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
@@ -107,35 +105,19 @@ type InboundOptions struct {
}
type ListenOptions struct {
- Listen *ListenAddress `json:"listen,omitempty"`
- ListenPort uint16 `json:"listen_port,omitempty"`
- TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
- TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
- UDPFragment *bool `json:"udp_fragment,omitempty"`
- UDPFragmentDefault bool `json:"-"`
- UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
- ProxyProtocol bool `json:"proxy_protocol,omitempty"`
- ProxyProtocolAcceptNoHeader bool `json:"proxy_protocol_accept_no_header,omitempty"`
- Detour string `json:"detour,omitempty"`
+ Listen *ListenAddress `json:"listen,omitempty"`
+ ListenPort uint16 `json:"listen_port,omitempty"`
+ TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
+ TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
+ UDPFragment *bool `json:"udp_fragment,omitempty"`
+ UDPFragmentDefault bool `json:"-"`
+ UDPTimeout int64 `json:"udp_timeout,omitempty"`
+ ProxyProtocol bool `json:"proxy_protocol,omitempty"`
+ ProxyProtocolAcceptNoHeader bool `json:"proxy_protocol_accept_no_header,omitempty"`
+ Detour string `json:"detour,omitempty"`
InboundOptions
}
-type UDPTimeoutCompat Duration
-
-func (c UDPTimeoutCompat) MarshalJSON() ([]byte, error) {
- return json.Marshal((time.Duration)(c).String())
-}
-
-func (c *UDPTimeoutCompat) UnmarshalJSON(data []byte) error {
- var valueNumber int64
- err := json.Unmarshal(data, &valueNumber)
- if err == nil {
- *c = UDPTimeoutCompat(time.Second * time.Duration(valueNumber))
- return nil
- }
- return json.Unmarshal(data, (*Duration)(c))
-}
-
type ListenOptionsWrapper interface {
TakeListenOptions() ListenOptions
ReplaceListenOptions(options ListenOptions)
diff --git a/option/jstest.go b/option/jstest.go
new file mode 100644
index 0000000000..086bccf71a
--- /dev/null
+++ b/option/jstest.go
@@ -0,0 +1,10 @@
+package option
+
+type JSTestOutboundOptions struct {
+ Outbounds []string `json:"outbounds"`
+ JSPath string `json:"js_path,omitempty"`
+ JSBase64 string `json:"js_base64,omitempty"`
+ JSGlobalVar map[string]any `json:"js_global_var,omitempty"`
+ Interval Duration `json:"interval,omitempty"`
+ InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
+}
diff --git a/option/outbound.go b/option/outbound.go
index 59ee85ab69..2c18da5990 100644
--- a/option/outbound.go
+++ b/option/outbound.go
@@ -25,8 +25,10 @@ type _Outbound struct {
VLESSOptions VLESSOutboundOptions `json:"-"`
TUICOptions TUICOutboundOptions `json:"-"`
Hysteria2Options Hysteria2OutboundOptions `json:"-"`
+ RandomAddrOptions RandomAddrOutboundOptions `json:"-"`
SelectorOptions SelectorOutboundOptions `json:"-"`
URLTestOptions URLTestOutboundOptions `json:"-"`
+ JSTestOptions JSTestOutboundOptions `json:"-"`
}
type Outbound _Outbound
@@ -66,10 +68,14 @@ func (h *Outbound) RawOptions() (any, error) {
rawOptionsPtr = &h.TUICOptions
case C.TypeHysteria2:
rawOptionsPtr = &h.Hysteria2Options
+ case C.TypeRandomAddr:
+ rawOptionsPtr = &h.RandomAddrOptions
case C.TypeSelector:
rawOptionsPtr = &h.SelectorOptions
case C.TypeURLTest:
rawOptionsPtr = &h.URLTestOptions
+ case C.TypeJSTest:
+ rawOptionsPtr = &h.JSTestOptions
case "":
return nil, E.New("missing outbound type")
default:
@@ -108,21 +114,20 @@ type DialerOptionsWrapper interface {
}
type DialerOptions struct {
- Detour string `json:"detour,omitempty"`
- BindInterface string `json:"bind_interface,omitempty"`
- Inet4BindAddress *ListenAddress `json:"inet4_bind_address,omitempty"`
- Inet6BindAddress *ListenAddress `json:"inet6_bind_address,omitempty"`
- ProtectPath string `json:"protect_path,omitempty"`
- RoutingMark int `json:"routing_mark,omitempty"`
- ReuseAddr bool `json:"reuse_addr,omitempty"`
- ConnectTimeout Duration `json:"connect_timeout,omitempty"`
- TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
- TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
- UDPFragment *bool `json:"udp_fragment,omitempty"`
- UDPFragmentDefault bool `json:"-"`
- DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
- FallbackDelay Duration `json:"fallback_delay,omitempty"`
- IsWireGuardListener bool `json:"-"`
+ Detour string `json:"detour,omitempty"`
+ BindInterface string `json:"bind_interface,omitempty"`
+ Inet4BindAddress *ListenAddress `json:"inet4_bind_address,omitempty"`
+ Inet6BindAddress *ListenAddress `json:"inet6_bind_address,omitempty"`
+ ProtectPath string `json:"protect_path,omitempty"`
+ RoutingMark int `json:"routing_mark,omitempty"`
+ ReuseAddr bool `json:"reuse_addr,omitempty"`
+ ConnectTimeout Duration `json:"connect_timeout,omitempty"`
+ TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
+ TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
+ UDPFragment *bool `json:"udp_fragment,omitempty"`
+ UDPFragmentDefault bool `json:"-"`
+ DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
+ FallbackDelay Duration `json:"fallback_delay,omitempty"`
}
func (o *DialerOptions) TakeDialerOptions() DialerOptions {
@@ -133,11 +138,6 @@ func (o *DialerOptions) ReplaceDialerOptions(options DialerOptions) {
*o = options
}
-type ServerOptionsWrapper interface {
- TakeServerOptions() ServerOptions
- ReplaceServerOptions(options ServerOptions)
-}
-
type ServerOptions struct {
Server string `json:"server"`
ServerPort uint16 `json:"server_port"`
@@ -146,11 +146,3 @@ type ServerOptions struct {
func (o ServerOptions) Build() M.Socksaddr {
return M.ParseSocksaddrHostPort(o.Server, o.ServerPort)
}
-
-func (o *ServerOptions) TakeServerOptions() ServerOptions {
- return *o
-}
-
-func (o *ServerOptions) ReplaceServerOptions(options ServerOptions) {
- *o = options
-}
diff --git a/option/proxyprovider.go b/option/proxyprovider.go
new file mode 100644
index 0000000000..b565f7f99d
--- /dev/null
+++ b/option/proxyprovider.go
@@ -0,0 +1,79 @@
+package option
+
+import (
+ C "github.com/sagernet/sing-box/constant"
+ E "github.com/sagernet/sing/common/exceptions"
+ "github.com/sagernet/sing/common/json"
+)
+
+type ProxyProvider struct {
+ Tag string `json:"tag"`
+ Url string `json:"url"`
+ UserAgent string `json:"download_ua,omitempty"`
+ CacheFile string `json:"cache_file,omitempty"`
+ UpdateInterval Duration `json:"update_interval,omitempty"`
+ RequestTimeout Duration `json:"request_timeout,omitempty"`
+ UseH3 bool `json:"use_h3,omitempty"`
+ DNS string `json:"dns,omitempty"`
+ TagFormat string `json:"tag_format,omitempty"`
+ GlobalFilter *ProxyProviderFilter `json:"global_filter,omitempty"`
+ Groups []ProxyProviderGroup `json:"groups,omitempty"`
+ RequestDialer DialerOptions `json:"request_dialer,omitempty"`
+ Dialer *DialerOptions `json:"dialer,omitempty"`
+ LookupIP bool `json:"lookup_ip,omitempty"`
+ RunningDetour string `json:"running_detour,omitempty"`
+}
+
+type ProxyProviderFilter struct {
+ WhiteMode bool `json:"white_mode,omitempty"`
+ Rules Listable[string] `json:"rules,omitempty"`
+}
+
+type _ProxyProviderGroup struct {
+ Tag string `json:"tag"`
+ Type string `json:"type"`
+ SelectorOptions SelectorOutboundOptions `json:"-"`
+ URLTestOptions URLTestOutboundOptions `json:"-"`
+ JSTestOptions JSTestOutboundOptions `json:"-"`
+ Filter *ProxyProviderFilter `json:"filter,omitempty"`
+}
+
+type ProxyProviderGroup _ProxyProviderGroup
+
+func (p ProxyProviderGroup) MarshalJSON() ([]byte, error) {
+ var v any
+ switch p.Type {
+ case C.TypeSelector:
+ v = p.SelectorOptions
+ case C.TypeURLTest:
+ v = p.URLTestOptions
+ case C.TypeJSTest:
+ v = p.JSTestOptions
+ default:
+ return nil, E.New("unknown outbound type: ", p.Type)
+ }
+ return MarshallObjects((_ProxyProviderGroup)(p), v)
+}
+
+func (p *ProxyProviderGroup) UnmarshalJSON(bytes []byte) error {
+ err := json.Unmarshal(bytes, (*_ProxyProviderGroup)(p))
+ if err != nil {
+ return err
+ }
+ var v any
+ switch p.Type {
+ case C.TypeSelector:
+ v = &p.SelectorOptions
+ case C.TypeURLTest:
+ v = &p.URLTestOptions
+ case C.TypeJSTest:
+ v = &p.JSTestOptions
+ default:
+ return E.New("unknown outbound type: ", p.Type)
+ }
+ err = UnmarshallExcluded(bytes, (*_ProxyProviderGroup)(p), v)
+ if err != nil {
+ return E.Cause(err, "proxyprovider group options")
+ }
+ return nil
+}
diff --git a/option/randomaddr.go b/option/randomaddr.go
new file mode 100644
index 0000000000..a2f3b18b2a
--- /dev/null
+++ b/option/randomaddr.go
@@ -0,0 +1,14 @@
+package option
+
+type RandomAddrOutboundOptions struct {
+ Addresses Listable[RandomAddress] `json:"addresses,omitempty"`
+ IgnoreFqdn bool `json:"ignore_fqdn,omitempty"`
+ DeleteFqdn bool `json:"delete_fqdn,omitempty"`
+ UDP bool `json:"udp,omitempty"`
+ DialerOptions
+}
+
+type RandomAddress struct {
+ IP string `json:"ip,omitempty"`
+ Port uint16 `json:"port,omitempty"`
+}
diff --git a/option/route.go b/option/route.go
index e313fcf242..8c880bc845 100644
--- a/option/route.go
+++ b/option/route.go
@@ -14,13 +14,15 @@ type RouteOptions struct {
}
type GeoIPOptions struct {
- Path string `json:"path,omitempty"`
- DownloadURL string `json:"download_url,omitempty"`
- DownloadDetour string `json:"download_detour,omitempty"`
+ Path string `json:"path,omitempty"`
+ DownloadURL string `json:"download_url,omitempty"`
+ DownloadDetour string `json:"download_detour,omitempty"`
+ AutoUpdateInterval Duration `json:"auto_update_interval,omitempty"`
}
type GeositeOptions struct {
- Path string `json:"path,omitempty"`
- DownloadURL string `json:"download_url,omitempty"`
- DownloadDetour string `json:"download_detour,omitempty"`
+ Path string `json:"path,omitempty"`
+ DownloadURL string `json:"download_url,omitempty"`
+ DownloadDetour string `json:"download_detour,omitempty"`
+ AutoUpdateInterval Duration `json:"auto_update_interval,omitempty"`
}
diff --git a/option/script.go b/option/script.go
new file mode 100644
index 0000000000..4270976ef7
--- /dev/null
+++ b/option/script.go
@@ -0,0 +1,18 @@
+package option
+
+type ScriptOptions struct {
+ Tag string `json:"tag"`
+ Command string `json:"command"`
+ Args Listable[string] `json:"args,omitempty"`
+ Directory string `json:"directory,omitempty"`
+ Mode string `json:"mode"`
+ Env map[string]string `json:"env,omitempty"`
+ NoFatal bool `json:"no_fatal,omitempty"`
+ LogOptions ScriptLogOptions `json:"log,omitempty"`
+}
+
+type ScriptLogOptions struct {
+ Enabled bool `json:"enabled"`
+ StdoutLogLevel string `json:"stdout_log_level,omitempty"`
+ StderrLogLevel string `json:"stderr_log_level,omitempty"`
+}
diff --git a/option/tor.go b/option/tor.go
index a56f70aea2..07bdab330c 100644
--- a/option/tor.go
+++ b/option/tor.go
@@ -6,4 +6,5 @@ type TorOutboundOptions struct {
ExtraArgs []string `json:"extra_args,omitempty"`
DataDirectory string `json:"data_directory,omitempty"`
Options map[string]string `json:"torrc,omitempty"`
+ NoFatal bool `json:"no_fatal,omitempty"`
}
diff --git a/option/tun.go b/option/tun.go
index ac66a8061c..51e8f91555 100644
--- a/option/tun.go
+++ b/option/tun.go
@@ -6,6 +6,7 @@ type TunInboundOptions struct {
InterfaceName string `json:"interface_name,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
GSO bool `json:"gso,omitempty"`
+ GSOMaxSize uint32 `json:"gso_max_size,omitempty"`
Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"`
Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"`
AutoRoute bool `json:"auto_route,omitempty"`
@@ -24,7 +25,7 @@ type TunInboundOptions struct {
IncludePackage Listable[string] `json:"include_package,omitempty"`
ExcludePackage Listable[string] `json:"exclude_package,omitempty"`
EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"`
- UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
+ UDPTimeout int64 `json:"udp_timeout,omitempty"`
Stack string `json:"stack,omitempty"`
Platform *TunPlatformOptions `json:"platform,omitempty"`
InboundOptions
diff --git a/option/types.go b/option/types.go
index aba445eead..e6ab82493b 100644
--- a/option/types.go
+++ b/option/types.go
@@ -3,7 +3,6 @@ package option
import (
"net/http"
"net/netip"
- "strings"
"time"
"github.com/sagernet/sing-dns"
@@ -51,60 +50,60 @@ func (a *ListenAddress) Build() netip.Addr {
return (netip.Addr)(*a)
}
-type NetworkList string
+type NetworkList []string
func (v *NetworkList) UnmarshalJSON(content []byte) error {
- var networkList []string
- err := json.Unmarshal(content, &networkList)
+ var (
+ networkList []string
+ err error
+ )
+ if len(content) > 0 && content[0] != '[' {
+ networkList = make([]string, 1)
+ err = json.Unmarshal(content, &networkList[0])
+ } else {
+ err = json.Unmarshal(content, &networkList)
+ }
if err != nil {
- var networkItem string
- err = json.Unmarshal(content, &networkItem)
- if err != nil {
- return err
- }
- networkList = []string{networkItem}
+ return err
}
for _, networkName := range networkList {
switch networkName {
case N.NetworkTCP, N.NetworkUDP:
break
default:
- return E.New("unknown network: " + networkName)
+ return E.Extend(N.ErrUnknownNetwork, networkName)
}
}
- *v = NetworkList(strings.Join(networkList, "\n"))
+ *v = networkList
return nil
}
func (v NetworkList) Build() []string {
- if v == "" {
+ if len(v) == 0 {
return []string{N.NetworkTCP, N.NetworkUDP}
}
- return strings.Split(string(v), "\n")
+ return v
}
type Listable[T any] []T
func (l Listable[T]) MarshalJSON() ([]byte, error) {
- arrayList := []T(l)
- if len(arrayList) == 1 {
- return json.Marshal(arrayList[0])
+ if len(l) == 1 {
+ return json.Marshal(l[0])
}
- return json.Marshal(arrayList)
+ return json.Marshal([]T(l))
}
func (l *Listable[T]) UnmarshalJSON(content []byte) error {
- err := json.Unmarshal(content, (*[]T)(l))
- if err == nil {
- return nil
- }
- var singleItem T
- newError := json.Unmarshal(content, &singleItem)
- if newError != nil {
- return E.Errors(err, newError)
+ if len(content) > 0 && content[0] != '[' {
+ var element T
+ err := json.Unmarshal(content, &element)
+ if err == nil {
+ *l = []T{element}
+ return nil
+ }
}
- *l = []T{singleItem}
- return nil
+ return json.Unmarshal(content, (*[]T)(l))
}
type DomainStrategy dns.DomainStrategy
diff --git a/option/wireguard.go b/option/wireguard.go
index 65bfad2061..78d7d1f3d4 100644
--- a/option/wireguard.go
+++ b/option/wireguard.go
@@ -6,6 +6,7 @@ type WireGuardOutboundOptions struct {
DialerOptions
SystemInterface bool `json:"system_interface,omitempty"`
GSO bool `json:"gso,omitempty"`
+ GSOMaxSize uint32 `json:"gso_max_size,omitempty"`
InterfaceName string `json:"interface_name,omitempty"`
LocalAddress Listable[netip.Prefix] `json:"local_address"`
PrivateKey string `json:"private_key"`
diff --git a/outbound/builder.go b/outbound/builder.go
index e4d6a80e06..93e79fa6fc 100644
--- a/outbound/builder.go
+++ b/outbound/builder.go
@@ -55,10 +55,14 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t
return NewTUIC(ctx, router, logger, tag, options.TUICOptions)
case C.TypeHysteria2:
return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options)
+ case C.TypeRandomAddr:
+ return NewRandomAddr(ctx, router, logger, tag, options.RandomAddrOptions)
case C.TypeSelector:
return NewSelector(ctx, router, logger, tag, options.SelectorOptions)
case C.TypeURLTest:
return NewURLTest(ctx, router, logger, tag, options.URLTestOptions)
+ case C.TypeJSTest:
+ return NewJSTest(ctx, router, logger, tag, options.JSTestOptions)
default:
return nil, E.New("unknown outbound type: ", options.Type)
}
diff --git a/outbound/direct.go b/outbound/direct.go
index 259205e4c0..3bf80494b9 100644
--- a/outbound/direct.go
+++ b/outbound/direct.go
@@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-dns"
+ "github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@@ -122,7 +123,6 @@ func (h *Direct) ListenPacket(ctx context.Context, destination M.Socksaddr) (net
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Outbound = h.tag
metadata.Destination = destination
- originDestination := destination
switch h.overrideOption {
case 1:
destination = h.overrideDestination
@@ -142,10 +142,11 @@ func (h *Direct) ListenPacket(ctx context.Context, destination M.Socksaddr) (net
if err != nil {
return nil, err
}
- if originDestination != destination {
- conn = bufio.NewNATPacketConn(bufio.NewPacketConn(conn), destination, originDestination)
+ if h.overrideOption == 0 {
+ return conn, nil
+ } else {
+ return &overridePacketConn{bufio.NewPacketConn(conn), destination}, nil
}
- return conn, nil
}
func (h *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
@@ -155,3 +156,20 @@ func (h *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adap
func (h *Direct) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
return NewPacketConnection(ctx, h, conn, metadata)
}
+
+type overridePacketConn struct {
+ N.NetPacketConn
+ overrideDestination M.Socksaddr
+}
+
+func (c *overridePacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
+ return c.NetPacketConn.WritePacket(buffer, c.overrideDestination)
+}
+
+func (c *overridePacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+ return c.NetPacketConn.WriteTo(p, c.overrideDestination.UDPAddr())
+}
+
+func (c *overridePacketConn) Upstream() any {
+ return c.NetPacketConn
+}
diff --git a/outbound/jstest.go b/outbound/jstest.go
new file mode 100644
index 0000000000..e7cbed96b5
--- /dev/null
+++ b/outbound/jstest.go
@@ -0,0 +1,340 @@
+//go:build with_jstest
+
+package outbound
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/sagernet/sing-box/adapter"
+ "github.com/sagernet/sing-box/common/interrupt"
+ C "github.com/sagernet/sing-box/constant"
+ jg "github.com/sagernet/sing-box/jstest/golang"
+ "github.com/sagernet/sing-box/log"
+ "github.com/sagernet/sing-box/option"
+ E "github.com/sagernet/sing/common/exceptions"
+ M "github.com/sagernet/sing/common/metadata"
+ N "github.com/sagernet/sing/common/network"
+ "github.com/sagernet/sing/service"
+
+ "github.com/robertkrimen/otto"
+)
+
+const DefaultJSTestInterval = 1 * time.Minute
+
+var _ adapter.Outbound = (*JSTest)(nil)
+
+type JSTest struct {
+ myOutboundAdapter
+ ctx context.Context
+ tags []string
+ outbounds map[string]adapter.Outbound
+ selected adapter.Outbound
+ interruptGroup *interrupt.Group
+ interruptExternalConnections bool
+ interval time.Duration
+ jsPath string
+ jsBase64 string
+ jsGlobalVar map[string]any
+ jsVM *otto.Otto
+ jsCtx context.Context
+ jsCancel context.CancelFunc
+ jsCloseDone chan struct{}
+}
+
+func NewJSTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.JSTestOutboundOptions) (adapter.Outbound, error) {
+ outbound := &JSTest{
+ myOutboundAdapter: myOutboundAdapter{
+ protocol: C.TypeJSTest,
+ router: router,
+ logger: logger,
+ tag: tag,
+ dependencies: options.Outbounds,
+ },
+ ctx: ctx,
+ tags: options.Outbounds,
+ outbounds: make(map[string]adapter.Outbound),
+ interruptGroup: interrupt.NewGroup(),
+ interruptExternalConnections: options.InterruptExistConnections,
+ jsPath: options.JSPath,
+ jsBase64: options.JSBase64,
+ jsGlobalVar: options.JSGlobalVar,
+ interval: time.Duration(options.Interval),
+ }
+ if len(outbound.tags) == 0 {
+ return nil, E.New("missing tags")
+ }
+ if outbound.jsPath == "" && outbound.jsBase64 == "" {
+ return nil, E.New("missing js path or base64")
+ }
+ if outbound.interval <= 0 {
+ outbound.interval = DefaultJSTestInterval
+ }
+ return outbound, nil
+}
+
+func (j *JSTest) Network() []string {
+ if j.selected == nil {
+ return []string{N.NetworkTCP, N.NetworkUDP}
+ }
+ return j.selected.Network()
+}
+
+func (j *JSTest) Start() error {
+ for i, tag := range j.tags {
+ detour, loaded := j.router.Outbound(tag)
+ if !loaded {
+ return E.New("outbound ", i, " not found: ", tag)
+ }
+ j.outbounds[tag] = detour
+ }
+
+ if j.tag != "" {
+ cacheFile := service.FromContext[adapter.CacheFile](j.ctx)
+ if cacheFile != nil {
+ selected := cacheFile.LoadSelected(j.tag)
+ if selected != "" {
+ detour, loaded := j.outbounds[selected]
+ if loaded {
+ j.selected = detour
+ }
+ }
+ }
+ }
+
+ if j.selected == nil {
+ j.selected = j.outbounds[j.tags[0]]
+ }
+
+ // JS
+ j.jsVM = otto.New()
+ j.jsVM.Interrupt = make(chan func(), 1)
+ {
+ j.jsVM.Set("log_trace", jg.JSGoLog(j.jsVM, j.logger.Trace))
+ j.jsVM.Set("log_debug", jg.JSGoLog(j.jsVM, j.logger.Debug))
+ j.jsVM.Set("log_info", jg.JSGoLog(j.jsVM, j.logger.Info))
+ j.jsVM.Set("log_warn", jg.JSGoLog(j.jsVM, j.logger.Warn))
+ j.jsVM.Set("log_error", jg.JSGoLog(j.jsVM, j.logger.Error))
+ j.jsVM.Set("log_fatal", jg.JSGoLog(j.jsVM, j.logger.Fatal))
+ }
+ var err error
+ if j.jsGlobalVar != nil {
+ var vv otto.Value
+ for k, v := range j.jsGlobalVar {
+ if k == "" {
+ continue
+ }
+ vv, err = j.jsVM.ToValue(v)
+ if err != nil {
+ return E.New("convert js global var: ", err)
+ }
+ j.jsVM.Set(k, vv)
+ }
+ }
+ var raw []byte
+ if j.jsPath != "" {
+ raw, err = os.ReadFile(j.jsPath)
+ if err != nil {
+ return E.New("read js file: ", err)
+ }
+ } else {
+ raw, err = base64.RawStdEncoding.DecodeString(strings.TrimSpace(j.jsBase64))
+ if err != nil {
+ return E.New("decode js base64: ", err)
+ }
+ j.jsBase64 = ""
+ }
+ if len(raw) == 0 {
+ return E.New("empty js code")
+ }
+ _, err = j.jsVM.Run(raw)
+ if err != nil {
+ return E.New("load js file: ", err)
+ }
+ j.jsCtx, j.jsCancel = context.WithCancel(j.ctx)
+ j.jsCloseDone = make(chan struct{}, 1)
+ go func() {
+ <-j.jsCtx.Done()
+ j.jsVM.Interrupt <- func() {}
+ close(j.jsVM.Interrupt)
+ }()
+ httpClient := &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
+ httpRequest := ctx.Value(jg.HTTPRequestKey).(*jg.HTTPRequest)
+ detour, loaded := j.outbounds[httpRequest.Detour]
+ if !loaded {
+ return nil, E.New("outbound not found: ", httpRequest.Detour)
+ }
+ return detour.DialContext(ctx, network, M.ParseSocksaddr(address))
+ },
+ },
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ httpRequest := req.Context().Value(jg.HTTPRequestKey).(*jg.HTTPRequest)
+ if httpRequest.DisableRedirect {
+ return http.ErrUseLastResponse
+ }
+ return nil
+ },
+ }
+ j.jsVM.Set("http_requests", jg.JSGoHTTPRequests(j.jsCtx, j.jsVM, httpClient))
+ j.jsVM.Set("urltests", jg.JSGoURLTest(j.jsCtx, j.router, j.jsVM))
+ go j.loopTest()
+
+ return nil
+}
+
+func (j *JSTest) Close() error {
+ if j.jsCtx != nil {
+ j.jsCancel()
+ <-j.jsCloseDone
+ close(j.jsCloseDone)
+ }
+ return nil
+}
+
+func (j *JSTest) loopTest() {
+ defer func() {
+ j.jsCloseDone <- struct{}{}
+ }()
+ j.test()
+ ticker := time.NewTicker(j.interval)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-j.jsCtx.Done():
+ return
+ case <-ticker.C:
+ j.test()
+ }
+ }
+}
+
+// function Test(outbounds, now_selected)
+//
+// Params:
+// * outbounds: []string
+// * now_selected: string
+//
+// Returns:
+// * result => {value: selected(string), error: string}
+
+type jsResponse struct {
+ Value string `json:"value,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+func (j *JSTest) test() {
+ defer func() {
+ err := recover()
+ if err != nil {
+ j.logger.Error("js test painc: ", err)
+ }
+ }()
+ j.logger.Info("run js test")
+ defer j.logger.Info("js test run done")
+ value, err := j.jsVM.Call("Test", nil, j.tags, j.selected.Tag())
+ if err != nil {
+ select {
+ case <-j.jsCtx.Done():
+ return
+ default:
+ }
+ j.logger.Error("js test run failed: ", err)
+ return
+ }
+ select {
+ case <-j.jsCtx.Done():
+ return
+ default:
+ }
+ j.logger.Info("js test run success")
+ if !value.IsObject() {
+ j.logger.Error("js test run: invalid return value: ", fmt.Sprintf("%v", value))
+ return
+ }
+ raw, err := value.Object().MarshalJSON()
+ if err != nil {
+ j.logger.Error("js test run: invalid return value: ", err)
+ return
+ }
+ var response jsResponse
+ err = json.Unmarshal(raw, &response)
+ if err != nil {
+ j.logger.Error("js test run: invalid return value: ", err)
+ return
+ }
+ if response.Error != "" {
+ j.logger.Error("js test run: ", response.Error)
+ return
+ }
+ if response.Value == "" {
+ j.logger.Error("js test run: invalid return value: ", response.Value)
+ return
+ }
+ j.SelectOutbound(response.Value)
+ j.logger.Info("js test run: select [", response.Value, "]")
+}
+
+func (j *JSTest) SelectOutbound(tag string) bool {
+ detour, loaded := j.outbounds[tag]
+ if !loaded {
+ return false
+ }
+ if j.selected == detour {
+ return true
+ }
+ j.selected = detour
+ if j.tag != "" {
+ cacheFile := service.FromContext[adapter.CacheFile](j.ctx)
+ if cacheFile != nil {
+ err := cacheFile.StoreSelected(j.tag, tag)
+ if err != nil {
+ j.logger.Error("store selected: ", err)
+ }
+ }
+ }
+ j.interruptGroup.Interrupt(j.interruptExternalConnections)
+ return true
+}
+
+func (j *JSTest) Now() string {
+ return j.selected.Tag()
+}
+
+func (j *JSTest) All() []string {
+ return j.tags
+}
+
+func (j *JSTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+ conn, err := j.selected.DialContext(ctx, network, destination)
+ if err != nil {
+ return nil, err
+ }
+ return j.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil
+}
+
+func (j *JSTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+ conn, err := j.selected.ListenPacket(ctx, destination)
+ if err != nil {
+ return nil, err
+ }
+ return j.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil
+}
+
+func (j *JSTest) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+ ctx = interrupt.ContextWithIsExternalConnection(ctx)
+ return j.selected.NewConnection(ctx, conn, metadata)
+}
+
+func (j *JSTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+ ctx = interrupt.ContextWithIsExternalConnection(ctx)
+ return j.selected.NewPacketConnection(ctx, conn, metadata)
+}
diff --git a/outbound/jstest_stub.go b/outbound/jstest_stub.go
new file mode 100644
index 0000000000..5c5f52dfa6
--- /dev/null
+++ b/outbound/jstest_stub.go
@@ -0,0 +1,16 @@
+//go:build !with_jstest
+
+package outbound
+
+import (
+ "context"
+
+ "github.com/sagernet/sing-box/adapter"
+ "github.com/sagernet/sing-box/log"
+ "github.com/sagernet/sing-box/option"
+ E "github.com/sagernet/sing/common/exceptions"
+)
+
+func NewJSTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.JSTestOutboundOptions) (adapter.Outbound, error) {
+ return nil, E.New(`JSTest is not included in this build, rebuild with -tags with_jstest`)
+}
diff --git a/outbound/randomaddr.go b/outbound/randomaddr.go
new file mode 100644
index 0000000000..3f6837ec20
--- /dev/null
+++ b/outbound/randomaddr.go
@@ -0,0 +1,187 @@
+//go:build with_randomaddr
+
+package outbound
+
+import (
+ "context"
+ "math/big"
+ "math/rand"
+ "net"
+ "net/netip"
+ "strings"
+ "time"
+
+ "github.com/sagernet/sing-box/adapter"
+ "github.com/sagernet/sing-box/common/dialer"
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/log"
+ "github.com/sagernet/sing-box/option"
+ E "github.com/sagernet/sing/common/exceptions"
+ M "github.com/sagernet/sing/common/metadata"
+ N "github.com/sagernet/sing/common/network"
+)
+
+var _ adapter.Outbound = (*RandomAddr)(nil)
+
+type RandomAddr struct {
+ myOutboundAdapter
+ ctx context.Context
+ dialer N.Dialer
+ randomAddresses []randomAddress
+ ignoreFqdn bool
+ deleteFqdn bool
+ udp bool
+}
+
+func NewRandomAddr(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RandomAddrOutboundOptions) (adapter.Outbound, error) {
+ if len(options.Addresses) == 0 {
+ return nil, E.New("no addresses")
+ }
+ outboundDialer, err := dialer.New(router, options.DialerOptions)
+ if err != nil {
+ return nil, err
+ }
+ randomAddresses := make([]randomAddress, 0, len(options.Addresses))
+ for _, address := range options.Addresses {
+ randomAddress, err := newRandomAddress(address.IP, address.Port)
+ if err != nil {
+ return nil, E.Cause(err, address)
+ }
+ randomAddresses = append(randomAddresses, *randomAddress)
+ }
+ r := &RandomAddr{
+ myOutboundAdapter: myOutboundAdapter{
+ protocol: C.TypeRandomAddr,
+ router: router,
+ logger: logger,
+ tag: tag,
+ dependencies: withDialerDependency(options.DialerOptions),
+ },
+ ctx: ctx,
+ dialer: outboundDialer,
+ randomAddresses: randomAddresses,
+ ignoreFqdn: options.IgnoreFqdn,
+ deleteFqdn: options.DeleteFqdn,
+ udp: options.UDP,
+ }
+ return r, nil
+}
+
+func (r *RandomAddr) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+ r.overrideDestination(ctx, &destination)
+ return r.dialer.DialContext(ctx, network, destination)
+}
+
+func (r *RandomAddr) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+ if r.udp {
+ r.overrideDestination(ctx, &destination)
+ }
+ return r.dialer.ListenPacket(ctx, destination)
+}
+
+func (r *RandomAddr) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
+ return NewConnection(ctx, r, conn, metadata)
+}
+
+func (r *RandomAddr) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
+ return NewPacketConnection(ctx, r, conn, metadata)
+}
+
+func (r *RandomAddr) overrideDestination(ctx context.Context, destination *M.Socksaddr) {
+ if !destination.IsFqdn() || !r.ignoreFqdn {
+ address := r.randomAddresses[randomRand().Intn(len(r.randomAddresses))].randomAddr(destination.Port)
+ r.logger.DebugContext(ctx, "random address: ", address.String())
+ destination.Addr = address.Addr()
+ destination.Port = address.Port()
+ if r.deleteFqdn {
+ destination.Fqdn = ""
+ }
+ }
+}
+
+func randomRand() *rand.Rand {
+ return rand.New(rand.NewSource(time.Now().UnixNano()))
+}
+
+type randomAddress struct {
+ start *netip.Addr
+ end *netip.Addr
+ prefix *netip.Prefix
+ port uint16
+}
+
+func newRandomAddress(address string, port uint16) (*randomAddress, error) {
+ if address == "" {
+ return nil, E.New("empty ip address")
+ }
+ ip, err := netip.ParseAddr(address)
+ if err == nil {
+ return &randomAddress{start: &ip, port: port}, nil
+ }
+ prefix, err := netip.ParsePrefix(address)
+ if err == nil {
+ addr := prefix.Addr()
+ if addr.Is4() && prefix.Bits() == 32 {
+ return &randomAddress{start: &addr, port: port}, nil
+ }
+ if !addr.Is6() && prefix.Bits() == 128 {
+ return &randomAddress{start: &addr, port: port}, nil
+ }
+ return &randomAddress{prefix: &prefix, port: port}, nil
+ }
+ addrs := strings.SplitN(address, "-", 2)
+ if len(addrs) < 2 {
+ return nil, E.New("invalid ip address")
+ }
+ start, err := netip.ParseAddr(addrs[0])
+ if err != nil {
+ return nil, E.New("invalid ip address")
+ }
+ end, err := netip.ParseAddr(addrs[1])
+ if err != nil {
+ return nil, E.New("invalid ip address")
+ }
+ if start.Compare(end) > 0 {
+ return nil, E.New("invalid ip address")
+ }
+ if (start.Is4() && end.Is6()) || (start.Is6() && end.Is4()) {
+ return nil, E.New("invalid ip address")
+ }
+ return &randomAddress{start: &start, end: &end, port: port}, nil
+}
+
+func (i *randomAddress) randomIP() netip.Addr {
+ if i.prefix != nil {
+ startN := big.NewInt(0).SetBytes(i.prefix.Addr().AsSlice())
+ var bits int
+ if i.prefix.Addr().Is4() {
+ bits = 5
+ } else {
+ bits = 7
+ }
+ bt := big.NewInt(0).Exp(big.NewInt(2), big.NewInt(1< 0 {
+ if len(options.Reserved) != 3 {
+ return nil, E.New("invalid reserved value, required 3 bytes, got ", len(options.Reserved))
+ }
+ copy(reserved[:], options.Reserved)
+ }
+ var isConnect bool
+ var connectAddr M.Socksaddr
+ if len(options.Peers) < 2 {
+ isConnect = true
+ if len(options.Peers) == 1 {
+ connectAddr = options.Peers[0].ServerOptions.Build()
+ } else {
+ connectAddr = options.ServerOptions.Build()
+ }
+ }
+ outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
- outbound.peers = peers
+ outbound.bind = wireguard.NewClientBind(ctx, outbound, outboundDialer, isConnect, connectAddr, reserved)
if len(options.LocalAddress) == 0 {
return nil, E.New("missing local address")
}
- if options.GSO {
- if options.GSO && options.Detour != "" {
- return nil, E.New("gso is conflict with detour")
- }
- options.IsWireGuardListener = true
- outbound.useStdNetBind = true
- }
- listener, err := dialer.New(router, options.DialerOptions)
- if err != nil {
- return nil, err
- }
- outbound.listener = listener
var privateKey string
{
bytes, err := base64.StdEncoding.DecodeString(options.PrivateKey)
@@ -92,7 +81,80 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
}
privateKey = hex.EncodeToString(bytes)
}
- outbound.ipcConf = "private_key=" + privateKey
+ ipcConf := "private_key=" + privateKey
+ if len(options.Peers) > 0 {
+ for i, peer := range options.Peers {
+ var peerPublicKey, preSharedKey string
+ {
+ bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey)
+ if err != nil {
+ return nil, E.Cause(err, "decode public key for peer ", i)
+ }
+ peerPublicKey = hex.EncodeToString(bytes)
+ }
+ if peer.PreSharedKey != "" {
+ bytes, err := base64.StdEncoding.DecodeString(peer.PreSharedKey)
+ if err != nil {
+ return nil, E.Cause(err, "decode pre shared key for peer ", i)
+ }
+ preSharedKey = hex.EncodeToString(bytes)
+ }
+ destination := peer.ServerOptions.Build()
+ ipcConf += "\npublic_key=" + peerPublicKey
+ ipcConf += "\nendpoint=" + destination.String()
+ if preSharedKey != "" {
+ ipcConf += "\npreshared_key=" + preSharedKey
+ }
+ if len(peer.AllowedIPs) == 0 {
+ return nil, E.New("missing allowed_ips for peer ", i)
+ }
+ for _, allowedIP := range peer.AllowedIPs {
+ ipcConf += "\nallowed_ip=" + allowedIP
+ }
+ if len(peer.Reserved) > 0 {
+ if len(peer.Reserved) != 3 {
+ return nil, E.New("invalid reserved value for peer ", i, ", required 3 bytes, got ", len(peer.Reserved))
+ }
+ copy(reserved[:], options.Reserved)
+ outbound.bind.SetReservedForEndpoint(destination, reserved)
+ }
+ }
+ } else {
+ var peerPublicKey, preSharedKey string
+ {
+ bytes, err := base64.StdEncoding.DecodeString(options.PeerPublicKey)
+ if err != nil {
+ return nil, E.Cause(err, "decode peer public key")
+ }
+ peerPublicKey = hex.EncodeToString(bytes)
+ }
+ if options.PreSharedKey != "" {
+ bytes, err := base64.StdEncoding.DecodeString(options.PreSharedKey)
+ if err != nil {
+ return nil, E.Cause(err, "decode pre shared key")
+ }
+ preSharedKey = hex.EncodeToString(bytes)
+ }
+ ipcConf += "\npublic_key=" + peerPublicKey
+ ipcConf += "\nendpoint=" + options.ServerOptions.Build().String()
+ if preSharedKey != "" {
+ ipcConf += "\npreshared_key=" + preSharedKey
+ }
+ var has4, has6 bool
+ for _, address := range options.LocalAddress {
+ if address.Addr().Is4() {
+ has4 = true
+ } else {
+ has6 = true
+ }
+ }
+ if has4 {
+ ipcConf += "\nallowed_ip=0.0.0.0/0"
+ }
+ if has6 {
+ ipcConf += "\nallowed_ip=::/0"
+ }
+ }
mtu := options.MTU
if mtu == 0 {
mtu = 1408
@@ -101,83 +163,36 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
if !options.SystemInterface && tun.WithGVisor {
wireTunDevice, err = wireguard.NewStackDevice(options.LocalAddress, mtu)
} else {
- wireTunDevice, err = wireguard.NewSystemDevice(router, options.InterfaceName, options.LocalAddress, mtu, options.GSO)
+ wireTunDevice, err = wireguard.NewSystemDevice(router, options.InterfaceName, options.LocalAddress, mtu, options.GSO, options.GSOMaxSize)
}
if err != nil {
return nil, E.Cause(err, "create WireGuard device")
}
- outbound.tunDevice = wireTunDevice
- return outbound, nil
-}
-
-func (w *WireGuard) Start() error {
- err := wireguard.ResolvePeers(w.ctx, w.router, w.peers)
- if err != nil {
- return err
- }
- var bind conn.Bind
- if w.useStdNetBind {
- bind = conn.NewStdNetBind(w.listener.(dialer.WireGuardListener))
- } else {
- var (
- isConnect bool
- connectAddr netip.AddrPort
- reserved [3]uint8
- )
- peerLen := len(w.peers)
- if peerLen == 1 {
- isConnect = true
- connectAddr = w.peers[0].Endpoint
- reserved = w.peers[0].Reserved
- }
- bind = wireguard.NewClientBind(w.ctx, w, w.listener, isConnect, connectAddr, reserved)
- }
- wgDevice := device.NewDevice(w.tunDevice, bind, &device.Logger{
+ wgDevice := device.NewDevice(ctx, wireTunDevice, outbound.bind, &device.Logger{
Verbosef: func(format string, args ...interface{}) {
- w.logger.Debug(fmt.Sprintf(strings.ToLower(format), args...))
+ logger.Debug(fmt.Sprintf(strings.ToLower(format), args...))
},
Errorf: func(format string, args ...interface{}) {
- w.logger.Error(fmt.Sprintf(strings.ToLower(format), args...))
+ logger.Error(fmt.Sprintf(strings.ToLower(format), args...))
},
- }, w.workers)
- ipcConf := w.ipcConf
- for _, peer := range w.peers {
- ipcConf += peer.GenerateIpcLines()
+ }, options.Workers)
+ if debug.Enabled {
+ logger.Trace("created wireguard ipc conf: \n", ipcConf)
}
err = wgDevice.IpcSet(ipcConf)
if err != nil {
- return E.Cause(err, "setup wireguard: \n", ipcConf)
+ return nil, E.Cause(err, "setup wireguard")
}
- w.device = wgDevice
- w.pauseCallback = w.pauseManager.RegisterCallback(w.onPauseUpdated)
- return w.tunDevice.Start()
-}
-
-func (w *WireGuard) Close() error {
- if w.device != nil {
- w.device.Close()
- }
- if w.pauseCallback != nil {
- w.pauseManager.UnregisterCallback(w.pauseCallback)
- }
- w.tunDevice.Close()
- return nil
+ outbound.device = wgDevice
+ outbound.tunDevice = wireTunDevice
+ return outbound, nil
}
func (w *WireGuard) InterfaceUpdated() {
- w.device.BindUpdate()
+ w.bind.Reset()
return
}
-func (w *WireGuard) onPauseUpdated(event int) {
- switch event {
- case pause.EventDevicePaused:
- w.device.Down()
- case pause.EventDeviceWake:
- w.device.Up()
- }
-}
-
func (w *WireGuard) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch network {
case N.NetworkTCP:
@@ -218,3 +233,15 @@ func (w *WireGuard) NewConnection(ctx context.Context, conn net.Conn, metadata a
func (w *WireGuard) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
return NewDirectPacketConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS)
}
+
+func (w *WireGuard) Start() error {
+ return w.tunDevice.Start()
+}
+
+func (w *WireGuard) Close() error {
+ if w.device != nil {
+ w.device.Close()
+ }
+ w.tunDevice.Close()
+ return nil
+}
diff --git a/proxyprovider/clash/http.go b/proxyprovider/clash/http.go
new file mode 100644
index 0000000000..af1b36f6a7
--- /dev/null
+++ b/proxyprovider/clash/http.go
@@ -0,0 +1,89 @@
+package clash
+
+import (
+ "net"
+ "strconv"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type ClashHTTP struct {
+ ClashProxyBasic `yaml:",inline"`
+ //
+ Username string `yaml:"username"`
+ Password string `yaml:"password"`
+ //
+ TLS bool `yaml:"tls"`
+ SkipCertVerify bool `yaml:"skip-cert-verify"`
+ ServerName string `yaml:"servername"`
+ SNI string `yaml:"sni"`
+ ClientFingerprint string `yaml:"client-fingerprint"`
+ //
+ TFO bool `yaml:"tfo,omitempty"`
+}
+
+func (c *ClashHTTP) Tag() string {
+ if c.ClashProxyBasic.Name == "" {
+ c.ClashProxyBasic.Name = net.JoinHostPort(c.ClashProxyBasic.Server, strconv.Itoa(int(c.ClashProxyBasic.ServerPort)))
+ }
+ return c.ClashProxyBasic.Name
+}
+
+func (c *ClashHTTP) GenerateOptions() (*option.Outbound, error) {
+ outboundOptions := &option.Outbound{
+ Tag: c.Tag(),
+ Type: C.TypeHTTP,
+ HTTPOptions: option.HTTPOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: c.ClashProxyBasic.Server,
+ ServerPort: uint16(c.ClashProxyBasic.ServerPort),
+ },
+ Username: c.Username,
+ Password: c.Password,
+ },
+ }
+
+ if c.TLS {
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ Insecure: c.SkipCertVerify,
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ } else {
+ tlsOptions.ServerName = c.ClashProxyBasic.Server
+ }
+
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ outboundOptions.HTTPOptions.TLS = tlsOptions
+ }
+
+ if c.TFO {
+ outboundOptions.HTTPOptions.TCPFastOpen = true
+ }
+
+ switch c.ClashProxyBasic.IPVersion {
+ case "dual":
+ outboundOptions.HTTPOptions.DomainStrategy = 0
+ case "ipv4":
+ outboundOptions.HTTPOptions.DomainStrategy = 3
+ case "ipv6":
+ outboundOptions.HTTPOptions.DomainStrategy = 4
+ case "ipv4-prefer":
+ outboundOptions.HTTPOptions.DomainStrategy = 1
+ case "ipv6-prefer":
+ outboundOptions.HTTPOptions.DomainStrategy = 2
+ }
+
+ return outboundOptions, nil
+}
diff --git a/proxyprovider/clash/hysteria.go b/proxyprovider/clash/hysteria.go
new file mode 100644
index 0000000000..bdf0658cb1
--- /dev/null
+++ b/proxyprovider/clash/hysteria.go
@@ -0,0 +1,146 @@
+package clash
+
+import (
+ "fmt"
+ "net"
+ "strconv"
+ "strings"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type ClashHysteria struct {
+ ClashProxyBasic `yaml:",inline"`
+ //
+ Ports string `yaml:"ports"`
+ AuthStr string `yaml:"auth_str"`
+ AuthStrNew string `yaml:"auth-str"`
+ Obfs string `yaml:"obfs"`
+ Protocol string `yaml:"protocol"`
+ Up string `yaml:"up"`
+ Down string `yaml:"down"`
+ RecvWindowConn uint64 `yaml:"recv_window_conn"`
+ RecvWindowConnNew uint64 `yaml:"recv-window-conn"`
+ RecvWindow uint64 `yaml:"recv_window"`
+ RecvWindowNew uint64 `yaml:"recv-window"`
+ DisableMTUDiscovery bool `yaml:"disable_mtu_discovery"`
+ FastOpen bool `yaml:"fast-open"`
+ //
+ ALPN []string `yaml:"alpn"`
+ ServerName string `yaml:"servername"`
+ SNI string `yaml:"sni"`
+ SkipCertVerify bool `yaml:"skip-cert-verify"`
+ ClientFingerprint string `yaml:"client-fingerprint"`
+ CA string `yaml:"ca"`
+ CAStr string `yaml:"ca_str"`
+ CAStrNew string `yaml:"ca-str"`
+}
+
+func (c *ClashHysteria) Tag() string {
+ if c.ClashProxyBasic.Name == "" {
+ c.ClashProxyBasic.Name = net.JoinHostPort(c.ClashProxyBasic.Server, strconv.Itoa(int(c.ClashProxyBasic.ServerPort)))
+ }
+ return c.ClashProxyBasic.Name
+}
+
+func (c *ClashHysteria) GenerateOptions() (*option.Outbound, error) {
+ outboundOptions := &option.Outbound{
+ Tag: c.Tag(),
+ Type: C.TypeHysteria,
+ HysteriaOptions: option.HysteriaOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: c.ClashProxyBasic.Server,
+ ServerPort: uint16(c.ClashProxyBasic.ServerPort),
+ },
+ },
+ }
+
+ if c.Ports != "" {
+ return nil, fmt.Errorf("ports is not supported")
+ }
+
+ if c.AuthStr != "" {
+ outboundOptions.HysteriaOptions.AuthString = c.AuthStr
+ } else if c.AuthStrNew != "" {
+ outboundOptions.HysteriaOptions.AuthString = c.AuthStrNew
+ }
+
+ outboundOptions.HysteriaOptions.Obfs = c.Obfs
+ if c.Protocol != "udp" {
+ return nil, fmt.Errorf("wechat-video and faketcp are not supported")
+ }
+
+ upUint64, err := strconv.ParseUint(c.Up, 10, 64)
+ if err == nil {
+ outboundOptions.HysteriaOptions.UpMbps = int(upUint64)
+ } else {
+ outboundOptions.HysteriaOptions.Up = c.Up
+ }
+
+ downUint64, err := strconv.ParseUint(c.Down, 10, 64)
+ if err == nil {
+ outboundOptions.HysteriaOptions.DownMbps = int(downUint64)
+ } else {
+ outboundOptions.HysteriaOptions.Down = c.Down
+ }
+
+ if c.RecvWindowConn > 0 {
+ outboundOptions.HysteriaOptions.ReceiveWindowConn = c.RecvWindowConn
+ } else if c.RecvWindowConnNew > 0 {
+ outboundOptions.HysteriaOptions.ReceiveWindowConn = c.RecvWindowConnNew
+ }
+
+ if c.RecvWindow > 0 {
+ outboundOptions.HysteriaOptions.ReceiveWindow = c.RecvWindow
+ } else if c.RecvWindowNew > 0 {
+ outboundOptions.HysteriaOptions.ReceiveWindow = c.RecvWindowNew
+ }
+
+ outboundOptions.HysteriaOptions.DisableMTUDiscovery = c.DisableMTUDiscovery
+
+ if c.FastOpen {
+ return nil, fmt.Errorf("fast-open is not supported")
+ }
+
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ Insecure: c.SkipCertVerify,
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ } else {
+ tlsOptions.ServerName = c.ClashProxyBasic.Server
+ }
+ if c.ALPN != nil && len(c.ALPN) > 0 {
+ tlsOptions.ALPN = c.ALPN
+ }
+
+ var ca string
+ if c.CAStr != "" {
+ ca = c.CAStr
+ } else if c.CAStrNew != "" {
+ ca = c.CAStrNew
+ }
+ if ca != "" {
+ cas := strings.Split(ca, "\n")
+ var cert []string
+ for _, ca := range cas {
+ ca = strings.Trim("ca", "\r")
+ if ca == "" {
+ continue
+ }
+ cert = append(cert, ca)
+ }
+ if len(cert) > 0 {
+ tlsOptions.Certificate = cert
+ }
+ }
+
+ outboundOptions.HysteriaOptions.TLS = tlsOptions
+
+ return outboundOptions, nil
+}
diff --git a/proxyprovider/clash/hysteria2.go b/proxyprovider/clash/hysteria2.go
new file mode 100644
index 0000000000..1805f725dc
--- /dev/null
+++ b/proxyprovider/clash/hysteria2.go
@@ -0,0 +1,107 @@
+package clash
+
+import (
+ "fmt"
+ "net"
+ "strconv"
+ "strings"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+ "github.com/sagernet/sing-box/proxyprovider/utils"
+)
+
+type ClashHysteria2 struct {
+ ClashProxyBasic `yaml:",inline"`
+ //
+ Password string `yaml:"password"`
+ Obfs string `yaml:"obfs"`
+ ObfsPassword string `yaml:"obfs-password"`
+ Up string `yaml:"up"`
+ Down string `yaml:"down"`
+ //
+ ALPN []string `yaml:"alpn"`
+ ServerName string `yaml:"servername"`
+ SNI string `yaml:"sni"`
+ SkipCertVerify bool `yaml:"skip-cert-verify"`
+ ClientFingerprint string `yaml:"client-fingerprint"`
+ CA string `yaml:"ca"`
+ CAStr string `yaml:"ca_str"`
+ CAStrNew string `yaml:"ca-str"`
+}
+
+func (c *ClashHysteria2) Tag() string {
+ if c.ClashProxyBasic.Name == "" {
+ c.ClashProxyBasic.Name = net.JoinHostPort(c.ClashProxyBasic.Server, strconv.Itoa(int(c.ClashProxyBasic.ServerPort)))
+ }
+ return c.ClashProxyBasic.Name
+}
+
+func (c *ClashHysteria2) GenerateOptions() (*option.Outbound, error) {
+ outboundOptions := &option.Outbound{
+ Tag: c.Tag(),
+ Type: C.TypeHysteria2,
+ Hysteria2Options: option.Hysteria2OutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: c.ClashProxyBasic.Server,
+ ServerPort: uint16(c.ClashProxyBasic.ServerPort),
+ },
+ Password: c.Password,
+ },
+ }
+
+ if c.Obfs != "" {
+ if c.Obfs != "salamander" {
+ return nil, fmt.Errorf("obfs %s is not supported", c.Obfs)
+ }
+ obfsOptions := &option.Hysteria2Obfs{
+ Type: c.Obfs,
+ Password: c.ObfsPassword,
+ }
+ outboundOptions.Hysteria2Options.Obfs = obfsOptions
+ }
+
+ outboundOptions.Hysteria2Options.UpMbps = int(utils.StringToMbps(c.Up))
+ outboundOptions.Hysteria2Options.DownMbps = int(utils.StringToMbps(c.Down))
+
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ Insecure: c.SkipCertVerify,
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ } else {
+ tlsOptions.ServerName = c.ClashProxyBasic.Server
+ }
+ if c.ALPN != nil && len(c.ALPN) > 0 {
+ tlsOptions.ALPN = c.ALPN
+ }
+
+ var ca string
+ if c.CAStr != "" {
+ ca = c.CAStr
+ } else if c.CAStrNew != "" {
+ ca = c.CAStrNew
+ }
+ if ca != "" {
+ cas := strings.Split(ca, "\n")
+ var cert []string
+ for _, ca := range cas {
+ ca = strings.Trim("ca", "\r")
+ if ca == "" {
+ continue
+ }
+ cert = append(cert, ca)
+ }
+ if len(cert) > 0 {
+ tlsOptions.Certificate = cert
+ }
+ }
+
+ outboundOptions.Hysteria2Options.TLS = tlsOptions
+
+ return outboundOptions, nil
+}
diff --git a/proxyprovider/clash/shadowsocks.go b/proxyprovider/clash/shadowsocks.go
new file mode 100644
index 0000000000..95f07ef2e1
--- /dev/null
+++ b/proxyprovider/clash/shadowsocks.go
@@ -0,0 +1,160 @@
+package clash
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "net"
+ "sort"
+ "strconv"
+ "strings"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+ "github.com/sagernet/sing-box/proxyprovider/utils"
+)
+
+type ClashShadowsocks struct {
+ ClashProxyBasic `yaml:",inline"`
+ //
+ Cipher string `yaml:"cipher"`
+ Password string `yaml:"password"`
+ //
+ Plugin string `yaml:"plugin"`
+ PluginOpts map[string]any `yaml:"plugin-opts"`
+ UDP *bool `yaml:"udp"`
+ UDPOverTCP bool `yaml:"udp-over-tcp"`
+ UDPOverTCPVersion uint8 `yaml:"udp-over-tcp-version,omitempty"`
+ //
+ TFO bool `yaml:"tfo,omitempty"`
+ //
+ MuxOptions *ClashSingMuxOptions `yaml:"smux,omitempty"`
+}
+
+func (c *ClashShadowsocks) Tag() string {
+ if c.ClashProxyBasic.Name == "" {
+ c.ClashProxyBasic.Name = net.JoinHostPort(c.ClashProxyBasic.Server, strconv.Itoa(int(c.ClashProxyBasic.ServerPort)))
+ }
+ return c.ClashProxyBasic.Name
+}
+
+func (c *ClashShadowsocks) GenerateOptions() (*option.Outbound, error) {
+ outboundOptions := &option.Outbound{
+ Tag: c.Tag(),
+ Type: C.TypeShadowsocks,
+ ShadowsocksOptions: option.ShadowsocksOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: c.ClashProxyBasic.Server,
+ ServerPort: uint16(c.ClashProxyBasic.ServerPort),
+ },
+ Method: c.Cipher,
+ Password: c.Password,
+ },
+ }
+ if !utils.CheckShadowsocksMethod(c.Cipher) {
+ return nil, fmt.Errorf("unsupported cipher: %s", c.Cipher)
+ }
+
+ switch c.Plugin {
+ case "":
+ case "obfs":
+ obfsOptions := make(map[string][]string)
+ modeAny, ok := c.PluginOpts["mode"]
+ if ok {
+ mode, ok := modeAny.(string)
+ if ok {
+ obfsOptions["obfs"] = []string{mode}
+ }
+ }
+ hostAny, ok := c.PluginOpts["host"]
+ if ok {
+ host, ok := hostAny.(string)
+ if ok {
+ obfsOptions["obfs-host"] = []string{host}
+ }
+ }
+ pluginOptsStr := encodeSmethodArgs(obfsOptions)
+ outboundOptions.ShadowsocksOptions.Plugin = "obfs-local"
+ outboundOptions.ShadowsocksOptions.PluginOptions = pluginOptsStr
+ case "v2ray-plugin":
+ return nil, errors.New("v2ray-plugin is not supported")
+ default:
+ return nil, fmt.Errorf("unsupported plugin: %s", c.Plugin)
+ }
+
+ if c.UDP != nil && !*c.UDP {
+ outboundOptions.ShadowsocksOptions.Network = option.NetworkList{"tcp"}
+ }
+ if c.UDPOverTCP {
+ outboundOptions.ShadowsocksOptions.UDPOverTCP = &option.UDPOverTCPOptions{
+ Enabled: true,
+ Version: c.UDPOverTCPVersion,
+ }
+ }
+
+ if c.TFO {
+ outboundOptions.ShadowsocksOptions.TCPFastOpen = true
+ }
+
+ if c.MuxOptions != nil && c.MuxOptions.Enabled {
+ outboundOptions.ShadowsocksOptions.Multiplex = &option.OutboundMultiplexOptions{
+ Enabled: true,
+ Protocol: c.MuxOptions.Protocol,
+ MaxConnections: c.MuxOptions.MaxConnections,
+ MinStreams: c.MuxOptions.MinStreams,
+ MaxStreams: c.MuxOptions.MaxStreams,
+ Padding: c.MuxOptions.Padding,
+ }
+ }
+
+ switch c.ClashProxyBasic.IPVersion {
+ case "dual":
+ outboundOptions.ShadowsocksOptions.DomainStrategy = 0
+ case "ipv4":
+ outboundOptions.ShadowsocksOptions.DomainStrategy = 3
+ case "ipv6":
+ outboundOptions.ShadowsocksOptions.DomainStrategy = 4
+ case "ipv4-prefer":
+ outboundOptions.ShadowsocksOptions.DomainStrategy = 1
+ case "ipv6-prefer":
+ outboundOptions.ShadowsocksOptions.DomainStrategy = 2
+ }
+
+ return outboundOptions, nil
+}
+
+func backslashEscape(s string, set []byte) string {
+ var buf bytes.Buffer
+ for _, b := range []byte(s) {
+ if b == '\\' || bytes.IndexByte(set, b) != -1 {
+ buf.WriteByte('\\')
+ }
+ buf.WriteByte(b)
+ }
+ return buf.String()
+}
+
+func encodeSmethodArgs(args map[string][]string) string {
+ if args == nil {
+ return ""
+ }
+
+ keys := make([]string, 0, len(args))
+ for key := range args {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+
+ escape := func(s string) string {
+ return backslashEscape(s, []byte{'=', ','})
+ }
+
+ var pairs []string
+ for _, key := range keys {
+ for _, value := range args[key] {
+ pairs = append(pairs, escape(key)+"="+escape(value))
+ }
+ }
+
+ return strings.Join(pairs, ";")
+}
diff --git a/proxyprovider/clash/singmux.go b/proxyprovider/clash/singmux.go
new file mode 100644
index 0000000000..c0daa31c99
--- /dev/null
+++ b/proxyprovider/clash/singmux.go
@@ -0,0 +1,10 @@
+package clash
+
+type ClashSingMuxOptions struct {
+ Enabled bool `yaml:"enabled,omitempty"`
+ Protocol string `yaml:"protocol,omitempty"`
+ MaxConnections int `yaml:"max-connections,omitempty"`
+ MinStreams int `yaml:"min-streams,omitempty"`
+ MaxStreams int `yaml:"max-streams,omitempty"`
+ Padding bool `yaml:"padding,omitempty"`
+}
diff --git a/proxyprovider/clash/socks.go b/proxyprovider/clash/socks.go
new file mode 100644
index 0000000000..e72cfff67d
--- /dev/null
+++ b/proxyprovider/clash/socks.go
@@ -0,0 +1,76 @@
+package clash
+
+import (
+ "errors"
+ "net"
+ "strconv"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type ClashSocks struct {
+ ClashProxyBasic `yaml:",inline"`
+ //
+ Username string `yaml:"username"`
+ Password string `yaml:"password"`
+ //
+ TLS bool `yaml:"tls"`
+ SkipCertVerify bool `yaml:"skip-cert-verify"`
+ ServerName string `yaml:"servername"`
+ SNI string `yaml:"sni"`
+ ClientFingerprint string `yaml:"client-fingerprint"`
+ //
+ UDP *bool `yaml:"udp"`
+ //
+ TFO bool `yaml:"tfo,omitempty"`
+}
+
+func (c *ClashSocks) Tag() string {
+ if c.ClashProxyBasic.Name == "" {
+ c.ClashProxyBasic.Name = net.JoinHostPort(c.ClashProxyBasic.Server, strconv.Itoa(int(c.ClashProxyBasic.ServerPort)))
+ }
+ return c.ClashProxyBasic.Name
+}
+
+func (c *ClashSocks) GenerateOptions() (*option.Outbound, error) {
+ outboundOptions := &option.Outbound{
+ Tag: c.Tag(),
+ Type: C.TypeSOCKS,
+ SocksOptions: option.SocksOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: c.ClashProxyBasic.Server,
+ ServerPort: uint16(c.ClashProxyBasic.ServerPort),
+ },
+ Username: c.Username,
+ Password: c.Password,
+ Version: "5",
+ },
+ }
+
+ if c.TLS {
+ return nil, errors.New("socks5 tls is not supported")
+ }
+ if c.UDP != nil && !*c.UDP {
+ outboundOptions.SocksOptions.Network = option.NetworkList{"tcp"}
+ }
+
+ if c.TFO {
+ outboundOptions.SocksOptions.TCPFastOpen = true
+ }
+
+ switch c.ClashProxyBasic.IPVersion {
+ case "dual":
+ outboundOptions.SocksOptions.DomainStrategy = 0
+ case "ipv4":
+ outboundOptions.SocksOptions.DomainStrategy = 3
+ case "ipv6":
+ outboundOptions.SocksOptions.DomainStrategy = 4
+ case "ipv4-prefer":
+ outboundOptions.SocksOptions.DomainStrategy = 1
+ case "ipv6-prefer":
+ outboundOptions.SocksOptions.DomainStrategy = 2
+ }
+
+ return outboundOptions, nil
+}
diff --git a/proxyprovider/clash/trojan.go b/proxyprovider/clash/trojan.go
new file mode 100644
index 0000000000..1c0ec69f49
--- /dev/null
+++ b/proxyprovider/clash/trojan.go
@@ -0,0 +1,168 @@
+package clash
+
+import (
+ "errors"
+ "net"
+ "strconv"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type ClashTrojan struct {
+ ClashProxyBasic `yaml:",inline"`
+ //
+ Password string `yaml:"password"`
+ Flow string `yaml:"flow"`
+ FlowShow string `yaml:"flow-show"`
+ UDP *bool `yaml:"udp"`
+ //
+ ALPN []string `yaml:"alpn"`
+ SkipCertVerify bool `yaml:"skip-cert-verify"`
+ ClientFingerprint string `yaml:"client-fingerprint"`
+ ServerName string `yaml:"servername"`
+ SNI string `yaml:"sni"`
+ //
+ Network string `yaml:"network"`
+ //
+ WSOptions *ClashTransportWebsocket `yaml:"ws-opts"`
+ GrpcOptions *ClashTransportGRPC `yaml:"grpc-opts"`
+ //
+ RealityOptions *ClashTransportReality `yaml:"reality-opts"`
+ //
+ TFO bool `yaml:"tfo,omitempty"`
+ //
+ MuxOptions *ClashSingMuxOptions `yaml:"smux,omitempty"`
+}
+
+func (c *ClashTrojan) Tag() string {
+ if c.ClashProxyBasic.Name == "" {
+ c.ClashProxyBasic.Name = net.JoinHostPort(c.ClashProxyBasic.Server, strconv.Itoa(int(c.ClashProxyBasic.ServerPort)))
+ }
+ return c.ClashProxyBasic.Name
+}
+
+func (c *ClashTrojan) GenerateOptions() (*option.Outbound, error) {
+ outboundOptions := &option.Outbound{
+ Tag: c.Tag(),
+ Type: C.TypeTrojan,
+ TrojanOptions: option.TrojanOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: c.ClashProxyBasic.Server,
+ ServerPort: uint16(c.ClashProxyBasic.ServerPort),
+ },
+ Password: c.Password,
+ },
+ }
+
+ if c.Flow != "" || c.FlowShow != "" {
+ return nil, errors.New("trojan flow and flow-show is not supported")
+ }
+
+ if c.UDP != nil && !*c.UDP {
+ outboundOptions.TrojanOptions.Network = option.NetworkList{"tcp"}
+ }
+
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ Insecure: c.SkipCertVerify,
+ }
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ } else {
+ tlsOptions.ServerName = c.ClashProxyBasic.Server
+ }
+
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ if c.ALPN != nil && len(c.ALPN) > 0 {
+ tlsOptions.ALPN = c.ALPN
+ }
+
+ if c.RealityOptions != nil {
+ tlsOptions.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: c.RealityOptions.PublicKey,
+ ShortID: c.RealityOptions.ShortID,
+ }
+ }
+
+ outboundOptions.TrojanOptions.TLS = tlsOptions
+
+ switch c.Network {
+ case "ws":
+ if c.WSOptions == nil {
+ c.WSOptions = &ClashTransportWebsocket{}
+ }
+ websocketOptions := &option.V2RayWebsocketOptions{
+ Path: c.WSOptions.Path,
+ MaxEarlyData: uint32(c.WSOptions.MaxEarlyData),
+ EarlyDataHeaderName: c.WSOptions.EarlyDataHeaderName,
+ }
+
+ headers := make(map[string]option.Listable[string])
+ if c.WSOptions.Headers != nil {
+ for k, v := range c.WSOptions.Headers {
+ headers[k] = []string{v}
+ }
+ }
+ if headers["Host"] == nil {
+ headers["Host"] = []string{c.ClashProxyBasic.Server}
+ }
+
+ websocketOptions.Headers = headers
+ outboundOptions.TrojanOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeWebsocket,
+ WebsocketOptions: *websocketOptions,
+ }
+ case "grpc":
+ if c.GrpcOptions == nil {
+ c.GrpcOptions = &ClashTransportGRPC{}
+ }
+ grpcOptions := &option.V2RayGRPCOptions{
+ ServiceName: c.GrpcOptions.ServiceName,
+ }
+
+ outboundOptions.TrojanOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeGRPC,
+ GRPCOptions: *grpcOptions,
+ }
+ }
+
+ if c.TFO {
+ outboundOptions.TrojanOptions.TCPFastOpen = true
+ }
+
+ if c.MuxOptions != nil && c.MuxOptions.Enabled {
+ outboundOptions.TrojanOptions.Multiplex = &option.OutboundMultiplexOptions{
+ Enabled: true,
+ Protocol: c.MuxOptions.Protocol,
+ MaxConnections: c.MuxOptions.MaxConnections,
+ MinStreams: c.MuxOptions.MinStreams,
+ MaxStreams: c.MuxOptions.MaxStreams,
+ Padding: c.MuxOptions.Padding,
+ }
+ }
+
+ switch c.ClashProxyBasic.IPVersion {
+ case "dual":
+ outboundOptions.TrojanOptions.DomainStrategy = 0
+ case "ipv4":
+ outboundOptions.TrojanOptions.DomainStrategy = 3
+ case "ipv6":
+ outboundOptions.TrojanOptions.DomainStrategy = 4
+ case "ipv4-prefer":
+ outboundOptions.TrojanOptions.DomainStrategy = 1
+ case "ipv6-prefer":
+ outboundOptions.TrojanOptions.DomainStrategy = 2
+ }
+
+ return outboundOptions, nil
+}
diff --git a/proxyprovider/clash/tuic.go b/proxyprovider/clash/tuic.go
new file mode 100644
index 0000000000..9da82cf59e
--- /dev/null
+++ b/proxyprovider/clash/tuic.go
@@ -0,0 +1,97 @@
+package clash
+
+import (
+ "net"
+ "strconv"
+ "strings"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type ClashTUIC struct {
+ ClashProxyBasic `yaml:",inline"`
+ //
+ UUID string `yaml:"uuid"`
+ Password string `yaml:"password,omitempty"`
+ CongestionController string `yaml:"congestion-controller,omitempty"`
+ UdpRelayMode string `yaml:"udp-relay-mode,omitempty"`
+ UDPOverStream bool `yaml:"udp-over-stream,omitempty"`
+ ReduceRtt bool `yaml:"reduce-rtt,omitempty"`
+ HeartbeatInterval int `yaml:"heartbeat-interval,omitempty"`
+ //
+ SNI string `yaml:"sni,omitempty"`
+ DisableSni bool `yaml:"disable-sni,omitempty"`
+ SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
+ ALPN []string `yaml:"alpn,omitempty"`
+ CA string `yaml:"ca"`
+ CAStr string `yaml:"ca_str"`
+ CAStrNew string `yaml:"ca-str"`
+ ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
+}
+
+func (c *ClashTUIC) Tag() string {
+ if c.ClashProxyBasic.Name == "" {
+ c.ClashProxyBasic.Name = net.JoinHostPort(c.ClashProxyBasic.Server, strconv.Itoa(int(c.ClashProxyBasic.ServerPort)))
+ }
+ return c.ClashProxyBasic.Name
+}
+
+func (c *ClashTUIC) GenerateOptions() (*option.Outbound, error) {
+ outboundOptions := &option.Outbound{
+ Tag: c.Tag(),
+ Type: C.TypeTUIC,
+ TUICOptions: option.TUICOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: c.ClashProxyBasic.Server,
+ ServerPort: uint16(c.ClashProxyBasic.ServerPort),
+ },
+ UUID: c.UUID,
+ Password: c.Password,
+ CongestionControl: c.CongestionController,
+ UDPRelayMode: c.UdpRelayMode,
+ UDPOverStream: c.UDPOverStream,
+ ZeroRTTHandshake: c.ReduceRtt,
+ Heartbeat: option.Duration(1000000 * c.HeartbeatInterval),
+ },
+ }
+
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ Insecure: c.SkipCertVerify,
+ }
+
+ if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ } else {
+ tlsOptions.ServerName = c.ClashProxyBasic.Server
+ }
+ if c.ALPN != nil && len(c.ALPN) > 0 {
+ tlsOptions.ALPN = c.ALPN
+ }
+
+ var ca string
+ if c.CAStr != "" {
+ ca = c.CAStr
+ } else if c.CAStrNew != "" {
+ ca = c.CAStrNew
+ }
+ if ca != "" {
+ cas := strings.Split(ca, "\n")
+ var cert []string
+ for _, ca := range cas {
+ ca = strings.Trim("ca", "\r")
+ if ca == "" {
+ continue
+ }
+ cert = append(cert, ca)
+ }
+ if len(cert) > 0 {
+ tlsOptions.Certificate = cert
+ }
+ }
+
+ outboundOptions.TUICOptions.TLS = tlsOptions
+
+ return outboundOptions, nil
+}
diff --git a/proxyprovider/clash/type.go b/proxyprovider/clash/type.go
new file mode 100644
index 0000000000..9afdf6fc7c
--- /dev/null
+++ b/proxyprovider/clash/type.go
@@ -0,0 +1,123 @@
+package clash
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/sagernet/sing-box/option"
+
+ "gopkg.in/yaml.v3"
+)
+
+type ClashProxyInterface interface {
+ Tag() string
+ GenerateOptions() (*option.Outbound, error)
+}
+
+type ClashConfig struct {
+ Proxies []ClashProxy `yaml:"proxies"`
+}
+
+const (
+ ClashTypeHTTP = "http"
+ ClashTypeSocks5 = "socks5"
+ ClashTypeShadowsocks = "ss"
+ ClashTypeVMess = "vmess"
+ ClashTypeTrojan = "trojan"
+ ClashTypeVLESS = "vless"
+ ClashTypeHysteria = "hysteria"
+ ClashTypeHysteria2 = "hysteria2"
+ ClashTypeTUIC = "tuic"
+)
+
+type Port uint16
+
+func (p *Port) UnmarshalYAML(node *yaml.Node) error {
+ var port uint16
+ err := node.Decode(&port)
+ if err == nil {
+ *p = Port(port)
+ return nil
+ }
+ var portStr string
+ err2 := node.Decode(&portStr)
+ if err2 != nil {
+ return err
+ }
+ portUint64, err3 := strconv.ParseUint(portStr, 10, 16)
+ if err3 != nil {
+ return err
+ }
+ *p = Port(portUint64)
+ return nil
+}
+
+type ClashProxyBasic struct {
+ Name string `yaml:"name"`
+ Type string `yaml:"type"`
+ Server string `yaml:"server"`
+ ServerPort Port `yaml:"port"`
+ //
+ IPVersion string `yaml:"ip-version,omitempty"`
+}
+
+type ClashProxy struct {
+ Type string
+ Proxy ClashProxyInterface
+}
+
+type ClashProxyPre struct {
+ Type string `yaml:"type"`
+}
+
+func (c *ClashProxy) UnmarshalYAML(node *yaml.Node) error {
+ var pre ClashProxyPre
+ err := node.Decode(&pre)
+ if err != nil {
+ return err
+ }
+ switch pre.Type {
+ case ClashTypeHTTP:
+ c.Proxy = &ClashHTTP{}
+ case ClashTypeSocks5:
+ c.Proxy = &ClashSocks{}
+ case ClashTypeShadowsocks:
+ c.Proxy = &ClashShadowsocks{}
+ case ClashTypeVMess:
+ c.Proxy = &ClashVMess{}
+ case ClashTypeVLESS:
+ c.Proxy = &ClashVLESS{}
+ case ClashTypeTrojan:
+ c.Proxy = &ClashTrojan{}
+ case ClashTypeHysteria:
+ c.Proxy = &ClashHysteria{}
+ case ClashTypeHysteria2:
+ c.Proxy = &ClashHysteria2{}
+ case ClashTypeTUIC:
+ c.Proxy = &ClashTUIC{}
+ default:
+ return fmt.Errorf("unknown clash proxy type: %s", pre.Type)
+ }
+ err = node.Decode(c.Proxy)
+ return err
+}
+
+func ParseClashConfig(raw []byte) ([]option.Outbound, error) {
+ var config ClashConfig
+ err := yaml.Unmarshal(raw, &config)
+ if err != nil {
+ return nil, err
+ }
+ if config.Proxies == nil || len(config.Proxies) == 0 {
+ return nil, fmt.Errorf("no outbounds found in clash config")
+ }
+ m := make([]option.Outbound, 0, len(config.Proxies))
+ for i, proxy := range config.Proxies {
+ options, err := proxy.Proxy.GenerateOptions()
+ if err != nil {
+ return nil, fmt.Errorf("parse outbound[%d], tag: `%s` failed: %s", i+1, proxy.Proxy.Tag(), err)
+ }
+ m = append(m, *options)
+ }
+ return m, nil
+}
diff --git a/proxyprovider/clash/v2xray.go b/proxyprovider/clash/v2xray.go
new file mode 100644
index 0000000000..d0d8be9aae
--- /dev/null
+++ b/proxyprovider/clash/v2xray.go
@@ -0,0 +1,28 @@
+package clash
+
+type ClashTransportWebsocket struct {
+ Path string `yaml:"path"`
+ Headers map[string]string `yaml:"headers"`
+ MaxEarlyData int `yaml:"max-early-data"`
+ EarlyDataHeaderName string `yaml:"early-data-header-name"`
+}
+
+type ClashTransportGRPC struct {
+ ServiceName string `yaml:"grpc-service-name"`
+}
+
+type ClashTransportHTTP struct {
+ Method string `yaml:"method"`
+ Path []string `yaml:"path"`
+ Headers map[string][]string `yaml:"headers"`
+}
+
+type ClashTransportHTTP2 struct {
+ Host []string `yaml:"host"`
+ Path string `yaml:"path"`
+}
+
+type ClashTransportReality struct {
+ PublicKey string `yaml:"public-key"`
+ ShortID string `yaml:"short-id"`
+}
diff --git a/proxyprovider/clash/vless.go b/proxyprovider/clash/vless.go
new file mode 100644
index 0000000000..8e2ceff1d0
--- /dev/null
+++ b/proxyprovider/clash/vless.go
@@ -0,0 +1,342 @@
+package clash
+
+import (
+ "errors"
+ "net"
+ "strconv"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type ClashVLESS struct {
+ ClashProxyBasic `yaml:",inline"`
+ //
+ UUID string `yaml:"uuid"`
+ Flow string `yaml:"flow"`
+ FlowShow string `yaml:"flow-show"`
+ XUDP bool `yaml:"xudp,omitempty"`
+ PacketAddr bool `yaml:"packet-addr,omitempty"`
+ UDP *bool `yaml:"udp"`
+ PacketEncoding *string `yaml:"packet-encoding"`
+ //
+ TLS bool `yaml:"tls"`
+ SkipCertVerify bool `yaml:"skip-cert-verify"`
+ ClientFingerprint string `yaml:"client-fingerprint"`
+ ServerName string `yaml:"servername"`
+ SNI string `yaml:"sni"`
+ //
+ Network string `yaml:"network"`
+ //
+ WSOptions *ClashTransportWebsocket `yaml:"ws-opts"`
+ HTTPOptions *ClashTransportHTTP `yaml:"http-opts"`
+ HTTP2Options *ClashTransportHTTP2 `yaml:"h2-opts"`
+ GrpcOptions *ClashTransportGRPC `yaml:"grpc-opts"`
+ //
+ RealityOptions *ClashTransportReality `yaml:"reality-opts"`
+ //
+ TFO bool `yaml:"tfo,omitempty"`
+ //
+ MuxOptions *ClashSingMuxOptions `yaml:"smux,omitempty"`
+}
+
+func (c *ClashVLESS) Tag() string {
+ if c.ClashProxyBasic.Name == "" {
+ c.ClashProxyBasic.Name = net.JoinHostPort(c.ClashProxyBasic.Server, strconv.Itoa(int(c.ClashProxyBasic.ServerPort)))
+ }
+ return c.ClashProxyBasic.Name
+}
+
+func (c *ClashVLESS) GenerateOptions() (*option.Outbound, error) {
+ outboundOptions := &option.Outbound{
+ Tag: c.Tag(),
+ Type: C.TypeVLESS,
+ VLESSOptions: option.VLESSOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: c.ClashProxyBasic.Server,
+ ServerPort: uint16(c.ClashProxyBasic.ServerPort),
+ },
+ UUID: c.UUID,
+ Flow: c.Flow,
+ },
+ }
+
+ if c.FlowShow != "" {
+ return nil, errors.New("flow-show is not supported")
+ }
+
+ if c.UDP != nil && !*c.UDP {
+ outboundOptions.VLESSOptions.Network = option.NetworkList{"tcp"}
+ }
+
+ if c.PacketEncoding != nil {
+ outboundOptions.VLESSOptions.PacketEncoding = new(string)
+ *outboundOptions.VLESSOptions.PacketEncoding = *c.PacketEncoding
+ } else if c.XUDP {
+ outboundOptions.VLESSOptions.PacketEncoding = new(string)
+ *outboundOptions.VLESSOptions.PacketEncoding = "xudp"
+ } else if c.PacketAddr {
+ outboundOptions.VLESSOptions.PacketEncoding = new(string)
+ *outboundOptions.VLESSOptions.PacketEncoding = "packetaddr"
+ }
+
+ switch c.Network {
+ case "ws":
+ if c.WSOptions == nil {
+ c.WSOptions = &ClashTransportWebsocket{}
+ }
+ websocketOptions := &option.V2RayWebsocketOptions{
+ Path: c.WSOptions.Path,
+ EarlyDataHeaderName: c.WSOptions.EarlyDataHeaderName,
+ MaxEarlyData: uint32(c.WSOptions.MaxEarlyData),
+ }
+
+ headers := make(map[string]option.Listable[string])
+ if c.WSOptions.Headers != nil {
+ for k, v := range c.WSOptions.Headers {
+ headers[k] = []string{v}
+ }
+ }
+ if headers["Host"] == nil {
+ headers["Host"] = []string{c.ClashProxyBasic.Server}
+ }
+
+ if c.TLS {
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: c.ClashProxyBasic.Server,
+ Insecure: c.SkipCertVerify,
+ ALPN: []string{"http/1.1"},
+ }
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ } else if headers["Host"] != nil {
+ tlsOptions.ServerName = headers["Host"][0]
+ }
+
+ if c.RealityOptions != nil {
+ tlsOptions.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: c.RealityOptions.PublicKey,
+ ShortID: c.RealityOptions.ShortID,
+ }
+ }
+
+ outboundOptions.VLESSOptions.TLS = tlsOptions
+ }
+
+ websocketOptions.Headers = headers
+ outboundOptions.VLESSOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeWebsocket,
+ WebsocketOptions: *websocketOptions,
+ }
+ case "http":
+ if c.HTTPOptions == nil {
+ c.HTTPOptions = &ClashTransportHTTP{}
+ }
+ httpOptions := &option.V2RayHTTPOptions{
+ Method: c.HTTPOptions.Method,
+ }
+ if c.HTTPOptions.Path != nil && len(c.HTTPOptions.Path) > 0 {
+ httpOptions.Path = c.HTTPOptions.Path[0]
+ }
+
+ headers := make(map[string]option.Listable[string])
+ if c.HTTPOptions.Headers != nil {
+ for k, v := range c.HTTPOptions.Headers {
+ headers[k] = v
+ }
+ }
+ if headers["Host"] != nil {
+ httpOptions.Host = headers["Host"]
+ }
+
+ if c.TLS {
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: c.ClashProxyBasic.Server,
+ Insecure: c.SkipCertVerify,
+ ALPN: []string{"h2"},
+ }
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ } else if headers["Host"] != nil {
+ tlsOptions.ServerName = headers["Host"][0]
+ }
+
+ if c.RealityOptions != nil {
+ tlsOptions.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: c.RealityOptions.PublicKey,
+ ShortID: c.RealityOptions.ShortID,
+ }
+ }
+
+ outboundOptions.VLESSOptions.TLS = tlsOptions
+ }
+
+ httpOptions.Headers = headers
+ outboundOptions.VLESSOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeHTTP,
+ HTTPOptions: *httpOptions,
+ }
+ case "h2":
+ if c.HTTP2Options == nil {
+ c.HTTP2Options = &ClashTransportHTTP2{}
+ }
+ http2Options := &option.V2RayHTTPOptions{
+ Host: c.HTTP2Options.Host,
+ Path: c.HTTP2Options.Path,
+ }
+
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: c.ClashProxyBasic.Server,
+ Insecure: c.SkipCertVerify,
+ ALPN: []string{"h2"},
+ }
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ }
+
+ if c.RealityOptions != nil {
+ tlsOptions.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: c.RealityOptions.PublicKey,
+ ShortID: c.RealityOptions.ShortID,
+ }
+ }
+
+ outboundOptions.VLESSOptions.TLS = tlsOptions
+ outboundOptions.VLESSOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeHTTP,
+ HTTPOptions: *http2Options,
+ }
+ case "grpc":
+ if c.GrpcOptions == nil {
+ c.GrpcOptions = &ClashTransportGRPC{}
+ }
+ grpcOptions := &option.V2RayGRPCOptions{
+ ServiceName: c.GrpcOptions.ServiceName,
+ }
+
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: c.ClashProxyBasic.Server,
+ Insecure: c.SkipCertVerify,
+ }
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ }
+
+ if c.RealityOptions != nil {
+ tlsOptions.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: c.RealityOptions.PublicKey,
+ ShortID: c.RealityOptions.ShortID,
+ }
+ }
+
+ outboundOptions.VLESSOptions.TLS = tlsOptions
+ outboundOptions.VLESSOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeGRPC,
+ GRPCOptions: *grpcOptions,
+ }
+ default:
+ if c.TLS {
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: c.ClashProxyBasic.Server,
+ Insecure: c.SkipCertVerify,
+ }
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ }
+
+ if c.RealityOptions != nil {
+ tlsOptions.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: c.RealityOptions.PublicKey,
+ ShortID: c.RealityOptions.ShortID,
+ }
+ }
+
+ outboundOptions.VLESSOptions.TLS = tlsOptions
+ }
+ }
+
+ if c.TFO {
+ outboundOptions.VLESSOptions.TCPFastOpen = true
+ }
+
+ if c.MuxOptions != nil && c.MuxOptions.Enabled {
+ outboundOptions.VLESSOptions.Multiplex = &option.OutboundMultiplexOptions{
+ Enabled: true,
+ Protocol: c.MuxOptions.Protocol,
+ MaxConnections: c.MuxOptions.MaxConnections,
+ MinStreams: c.MuxOptions.MinStreams,
+ MaxStreams: c.MuxOptions.MaxStreams,
+ Padding: c.MuxOptions.Padding,
+ }
+ }
+
+ switch c.ClashProxyBasic.IPVersion {
+ case "dual":
+ outboundOptions.VLESSOptions.DomainStrategy = 0
+ case "ipv4":
+ outboundOptions.VLESSOptions.DomainStrategy = 3
+ case "ipv6":
+ outboundOptions.VLESSOptions.DomainStrategy = 4
+ case "ipv4-prefer":
+ outboundOptions.VLESSOptions.DomainStrategy = 1
+ case "ipv6-prefer":
+ outboundOptions.VLESSOptions.DomainStrategy = 2
+ }
+
+ return outboundOptions, nil
+}
diff --git a/proxyprovider/clash/vmess.go b/proxyprovider/clash/vmess.go
new file mode 100644
index 0000000000..150029b5c7
--- /dev/null
+++ b/proxyprovider/clash/vmess.go
@@ -0,0 +1,330 @@
+package clash
+
+import (
+ "net"
+ "strconv"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type ClashVMess struct {
+ ClashProxyBasic `yaml:",inline"`
+ //
+ UUID string `yaml:"uuid"`
+ AlterID int `yaml:"alterId"`
+ Cipher string `yaml:"cipher"`
+ UDP *bool `yaml:"udp"`
+ PacketEncoding string `yaml:"packet-encoding"`
+ GlobalPadding bool `yaml:"global-padding"`
+ AuthenticatedLength bool `yaml:"authenticated-length"`
+ //
+ TLS bool `yaml:"tls"`
+ SkipCertVerify bool `yaml:"skip-cert-verify"`
+ ClientFingerprint string `yaml:"client-fingerprint"`
+ ServerName string `yaml:"servername"`
+ SNI string `yaml:"sni"`
+ //
+ Network string `yaml:"network"`
+ //
+ WSOptions *ClashTransportWebsocket `yaml:"ws-opts"`
+ HTTPOptions *ClashTransportHTTP `yaml:"http-opts"`
+ HTTP2Options *ClashTransportHTTP2 `yaml:"h2-opts"`
+ GrpcOptions *ClashTransportGRPC `yaml:"grpc-opts"`
+ //
+ RealityOptions *ClashTransportReality `yaml:"reality-opts"`
+ //
+ TFO bool `yaml:"tfo,omitempty"`
+ //
+ MuxOptions *ClashSingMuxOptions `yaml:"smux,omitempty"`
+}
+
+func (c *ClashVMess) Tag() string {
+ if c.ClashProxyBasic.Name == "" {
+ c.ClashProxyBasic.Name = net.JoinHostPort(c.ClashProxyBasic.Server, strconv.Itoa(int(c.ClashProxyBasic.ServerPort)))
+ }
+ return c.ClashProxyBasic.Name
+}
+
+func (c *ClashVMess) GenerateOptions() (*option.Outbound, error) {
+ outboundOptions := &option.Outbound{
+ Tag: c.Tag(),
+ Type: C.TypeVMess,
+ VMessOptions: option.VMessOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: c.ClashProxyBasic.Server,
+ ServerPort: uint16(c.ClashProxyBasic.ServerPort),
+ },
+ UUID: c.UUID,
+ AlterId: c.AlterID,
+ Security: c.Cipher,
+ GlobalPadding: c.GlobalPadding,
+ AuthenticatedLength: c.AuthenticatedLength,
+ PacketEncoding: c.PacketEncoding,
+ },
+ }
+
+ if c.UDP != nil && !*c.UDP {
+ outboundOptions.VMessOptions.Network = option.NetworkList{"tcp"}
+ }
+
+ switch c.Network {
+ case "ws":
+ if c.WSOptions == nil {
+ c.WSOptions = &ClashTransportWebsocket{}
+ }
+ websocketOptions := &option.V2RayWebsocketOptions{
+ Path: c.WSOptions.Path,
+ EarlyDataHeaderName: c.WSOptions.EarlyDataHeaderName,
+ MaxEarlyData: uint32(c.WSOptions.MaxEarlyData),
+ }
+
+ headers := make(map[string]option.Listable[string])
+ if c.WSOptions.Headers != nil {
+ for k, v := range c.WSOptions.Headers {
+ headers[k] = []string{v}
+ }
+ }
+ if headers["Host"] == nil {
+ headers["Host"] = []string{c.ClashProxyBasic.Server}
+ }
+
+ if c.TLS {
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: c.ClashProxyBasic.Server,
+ Insecure: c.SkipCertVerify,
+ ALPN: []string{"http/1.1"},
+ }
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ } else if headers["Host"] != nil {
+ tlsOptions.ServerName = headers["Host"][0]
+ }
+
+ if c.RealityOptions != nil {
+ tlsOptions.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: c.RealityOptions.PublicKey,
+ ShortID: c.RealityOptions.ShortID,
+ }
+ }
+
+ outboundOptions.VMessOptions.TLS = tlsOptions
+ }
+
+ websocketOptions.Headers = headers
+ outboundOptions.VMessOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeWebsocket,
+ WebsocketOptions: *websocketOptions,
+ }
+ case "http":
+ if c.HTTPOptions == nil {
+ c.HTTPOptions = &ClashTransportHTTP{}
+ }
+ httpOptions := &option.V2RayHTTPOptions{
+ Method: c.HTTPOptions.Method,
+ }
+ if c.HTTPOptions.Path != nil && len(c.HTTPOptions.Path) > 0 {
+ httpOptions.Path = c.HTTPOptions.Path[0]
+ }
+
+ headers := make(map[string]option.Listable[string])
+ if c.HTTPOptions.Headers != nil {
+ for k, v := range c.HTTPOptions.Headers {
+ headers[k] = v
+ }
+ }
+ if headers["Host"] != nil {
+ httpOptions.Host = headers["Host"]
+ }
+
+ if c.TLS {
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: c.ClashProxyBasic.Server,
+ Insecure: c.SkipCertVerify,
+ ALPN: []string{"h2"},
+ }
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ } else if headers["Host"] != nil {
+ tlsOptions.ServerName = headers["Host"][0]
+ }
+
+ if c.RealityOptions != nil {
+ tlsOptions.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: c.RealityOptions.PublicKey,
+ ShortID: c.RealityOptions.ShortID,
+ }
+ }
+
+ outboundOptions.VMessOptions.TLS = tlsOptions
+ }
+
+ httpOptions.Headers = headers
+ outboundOptions.VMessOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeHTTP,
+ HTTPOptions: *httpOptions,
+ }
+ case "h2":
+ if c.HTTP2Options == nil {
+ c.HTTP2Options = &ClashTransportHTTP2{}
+ }
+ http2Options := &option.V2RayHTTPOptions{
+ Host: c.HTTP2Options.Host,
+ Path: c.HTTP2Options.Path,
+ }
+
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: c.ClashProxyBasic.Server,
+ Insecure: c.SkipCertVerify,
+ ALPN: []string{"h2"},
+ }
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ }
+
+ if c.RealityOptions != nil {
+ tlsOptions.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: c.RealityOptions.PublicKey,
+ ShortID: c.RealityOptions.ShortID,
+ }
+ }
+
+ outboundOptions.VMessOptions.TLS = tlsOptions
+ outboundOptions.VMessOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeHTTP,
+ HTTPOptions: *http2Options,
+ }
+ case "grpc":
+ if c.GrpcOptions == nil {
+ c.GrpcOptions = &ClashTransportGRPC{}
+ }
+ grpcOptions := &option.V2RayGRPCOptions{
+ ServiceName: c.GrpcOptions.ServiceName,
+ }
+
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: c.ClashProxyBasic.Server,
+ Insecure: c.SkipCertVerify,
+ }
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ }
+
+ if c.RealityOptions != nil {
+ tlsOptions.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: c.RealityOptions.PublicKey,
+ ShortID: c.RealityOptions.ShortID,
+ }
+ }
+
+ outboundOptions.VMessOptions.TLS = tlsOptions
+ outboundOptions.VMessOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeGRPC,
+ GRPCOptions: *grpcOptions,
+ }
+ default:
+ if c.TLS {
+ tlsOptions := &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: c.ClashProxyBasic.Server,
+ Insecure: c.SkipCertVerify,
+ }
+ if c.ClientFingerprint != "" {
+ tlsOptions.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: c.ClientFingerprint,
+ }
+ }
+
+ if c.ServerName != "" {
+ tlsOptions.ServerName = c.ServerName
+ } else if c.SNI != "" {
+ tlsOptions.ServerName = c.SNI
+ }
+
+ if c.RealityOptions != nil {
+ tlsOptions.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: c.RealityOptions.PublicKey,
+ ShortID: c.RealityOptions.ShortID,
+ }
+ }
+
+ outboundOptions.VMessOptions.TLS = tlsOptions
+ }
+ }
+
+ if c.TFO {
+ outboundOptions.VMessOptions.TCPFastOpen = true
+ }
+
+ if c.MuxOptions != nil && c.MuxOptions.Enabled {
+ outboundOptions.VLESSOptions.Multiplex = &option.OutboundMultiplexOptions{
+ Enabled: true,
+ Protocol: c.MuxOptions.Protocol,
+ MaxConnections: c.MuxOptions.MaxConnections,
+ MinStreams: c.MuxOptions.MinStreams,
+ MaxStreams: c.MuxOptions.MaxStreams,
+ Padding: c.MuxOptions.Padding,
+ }
+ }
+
+ switch c.ClashProxyBasic.IPVersion {
+ case "dual":
+ outboundOptions.VMessOptions.DomainStrategy = 0
+ case "ipv4":
+ outboundOptions.VMessOptions.DomainStrategy = 3
+ case "ipv6":
+ outboundOptions.VMessOptions.DomainStrategy = 4
+ case "ipv4-prefer":
+ outboundOptions.VMessOptions.DomainStrategy = 1
+ case "ipv6-prefer":
+ outboundOptions.VMessOptions.DomainStrategy = 2
+ }
+
+ return outboundOptions, nil
+}
diff --git a/proxyprovider/filter.go b/proxyprovider/filter.go
new file mode 100644
index 0000000000..371521e1be
--- /dev/null
+++ b/proxyprovider/filter.go
@@ -0,0 +1,172 @@
+package proxyprovider
+
+import (
+ "strings"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+ E "github.com/sagernet/sing/common/exceptions"
+
+ "github.com/dlclark/regexp2"
+)
+
+type Filter struct {
+ whiteMode bool
+ rules []FilterItem
+}
+
+func NewFilter(f *option.ProxyProviderFilter) (*Filter, error) {
+ ff := &Filter{
+ whiteMode: f.WhiteMode,
+ }
+ var rules []FilterItem
+ if f.Rules != nil && len(f.Rules) > 0 {
+ for _, rule := range f.Rules {
+ re, err := newFilterItem(rule)
+ if err != nil {
+ return nil, err
+ }
+ rules = append(rules, *re)
+ }
+ }
+ if len(rules) > 0 {
+ ff.rules = rules
+ }
+ return ff, nil
+}
+
+func (f *Filter) Filter(list []option.Outbound, tagMap map[string]string) []option.Outbound {
+ if f.rules != nil && len(f.rules) > 0 {
+ newList := make([]option.Outbound, 0, len(list))
+ for _, s := range list {
+ match := false
+ for _, rule := range f.rules {
+ if rule.match(&s, tagMap) {
+ match = true
+ break
+ }
+ }
+ if f.whiteMode {
+ if match {
+ newList = append(newList, s)
+ }
+ } else {
+ if !match {
+ newList = append(newList, s)
+ }
+ }
+ }
+ return newList
+ }
+ return list
+}
+
+type FilterItem struct {
+ isTag bool
+ isType bool
+ isServer bool
+
+ regex *regexp2.Regexp
+}
+
+func newFilterItem(rule string) (*FilterItem, error) {
+ var item FilterItem
+ var bRule string
+ switch {
+ case strings.HasPrefix(rule, "tag:"):
+ bRule = strings.TrimPrefix(rule, "tag:")
+ item.isTag = true
+ case strings.HasPrefix(rule, "type:"):
+ bRule = strings.TrimPrefix(rule, "type:")
+ item.isType = true
+ case strings.HasPrefix(rule, "server:"):
+ bRule = strings.TrimPrefix(rule, "server:")
+ item.isServer = true
+ default:
+ bRule = rule
+ item.isTag = true
+ }
+ regex, err := regexp2.Compile(bRule, regexp2.RE2)
+ if err != nil {
+ return nil, E.Cause(err, "invalid rule: ", rule)
+ }
+ item.regex = regex
+ return &item, nil
+}
+
+func (i *FilterItem) match(outbound *option.Outbound, tagMap map[string]string) bool { // append ==> true
+ var s string
+ if i.isType {
+ s = outbound.Type
+ } else if i.isServer {
+ s = getServer(outbound)
+ } else {
+ if tagMap != nil {
+ s = tagMap[outbound.Tag]
+ } else {
+ s = outbound.Tag
+ }
+ }
+ b, err := i.regex.MatchString(s)
+ return err == nil && b
+}
+
+func getServer(outbound *option.Outbound) string {
+ var server string
+ switch outbound.Type {
+ case C.TypeHTTP:
+ server = outbound.HTTPOptions.Server
+ case C.TypeShadowsocks:
+ server = outbound.ShadowsocksOptions.Server
+ case C.TypeVMess:
+ server = outbound.VMessOptions.Server
+ case C.TypeTrojan:
+ server = outbound.TrojanOptions.Server
+ case C.TypeWireGuard:
+ server = outbound.WireGuardOptions.Server
+ case C.TypeHysteria:
+ server = outbound.HysteriaOptions.Server
+ case C.TypeSSH:
+ server = outbound.SSHOptions.Server
+ case C.TypeShadowTLS:
+ server = outbound.ShadowTLSOptions.Server
+ case C.TypeShadowsocksR:
+ server = outbound.ShadowsocksROptions.Server
+ case C.TypeVLESS:
+ server = outbound.VLESSOptions.Server
+ case C.TypeTUIC:
+ server = outbound.TUICOptions.Server
+ case C.TypeHysteria2:
+ server = outbound.Hysteria2Options.Server
+ }
+ return server
+}
+
+func setServer(outbound *option.Outbound, server string) {
+ switch outbound.Type {
+ case C.TypeHTTP:
+ outbound.HTTPOptions.Server = server
+ case C.TypeShadowsocks:
+ outbound.ShadowsocksOptions.Server = server
+ case C.TypeVMess:
+ outbound.VMessOptions.Server = server
+ case C.TypeTrojan:
+ outbound.TrojanOptions.Server = server
+ case C.TypeWireGuard:
+ outbound.WireGuardOptions.Server = server
+ case C.TypeHysteria:
+ outbound.HysteriaOptions.Server = server
+ case C.TypeSSH:
+ outbound.SSHOptions.Server = server
+ case C.TypeShadowTLS:
+ outbound.ShadowTLSOptions.Server = server
+ case C.TypeShadowsocksR:
+ outbound.ShadowsocksROptions.Server = server
+ case C.TypeVLESS:
+ outbound.VLESSOptions.Server = server
+ case C.TypeTUIC:
+ outbound.TUICOptions.Server = server
+ case C.TypeHysteria2:
+ outbound.Hysteria2Options.Server = server
+ }
+}
diff --git a/proxyprovider/proxyprovider.go b/proxyprovider/proxyprovider.go
new file mode 100644
index 0000000000..da2e16bf8d
--- /dev/null
+++ b/proxyprovider/proxyprovider.go
@@ -0,0 +1,651 @@
+//go:build with_proxyprovider
+
+package proxyprovider
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "sync"
+ "time"
+
+ "github.com/sagernet/quic-go"
+ "github.com/sagernet/quic-go/http3"
+ "github.com/sagernet/sing-box/adapter"
+ "github.com/sagernet/sing-box/common/dialer"
+ "github.com/sagernet/sing-box/common/simpledns"
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/log"
+ "github.com/sagernet/sing-box/option"
+ "github.com/sagernet/sing-box/proxyprovider/clash"
+ "github.com/sagernet/sing-box/proxyprovider/raw"
+ "github.com/sagernet/sing-box/proxyprovider/singbox"
+ "github.com/sagernet/sing/common/bufio"
+ E "github.com/sagernet/sing/common/exceptions"
+ M "github.com/sagernet/sing/common/metadata"
+ N "github.com/sagernet/sing/common/network"
+ "github.com/sagernet/sing/common/rw"
+)
+
+var _ adapter.ProxyProvider = (*ProxyProvider)(nil)
+
+type ProxyProvider struct {
+ ctx context.Context
+ router adapter.Router
+ logger log.ContextLogger
+ tag string
+
+ url string
+ ua string
+ useH3 bool
+ cacheFile string
+ updateInterval time.Duration
+ requestTimeout time.Duration
+ dns string
+ tagFormat string
+ globalFilter *Filter
+ groups []Group
+ dialer *option.DialerOptions
+ requestDialer N.Dialer
+ runningDetour string
+ lookupIP bool
+
+ cacheLock sync.RWMutex
+ cache *Cache
+ autoUpdateCtx context.Context
+ autoUpdateCancel context.CancelFunc
+ autoUpdateCancelDone chan struct{}
+ updateLock sync.Mutex
+
+ httpClient *http.Client
+}
+
+func NewProxyProvider(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ProxyProvider) (adapter.ProxyProvider, error) {
+ if tag == "" {
+ return nil, E.New("tag is empty")
+ }
+ if options.Url == "" {
+ return nil, E.New("url is empty")
+ }
+ if options.UserAgent == "" {
+ options.UserAgent = "clash.meta; sing-box"
+ }
+ var globalFilter *Filter
+ if options.GlobalFilter != nil {
+ var err error
+ globalFilter, err = NewFilter(options.GlobalFilter)
+ if err != nil {
+ return nil, E.Cause(err, "initialize global filter failed")
+ }
+ }
+ p := &ProxyProvider{
+ ctx: ctx,
+ router: router,
+ logger: logger,
+ //
+ tag: tag,
+ url: options.Url,
+ ua: options.UserAgent,
+ useH3: options.UseH3,
+ cacheFile: options.CacheFile,
+ dns: options.DNS,
+ dialer: options.Dialer,
+ runningDetour: options.RunningDetour,
+ lookupIP: options.LookupIP,
+ tagFormat: options.TagFormat,
+ updateInterval: time.Duration(options.UpdateInterval),
+ requestTimeout: time.Duration(options.RequestTimeout),
+ globalFilter: globalFilter,
+ }
+ if options.Groups != nil && len(options.Groups) > 0 {
+ groups := make([]Group, 0, len(options.Groups))
+ for _, groupOptions := range options.Groups {
+ g := Group{
+ Tag: groupOptions.Tag,
+ Type: groupOptions.Type,
+ SelectorOptions: groupOptions.SelectorOptions,
+ URLTestOptions: groupOptions.URLTestOptions,
+ JSTestOptions: groupOptions.JSTestOptions,
+ }
+ if groupOptions.Filter != nil {
+ filter, err := NewFilter(groupOptions.Filter)
+ if err != nil {
+ return nil, E.Cause(err, "initialize group filter failed")
+ }
+ g.Filter = filter
+ }
+ groups = append(groups, g)
+ }
+ p.groups = groups
+ }
+ if options.RequestDialer.Detour != "" {
+ return nil, E.New("request dialer detour is not supported")
+ }
+ d, err := dialer.NewSimple(options.RequestDialer)
+ if err != nil {
+ return nil, E.Cause(err, "initialize request dialer failed")
+ }
+ p.requestDialer = d
+ return p, nil
+}
+
+func (p *ProxyProvider) Tag() string {
+ return p.tag
+}
+
+func (p *ProxyProvider) StartGetOutbounds() ([]option.Outbound, error) {
+ p.logger.Info("proxyprovider get outbounds")
+ if p.cacheFile != "" {
+ if rw.FileExists(p.cacheFile) {
+ p.logger.Info("loading cache file: ", p.cacheFile)
+ var cache Cache
+ err := cache.ReadFromFile(p.cacheFile)
+ if err != nil {
+ return nil, E.Cause(err, "invalid cache file")
+ }
+ if !cache.IsNil() {
+ p.cache = new(Cache)
+ *p.cache = cache
+ p.logger.Info("cache file loaded")
+ } else {
+ p.logger.Info("cache file is empty")
+ }
+ }
+ }
+ if p.cache == nil || (p.cache != nil && p.updateInterval > 0 && p.cache.LastUpdate.Add(p.updateInterval).Before(time.Now())) {
+ p.logger.Info("updating outbounds")
+ cache, err := p.wrapUpdate(p.ctx, true)
+ if err == nil {
+ p.cache = cache
+ if p.cacheFile != "" {
+ p.logger.Info("writing cache file: ", p.cacheFile)
+ err := cache.WriteToFile(p.cacheFile)
+ if err != nil {
+ return nil, E.Cause(err, "write cache file failed")
+ }
+ p.logger.Info("write cache file done")
+ }
+ p.logger.Info("outbounds updated")
+ }
+ if err != nil {
+ if p.cache == nil {
+ return nil, E.Cause(err, "update outbounds failed")
+ } else {
+ p.logger.Warn("update cache failed: ", err)
+ }
+ }
+ }
+ defer func() {
+ p.cache.Outbounds = nil
+ }()
+ return p.getFullOutboundOptions(p.ctx)
+}
+
+func (p *ProxyProvider) Start() error {
+ if p.updateInterval > 0 && p.cacheFile != "" {
+ p.autoUpdateCtx, p.autoUpdateCancel = context.WithCancel(p.ctx)
+ p.autoUpdateCancelDone = make(chan struct{}, 1)
+ go p.loopUpdate()
+ }
+ return nil
+}
+
+func (p *ProxyProvider) loopUpdate() {
+ defer func() {
+ p.autoUpdateCancelDone <- struct{}{}
+ }()
+ ticker := time.NewTicker(p.updateInterval)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ p.update(p.autoUpdateCtx, false)
+ case <-p.autoUpdateCtx.Done():
+ return
+ }
+ }
+}
+
+func (p *ProxyProvider) Close() error {
+ if p.autoUpdateCtx != nil {
+ p.autoUpdateCancel()
+ <-p.autoUpdateCancelDone
+ }
+ return nil
+}
+
+func (p *ProxyProvider) GetOutboundOptions() ([]option.Outbound, error) {
+ p.cacheLock.RLock()
+ defer p.cacheLock.RUnlock()
+ return p.cache.Outbounds, nil
+}
+
+func (p *ProxyProvider) GetFullOutboundOptions() ([]option.Outbound, error) {
+ return p.getFullOutboundOptions(p.ctx)
+}
+
+func (p *ProxyProvider) getFullOutboundOptions(ctx context.Context) ([]option.Outbound, error) {
+ p.cacheLock.RLock()
+ outbounds := p.cache.Outbounds
+ p.cacheLock.RUnlock()
+
+ if p.dialer != nil {
+ for i := range outbounds {
+ outbound := &outbounds[i]
+ setDialerOptions(outbound, p.dialer)
+ }
+ }
+
+ if p.lookupIP && p.dns != "" {
+ for i := range outbounds {
+ outbound := &outbounds[i]
+ ips, err := simpledns.DNSLookup(ctx, p.requestDialer, p.dns, getServer(outbound), true, true)
+ if err != nil {
+ return nil, err
+ }
+ setServer(outbound, ips[0].String())
+ }
+ }
+
+ var outboundTagMap map[string]string
+ finalOutbounds := make([]option.Outbound, 0, len(outbounds))
+ finalOutbounds = append(finalOutbounds, outbounds...)
+
+ if p.tagFormat != "" {
+ outboundTagMap = make(map[string]string, len(outbounds))
+ for i := range finalOutbounds {
+ tag := finalOutbounds[i].Tag
+ finalTag := fmt.Sprintf(p.tagFormat, tag)
+ outboundTagMap[finalTag] = tag
+ finalOutbounds[i].Tag = finalTag
+ }
+ }
+
+ outboundOptionsMap := make(map[string]*option.Outbound)
+ for i := range finalOutbounds {
+ outbound := &finalOutbounds[i]
+ outboundOptionsMap[outbound.Tag] = outbound
+ }
+
+ var allOutboundTags []string
+ for _, outbound := range finalOutbounds {
+ allOutboundTags = append(allOutboundTags, outbound.Tag)
+ }
+
+ var groupOutbounds []option.Outbound
+ var groupOutboundTags []string
+ if p.groups != nil && len(p.groups) > 0 {
+ groupOutbounds = make([]option.Outbound, 0, len(p.groups))
+ for _, group := range p.groups {
+ var outboundTags []string
+ if group.Filter != nil {
+ groupOutbounds := group.Filter.Filter(finalOutbounds, outboundTagMap)
+ for _, outbound := range groupOutbounds {
+ outboundTags = append(outboundTags, outbound.Tag)
+ }
+ } else {
+ outboundTags = allOutboundTags
+ }
+ if len(outboundTags) == 0 {
+ return nil, E.New("no outbound available for group: ", group.Tag)
+ }
+ outboundOptions := option.Outbound{
+ Tag: group.Tag,
+ Type: group.Type,
+ SelectorOptions: group.SelectorOptions,
+ URLTestOptions: group.URLTestOptions,
+ JSTestOptions: group.JSTestOptions,
+ }
+ var outbounds []string
+ switch group.Type {
+ case C.TypeSelector:
+ outbounds = append(outbounds, group.SelectorOptions.Outbounds...)
+ outbounds = append(outbounds, outboundTags...)
+ outboundOptions.SelectorOptions.Outbounds = outbounds
+ case C.TypeURLTest:
+ outbounds = append(outbounds, group.URLTestOptions.Outbounds...)
+ outbounds = append(outbounds, outboundTags...)
+ outboundOptions.URLTestOptions.Outbounds = outbounds
+ case C.TypeJSTest:
+ outbounds = append(outbounds, group.JSTestOptions.Outbounds...)
+ outbounds = append(outbounds, outboundTags...)
+ outboundOptions.JSTestOptions.Outbounds = outbounds
+ }
+ groupOutbounds = append(groupOutbounds, outboundOptions)
+ groupOutboundTags = append(groupOutboundTags, group.Tag)
+ }
+ }
+
+ globalOutbound := option.Outbound{
+ Tag: p.tag,
+ Type: C.TypeSelector,
+ SelectorOptions: option.SelectorOutboundOptions{
+ Outbounds: allOutboundTags,
+ },
+ }
+ if len(groupOutboundTags) > 0 {
+ finalOutbounds = append(finalOutbounds, groupOutbounds...)
+ globalOutbound.SelectorOptions.Outbounds = append(globalOutbound.SelectorOptions.Outbounds, groupOutboundTags...)
+ }
+
+ finalOutbounds = append(finalOutbounds, globalOutbound)
+
+ return finalOutbounds, nil
+}
+
+func (p *ProxyProvider) GetClashInfo() (download uint64, upload uint64, total uint64, expire time.Time, err error) {
+ p.cacheLock.RLock()
+ defer p.cacheLock.RUnlock()
+ if p.cache.ClashInfo != nil {
+ download = p.cache.ClashInfo.Download
+ upload = p.cache.ClashInfo.Upload
+ total = p.cache.ClashInfo.Total
+ expire = p.cache.ClashInfo.Expire
+ }
+ return
+}
+
+func (p *ProxyProvider) Update() {
+ if p.updateInterval > 0 && p.cacheFile != "" {
+ p.update(p.ctx, false)
+ }
+}
+
+func (p *ProxyProvider) update(ctx context.Context, isFirst bool) {
+ if !p.updateLock.TryLock() {
+ return
+ }
+ defer p.updateLock.Unlock()
+
+ p.logger.Info("updating cache")
+ cache, err := p.wrapUpdate(ctx, false)
+ if err != nil {
+ p.logger.Error("update cache failed: ", err)
+ return
+ }
+ p.cacheLock.Lock()
+ p.cache = cache
+ if p.cacheFile != "" {
+ err = cache.WriteToFile(p.cacheFile)
+ if err != nil {
+ p.logger.Error("write cache file failed: ", err)
+ return
+ }
+ }
+ p.cache.Outbounds = nil
+ p.cacheLock.Unlock()
+}
+
+func (p *ProxyProvider) wrapUpdate(ctx context.Context, isFirst bool) (*Cache, error) {
+ var httpClient *http.Client
+ if isFirst {
+ if !p.useH3 {
+ httpClient = &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ if p.dns != "" {
+ host, _, err := net.SplitHostPort(addr)
+ if err != nil {
+ return nil, err
+ }
+ ips, err := simpledns.DNSLookup(ctx, p.requestDialer, p.dns, host, true, true)
+ if err != nil {
+ return nil, err
+ }
+ return N.DialParallel(ctx, p.requestDialer, network, M.ParseSocksaddr(addr), ips, false, 5*time.Second)
+ } else {
+ return p.requestDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
+ }
+ },
+ ForceAttemptHTTP2: true,
+ },
+ }
+ } else {
+ httpClient = &http.Client{
+ Transport: &http3.RoundTripper{
+ Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
+ var conn net.Conn
+ var err error
+ if p.dns != "" {
+ host, _, err := net.SplitHostPort(addr)
+ if err != nil {
+ return nil, err
+ }
+ ips, err := simpledns.DNSLookup(ctx, p.requestDialer, p.dns, host, true, true)
+ if err != nil {
+ return nil, err
+ }
+ conn, err = N.DialParallel(ctx, p.requestDialer, N.NetworkUDP, M.ParseSocksaddr(addr), ips, false, 5*time.Second)
+ } else {
+ conn, err = p.requestDialer.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddr(addr))
+ }
+ if err != nil {
+ return nil, err
+ }
+ return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsCfg, cfg)
+ },
+ },
+ }
+ }
+ } else if p.httpClient == nil {
+ if !p.useH3 {
+ httpClient = &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ dialer := p.requestDialer
+ if p.runningDetour != "" {
+ var loaded bool
+ dialer, loaded = p.router.Outbound(p.runningDetour)
+ if !loaded {
+ return nil, E.New("running detour not found")
+ }
+ }
+ if p.dns != "" {
+ host, _, err := net.SplitHostPort(addr)
+ if err != nil {
+ return nil, err
+ }
+ ips, err := simpledns.DNSLookup(ctx, dialer, p.dns, host, true, true)
+ if err != nil {
+ return nil, err
+ }
+ return N.DialParallel(ctx, dialer, network, M.ParseSocksaddr(addr), ips, false, 5*time.Second)
+ } else {
+ return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
+ }
+ },
+ ForceAttemptHTTP2: true,
+ },
+ }
+ } else {
+ httpClient = &http.Client{
+ Transport: &http3.RoundTripper{
+ Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
+ var conn net.Conn
+ var err error
+ dialer := p.requestDialer
+ if p.runningDetour != "" {
+ var loaded bool
+ dialer, loaded = p.router.Outbound(p.runningDetour)
+ if !loaded {
+ return nil, E.New("running detour not found")
+ }
+ }
+ if p.dns != "" {
+ host, _, err := net.SplitHostPort(addr)
+ if err != nil {
+ return nil, err
+ }
+ ips, err := simpledns.DNSLookup(ctx, dialer, p.dns, host, true, true)
+ if err != nil {
+ return nil, err
+ }
+ conn, err = N.DialParallel(ctx, dialer, N.NetworkUDP, M.ParseSocksaddr(addr), ips, false, 5*time.Second)
+ } else {
+ conn, err = dialer.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddr(addr))
+ }
+ if err != nil {
+ return nil, err
+ }
+ return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsCfg, cfg)
+ },
+ },
+ }
+ }
+ p.httpClient = httpClient
+ } else {
+ httpClient = p.httpClient
+ }
+ if p.requestTimeout > 0 {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, p.requestTimeout)
+ defer cancel()
+ }
+ cache, err := request(ctx, httpClient, p.url, p.ua)
+ if err != nil {
+ return nil, err
+ }
+ if p.globalFilter != nil {
+ newOutbounds := p.globalFilter.Filter(cache.Outbounds, nil)
+ if len(newOutbounds) == 0 {
+ return nil, E.New("no outbound available")
+ }
+ cache.Outbounds = newOutbounds
+ }
+ return cache, nil
+}
+
+func (p *ProxyProvider) LastUpdateTime() time.Time {
+ p.cacheLock.RLock()
+ defer p.cacheLock.RUnlock()
+ if p.cache != nil {
+ return p.cache.LastUpdate
+ }
+ return time.Time{}
+}
+
+func setDialerOptions(outbound *option.Outbound, dialer *option.DialerOptions) {
+ newDialer := copyDialerOptions(dialer)
+ switch outbound.Type {
+ case C.TypeDirect:
+ outbound.DirectOptions.DialerOptions = newDialer
+ case C.TypeHTTP:
+ outbound.HTTPOptions.DialerOptions = newDialer
+ case C.TypeShadowsocks:
+ outbound.ShadowsocksOptions.DialerOptions = newDialer
+ case C.TypeVMess:
+ outbound.VMessOptions.DialerOptions = newDialer
+ case C.TypeTrojan:
+ outbound.TrojanOptions.DialerOptions = newDialer
+ case C.TypeWireGuard:
+ outbound.WireGuardOptions.DialerOptions = newDialer
+ case C.TypeHysteria:
+ outbound.HysteriaOptions.DialerOptions = newDialer
+ case C.TypeTor:
+ outbound.TorOptions.DialerOptions = newDialer
+ case C.TypeSSH:
+ outbound.SSHOptions.DialerOptions = newDialer
+ case C.TypeShadowTLS:
+ outbound.ShadowTLSOptions.DialerOptions = newDialer
+ case C.TypeShadowsocksR:
+ outbound.ShadowsocksROptions.DialerOptions = newDialer
+ case C.TypeVLESS:
+ outbound.VLESSOptions.DialerOptions = newDialer
+ case C.TypeTUIC:
+ outbound.TUICOptions.DialerOptions = newDialer
+ case C.TypeHysteria2:
+ outbound.Hysteria2Options.DialerOptions = newDialer
+ case C.TypeRandomAddr:
+ outbound.RandomAddrOptions.DialerOptions = newDialer
+ }
+}
+
+func copyDialerOptions(dialer *option.DialerOptions) option.DialerOptions {
+ newDialer := option.DialerOptions{
+ Detour: dialer.Detour,
+ BindInterface: dialer.BindInterface,
+ ProtectPath: dialer.ProtectPath,
+ RoutingMark: dialer.RoutingMark,
+ ReuseAddr: dialer.ReuseAddr,
+ ConnectTimeout: dialer.ConnectTimeout,
+ TCPFastOpen: dialer.TCPFastOpen,
+ TCPMultiPath: dialer.TCPMultiPath,
+ UDPFragmentDefault: dialer.UDPFragmentDefault,
+ DomainStrategy: dialer.DomainStrategy,
+ FallbackDelay: dialer.FallbackDelay,
+ }
+ if dialer.Inet4BindAddress != nil {
+ newDialer.Inet4BindAddress = new(option.ListenAddress)
+ *newDialer.Inet4BindAddress = *dialer.Inet4BindAddress
+ }
+ if dialer.Inet6BindAddress != nil {
+ newDialer.Inet6BindAddress = new(option.ListenAddress)
+ *newDialer.Inet6BindAddress = *dialer.Inet6BindAddress
+ }
+ if dialer.UDPFragment != nil {
+ newDialer.UDPFragment = new(bool)
+ *newDialer.UDPFragment = *dialer.UDPFragment
+ }
+ return newDialer
+}
+
+func ParseLink(ctx context.Context, link string) ([]option.Outbound, error) {
+ u, err := url.Parse(link)
+ if err != nil {
+ return nil, fmt.Errorf("invalid link")
+ }
+ switch u.Scheme {
+ case "http", "https":
+ default:
+ // Try Raw Config
+ outbound, err := raw.ParseRawLink(link)
+ if err != nil {
+ return nil, err
+ }
+ return []option.Outbound{*outbound}, nil
+ }
+
+ req, err := http.NewRequest(http.MethodGet, link, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("User-Agent", "clash.meta; clashmeta; sing-box; singbox; SFA; SFI; SFM; SFT") // TODO: UA??
+
+ resp, err := http.DefaultClient.Do(req.WithContext(ctx))
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("invalid http status code: %d", resp.StatusCode)
+ }
+
+ buffer := bytes.NewBuffer(nil)
+ _, err = buffer.ReadFrom(resp.Body)
+ resp.Body.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ data := buffer.Bytes()
+
+ // Try Clash Config
+ outbounds, err := clash.ParseClashConfig(data)
+ if err != nil {
+ // Try Raw Config
+ outbounds, err = raw.ParseRawConfig(data)
+ if err != nil {
+ // Try Singbox Config
+ outbounds, err = singbox.ParseSingboxConfig(data)
+ if err != nil {
+ return nil, fmt.Errorf("parse config failed, config is not clash config or raw links or sing-box config")
+ }
+ }
+ }
+
+ return outbounds, nil
+}
diff --git a/proxyprovider/proxyprovider_stub.go b/proxyprovider/proxyprovider_stub.go
new file mode 100644
index 0000000000..8c65871cd0
--- /dev/null
+++ b/proxyprovider/proxyprovider_stub.go
@@ -0,0 +1,16 @@
+//go:build !with_proxyprovider
+
+package proxyprovider
+
+import (
+ "context"
+
+ "github.com/sagernet/sing-box/adapter"
+ "github.com/sagernet/sing-box/log"
+ "github.com/sagernet/sing-box/option"
+ E "github.com/sagernet/sing/common/exceptions"
+)
+
+func NewProxyProvider(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ProxyProvider) (adapter.ProxyProvider, error) {
+ return nil, E.New(`ProxyProvider is not included in this build, rebuild with -tags with_proxyprovider`)
+}
diff --git a/proxyprovider/raw/http.go b/proxyprovider/raw/http.go
new file mode 100644
index 0000000000..7f5932e664
--- /dev/null
+++ b/proxyprovider/raw/http.go
@@ -0,0 +1,75 @@
+package raw
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "strconv"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type HTTP struct {
+ options *option.Outbound
+}
+
+func (p *HTTP) Tag() string {
+ return p.options.Tag
+}
+
+func (p *HTTP) ParseLink(link string) error {
+ u, err := url.Parse(link)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: %w", link, err)
+ }
+ var port uint16
+ portStr := u.Port()
+ if portStr != "" {
+ portUint64, err := strconv.ParseUint(u.Port(), 10, 16)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`, error: %s", link, portStr, err)
+ }
+ if portUint64 == 0 || portUint64 > 0xffff {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`", link, portStr)
+ }
+ port = uint16(portUint64)
+ } else {
+ if u.Scheme == "https" {
+ port = 443
+ } else {
+ port = 80
+ }
+ }
+ username := u.User.Username()
+ password, _ := u.User.Password()
+ options := &option.Outbound{
+ Type: C.TypeHTTP,
+ HTTPOptions: option.HTTPOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: u.Hostname(),
+ ServerPort: port,
+ },
+ Username: username,
+ Password: password,
+ Path: u.Path,
+ },
+ }
+ if u.Scheme == "https" {
+ options.HTTPOptions.TLS = &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: u.Hostname(),
+ }
+ }
+ if u.Fragment != "" {
+ options.Tag = u.Fragment
+ } else {
+ options.Tag = net.JoinHostPort(options.HTTPOptions.Server, strconv.Itoa(int(options.HTTPOptions.ServerPort)))
+ }
+ p.options = options
+ return nil
+}
+
+func (p *HTTP) Options() *option.Outbound {
+ return p.options
+}
diff --git a/proxyprovider/raw/hysteria.go b/proxyprovider/raw/hysteria.go
new file mode 100644
index 0000000000..10bf26b562
--- /dev/null
+++ b/proxyprovider/raw/hysteria.go
@@ -0,0 +1,110 @@
+package raw
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "strconv"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type Hysteria struct {
+ options *option.Outbound
+}
+
+func (p *Hysteria) Tag() string {
+ return p.options.Tag
+}
+
+func (p *Hysteria) ParseLink(link string) error {
+ u, err := url.Parse(link)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: %w", link, err)
+ }
+ portStr := u.Port()
+ if portStr == "" {
+ return fmt.Errorf("parse link `%s` failed: port is empty", link)
+ }
+ portUint64, err := strconv.ParseUint(u.Port(), 10, 16)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`, error: %s", link, portStr, err)
+ }
+ port := uint16(portUint64)
+ args, err := url.ParseQuery(u.RawQuery)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: parse args failed: %w", link, err)
+ }
+ protocol := args.Get("protocol")
+ switch protocol {
+ case "udp", "":
+ case "wechat-video", "faketcp":
+ return fmt.Errorf("parse link `%s` failed: protocol `%s` is not supported", link, protocol)
+ default:
+ return fmt.Errorf("parse link `%s` failed: invalid protocol: %s", link, protocol)
+ }
+ auth := args.Get("auth")
+ sni := args.Get("peer")
+ var insecure bool
+ insecureStr := args.Get("insecure")
+ switch insecureStr {
+ case "1", "true":
+ insecure = true
+ }
+ upmbpsStr := args.Get("upmbps")
+ upmbps, err := strconv.ParseUint(upmbpsStr, 10, 64)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid upmbps `%s`", link, upmbpsStr)
+ }
+ downmbpsStr := args.Get("downmbps")
+ downmbps, err := strconv.ParseUint(downmbpsStr, 10, 64)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid downmbps `%s`", link, downmbpsStr)
+ }
+ alpn := args.Get("alpn")
+ // TODO: How to do ??
+ obfs := args.Get("obfs")
+ if obfs == "" {
+ obfs = "xplus"
+ }
+ //
+ obfsParam := args.Get("obfsParam")
+ options := &option.Outbound{
+ Type: C.TypeHysteria,
+ HysteriaOptions: option.HysteriaOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: u.Hostname(),
+ ServerPort: port,
+ },
+ AuthString: auth,
+ UpMbps: int(upmbps),
+ DownMbps: int(downmbps),
+ Obfs: obfsParam,
+ OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
+ TLS: &option.OutboundTLSOptions{
+ Enabled: true,
+ Insecure: insecure,
+ ServerName: sni,
+ },
+ },
+ },
+ }
+ if options.HysteriaOptions.TLS.ServerName == "" {
+ options.HysteriaOptions.TLS.ServerName = options.HysteriaOptions.Server
+ }
+ if alpn != "" {
+ options.HysteriaOptions.TLS.ALPN = []string{alpn}
+ }
+ if u.Fragment != "" {
+ options.Tag = u.Fragment
+ } else {
+ options.Tag = net.JoinHostPort(options.HysteriaOptions.Server, strconv.Itoa(int(options.HysteriaOptions.ServerPort)))
+ }
+ p.options = options
+ return nil
+}
+
+func (p *Hysteria) Options() *option.Outbound {
+ return p.options
+}
diff --git a/proxyprovider/raw/hysteria2.go b/proxyprovider/raw/hysteria2.go
new file mode 100644
index 0000000000..3a5e40f912
--- /dev/null
+++ b/proxyprovider/raw/hysteria2.go
@@ -0,0 +1,94 @@
+package raw
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "strconv"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type Hysteria2 struct {
+ options *option.Outbound
+}
+
+func (p *Hysteria2) Tag() string {
+ return p.options.Tag
+}
+
+func (p *Hysteria2) ParseLink(link string) error {
+ u, err := url.Parse(link)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: %w", link, err)
+ }
+ var port uint16
+ portStr := u.Port()
+ if portStr != "" {
+ portUint64, err := strconv.ParseUint(u.Port(), 10, 16)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`, error: %s", link, portStr, err)
+ }
+ if portUint64 == 0 || portUint64 > 0xffff {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`", link, portStr)
+ }
+ port = uint16(portUint64)
+ } else {
+ port = 443
+ }
+ args, err := url.ParseQuery(u.RawQuery)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: parse args failed: %w", link, err)
+ }
+ sni := args.Get("sni")
+ if sni == "" {
+ sni = args.Get("peer")
+ }
+ var insecure bool
+ insecureStr := args.Get("insecure")
+ switch insecureStr {
+ case "1", "true":
+ insecure = true
+ }
+ obfs := args.Get("obfs")
+ obfsPassword := args.Get("obfs-password")
+ // TODO: pinSHA256 ????
+ options := &option.Outbound{
+ Type: C.TypeHysteria2,
+ Hysteria2Options: option.Hysteria2OutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: u.Hostname(),
+ ServerPort: port,
+ },
+ Password: u.User.Username(),
+ OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
+ TLS: &option.OutboundTLSOptions{
+ Enabled: true,
+ Insecure: insecure,
+ ServerName: sni,
+ },
+ },
+ },
+ }
+ if obfs != "" && obfsPassword != "" {
+ options.Hysteria2Options.Obfs = &option.Hysteria2Obfs{
+ Type: obfs,
+ Password: obfsPassword,
+ }
+ }
+ if options.Hysteria2Options.TLS.ServerName == "" {
+ options.Hysteria2Options.TLS.ServerName = options.Hysteria2Options.Server
+ }
+ if u.Fragment != "" {
+ options.Tag = u.Fragment
+ } else {
+ options.Tag = net.JoinHostPort(options.Hysteria2Options.Server, strconv.Itoa(int(options.Hysteria2Options.ServerPort)))
+ }
+ p.options = options
+ return nil
+}
+
+func (p *Hysteria2) Options() *option.Outbound {
+ return p.options
+}
diff --git a/proxyprovider/raw/shadowsocks.go b/proxyprovider/raw/shadowsocks.go
new file mode 100644
index 0000000000..4334e9c80f
--- /dev/null
+++ b/proxyprovider/raw/shadowsocks.go
@@ -0,0 +1,169 @@
+package raw
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net"
+ "net/url"
+ "strconv"
+ "strings"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+ "github.com/sagernet/sing-box/proxyprovider/utils"
+)
+
+type Shadowsocks struct {
+ options *option.Outbound
+}
+
+func (p *Shadowsocks) Tag() string {
+ return p.options.Tag
+}
+
+func (p *Shadowsocks) ParseLink(link string) error {
+ sLink := link
+ tryOk, tryErr := func() (bool, error) {
+ // tryParseLink
+ func() {
+ _sLink := strings.TrimPrefix(sLink, "ss://")
+ suri := strings.SplitAfterN(_sLink, "#", 2)
+ var sLabel string
+ if len(suri) <= 2 {
+ if len(suri) == 2 {
+ sLabel = "#" + suri[1]
+ }
+ l, err := base64.RawURLEncoding.DecodeString(suri[0])
+ if err != nil {
+ return
+ }
+ sLink = "ss://" + string(l) + sLabel
+ }
+ }()
+ // SIP002 format https://shadowsocks.org/guide/sip002.html
+ u, err := url.Parse(sLink)
+ if err != nil {
+ return false, nil
+ }
+ var port uint16
+ portStr := u.Port()
+ if portStr != "" {
+ portUint64, err := strconv.ParseUint(u.Port(), 10, 16)
+ if err != nil {
+ return false, nil
+ }
+ if portUint64 == 0 || portUint64 > 0xffff {
+ return false, nil
+ }
+ port = uint16(portUint64)
+ } else {
+ port = 1080
+ }
+ method := u.User.Username()
+ password, ok := u.User.Password()
+ if !ok {
+ s, err := base64.RawURLEncoding.DecodeString(method)
+ if err != nil {
+ return false, nil
+ }
+ ss := strings.SplitN(string(s), ":", 2)
+ if len(ss) != 2 {
+ return false, nil
+ }
+ method = ss[0]
+ password = ss[1]
+ }
+ if !utils.CheckShadowsocksMethod(method) {
+ return false, fmt.Errorf("parse link `%s` failed: invalid method: %s", link, method)
+ }
+ options := &option.Outbound{
+ Type: C.TypeShadowsocks,
+ ShadowsocksOptions: option.ShadowsocksOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: u.Hostname(),
+ ServerPort: port,
+ },
+ Method: method,
+ Password: password,
+ },
+ }
+ args, err := url.ParseQuery(u.RawQuery)
+ if err == nil {
+ plugin := args.Get("plugin")
+ if plugin != "" {
+ plugins := strings.SplitN(plugin, ";", 2)
+ options.ShadowsocksOptions.Plugin = plugins[0]
+ if len(plugins) == 2 {
+ options.ShadowsocksOptions.PluginOptions = plugins[1]
+ }
+ }
+ }
+ if u.Fragment != "" {
+ options.Tag = u.Fragment
+ } else {
+ options.Tag = net.JoinHostPort(options.ShadowsocksOptions.Server, strconv.Itoa(int(options.ShadowsocksOptions.ServerPort)))
+ }
+ p.options = options
+ return true, nil
+ }()
+ if tryOk {
+ return nil
+ }
+ if tryErr != nil {
+ return tryErr
+ }
+ sLink = strings.TrimPrefix(sLink, "ss://")
+ sLinks := strings.SplitAfterN(sLink, "#", 2)
+ sLink = sLinks[0]
+ var tag string
+ if len(sLinks) == 2 {
+ tag = sLinks[1]
+ }
+ uri := strings.SplitAfterN(sLink, "@", 2)
+ if len(uri) != 2 {
+ return fmt.Errorf("parse link `%s` failed", link)
+ }
+ us := strings.SplitN(uri[0], ":", 2)
+ if len(us) != 2 {
+ return fmt.Errorf("parse link `%s` failed", link)
+ }
+ method := us[0]
+ if !utils.CheckShadowsocksMethod(method) {
+ return fmt.Errorf("parse link `%s` failed: invalid method: %s", link, method)
+ }
+ password := us[1]
+ host, portStr, err := net.SplitHostPort(uri[1])
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid address, error: %s", link, err)
+ }
+ if host == "" {
+ return fmt.Errorf("parse link `%s` failed: invalid address", link)
+ }
+ portUint64, err := strconv.ParseUint(portStr, 10, 16)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`, error: %s", link, portStr, err)
+ }
+ port := uint16(portUint64)
+ options := &option.Outbound{
+ Type: C.TypeShadowsocks,
+ ShadowsocksOptions: option.ShadowsocksOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: host,
+ ServerPort: port,
+ },
+ Method: method,
+ Password: password,
+ },
+ }
+ if tag != "" {
+ options.Tag = tag
+ } else {
+ options.Tag = net.JoinHostPort(options.ShadowsocksOptions.Server, strconv.Itoa(int(options.ShadowsocksOptions.ServerPort)))
+ }
+ p.options = options
+ return nil
+}
+
+func (p *Shadowsocks) Options() *option.Outbound {
+ return p.options
+}
diff --git a/proxyprovider/raw/socks.go b/proxyprovider/raw/socks.go
new file mode 100644
index 0000000000..b4f576235a
--- /dev/null
+++ b/proxyprovider/raw/socks.go
@@ -0,0 +1,72 @@
+package raw
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "strconv"
+ "strings"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type Socks struct {
+ options *option.Outbound
+}
+
+func (p *Socks) Tag() string {
+ return p.options.Tag
+}
+
+func (p *Socks) ParseLink(link string) error {
+ u, err := url.Parse(link)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: %w", link, err)
+ }
+ var port uint16
+ portStr := u.Port()
+ if portStr != "" {
+ portUint64, err := strconv.ParseUint(u.Port(), 10, 16)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`, error: %s", link, portStr, err)
+ }
+ if portUint64 == 0 || portUint64 > 0xffff {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`", link, portStr)
+ }
+ port = uint16(portUint64)
+ } else {
+ port = 1080
+ }
+ username := u.User.Username()
+ password, _ := u.User.Password()
+ options := &option.Outbound{
+ Type: C.TypeSOCKS,
+ SocksOptions: option.SocksOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: u.Hostname(),
+ ServerPort: port,
+ },
+ Username: username,
+ Password: password,
+ },
+ }
+ if strings.Contains(u.Scheme, "4") {
+ options.SocksOptions.Version = "4"
+ } else if strings.Contains(u.Scheme, "4a") {
+ options.SocksOptions.Version = "4a"
+ } else {
+ options.SocksOptions.Version = "5"
+ }
+ if u.Fragment != "" {
+ options.Tag = u.Fragment
+ } else {
+ options.Tag = net.JoinHostPort(options.SocksOptions.Server, strconv.Itoa(int(options.SocksOptions.ServerPort)))
+ }
+ p.options = options
+ return nil
+}
+
+func (p *Socks) Options() *option.Outbound {
+ return p.options
+}
diff --git a/proxyprovider/raw/trojan.go b/proxyprovider/raw/trojan.go
new file mode 100644
index 0000000000..033d6b5ae4
--- /dev/null
+++ b/proxyprovider/raw/trojan.go
@@ -0,0 +1,115 @@
+package raw
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "strconv"
+ "strings"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type Trojan struct {
+ options *option.Outbound
+}
+
+func (p *Trojan) Tag() string {
+ return p.options.Tag
+}
+
+func (p *Trojan) ParseLink(link string) error {
+ u, err := url.Parse(link)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: %w", link, err)
+ }
+ var port uint16
+ portStr := u.Port()
+ if portStr != "" {
+ portUint64, err := strconv.ParseUint(u.Port(), 10, 16)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`, error: %s", link, portStr, err)
+ }
+ if portUint64 == 0 || portUint64 > 0xffff {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`", link, portStr)
+ }
+ port = uint16(portUint64)
+ } else {
+ port = 443
+ }
+ password := u.User.Username()
+ if password == "" {
+ return fmt.Errorf("parse link `%s` failed: password is empty", link)
+ }
+ options := &option.Outbound{
+ Type: C.TypeTrojan,
+ TrojanOptions: option.TrojanOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: u.Hostname(),
+ ServerPort: port,
+ },
+ Password: password,
+ OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
+ TLS: &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: u.Hostname(),
+ },
+ },
+ },
+ }
+ args, err := url.ParseQuery(u.RawQuery)
+ if err == nil {
+ sni := args.Get("sni")
+ if sni != "" {
+ options.TrojanOptions.TLS.ServerName = sni
+ }
+ _type := args.Get("type")
+ switch _type {
+ case "tcp", "":
+ case "grpc":
+ serviceName := args.Get("serviceName")
+ options.TrojanOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeGRPC,
+ GRPCOptions: option.V2RayGRPCOptions{
+ ServiceName: serviceName,
+ },
+ }
+ case "ws":
+ path := args.Get("path")
+ var earlyData uint32
+ paths := strings.Split(path, "?ed=")
+ if len(paths) == 2 {
+ earlyDataUint64, err := strconv.ParseUint(paths[1], 10, 32)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid path `%s`", link, path)
+ }
+ earlyData = uint32(earlyDataUint64)
+ path = paths[0]
+ }
+ options.TrojanOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeWebsocket,
+ WebsocketOptions: option.V2RayWebsocketOptions{
+ Path: path,
+ MaxEarlyData: earlyData,
+ },
+ }
+ if earlyData > 0 {
+ options.TrojanOptions.Transport.WebsocketOptions.EarlyDataHeaderName = "Sec-WebSocket-Protocol"
+ }
+ default:
+ return fmt.Errorf("parse link `%s` failed: invalid type: %s", link, _type)
+ }
+ }
+ if u.Fragment != "" {
+ options.Tag = u.Fragment
+ } else {
+ options.Tag = net.JoinHostPort(options.TrojanOptions.Server, strconv.Itoa(int(options.TrojanOptions.ServerPort)))
+ }
+ p.options = options
+ return nil
+}
+
+func (p *Trojan) Options() *option.Outbound {
+ return p.options
+}
diff --git a/proxyprovider/raw/tuic.go b/proxyprovider/raw/tuic.go
new file mode 100644
index 0000000000..ec654f4b08
--- /dev/null
+++ b/proxyprovider/raw/tuic.go
@@ -0,0 +1,103 @@
+package raw
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "strconv"
+ "strings"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type Tuic struct {
+ options *option.Outbound
+}
+
+func (p *Tuic) Tag() string {
+ return p.options.Tag
+}
+
+func (p *Tuic) ParseLink(link string) error {
+ u, err := url.Parse(link)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: %w", link, err)
+ }
+ var port uint16
+ portStr := u.Port()
+ if portStr != "" {
+ portUint64, err := strconv.ParseUint(u.Port(), 10, 16)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`, error: %s", link, portStr, err)
+ }
+ if portUint64 == 0 || portUint64 > 0xffff {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`", link, portStr)
+ }
+ port = uint16(portUint64)
+ } else {
+ port = 443
+ }
+ uuid := u.User.Username()
+ if uuid == "" {
+ return fmt.Errorf("parse link `%s` failed: uuid is empty", link)
+ }
+ password, ok := u.User.Password()
+ if !ok || password == "" {
+ return fmt.Errorf("parse link `%s` failed: password is empty", link)
+ }
+ options := &option.Outbound{
+ Type: C.TypeTUIC,
+ TUICOptions: option.TUICOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: u.Hostname(),
+ ServerPort: port,
+ },
+ UUID: uuid,
+ Password: password,
+ OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
+ TLS: &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: u.Hostname(),
+ },
+ },
+ },
+ }
+ args, err := url.ParseQuery(u.RawQuery)
+ if err == nil {
+ congestionControl := args.Get("congestion_control")
+ udpRelayMode := args.Get("udp_relay_mode")
+ options.TUICOptions.CongestionControl = congestionControl
+ options.TUICOptions.UDPRelayMode = udpRelayMode
+
+ sni := args.Get("sni")
+ if sni != "" {
+ options.TUICOptions.TLS.ServerName = sni
+ }
+ alpn := args.Get("alpn")
+ if alpn != "" {
+ alpns := strings.Split(alpn, ",")
+ if len(alpns) > 1 {
+ options.TUICOptions.TLS.ALPN = make(option.Listable[string], 0)
+ for _, alpn := range alpns {
+ if alpn != "" {
+ options.TUICOptions.TLS.ALPN = append(options.TUICOptions.TLS.ALPN, alpn)
+ }
+ }
+ } else {
+ options.TUICOptions.TLS.ALPN = option.Listable[string]{alpn}
+ }
+ }
+ }
+ if u.Fragment != "" {
+ options.Tag = u.Fragment
+ } else {
+ options.Tag = net.JoinHostPort(options.TUICOptions.Server, strconv.Itoa(int(options.TUICOptions.ServerPort)))
+ }
+ p.options = options
+ return nil
+}
+
+func (p *Tuic) Options() *option.Outbound {
+ return p.options
+}
diff --git a/proxyprovider/raw/type.go b/proxyprovider/raw/type.go
new file mode 100644
index 0000000000..ce4853410b
--- /dev/null
+++ b/proxyprovider/raw/type.go
@@ -0,0 +1,123 @@
+package raw
+
+import (
+ "encoding/base64"
+ "fmt"
+ "strings"
+
+ "github.com/sagernet/sing-box/option"
+)
+
+// From Homeproxy
+
+type RawInterface interface {
+ Tag() string
+ ParseLink(link string) error
+ Options() *option.Outbound
+}
+
+func base64Decode(b64 string) ([]byte, error) {
+ b64 = strings.TrimSpace(b64)
+ stdb64 := b64
+ if pad := len(b64) % 4; pad != 0 {
+ stdb64 += strings.Repeat("=", 4-pad)
+ }
+
+ b, err := base64.StdEncoding.DecodeString(stdb64)
+ if err != nil {
+ return base64.URLEncoding.DecodeString(b64)
+ }
+ return b, nil
+}
+
+func ParseRawConfig(raw []byte) ([]option.Outbound, error) {
+ rawStr := string(raw)
+ _raw, err := base64Decode(rawStr)
+ if err != nil {
+ return nil, err
+ } else {
+ rawStr = string(_raw)
+ }
+ rawList := strings.Split(rawStr, "\n")
+ var peerList []option.Outbound
+ for i, r := range rawList {
+ rs := string(r)
+ rs = strings.TrimSpace(rs)
+ if rs == "" {
+ continue
+ }
+ ss := strings.SplitN(rs, "://", 2)
+ if len(ss) != 2 {
+ continue
+ }
+ head := ss[0]
+ var peer RawInterface
+ switch head {
+ case "http", "https":
+ peer = &HTTP{}
+ case "socks", "socks4", "socks4a", "socks5", "socks5h":
+ peer = &Socks{}
+ case "hysteria":
+ peer = &Hysteria{}
+ case "hy2", "hysteria2":
+ peer = &Hysteria2{}
+ case "ss":
+ peer = &Shadowsocks{}
+ case "trojan":
+ peer = &Trojan{}
+ case "vmess":
+ peer = &VMess{}
+ case "vless":
+ peer = &VLESS{}
+ case "tuic":
+ peer = &Tuic{}
+ default:
+ continue
+ }
+ err = peer.ParseLink(head + "://" + ss[1])
+ if err != nil {
+ return nil, fmt.Errorf("parse proxy[%d] failed: %s", i+1, err)
+ }
+ peerList = append(peerList, *peer.Options())
+ }
+ if len(peerList) == 0 {
+ return nil, fmt.Errorf("no outbounds found in raw link")
+ }
+ return peerList, nil
+}
+
+func ParseRawLink(link string) (*option.Outbound, error) {
+ ss := strings.SplitN(link, "://", 2)
+ if len(ss) != 2 {
+ return nil, fmt.Errorf("invalid link")
+ }
+ head := ss[0]
+ var peer RawInterface
+ switch head {
+ case "http", "https":
+ peer = &HTTP{}
+ case "socks", "socks4", "socks4a", "socks5", "socks5h":
+ peer = &Socks{}
+ case "hysteria":
+ peer = &Hysteria{}
+ case "hy2", "hysteria2":
+ peer = &Hysteria2{}
+ case "ss":
+ peer = &Shadowsocks{}
+ case "trojan":
+ peer = &Trojan{}
+ case "vmess":
+ peer = &VMess{}
+ case "vless":
+ peer = &VLESS{}
+ case "tuic":
+ peer = &Tuic{}
+ default:
+ return nil, fmt.Errorf("invalid link: unsupport protocol: %s", head)
+ }
+ err := peer.ParseLink(head + "://" + ss[1])
+ if err != nil {
+ return nil, fmt.Errorf("parse failed: %s", err)
+ }
+ return peer.Options(), nil
+}
diff --git a/proxyprovider/raw/vless.go b/proxyprovider/raw/vless.go
new file mode 100644
index 0000000000..a513337b26
--- /dev/null
+++ b/proxyprovider/raw/vless.go
@@ -0,0 +1,194 @@
+package raw
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "strconv"
+ "strings"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type VLESS struct {
+ options *option.Outbound
+}
+
+func (p *VLESS) Tag() string {
+ return p.options.Tag
+}
+
+func (p *VLESS) ParseLink(link string) error {
+ u, err := url.Parse(link)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: %w", link, err)
+ }
+ portStr := u.Port()
+ if portStr == "" {
+ return fmt.Errorf("parse link `%s` failed: port is empty", link)
+ }
+ portUint64, err := strconv.ParseUint(u.Port(), 10, 16)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`, error: %s", link, portStr, err)
+ }
+ port := uint16(portUint64)
+ uuid := u.User.Username()
+ if uuid == "" {
+ return fmt.Errorf("parse link `%s` failed: uuid is empty", link)
+ }
+ options := &option.Outbound{
+ Type: C.TypeVLESS,
+ VLESSOptions: option.VLESSOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: u.Hostname(),
+ ServerPort: port,
+ },
+ UUID: uuid,
+ },
+ }
+ args, err := url.ParseQuery(u.RawQuery)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: %w", link, err)
+ }
+ security := args.Get("security")
+ switch security {
+ case "tls", "xtls", "reality":
+ options.VLESSOptions.TLS = &option.OutboundTLSOptions{
+ Enabled: true,
+ ServerName: u.Hostname(),
+ }
+ sni := args.Get("sni")
+ if sni != "" {
+ options.VLESSOptions.TLS.ServerName = sni
+ }
+ alpn := args.Get("alpn")
+ if alpn != "" {
+ alpns := strings.Split(alpn, ",")
+ if len(alpns) > 1 {
+ options.VLESSOptions.TLS.ALPN = make(option.Listable[string], 0)
+ for _, alpn := range alpns {
+ if alpn != "" {
+ options.VLESSOptions.TLS.ALPN = append(options.VLESSOptions.TLS.ALPN, alpn)
+ }
+ }
+ } else {
+ options.VLESSOptions.TLS.ALPN = option.Listable[string]{alpn}
+ }
+ }
+ if security == "tls" || security == "reality" {
+ // TODO
+ flow := args.Get("flow")
+ options.VLESSOptions.Flow = flow
+ }
+ if security == "reality" {
+ publicKey := args.Get("pbk")
+ if publicKey == "" {
+ return fmt.Errorf("parse link `%s` failed: public_key is empty", link)
+ }
+ shortID := args.Get("sid")
+ if shortID == "" {
+ return fmt.Errorf("parse link `%s` failed: short_id is empty", link)
+ }
+ options.VLESSOptions.TLS.Reality = &option.OutboundRealityOptions{
+ Enabled: true,
+ PublicKey: publicKey,
+ ShortID: shortID,
+ }
+ }
+ fp := args.Get("fp")
+ if fp != "" {
+ options.VLESSOptions.TLS.UTLS = &option.OutboundUTLSOptions{
+ Enabled: true,
+ Fingerprint: fp,
+ }
+ }
+ default:
+ // TODO: security == 'none' || '' ???
+ }
+ _type := args.Get("type")
+ switch _type {
+ case "kcp":
+ return fmt.Errorf("parse link `%s` failed: kcp unsupported", link)
+ case "quic":
+ quicSecurity := args.Get("quicSecurity")
+ if quicSecurity != "" && quicSecurity != "none" {
+ return fmt.Errorf("parse link `%s` failed: quic security unsupported", link)
+ }
+ options.VLESSOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeQUIC,
+ QUICOptions: option.V2RayQUICOptions{},
+ }
+ case "grpc":
+ serviceName := args.Get("serviceName")
+ options.VLESSOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeGRPC,
+ GRPCOptions: option.V2RayGRPCOptions{
+ ServiceName: serviceName,
+ },
+ }
+ case "http":
+ fallthrough
+ case "tcp":
+ headerType := args.Get("headerType")
+ if _type == "http" || headerType == "http" {
+ host := args.Get("host")
+ var hosts []string
+ _hosts := strings.Split(host, ",")
+ for _, _host := range _hosts {
+ if _host != "" {
+ hosts = append(hosts, _host)
+ }
+ }
+ path := args.Get("path")
+ options.VLESSOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeHTTP,
+ HTTPOptions: option.V2RayHTTPOptions{
+ Host: hosts,
+ Path: path,
+ },
+ }
+ }
+ case "ws":
+ path := args.Get("path")
+ var earlyData uint32
+ paths := strings.Split(path, "?ed=")
+ if len(paths) == 2 {
+ earlyDataUint64, err := strconv.ParseUint(paths[1], 10, 32)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid path `%s`", link, path)
+ }
+ earlyData = uint32(earlyDataUint64)
+ path = paths[0]
+ }
+ options.VLESSOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeWebsocket,
+ WebsocketOptions: option.V2RayWebsocketOptions{
+ Path: path,
+ MaxEarlyData: earlyData,
+ },
+ }
+ if earlyData > 0 {
+ options.VLESSOptions.Transport.WebsocketOptions.EarlyDataHeaderName = "Sec-WebSocket-Protocol"
+ }
+ host := args.Get("host")
+ if host != "" && options.VLESSOptions.TLS == nil {
+ options.VLESSOptions.Transport.WebsocketOptions.Headers = option.HTTPHeader{
+ "Host": option.Listable[string]{host},
+ }
+ }
+ default:
+ return fmt.Errorf("parse link `%s` failed: invalid type: %s", link, _type)
+ }
+ if u.Fragment != "" {
+ options.Tag = u.Fragment
+ } else {
+ options.Tag = net.JoinHostPort(options.VLESSOptions.Server, strconv.Itoa(int(options.VLESSOptions.ServerPort)))
+ }
+ p.options = options
+ return nil
+}
+
+func (p *VLESS) Options() *option.Outbound {
+ return p.options
+}
diff --git a/proxyprovider/raw/vmess.go b/proxyprovider/raw/vmess.go
new file mode 100644
index 0000000000..7b0117b348
--- /dev/null
+++ b/proxyprovider/raw/vmess.go
@@ -0,0 +1,187 @@
+package raw
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net"
+ "strconv"
+ "strings"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+)
+
+type VMess struct {
+ options *option.Outbound
+}
+
+func (p *VMess) Tag() string {
+ return p.options.Tag
+}
+
+func (p *VMess) ParseLink(link string) error {
+ sLink := strings.TrimPrefix(link, "vmess://")
+ raw, err := base64.URLEncoding.DecodeString(sLink)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: %w", link, err)
+ }
+ var _vmessInfo vmessInfo
+ err = json.Unmarshal(raw, &_vmessInfo)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: %w", link, err)
+ }
+ //
+ if _vmessInfo.Version != "2" {
+ return fmt.Errorf("parse link `%s` failed: invalid version: `%s`", link, _vmessInfo.Version)
+ }
+ portUint64, err := strconv.ParseUint(_vmessInfo.Port, 10, 16)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid port: `%s`, error: %s", link, _vmessInfo.Port, err)
+ }
+ port := uint16(portUint64)
+ aidInt64, err := strconv.ParseInt(_vmessInfo.AID, 10, 64)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid alterId: `%s`, error: %s", link, _vmessInfo.AID, err)
+ }
+ options := &option.Outbound{
+ Type: C.TypeVMess,
+ VMessOptions: option.VMessOutboundOptions{
+ ServerOptions: option.ServerOptions{
+ Server: _vmessInfo.Address,
+ ServerPort: port,
+ },
+ UUID: _vmessInfo.ID,
+ AlterId: int(aidInt64),
+ },
+ }
+ if _vmessInfo.Security != "" {
+ options.VMessOptions.Security = _vmessInfo.Security
+ } else {
+ options.VMessOptions.Security = "auto"
+ }
+ if _vmessInfo.TLS == "tls" {
+ options.VMessOptions.TLS = &option.OutboundTLSOptions{
+ Enabled: true,
+ }
+ if _vmessInfo.SNI != "" {
+ options.VMessOptions.TLS.ServerName = _vmessInfo.SNI
+ } else if _vmessInfo.Host != "" {
+ options.VMessOptions.TLS.ServerName = _vmessInfo.Host
+ } else {
+ options.VMessOptions.TLS.ServerName = _vmessInfo.Address
+ }
+ if _vmessInfo.ALPN != "" {
+ alpns := strings.Split(_vmessInfo.ALPN, ",")
+ if len(alpns) > 1 {
+ options.VLESSOptions.TLS.ALPN = make(option.Listable[string], 0)
+ for _, alpn := range alpns {
+ if alpn != "" {
+ options.VLESSOptions.TLS.ALPN = append(options.VLESSOptions.TLS.ALPN, alpn)
+ }
+ }
+ } else {
+ options.VLESSOptions.TLS.ALPN = option.Listable[string]{_vmessInfo.ALPN}
+ }
+ }
+ }
+ switch _vmessInfo.Network {
+ case "kcp":
+ return fmt.Errorf("parse link `%s` failed: kcp unsupported", link)
+ case "quic":
+ quicSecurity := _vmessInfo.Type
+ if quicSecurity != "" && quicSecurity != "none" {
+ return fmt.Errorf("parse link `%s` failed: quic security unsupported", link)
+ }
+ options.VLESSOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeQUIC,
+ QUICOptions: option.V2RayQUICOptions{},
+ }
+ case "grpc":
+ options.VMessOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeGRPC,
+ GRPCOptions: option.V2RayGRPCOptions{
+ ServiceName: _vmessInfo.Path,
+ },
+ }
+ case "h2":
+ fallthrough
+ case "tcp":
+ if _vmessInfo.Network == "h2" || _vmessInfo.Type == "http" {
+ host := _vmessInfo.Host
+ var hosts []string
+ _hosts := strings.Split(host, ",")
+ for _, _host := range _hosts {
+ if _host != "" {
+ hosts = append(hosts, _host)
+ }
+ }
+ options.VMessOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeHTTP,
+ HTTPOptions: option.V2RayHTTPOptions{
+ Host: hosts,
+ Path: _vmessInfo.Path,
+ },
+ }
+ }
+ case "ws":
+ path := _vmessInfo.Path
+ var earlyData uint32
+ paths := strings.Split(path, "?ed=")
+ if len(paths) == 2 {
+ earlyDataUint64, err := strconv.ParseUint(paths[1], 10, 32)
+ if err != nil {
+ return fmt.Errorf("parse link `%s` failed: invalid path `%s`", link, path)
+ }
+ earlyData = uint32(earlyDataUint64)
+ path = paths[0]
+ }
+ options.VMessOptions.Transport = &option.V2RayTransportOptions{
+ Type: C.V2RayTransportTypeWebsocket,
+ WebsocketOptions: option.V2RayWebsocketOptions{
+ Path: path,
+ MaxEarlyData: earlyData,
+ },
+ }
+ if earlyData > 0 {
+ options.VMessOptions.Transport.WebsocketOptions.EarlyDataHeaderName = "Sec-WebSocket-Protocol"
+ }
+ host := _vmessInfo.Host
+ if host != "" && options.VMessOptions.TLS == nil {
+ options.VMessOptions.Transport.WebsocketOptions.Headers = option.HTTPHeader{
+ "Host": option.Listable[string]{host},
+ }
+ }
+ default:
+ return fmt.Errorf("parse link `%s` failed: invalid type: %s", link, _vmessInfo.Network)
+ }
+ if _vmessInfo.Tag != "" {
+ options.Tag = _vmessInfo.Tag
+ } else {
+ options.Tag = net.JoinHostPort(options.VMessOptions.Server, strconv.Itoa(int(options.VMessOptions.ServerPort)))
+ }
+ p.options = options
+ return nil
+}
+
+func (p *VMess) Options() *option.Outbound {
+ return p.options
+}
+
+type vmessInfo struct {
+ Version string `json:"v"`
+ Tag string `json:"ps"`
+ Address string `json:"add"`
+ Port string `json:"port"`
+ ID string `json:"id"`
+ AID string `json:"aid"`
+ Security string `json:"scy"`
+ Network string `json:"net"`
+ Type string `json:"type"`
+ Host string `json:"host"`
+ Path string `json:"path"`
+ TLS string `json:"tls"`
+ SNI string `json:"sni"`
+ ALPN string `json:"alpn"`
+ Fingerprint string `json:"fingerprint"`
+}
diff --git a/proxyprovider/request.go b/proxyprovider/request.go
new file mode 100644
index 0000000000..58e0b9b212
--- /dev/null
+++ b/proxyprovider/request.go
@@ -0,0 +1,98 @@
+package proxyprovider
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/sagernet/sing-box/proxyprovider/clash"
+ "github.com/sagernet/sing-box/proxyprovider/raw"
+ "github.com/sagernet/sing-box/proxyprovider/singbox"
+)
+
+func request(ctx context.Context, httpClient *http.Client, url string, ua string) (*Cache, error) {
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("User-Agent", ua)
+
+ req = req.WithContext(ctx)
+ resp, err := httpClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ buffer := bytes.NewBuffer(nil)
+ _, err = io.Copy(buffer, resp.Body)
+ if err != nil {
+ resp.Body.Close()
+ return nil, err
+ }
+ resp.Body.Close()
+
+ // Try Clash Config
+ outbounds, err := clash.ParseClashConfig(buffer.Bytes())
+ if err != nil {
+ // Try Raw Config
+ outbounds, err = raw.ParseRawConfig(buffer.Bytes())
+ if err != nil {
+ // Try Singbox Config
+ outbounds, err = singbox.ParseSingboxConfig(buffer.Bytes())
+ if err != nil {
+ return nil, fmt.Errorf("parse config failed, config is not clash config or raw links or sing-box config")
+ }
+ }
+ }
+
+ var clashInfo ClashInfo
+ var ok bool
+ subscriptionUserInfo := resp.Header.Get("subscription-userinfo")
+ if subscriptionUserInfo != "" {
+ subscriptionUserInfo = strings.ToLower(subscriptionUserInfo)
+ regTraffic := regexp.MustCompile(`upload=(\d+); download=(\d+); total=(\d+)`)
+ matchTraffic := regTraffic.FindStringSubmatch(subscriptionUserInfo)
+ if len(matchTraffic) == 4 {
+ uploadUint64, err := strconv.ParseUint(matchTraffic[1], 10, 64)
+ if err == nil {
+ clashInfo.Upload = uploadUint64
+ ok = true
+ }
+ downloadUint64, err := strconv.ParseUint(matchTraffic[2], 10, 64)
+ if err == nil {
+ clashInfo.Download = downloadUint64
+ ok = true
+ }
+ totalUint64, err := strconv.ParseUint(matchTraffic[3], 10, 64)
+ if err == nil {
+ clashInfo.Total = totalUint64
+ ok = true
+ }
+ }
+ regExpire := regexp.MustCompile(`expire=(\d+)`)
+ matchExpire := regExpire.FindStringSubmatch(subscriptionUserInfo)
+ if len(matchExpire) == 2 {
+ expireUint64, err := strconv.ParseUint(matchExpire[1], 10, 64)
+ if err == nil {
+ clashInfo.Expire = time.Unix(int64(expireUint64), 0)
+ ok = true
+ }
+ }
+ }
+
+ cache := &Cache{
+ Outbounds: outbounds,
+ LastUpdate: time.Now(),
+ }
+ if ok {
+ cache.ClashInfo = &clashInfo
+ }
+
+ return cache, nil
+}
diff --git a/proxyprovider/singbox/singbox.go b/proxyprovider/singbox/singbox.go
new file mode 100644
index 0000000000..fa97a40a91
--- /dev/null
+++ b/proxyprovider/singbox/singbox.go
@@ -0,0 +1,73 @@
+package singbox
+
+import (
+ "fmt"
+
+ C "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/option"
+ "github.com/sagernet/sing/common/json"
+)
+
+type OutboundConfig struct {
+ Outbounds []option.Outbound `yaml:"outbounds"`
+}
+
+func ParseSingboxConfig(raw []byte) ([]option.Outbound, error) {
+ var outboundConfig OutboundConfig
+ err := json.Unmarshal(raw, &outboundConfig)
+ if err != nil {
+ return nil, err
+ }
+ if len(outboundConfig.Outbounds) == 0 {
+ return nil, fmt.Errorf("no outbounds found in sing-box config")
+ }
+ var options []option.Outbound
+ for _, outboundOptions := range outboundConfig.Outbounds {
+ switch outboundOptions.Type {
+ // TODO: Remove Direct ???
+ case C.TypeBlock, C.TypeDNS, C.TypeURLTest, C.TypeSelector:
+ continue
+ default:
+ // TODO: Remove Detour ???
+ // removeDetour(&outboundOptions)
+ options = append(options, outboundOptions)
+ }
+ }
+ return options, nil
+}
+
+func removeDetour(outbound *option.Outbound) {
+ switch outbound.Type {
+ case C.TypeDirect:
+ outbound.DirectOptions.DialerOptions.Detour = ""
+ case C.TypeHTTP:
+ outbound.HTTPOptions.DialerOptions.Detour = ""
+ case C.TypeShadowsocks:
+ outbound.ShadowsocksOptions.DialerOptions.Detour = ""
+ case C.TypeVMess:
+ outbound.VMessOptions.DialerOptions.Detour = ""
+ case C.TypeTrojan:
+ outbound.TrojanOptions.DialerOptions.Detour = ""
+ case C.TypeWireGuard:
+ outbound.WireGuardOptions.DialerOptions.Detour = ""
+ case C.TypeHysteria:
+ outbound.HysteriaOptions.DialerOptions.Detour = ""
+ case C.TypeTor:
+ outbound.TorOptions.DialerOptions.Detour = ""
+ case C.TypeSSH:
+ outbound.SSHOptions.DialerOptions.Detour = ""
+ case C.TypeShadowTLS:
+ outbound.ShadowTLSOptions.DialerOptions.Detour = ""
+ case C.TypeShadowsocksR:
+ outbound.ShadowsocksROptions.DialerOptions.Detour = ""
+ case C.TypeVLESS:
+ outbound.VLESSOptions.DialerOptions.Detour = ""
+ case C.TypeTUIC:
+ outbound.TUICOptions.DialerOptions.Detour = ""
+ case C.TypeHysteria2:
+ outbound.Hysteria2Options.DialerOptions.Detour = ""
+ case C.TypeRandomAddr:
+ outbound.RandomAddrOptions.DialerOptions.Detour = ""
+ default:
+ }
+}
diff --git a/proxyprovider/type.go b/proxyprovider/type.go
new file mode 100644
index 0000000000..fa1d14447d
--- /dev/null
+++ b/proxyprovider/type.go
@@ -0,0 +1,56 @@
+package proxyprovider
+
+import (
+ "encoding/json"
+ "os"
+ "time"
+
+ "github.com/sagernet/sing-box/option"
+)
+
+type Cache struct {
+ LastUpdate time.Time `json:"last_update,omitempty"`
+ Outbounds []option.Outbound `json:"outbounds,omitempty"`
+ ClashInfo *ClashInfo `json:"clash_info,omitempty"`
+}
+
+type _Cache Cache
+
+func (c *Cache) IsNil() bool {
+ if c.Outbounds == nil || len(c.Outbounds) == 0 {
+ return true
+ }
+ return false
+}
+
+func (c *Cache) WriteToFile(path string) error {
+ raw, err := json.Marshal((*_Cache)(c))
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(path, raw, 0o644)
+}
+
+func (c *Cache) ReadFromFile(path string) error {
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(raw, (*_Cache)(c))
+}
+
+type ClashInfo struct {
+ Download uint64 `json:"download,omitempty"`
+ Upload uint64 `json:"upload,omitempty"`
+ Total uint64 `json:"total,omitempty"`
+ Expire time.Time `json:"expire,omitempty"`
+}
+
+type Group struct {
+ Tag string
+ Type string
+ SelectorOptions option.SelectorOutboundOptions
+ URLTestOptions option.URLTestOutboundOptions
+ JSTestOptions option.JSTestOutboundOptions
+ Filter *Filter
+}
diff --git a/proxyprovider/utils/hysteria2.go b/proxyprovider/utils/hysteria2.go
new file mode 100644
index 0000000000..6b6f0faec9
--- /dev/null
+++ b/proxyprovider/utils/hysteria2.go
@@ -0,0 +1,42 @@
+package utils
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+)
+
+func StringToMbps(s string) uint64 {
+ if s == "" {
+ return 0
+ }
+
+ // when have not unit, use Mbps
+ if v, err := strconv.Atoi(s); err == nil {
+ return StringToMbps(fmt.Sprintf("%d Mbps", v))
+ }
+
+ m := regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`).FindStringSubmatch(s)
+ if m == nil {
+ return 0
+ }
+ var n uint64
+ switch m[2] {
+ case "K":
+ n = 1 >> 10
+ case "M":
+ n = 1
+ case "G":
+ n = 1 << 10
+ case "T":
+ n = 1 << 20
+ default:
+ n = 1
+ }
+ v, _ := strconv.ParseUint(m[1], 10, 64)
+ n = v * n
+ if m[3] == "B" {
+ n = n << 3
+ }
+ return n
+}
diff --git a/proxyprovider/utils/shadowsocks.go b/proxyprovider/utils/shadowsocks.go
new file mode 100644
index 0000000000..fb4bb79fea
--- /dev/null
+++ b/proxyprovider/utils/shadowsocks.go
@@ -0,0 +1,26 @@
+package utils
+
+func CheckShadowsocksMethod(cipher string) bool {
+ switch cipher {
+ case "aes-128-gcm":
+ case "aes-192-gcm":
+ case "aes-256-gcm":
+ case "aes-128-cfb":
+ case "aes-192-cfb":
+ case "aes-256-cfb":
+ case "aes-128-ctr":
+ case "aes-192-ctr":
+ case "aes-256-ctr":
+ case "rc4-md5":
+ case "chacha20-ietf":
+ case "xchacha20":
+ case "chacha20-ietf-poly1305":
+ case "xchacha20-ietf-poly1305":
+ case "2022-blake3-aes-128-gcm":
+ case "2022-blake3-aes-256-gcm":
+ case "2022-blake3-chacha20-poly1305":
+ default:
+ return false
+ }
+ return true
+}
diff --git a/route/router.go b/route/router.go
index adbdbd2082..df9006f132 100644
--- a/route/router.go
+++ b/route/router.go
@@ -9,6 +9,7 @@ import (
"os"
"os/user"
"strings"
+ "sync"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -26,10 +27,10 @@ import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/outbound"
"github.com/sagernet/sing-box/transport/fakeip"
- "github.com/sagernet/sing-dns"
+ dns "github.com/sagernet/sing-dns"
mux "github.com/sagernet/sing-mux"
- "github.com/sagernet/sing-tun"
- "github.com/sagernet/sing-vmess"
+ tun "github.com/sagernet/sing-tun"
+ vmess "github.com/sagernet/sing-vmess"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
@@ -55,6 +56,8 @@ type Router struct {
inboundByTag map[string]adapter.Inbound
outbounds []adapter.Outbound
outboundByTag map[string]adapter.Outbound
+ proxyProviders []adapter.ProxyProvider
+ proxyProviderByTag map[string]adapter.ProxyProvider
rules []adapter.Rule
defaultDetour string
defaultOutboundForConnection adapter.Outbound
@@ -66,6 +69,11 @@ type Router struct {
geoIPReader *geoip.Reader
geositeReader *geosite.Reader
geositeCache map[string]adapter.Rule
+ geositeUpdateLock sync.Mutex
+ geoIPUpdateLock sync.Mutex
+ geositePath string
+ geoIPPath string
+ geoUpdateLock sync.Mutex
needFindProcess bool
dnsClient *dns.Client
defaultDomainStrategy dns.DomainStrategy
@@ -94,6 +102,7 @@ type Router struct {
needWIFIState bool
needPackageManager bool
wifiState adapter.WIFIState
+ reloadChan chan<- struct{}
started bool
}
@@ -105,6 +114,7 @@ func NewRouter(
ntpOptions option.NTPOptions,
inbounds []option.Inbound,
platformInterface platform.Interface,
+ reloadChan chan<- struct{},
) (*Router, error) {
router := &Router{
ctx: ctx,
@@ -125,9 +135,10 @@ func NewRouter(
autoDetectInterface: options.AutoDetectInterface,
defaultInterface: options.DefaultInterface,
defaultMark: options.DefaultMark,
- pauseManager: service.FromContext[pause.Manager](ctx),
+ pauseManager: pause.ManagerFromContext(ctx),
platformInterface: platformInterface,
needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule),
+ reloadChan: reloadChan,
needPackageManager: C.IsAndroid && platformInterface == nil && common.Any(inbounds, func(inbound option.Inbound) bool {
return len(inbound.TunOptions.IncludePackage) > 0 || len(inbound.TunOptions.ExcludePackage) > 0
}),
@@ -332,7 +343,7 @@ func NewRouter(
return router, nil
}
-func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outbound, defaultOutbound func() adapter.Outbound) error {
+func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outbound, defaultOutbound func() adapter.Outbound, proxyProviders []adapter.ProxyProvider) error {
inboundByTag := make(map[string]adapter.Inbound)
for _, inbound := range inbounds {
inboundByTag[inbound.Tag()] = inbound
@@ -341,6 +352,13 @@ func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outb
for _, detour := range outbounds {
outboundByTag[detour.Tag()] = detour
}
+ var proxyProviderByTag map[string]adapter.ProxyProvider
+ if len(proxyProviders) > 0 {
+ proxyProviderByTag = make(map[string]adapter.ProxyProvider)
+ for _, proxyProvider := range proxyProviders {
+ proxyProviderByTag[proxyProvider.Tag()] = proxyProvider
+ }
+ }
var defaultOutboundForConnection adapter.Outbound
var defaultOutboundForPacketConnection adapter.Outbound
if r.defaultDetour != "" {
@@ -411,6 +429,8 @@ func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outb
return E.New("outbound not found for rule[", i, "]: ", rule.Outbound())
}
}
+ r.proxyProviders = proxyProviders
+ r.proxyProviderByTag = proxyProviderByTag
return nil
}
@@ -418,48 +438,43 @@ func (r *Router) Outbounds() []adapter.Outbound {
return r.outbounds
}
-func (r *Router) PreStart() error {
+func (r *Router) Start() error {
monitor := taskmonitor.New(r.logger, C.DefaultStartTimeout)
- if r.interfaceMonitor != nil {
- monitor.Start("initialize interface monitor")
- err := r.interfaceMonitor.Start()
+ if r.needGeoIPDatabase {
+ monitor.Start("initialize geoip database")
+ err := r.prepareGeoIPDatabase()
monitor.Finish()
if err != nil {
return err
}
- }
- if r.networkMonitor != nil {
- monitor.Start("initialize network monitor")
- err := r.networkMonitor.Start()
- monitor.Finish()
- if err != nil {
- return err
+ if r.geoIPOptions.AutoUpdateInterval > 0 {
+ go r.loopUpdateGeoIPDatabase()
+ r.logger.Info("geoip database auto update enabled")
}
}
- if r.fakeIPStore != nil {
- monitor.Start("initialize fakeip store")
- err := r.fakeIPStore.Start()
+ if r.needGeositeDatabase {
+ monitor.Start("initialize geosite database")
+ err := r.prepareGeositeDatabase()
monitor.Finish()
if err != nil {
return err
}
+ if r.geositeOptions.AutoUpdateInterval > 0 {
+ go r.loopUpdateGeositeDatabase()
+ r.logger.Info("geosite database auto update enabled")
+ }
}
- return nil
-}
-
-func (r *Router) Start() error {
- monitor := taskmonitor.New(r.logger, C.DefaultStartTimeout)
- if r.needGeoIPDatabase {
- monitor.Start("initialize geoip database")
- err := r.prepareGeoIPDatabase()
+ if r.interfaceMonitor != nil {
+ monitor.Start("initialize interface monitor")
+ err := r.interfaceMonitor.Start()
monitor.Finish()
if err != nil {
return err
}
}
- if r.needGeositeDatabase {
- monitor.Start("initialize geosite database")
- err := r.prepareGeositeDatabase()
+ if r.networkMonitor != nil {
+ monitor.Start("initialize network monitor")
+ err := r.networkMonitor.Start()
monitor.Finish()
if err != nil {
return err
@@ -485,7 +500,14 @@ func (r *Router) Start() error {
r.geositeCache = nil
r.geositeReader = nil
}
-
+ if r.fakeIPStore != nil {
+ monitor.Start("initialize fakeip store")
+ err := r.fakeIPStore.Start()
+ monitor.Finish()
+ if err != nil {
+ return err
+ }
+ }
if len(r.ruleSets) > 0 {
monitor.Start("initialize rule-set")
ruleSetStartContext := NewRuleSetStartContext()
@@ -567,7 +589,6 @@ func (r *Router) Start() error {
r.updateWIFIState()
monitor.Finish()
}
-
for i, rule := range r.rules {
monitor.Start("initialize rule[", i, "]")
err := rule.Start()
@@ -629,7 +650,7 @@ func (r *Router) Close() error {
}
if r.geoIPReader != nil {
monitor.Start("close geoip reader")
- err = E.Append(err, r.geoIPReader.Close(), func(err error) error {
+ err = E.Append(err, common.Close(r.geoIPReader), func(err error) error {
return E.Cause(err, "close geoip reader")
})
monitor.Finish()
@@ -714,10 +735,6 @@ func (r *Router) RuleSet(tag string) (adapter.RuleSet, bool) {
}
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
- if r.pauseManager.IsDevicePaused() {
- return E.New("reject connection to ", metadata.Destination, " while device paused")
- }
-
if metadata.InboundDetour != "" {
if metadata.LastInbound == metadata.InboundDetour {
return E.New("routing loop on detour: ", metadata.InboundDetour)
@@ -842,9 +859,6 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
}
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
- if r.pauseManager.IsDevicePaused() {
- return E.New("reject packet connection to ", metadata.Destination, " while device paused")
- }
if metadata.InboundDetour != "" {
if metadata.LastInbound == metadata.InboundDetour {
return E.New("routing loop on detour: ", metadata.InboundDetour)
@@ -1169,6 +1183,26 @@ func (r *Router) ResetNetwork() error {
return nil
}
+func (r *Router) ProxyProviders() []adapter.ProxyProvider {
+ return r.proxyProviders
+}
+
+func (r *Router) ProxyProvider(tag string) (proxyProvider adapter.ProxyProvider, loaded bool) {
+ if r.proxyProviderByTag != nil {
+ proxyProvider, loaded = r.proxyProviderByTag[tag]
+ }
+ return
+}
+
+func (r *Router) Reload() {
+ if r.platformInterface == nil {
+ select {
+ case r.reloadChan <- struct{}{}:
+ default:
+ }
+ }
+}
+
func (r *Router) updateWIFIState() {
if r.platformInterface == nil {
return
diff --git a/route/router_geo_resources.go b/route/router_geo_resources.go
index e0a572c92f..4a3ed9afa2 100644
--- a/route/router_geo_resources.go
+++ b/route/router_geo_resources.go
@@ -61,6 +61,7 @@ func (r *Router) prepareGeoIPDatabase() error {
os.Remove(geoPath)
}
}
+ r.geoIPPath = geoPath
if !rw.FileExists(geoPath) {
r.logger.Warn("geoip database not exists: ", geoPath)
var err error
@@ -107,6 +108,7 @@ func (r *Router) prepareGeositeDatabase() error {
os.Remove(geoPath)
}
}
+ r.geositePath = geoPath
if !rw.FileExists(geoPath) {
r.logger.Warn("geosite database not exists: ", geoPath)
var err error
@@ -132,6 +134,117 @@ func (r *Router) prepareGeositeDatabase() error {
return nil
}
+func (r *Router) loopUpdateGeoIPDatabase() {
+ if stat, err := os.Stat(r.geoIPPath); err == nil {
+ if time.Since(stat.ModTime()) > time.Duration(r.geoIPOptions.AutoUpdateInterval) {
+ r.updateGeoIPDatabase()
+ }
+ }
+ ticker := time.NewTicker(time.Duration(r.geoIPOptions.AutoUpdateInterval))
+ defer ticker.Stop()
+ for {
+ select {
+ case <-r.ctx.Done():
+ return
+ case <-ticker.C:
+ r.updateGeoIPDatabase()
+ }
+ }
+}
+
+func (r *Router) updateGeoIPDatabase() {
+ if !r.geoIPUpdateLock.TryLock() {
+ return
+ }
+ defer r.geoIPUpdateLock.Unlock()
+ r.logger.Info("try to update geoip database...")
+ tempGeoPath := r.geoIPPath + ".tmp"
+ os.Remove(tempGeoPath)
+ err := r.downloadGeoIPDatabase(tempGeoPath)
+ if err != nil {
+ r.logger.Error("download geoip database failed: ", err)
+ return
+ }
+ r.logger.Info("download geoip database success")
+ geoReader, codes, err := geoip.Open(tempGeoPath)
+ if err != nil {
+ r.logger.Error(E.Cause(err, "open geoip database"))
+ os.Remove(tempGeoPath)
+ return
+ }
+ err = os.Rename(tempGeoPath, r.geoIPPath)
+ if err != nil {
+ r.logger.Error("save geoip database failed: ", err)
+ os.Remove(tempGeoPath)
+ return
+ }
+ r.logger.Info("loaded geoip database: ", len(codes), " codes")
+ r.geoIPReader = geoReader
+ r.logger.Info("reload geoip database success")
+}
+
+func (r *Router) loopUpdateGeositeDatabase() {
+ if stat, err := os.Stat(r.geositePath); err == nil {
+ if time.Since(stat.ModTime()) > time.Duration(r.geositeOptions.AutoUpdateInterval) {
+ r.updateGeositeDatabase()
+ }
+ }
+ ticker := time.NewTicker(time.Duration(r.geositeOptions.AutoUpdateInterval))
+ defer ticker.Stop()
+ for {
+ select {
+ case <-r.ctx.Done():
+ return
+ case <-ticker.C:
+ r.updateGeositeDatabase()
+ }
+ }
+}
+
+func (r *Router) updateGeositeDatabase() {
+ if !r.geositeUpdateLock.TryLock() {
+ return
+ }
+ defer r.geositeUpdateLock.Unlock()
+ r.logger.Info("try to update geosite database...")
+ tempGeoPath := r.geositePath + ".tmp"
+ os.Remove(tempGeoPath)
+ err := r.downloadGeositeDatabase(tempGeoPath)
+ if err != nil {
+ r.logger.Error("download geosite database failed: ", err)
+ return
+ }
+ r.logger.Info("download geosite database success")
+ geoReader, codes, err := geosite.Open(tempGeoPath)
+ if err == nil {
+ r.logger.Info("loaded geosite database: ", len(codes), " codes")
+ r.geositeReader = geoReader
+ r.geositeCache = make(map[string]adapter.Rule)
+ err = os.Rename(tempGeoPath, r.geositePath)
+ if err != nil {
+ r.logger.Error("save geosite database failed: ", err)
+ return
+ }
+ } else {
+ r.logger.Error("open geosite database failed: ", err)
+ return
+ }
+ r.logger.Info("reload geosite rules success")
+}
+
+func (r *Router) UpdateGeoDatabase() {
+ if !r.geoUpdateLock.TryLock() {
+ return
+ }
+ defer r.geoUpdateLock.Unlock()
+ if r.needGeositeDatabase {
+ r.updateGeositeDatabase()
+ }
+ if r.needGeoIPDatabase {
+ r.updateGeoIPDatabase()
+ }
+}
+
func (r *Router) downloadGeoIPDatabase(savePath string) error {
var downloadURL string
if r.geoIPOptions.DownloadURL != "" {
diff --git a/route/rule_set_local.go b/route/rule_set_local.go
index 635f22ed01..b6424ab3f4 100644
--- a/route/rule_set_local.go
+++ b/route/rule_set_local.go
@@ -20,23 +20,22 @@ type LocalRuleSet struct {
}
func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleSet, error) {
+ setFile, err := os.Open(options.LocalOptions.Path)
+ if err != nil {
+ return nil, err
+ }
var plainRuleSet option.PlainRuleSet
switch options.Format {
case C.RuleSetFormatSource, "":
- content, err := os.ReadFile(options.LocalOptions.Path)
- if err != nil {
- return nil, err
- }
- compat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
+ var compat option.PlainRuleSetCompat
+ decoder := json.NewDecoder(json.NewCommentFilter(setFile))
+ decoder.DisallowUnknownFields()
+ err = decoder.Decode(&compat)
if err != nil {
return nil, err
}
plainRuleSet = compat.Upgrade()
case C.RuleSetFormatBinary:
- setFile, err := os.Open(options.LocalOptions.Path)
- if err != nil {
- return nil, err
- }
plainRuleSet, err = srs.Read(setFile, false)
if err != nil {
return nil, err
@@ -45,7 +44,6 @@ func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleS
return nil, E.New("unknown rule set format: ", options.Format)
}
rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules))
- var err error
for i, ruleOptions := range plainRuleSet.Rules {
rules[i], err = NewHeadlessRule(router, ruleOptions)
if err != nil {
diff --git a/route/rule_set_remote.go b/route/rule_set_remote.go
index 595e328c5c..4e687e215f 100644
--- a/route/rule_set_remote.go
+++ b/route/rule_set_remote.go
@@ -55,7 +55,7 @@ func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.
logger: logger,
options: options,
updateInterval: updateInterval,
- pauseManager: service.FromContext[pause.Manager](ctx),
+ pauseManager: pause.ManagerFromContext(ctx),
}
}
@@ -128,7 +128,9 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
switch s.options.Format {
case C.RuleSetFormatSource:
var compat option.PlainRuleSetCompat
- compat, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
+ decoder := json.NewDecoder(json.NewCommentFilter(bytes.NewReader(content)))
+ decoder.DisallowUnknownFields()
+ err = decoder.Decode(&compat)
if err != nil {
return err
}
diff --git a/script/script.go b/script/script.go
new file mode 100644
index 0000000000..1498be1caf
--- /dev/null
+++ b/script/script.go
@@ -0,0 +1,296 @@
+//go:build with_script
+
+package script
+
+import (
+ "context"
+ "errors"
+ "os"
+ "os/exec"
+
+ "github.com/sagernet/sing-box/log"
+ "github.com/sagernet/sing-box/option"
+ E "github.com/sagernet/sing/common/exceptions"
+)
+
+const (
+ preStart string = "pre-start"
+ preStartServicePreClose string = "pre-start-service-pre-close"
+ preStartServicePostClose string = "pre-start-service-post-close"
+ postStart string = "post-start"
+ postStartServicePreClose string = "post-start-service-pre-close"
+ postStartServicePostClose string = "post-start-service-post-close"
+ preClose string = "pre-close"
+ postClose string = "post-close"
+)
+
+type Script struct {
+ tag string
+ ctx context.Context
+ logger log.ContextLogger
+
+ command string
+ args []string
+ directory string
+ env []string
+ stdoutLogLevel log.Level
+ stderrLogLevel log.Level
+ noFatal bool
+ mode string
+
+ cmd *exec.Cmd
+}
+
+func NewScript(ctx context.Context, logger log.ContextLogger, tag string, options option.ScriptOptions) (*Script, error) {
+ s := &Script{
+ tag: tag,
+ ctx: ctx,
+ logger: logger,
+ stdoutLogLevel: 0xff,
+ stderrLogLevel: 0xff,
+ }
+ if options.Command == "" {
+ return nil, E.New("missing command")
+ }
+ s.command = options.Command
+ s.args = options.Args
+ s.directory = options.Directory
+ s.noFatal = options.NoFatal
+ if options.Env != nil && len(options.Env) > 0 {
+ s.env = make([]string, 0, len(options.Env))
+ for k, v := range options.Env {
+ s.env = append(s.env, k+"="+v)
+ }
+ }
+ switch options.Mode {
+ case preStart, preStartServicePreClose, preStartServicePostClose, postStart, postStartServicePreClose, postStartServicePostClose, preClose, postClose:
+ s.mode = options.Mode
+ case "":
+ return nil, E.New("missing mode")
+ default:
+ return nil, E.New("invalid mode: ", options.Mode)
+ }
+ if options.LogOptions.Enabled {
+ stdoutLogLevelStr := options.LogOptions.StdoutLogLevel
+ if stdoutLogLevelStr == "" {
+ stdoutLogLevelStr = "info"
+ }
+ stdoutLogLevel, err := log.ParseLevel(stdoutLogLevelStr)
+ if err != nil {
+ return nil, E.New("invalid stdout log level: ", stdoutLogLevelStr)
+ }
+ s.stdoutLogLevel = stdoutLogLevel
+ stderrLogLevelStr := options.LogOptions.StderrLogLevel
+ if stderrLogLevelStr == "" {
+ stderrLogLevelStr = "error"
+ }
+ stderrLogLevel, err := log.ParseLevel(stderrLogLevelStr)
+ if err != nil {
+ return nil, E.New("invalid stderr log level: ", stderrLogLevelStr)
+ }
+ s.stderrLogLevel = stderrLogLevel
+ }
+ return s, nil
+}
+
+func (s *Script) Tag() string {
+ return s.tag
+}
+
+func (s *Script) newCommand(ctx context.Context) *exec.Cmd {
+ cmd := exec.CommandContext(ctx, s.command, s.args...)
+ cmd.Env = os.Environ()
+ if s.env != nil && len(s.env) > 0 {
+ cmd.Env = append(cmd.Env, s.env...)
+ }
+ cmd.Dir = s.directory
+ if s.stdoutLogLevel != 0xff {
+ var f func(...any)
+ switch s.stdoutLogLevel {
+ case log.LevelPanic:
+ f = s.logger.Panic
+ case log.LevelFatal:
+ f = s.logger.Fatal
+ case log.LevelError:
+ f = s.logger.Error
+ case log.LevelWarn:
+ f = s.logger.Warn
+ case log.LevelInfo:
+ f = s.logger.Info
+ case log.LevelDebug:
+ f = s.logger.Debug
+ case log.LevelTrace:
+ f = s.logger.Trace
+ }
+ if f != nil {
+ cmd.Stdout = &logWriter{f: f}
+ }
+ }
+ if s.stderrLogLevel != 0xff {
+ var f func(...any)
+ switch s.stdoutLogLevel {
+ case log.LevelPanic:
+ f = s.logger.Panic
+ case log.LevelFatal:
+ f = s.logger.Fatal
+ case log.LevelError:
+ f = s.logger.Error
+ case log.LevelWarn:
+ f = s.logger.Warn
+ case log.LevelInfo:
+ f = s.logger.Info
+ case log.LevelDebug:
+ f = s.logger.Debug
+ case log.LevelTrace:
+ f = s.logger.Trace
+ }
+ if f != nil {
+ cmd.Stderr = &logWriter{f: f}
+ }
+ }
+ return cmd
+}
+
+func (s *Script) PreStart() error {
+ switch s.mode {
+ case preStart:
+ cmd := s.newCommand(s.ctx)
+ s.logger.Info("executing pre-start script: ", cmd.String())
+ err := cmd.Run()
+ if err != nil {
+ s.logger.Error("failed to execute pre-start script: ", cmd.String(), ", error: ", err)
+ if !s.noFatal {
+ return err
+ }
+ } else {
+ s.logger.Info("pre-start script executed: ", cmd.String())
+ }
+ case preStartServicePreClose, preStartServicePostClose:
+ cmd := s.newCommand(s.ctx)
+ s.logger.Info("starting pre-start service script: ", cmd.String())
+ err := cmd.Start()
+ if err != nil {
+ s.logger.Error("failed to start pre-start service script: ", cmd.String(), ", error: ", err)
+ return err
+ } else {
+ s.logger.Info("pre-start service script started: ", cmd.String())
+ s.cmd = cmd
+ go func() {
+ cmd := s.cmd
+ err := cmd.Wait()
+ if err != nil {
+ if !s.noFatal {
+ s.logger.Fatal("service script executed failed: ", cmd.String(), ", error: ", err)
+ } else {
+ s.logger.Error("service script executed failed: ", cmd.String(), ", error: ", err)
+ }
+ }
+ s.cmd = nil
+ }()
+ }
+ default:
+ }
+ return nil
+}
+
+func (s *Script) PostStart() error {
+ switch s.mode {
+ case postStart:
+ cmd := s.newCommand(s.ctx)
+ s.logger.Info("executing post-start script: ", cmd.String())
+ err := cmd.Run()
+ if err != nil {
+ s.logger.Error("failed to execute post-start script: ", cmd.String(), ", error: ", err)
+ if !s.noFatal {
+ return err
+ }
+ } else {
+ s.logger.Info("post-start script executed: ", cmd.String())
+ }
+ case postStartServicePreClose, postStartServicePostClose:
+ cmd := s.newCommand(s.ctx)
+ s.logger.Info("starting post-start service script: ", cmd.String())
+ err := cmd.Start()
+ if err != nil {
+ s.logger.Error("failed to start post-start service script: ", cmd.String(), ", error: ", err)
+ return err
+ } else {
+ s.logger.Info("post-start service script started: ", cmd.String())
+ s.cmd = cmd
+ go func() {
+ cmd := s.cmd
+ err := cmd.Wait()
+ if err != nil && !errors.Is(err, context.Canceled) {
+ if !s.noFatal {
+ s.logger.Fatal("service script executed failed: ", cmd.String(), ", error: ", err)
+ } else {
+ s.logger.Error("service script executed failed: ", cmd.String(), ", error: ", err)
+ }
+ }
+ s.cmd = nil
+ }()
+ }
+ default:
+ }
+ return nil
+}
+
+func (s *Script) PreClose() error {
+ switch s.mode {
+ case preClose:
+ cmd := s.newCommand(context.Background())
+ s.logger.Info("executing pre-close script: ", cmd.String())
+ err := cmd.Run()
+ if err != nil {
+ s.logger.Error("failed to execute pre-close script: ", cmd.String(), ", error: ", err)
+ if !s.noFatal {
+ return err
+ }
+ } else {
+ s.logger.Info("pre-close script executed: ", cmd.String())
+ }
+ case preStartServicePreClose, postStartServicePreClose:
+ cmd := s.cmd
+ if cmd != nil {
+ err := cmd.Cancel()
+ if err != nil && !errors.Is(err, context.Canceled) {
+ s.logger.Error("failed to cancel service script: ", cmd.String(), ", error: ", err)
+ return err
+ } else {
+ s.logger.Info("service script canceled: ", cmd.String())
+ }
+ }
+ default:
+ }
+ return nil
+}
+
+func (s *Script) PostClose() error {
+ switch s.mode {
+ case postClose:
+ cmd := s.newCommand(context.Background())
+ s.logger.Info("executing post-close script: ", cmd.String())
+ err := cmd.Run()
+ if err != nil {
+ s.logger.Error("failed to execute post-close script: ", cmd.String(), ", error: ", err)
+ if !s.noFatal {
+ return err
+ }
+ } else {
+ s.logger.Info("post-close script executed: ", cmd.String())
+ }
+ case preStartServicePostClose, postStartServicePostClose:
+ cmd := s.cmd
+ if cmd != nil {
+ err := cmd.Cancel()
+ if err != nil && !errors.Is(err, context.Canceled) {
+ s.logger.Error("failed to cancel service script: ", cmd.String(), ", error: ", err)
+ return err
+ } else {
+ s.logger.Info("service script canceled: ", cmd.String())
+ }
+ }
+ default:
+ }
+ return nil
+}
diff --git a/script/script_stub.go b/script/script_stub.go
new file mode 100644
index 0000000000..eb0e275a0e
--- /dev/null
+++ b/script/script_stub.go
@@ -0,0 +1,37 @@
+//go:build !with_script
+
+package script
+
+import (
+ "context"
+
+ "github.com/sagernet/sing-box/log"
+ "github.com/sagernet/sing-box/option"
+ E "github.com/sagernet/sing/common/exceptions"
+)
+
+type Script struct{}
+
+func NewScript(_ context.Context, _ log.ContextLogger, _ string, _ option.ScriptOptions) (*Script, error) {
+ return nil, E.New(`Script is not included in this build, rebuild with -tags with_script`)
+}
+
+func (s *Script) Tag() string {
+ return ""
+}
+
+func (s *Script) PreStart() error {
+ return E.New(`Script is not included in this build, rebuild with -tags with_script`)
+}
+
+func (s *Script) PostStart() error {
+ return E.New(`Script is not included in this build, rebuild with -tags with_script`)
+}
+
+func (s *Script) PreClose() error {
+ return E.New(`Script is not included in this build, rebuild with -tags with_script`)
+}
+
+func (s *Script) PostClose() error {
+ return E.New(`Script is not included in this build, rebuild with -tags with_script`)
+}
diff --git a/script/writer.go b/script/writer.go
new file mode 100644
index 0000000000..62a68b0f96
--- /dev/null
+++ b/script/writer.go
@@ -0,0 +1,17 @@
+//go:build with_script
+
+package script
+
+import "strings"
+
+type logWriter struct {
+ f func(...any)
+}
+
+func (w *logWriter) Write(p []byte) (n int, err error) {
+ str := strings.TrimSpace(string(p))
+ if str != "" {
+ w.f(str)
+ }
+ return len(p), nil
+}
diff --git a/test/mux_test.go b/test/mux_test.go
index c02f270878..564b936d8e 100644
--- a/test/mux_test.go
+++ b/test/mux_test.go
@@ -75,9 +75,6 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) {
},
Method: method,
Password: password,
- Multiplex: &option.InboundMultiplexOptions{
- Enabled: true,
- },
},
},
},
diff --git a/transport/v2raygrpclite/conn.go b/transport/v2raygrpclite/conn.go
index f5a71939d3..156637623f 100644
--- a/transport/v2raygrpclite/conn.go
+++ b/transport/v2raygrpclite/conn.go
@@ -2,16 +2,19 @@ package v2raygrpclite
import (
std_bufio "bufio"
+ "bytes"
"encoding/binary"
"io"
"net"
"net/http"
"os"
+ "sync"
"time"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/baderror"
"github.com/sagernet/sing/common/buf"
+ "github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/rw"
)
@@ -27,6 +30,7 @@ type GunConn struct {
create chan struct{}
err error
readRemaining int
+ writeAccess sync.Mutex
}
func newGunConn(reader io.Reader, writer io.Writer, flusher http.Flusher) *GunConn {
@@ -96,22 +100,19 @@ func (c *GunConn) read(b []byte) (n int, err error) {
}
func (c *GunConn) Write(b []byte) (n int, err error) {
- varLen := rw.UVariantLen(uint64(len(b)))
- buffer := buf.NewSize(6 + varLen + len(b))
- header := buffer.Extend(6 + varLen)
- header[0] = 0x00
- binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+len(b)))
- header[5] = 0x0A
- binary.PutUvarint(header[6:], uint64(len(b)))
- common.Must1(buffer.Write(b))
- _, err = c.writer.Write(buffer.Bytes())
- if err != nil {
- return 0, baderror.WrapH2(err)
- }
- if c.flusher != nil {
+ protobufHeader := [1 + binary.MaxVarintLen64]byte{0x0A}
+ varuintLen := binary.PutUvarint(protobufHeader[1:], uint64(len(b)))
+ grpcHeader := buf.Get(5)
+ grpcPayloadLen := uint32(1 + varuintLen + len(b))
+ binary.BigEndian.PutUint32(grpcHeader[1:5], grpcPayloadLen)
+ c.writeAccess.Lock()
+ _, err = bufio.Copy(c.writer, io.MultiReader(bytes.NewReader(grpcHeader), bytes.NewReader(protobufHeader[:varuintLen+1]), bytes.NewReader(b)))
+ c.writeAccess.Unlock()
+ buf.Put(grpcHeader)
+ if err == nil && c.flusher != nil {
c.flusher.Flush()
}
- return len(b), nil
+ return len(b), baderror.WrapH2(err)
}
func (c *GunConn) WriteBuffer(buffer *buf.Buffer) error {
@@ -119,18 +120,16 @@ func (c *GunConn) WriteBuffer(buffer *buf.Buffer) error {
dataLen := buffer.Len()
varLen := rw.UVariantLen(uint64(dataLen))
header := buffer.ExtendHeader(6 + varLen)
+ _ = header[6]
header[0] = 0x00
binary.BigEndian.PutUint32(header[1:5], uint32(1+varLen+dataLen))
header[5] = 0x0A
binary.PutUvarint(header[6:], uint64(dataLen))
err := rw.WriteBytes(c.writer, buffer.Bytes())
- if err != nil {
- return baderror.WrapH2(err)
- }
- if c.flusher != nil {
+ if err == nil && c.flusher != nil {
c.flusher.Flush()
}
- return nil
+ return baderror.WrapH2(err)
}
func (c *GunConn) FrontHeadroom() int {
diff --git a/transport/v2rayhttp/client.go b/transport/v2rayhttp/client.go
index 4fa141cc81..44c135ef6f 100644
--- a/transport/v2rayhttp/client.go
+++ b/transport/v2rayhttp/client.go
@@ -7,7 +7,6 @@ import (
"net"
"net/http"
"net/url"
- "strings"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -29,7 +28,7 @@ type Client struct {
serverAddr M.Socksaddr
transport http.RoundTripper
http2 bool
- requestURL url.URL
+ url *url.URL
host []string
method string
headers http.Header
@@ -59,35 +58,33 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
},
}
}
- if options.Method == "" {
- options.Method = http.MethodPut
- }
- var requestURL url.URL
- if tlsConfig == nil {
- requestURL.Scheme = "http"
- } else {
- requestURL.Scheme = "https"
- }
- requestURL.Host = serverAddr.String()
- requestURL.Path = options.Path
- err := sHTTP.URLSetPath(&requestURL, options.Path)
- if err != nil {
- return nil, E.Cause(err, "parse path")
- }
- if !strings.HasPrefix(requestURL.Path, "/") {
- requestURL.Path = "/" + requestURL.Path
- }
- return &Client{
+ client := &Client{
ctx: ctx,
dialer: dialer,
serverAddr: serverAddr,
- requestURL: requestURL,
host: options.Host,
method: options.Method,
headers: options.Headers.Build(),
transport: transport,
http2: tlsConfig != nil,
- }, nil
+ }
+ if client.method == "" {
+ client.method = "PUT"
+ }
+ var uri url.URL
+ if tlsConfig == nil {
+ uri.Scheme = "http"
+ } else {
+ uri.Scheme = "https"
+ }
+ uri.Host = serverAddr.String()
+ uri.Path = options.Path
+ err := sHTTP.URLSetPath(&uri, options.Path)
+ if err != nil {
+ return nil, E.Cause(err, "parse path")
+ }
+ client.url = &uri
+ return client, nil
}
func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
@@ -106,7 +103,7 @@ func (c *Client) dialHTTP(ctx context.Context) (net.Conn, error) {
request := &http.Request{
Method: c.method,
- URL: &c.requestURL,
+ URL: c.url,
Header: c.headers.Clone(),
}
switch hostLen := len(c.host); hostLen {
@@ -126,7 +123,7 @@ func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) {
request := &http.Request{
Method: c.method,
Body: pipeInReader,
- URL: &c.requestURL,
+ URL: c.url,
Header: c.headers.Clone(),
}
request = request.WithContext(ctx)
diff --git a/transport/v2rayhttpupgrade/server.go b/transport/v2rayhttpupgrade/server.go
index a3b5d23ed9..653778f91d 100644
--- a/transport/v2rayhttpupgrade/server.go
+++ b/transport/v2rayhttpupgrade/server.go
@@ -65,7 +65,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
s.invalidRequest(writer, request, http.StatusBadRequest, E.New("bad host: ", host))
return
}
- if request.URL.Path != s.path {
+ if !strings.HasPrefix(request.URL.Path, s.path) {
s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path))
return
}
diff --git a/transport/v2raywebsocket/client.go b/transport/v2raywebsocket/client.go
index 5de610c2da..7fda40ccf8 100644
--- a/transport/v2raywebsocket/client.go
+++ b/transport/v2raywebsocket/client.go
@@ -55,10 +55,15 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
if !strings.HasPrefix(requestURL.Path, "/") {
requestURL.Path = "/" + requestURL.Path
}
- headers := options.Headers.Build()
- if host := headers.Get("Host"); host != "" {
- headers.Del("Host")
- requestURL.Host = host
+ headers := make(http.Header)
+ for key, value := range options.Headers {
+ headers[key] = value
+ if key == "Host" {
+ if len(value) > 1 {
+ return nil, E.New("multiple Host headers")
+ }
+ requestURL.Host = value[0]
+ }
}
if headers.Get("User-Agent") == "" {
headers.Set("User-Agent", "Go-http-client/1.1")
diff --git a/transport/v2raywebsocket/server.go b/transport/v2raywebsocket/server.go
index 86f2de9cd3..db078675a2 100644
--- a/transport/v2raywebsocket/server.go
+++ b/transport/v2raywebsocket/server.go
@@ -33,7 +33,6 @@ type Server struct {
path string
maxEarlyData uint32
earlyDataHeaderName string
- upgrader ws.HTTPUpgrader
}
func NewServer(ctx context.Context, options option.V2RayWebsocketOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) {
@@ -44,10 +43,6 @@ func NewServer(ctx context.Context, options option.V2RayWebsocketOptions, tlsCon
path: options.Path,
maxEarlyData: options.MaxEarlyData,
earlyDataHeaderName: options.EarlyDataHeaderName,
- upgrader: ws.HTTPUpgrader{
- Timeout: C.TCPTimeout,
- Header: options.Headers.Build(),
- },
}
if !strings.HasPrefix(server.path, "/") {
server.path = "/" + server.path
@@ -84,10 +79,6 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
return
}
} else {
- if request.URL.Path != s.path {
- s.invalidRequest(writer, request, http.StatusNotFound, E.New("bad path: ", request.URL.Path))
- return
- }
earlyDataStr := request.Header.Get(s.earlyDataHeaderName)
if earlyDataStr != "" {
earlyData, err = base64.RawURLEncoding.DecodeString(earlyDataStr)
diff --git a/transport/wireguard/client_bind.go b/transport/wireguard/client_bind.go
index 4d39120530..a72432d365 100644
--- a/transport/wireguard/client_bind.go
+++ b/transport/wireguard/client_bind.go
@@ -12,6 +12,7 @@ import (
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
+ "github.com/sagernet/sing/service/pause"
"github.com/sagernet/wireguard-go/conn"
)
@@ -21,27 +22,33 @@ type ClientBind struct {
ctx context.Context
errorHandler E.Handler
dialer N.Dialer
- reservedForEndpoint map[netip.AddrPort][3]uint8
+ reservedForEndpoint map[M.Socksaddr][3]uint8
connAccess sync.Mutex
conn *wireConn
done chan struct{}
isConnect bool
- connectAddr netip.AddrPort
+ connectAddr M.Socksaddr
reserved [3]uint8
+ pauseManager pause.Manager
}
-func NewClientBind(ctx context.Context, errorHandler E.Handler, dialer N.Dialer, isConnect bool, connectAddr netip.AddrPort, reserved [3]uint8) *ClientBind {
+func NewClientBind(ctx context.Context, errorHandler E.Handler, dialer N.Dialer, isConnect bool, connectAddr M.Socksaddr, reserved [3]uint8) *ClientBind {
return &ClientBind{
ctx: ctx,
errorHandler: errorHandler,
dialer: dialer,
- reservedForEndpoint: make(map[netip.AddrPort][3]uint8),
+ reservedForEndpoint: make(map[M.Socksaddr][3]uint8),
isConnect: isConnect,
connectAddr: connectAddr,
reserved: reserved,
+ pauseManager: pause.ManagerFromContext(ctx),
}
}
+func (c *ClientBind) SetReservedForEndpoint(destination M.Socksaddr, reserved [3]byte) {
+ c.reservedForEndpoint[destination] = reserved
+}
+
func (c *ClientBind) connect() (*wireConn, error) {
serverConn := c.conn
if serverConn != nil {
@@ -64,7 +71,7 @@ func (c *ClientBind) connect() (*wireConn, error) {
}
}
if c.isConnect {
- udpConn, err := c.dialer.DialContext(c.ctx, N.NetworkUDP, M.SocksaddrFromNetIP(c.connectAddr))
+ udpConn, err := c.dialer.DialContext(c.ctx, N.NetworkUDP, c.connectAddr)
if err != nil {
return nil, err
}
@@ -106,6 +113,7 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint)
c.errorHandler.NewError(context.Background(), E.Cause(err, "connect to server"))
err = nil
time.Sleep(time.Second)
+ c.pauseManager.WaitActive()
return
}
n, addr, err := udpConn.ReadFrom(packets[0])
@@ -122,9 +130,11 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint)
sizes[0] = n
if n > 3 {
b := packets[0]
- common.ClearArray(b[1:4])
+ b[1] = 0
+ b[2] = 0
+ b[3] = 0
}
- eps[0] = Endpoint(M.AddrPortFromNet(addr))
+ eps[0] = Endpoint(M.SocksaddrFromNet(addr))
count = 1
return
}
@@ -157,16 +167,18 @@ func (c *ClientBind) Send(bufs [][]byte, ep conn.Endpoint) error {
if err != nil {
return err
}
- destination := netip.AddrPort(ep.(Endpoint))
+ destination := M.Socksaddr(ep.(Endpoint))
for _, b := range bufs {
if len(b) > 3 {
reserved, loaded := c.reservedForEndpoint[destination]
if !loaded {
reserved = c.reserved
}
- copy(b[1:4], reserved[:])
+ b[1] = reserved[0]
+ b[2] = reserved[1]
+ b[3] = reserved[2]
}
- _, err = udpConn.WriteTo(b, M.SocksaddrFromNetIP(destination))
+ _, err = udpConn.WriteTo(b, destination)
if err != nil {
udpConn.Close()
return err
@@ -176,21 +188,13 @@ func (c *ClientBind) Send(bufs [][]byte, ep conn.Endpoint) error {
}
func (c *ClientBind) ParseEndpoint(s string) (conn.Endpoint, error) {
- ap, err := netip.ParseAddrPort(s)
- if err != nil {
- return nil, err
- }
- return Endpoint(ap), nil
+ return Endpoint(M.ParseSocksaddr(s)), nil
}
func (c *ClientBind) BatchSize() int {
return 1
}
-func (c *ClientBind) SetReservedForEndpoint(destination netip.AddrPort, reserved [3]byte) {
- c.reservedForEndpoint[destination] = reserved
-}
-
type wireConn struct {
net.PacketConn
access sync.Mutex
diff --git a/transport/wireguard/device_stack.go b/transport/wireguard/device_stack.go
index 9d9b4549bd..4ddaffe1f2 100644
--- a/transport/wireguard/device_stack.go
+++ b/transport/wireguard/device_stack.go
@@ -265,7 +265,7 @@ func (ep *wireEndpoint) LinkAddress() tcpip.LinkAddress {
}
func (ep *wireEndpoint) Capabilities() stack.LinkEndpointCapabilities {
- return stack.CapabilityRXChecksumOffload
+ return stack.CapabilityNone
}
func (ep *wireEndpoint) Attach(dispatcher stack.NetworkDispatcher) {
diff --git a/transport/wireguard/device_system.go b/transport/wireguard/device_system.go
index 49acc5b90e..e70c3f3564 100644
--- a/transport/wireguard/device_system.go
+++ b/transport/wireguard/device_system.go
@@ -12,7 +12,6 @@ import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
- E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
wgTun "github.com/sagernet/wireguard-go/tun"
@@ -21,17 +20,17 @@ import (
var _ Device = (*SystemDevice)(nil)
type SystemDevice struct {
- dialer N.Dialer
- device tun.Tun
- batchDevice tun.LinuxTUN
- name string
- mtu int
- events chan wgTun.Event
- addr4 netip.Addr
- addr6 netip.Addr
+ dialer N.Dialer
+ device tun.Tun
+ frontHeadroom int
+ name string
+ mtu int
+ events chan wgTun.Event
+ addr4 netip.Addr
+ addr6 netip.Addr
}
-func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes []netip.Prefix, mtu uint32, gso bool) (*SystemDevice, error) {
+func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes []netip.Prefix, mtu uint32, gso bool, gsoMaxsize uint32) (*SystemDevice, error) {
var inet4Addresses []netip.Prefix
var inet6Addresses []netip.Prefix
for _, prefixes := range localPrefixes {
@@ -44,12 +43,16 @@ func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes
if interfaceName == "" {
interfaceName = tun.CalculateInterfaceName("wg")
}
+ if gsoMaxsize == 0 {
+ gsoMaxsize = 65536
+ }
tunInterface, err := tun.New(tun.Options{
Name: interfaceName,
Inet4Address: inet4Addresses,
Inet6Address: inet6Addresses,
MTU: mtu,
GSO: gso,
+ GSOMaxSize: gsoMaxsize,
})
if err != nil {
return nil, err
@@ -62,25 +65,17 @@ func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes
if len(inet6Addresses) > 0 {
inet6Address = inet6Addresses[0].Addr()
}
- var batchDevice tun.LinuxTUN
- if gso {
- batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN)
- if !isBatchTUN {
- return nil, E.New("GSO is not supported on current platform")
- }
- batchDevice = batchTUN
- }
return &SystemDevice{
dialer: common.Must1(dialer.NewDefault(router, option.DialerOptions{
BindInterface: interfaceName,
})),
- device: tunInterface,
- batchDevice: batchDevice,
- name: interfaceName,
- mtu: int(mtu),
- events: make(chan wgTun.Event),
- addr4: inet4Address,
- addr6: inet6Address,
+ device: tunInterface,
+ frontHeadroom: tunInterface.FrontHeadroom(),
+ name: interfaceName,
+ mtu: int(mtu),
+ events: make(chan wgTun.Event),
+ addr4: inet4Address,
+ addr6: inet6Address,
}, nil
}
@@ -110,31 +105,23 @@ func (w *SystemDevice) File() *os.File {
}
func (w *SystemDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) {
- if w.batchDevice != nil {
- count, err = w.batchDevice.BatchRead(bufs, offset, sizes)
- } else {
- sizes[0], err = w.device.Read(bufs[0][offset:])
- if err == nil {
- count = 1
- } else if errors.Is(err, tun.ErrTooManySegments) {
- err = wgTun.ErrTooManySegments
- }
+ sizes[0], err = w.device.Read(bufs[0][offset-w.frontHeadroom:])
+ if err == nil {
+ count = 1
+ } else if errors.Is(err, tun.ErrTooManySegments) {
+ err = wgTun.ErrTooManySegments
}
return
}
func (w *SystemDevice) Write(bufs [][]byte, offset int) (count int, err error) {
- if w.batchDevice != nil {
- return 0, w.batchDevice.BatchWrite(bufs, offset)
- } else {
- for _, b := range bufs {
- _, err = w.device.Write(b[offset:])
- if err != nil {
- return
- }
+ for _, b := range bufs {
+ _, err = w.device.Write(b[offset-w.frontHeadroom:])
+ if err != nil {
+ return
}
+ count++
}
- // WireGuard will not read count
return
}
@@ -159,8 +146,5 @@ func (w *SystemDevice) Close() error {
}
func (w *SystemDevice) BatchSize() int {
- if w.batchDevice != nil {
- return w.batchDevice.BatchSize()
- }
return 1
}
diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go
index 3c3ec7db5c..dd2b7dbc03 100644
--- a/transport/wireguard/endpoint.go
+++ b/transport/wireguard/endpoint.go
@@ -3,12 +3,13 @@ package wireguard
import (
"net/netip"
+ M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/wireguard-go/conn"
)
var _ conn.Endpoint = (*Endpoint)(nil)
-type Endpoint netip.AddrPort
+type Endpoint M.Socksaddr
func (e Endpoint) ClearSrc() {
}
@@ -18,16 +19,16 @@ func (e Endpoint) SrcToString() string {
}
func (e Endpoint) DstToString() string {
- return (netip.AddrPort)(e).String()
+ return (M.Socksaddr)(e).String()
}
func (e Endpoint) DstToBytes() []byte {
- b, _ := (netip.AddrPort)(e).MarshalBinary()
+ b, _ := (M.Socksaddr)(e).AddrPort().MarshalBinary()
return b
}
func (e Endpoint) DstIP() netip.Addr {
- return (netip.AddrPort)(e).Addr()
+ return (M.Socksaddr)(e).Addr
}
func (e Endpoint) SrcIP() netip.Addr {