mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-08 11:44:13 +08:00
Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0ca7d15955 |
30
.fpm_openwrt
30
.fpm_openwrt
@ -1,30 +0,0 @@
|
||||
-s dir
|
||||
--name sing-box
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--no-deb-generate-changes
|
||||
|
||||
--config-files /etc/config/sing-box
|
||||
--config-files /etc/sing-box/config.json
|
||||
|
||||
--depends ca-bundle
|
||||
--depends kmod-inet-diag
|
||||
--depends kmod-tun
|
||||
--depends firewall4
|
||||
|
||||
--before-remove release/config/openwrt.prerm
|
||||
|
||||
release/config/config.json=/etc/sing-box/config.json
|
||||
|
||||
release/config/openwrt.conf=/etc/config/sing-box
|
||||
release/config/openwrt.init=/etc/init.d/sing-box
|
||||
release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box
|
||||
|
||||
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
||||
|
||||
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
25
.fpm_systemd
25
.fpm_systemd
@ -1,25 +0,0 @@
|
||||
-s dir
|
||||
--name sing-box
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||
--no-deb-generate-changes
|
||||
--config-files /etc/sing-box/config.json
|
||||
--after-install release/config/sing-box.postinst
|
||||
|
||||
release/config/config.json=/etc/sing-box/config.json
|
||||
|
||||
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
|
||||
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
|
||||
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
|
||||
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
|
||||
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||
|
||||
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
||||
|
||||
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +0,0 @@
|
||||
github: nekohasekai
|
132
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
132
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,88 +1,54 @@
|
||||
name: Bug report
|
||||
description: "Report sing-box bug"
|
||||
name: Bug Report
|
||||
description: "Create a report to help us improve."
|
||||
labels: [ 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
|
||||
id: supporter
|
||||
id: terms
|
||||
attributes:
|
||||
label: Supporter
|
||||
label: Welcome
|
||||
options:
|
||||
- label: I am a [sponsor](https://github.com/sponsors/nekohasekai/)
|
||||
- type: checkboxes
|
||||
- label: Yes, I'm using the latest major release. Only such installations are supported.
|
||||
required: true
|
||||
- label: Yes, I've searched similar issues on GitHub and didn't find any.
|
||||
required: true
|
||||
- label: Yes, I've included all information below (version, config, etc).
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
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
|
||||
label: Description of the problem
|
||||
placeholder: Your problem description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version of sing-box
|
||||
value: |-
|
||||
<details>
|
||||
|
||||
```console
|
||||
$ sing-box --version
|
||||
# Paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Server and client configuration file
|
||||
value: |-
|
||||
<details>
|
||||
|
||||
```console
|
||||
# paste json here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
88
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
88
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
@ -1,88 +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
|
||||
id: supporter
|
||||
attributes:
|
||||
label: 支持我们
|
||||
options:
|
||||
- label: 我已经 [赞助](https://github.com/sponsors/nekohasekai/)
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 完整性要求
|
||||
description: |-
|
||||
请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
|
||||
sing-box 不是讨好无法作出任何意义上的贡献的最终用户并获取非道德影响力的项目,如果您在此处欺骗以故意浪费开发者的时间,您将被永久封锁。
|
||||
options:
|
||||
- label: 我保证阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
|
||||
required: true
|
||||
- label: 我保证提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。
|
||||
required: true
|
||||
- label: 我保证提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器、TUN、图形界面客户端或者其他闭源软件。
|
||||
required: true
|
||||
- label: 我保证提供了完整的配置文件与日志,而不是出于对自身智力的自信而仅提供了部分认为有用的部分。
|
||||
required: true
|
28
.github/deb2ipk.sh
vendored
28
.github/deb2ipk.sh
vendored
@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
PROJECT=$(dirname "$0")/../..
|
||||
TMP_PATH=`mktemp -d`
|
||||
cp $2 $TMP_PATH
|
||||
pushd $TMP_PATH
|
||||
|
||||
DEB_NAME=`ls *.deb`
|
||||
ar x $DEB_NAME
|
||||
|
||||
mkdir control
|
||||
pushd control
|
||||
tar xf ../control.tar.gz
|
||||
rm md5sums
|
||||
sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i
|
||||
cat control
|
||||
tar czf ../control.tar.gz ./*
|
||||
popd
|
||||
|
||||
DEB_NAME=${DEB_NAME%.deb}
|
||||
tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary
|
||||
popd
|
||||
|
||||
cp $TMP_PATH/$DEB_NAME.ipk $3
|
||||
rm -r $TMP_PATH
|
28
.github/renovate.json
vendored
28
.github/renovate.json
vendored
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
25
.github/setup_legacy_go.sh
vendored
25
.github/setup_legacy_go.sh
vendored
@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION="1.23.6"
|
||||
|
||||
mkdir -p $HOME/go
|
||||
cd $HOME/go
|
||||
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
|
||||
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
|
||||
mv go go_legacy
|
||||
cd go_legacy
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# this patch file only works on golang1.23.x
|
||||
# that means after golang1.24 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
|
||||
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
|
14
.github/update_clients.sh
vendored
14
.github/update_clients.sh
vendored
@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PROJECTS=$(dirname "$0")/../..
|
||||
|
||||
function updateClient() {
|
||||
pushd clients/$1
|
||||
git fetch
|
||||
git reset FETCH_HEAD --hard
|
||||
popd
|
||||
git add clients/$1
|
||||
}
|
||||
|
||||
updateClient "apple"
|
||||
updateClient "android"
|
10
.github/update_dependencies.sh
vendored
10
.github/update_dependencies.sh
vendored
@ -1,5 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PROJECTS=$(dirname "$0")/../..
|
||||
go get -x github.com/sagernet/$1@$(git -C $PROJECTS/$1 rev-parse HEAD)
|
||||
|
||||
go get -x github.com/sagernet/sing@$(git -C $PROJECTS/sing rev-parse HEAD)
|
||||
go get -x github.com/sagernet/sing-dns@$(git -C $PROJECTS/sing-dns rev-parse HEAD)
|
||||
go get -x github.com/sagernet/sing-tun@$(git -C $PROJECTS/sing-tun rev-parse HEAD)
|
||||
go get -x github.com/sagernet/sing-shadowsocks@$(git -C $PROJECTS/sing-shadowsocks rev-parse HEAD)
|
||||
go get -x github.com/sagernet/sing-vmess@$(git -C $PROJECTS/sing-vmess rev-parse HEAD)
|
||||
go mod tidy
|
||||
pushd test
|
||||
go mod tidy
|
||||
popd
|
||||
|
667
.github/workflows/build.yml
vendored
667
.github/workflows/build.yml
vendored
@ -1,667 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version name"
|
||||
required: true
|
||||
type: string
|
||||
build:
|
||||
description: "Build type"
|
||||
required: true
|
||||
type: choice
|
||||
default: "All"
|
||||
options:
|
||||
- All
|
||||
- Binary
|
||||
- Android
|
||||
- Apple
|
||||
- app-store
|
||||
- iOS
|
||||
- macOS
|
||||
- tvOS
|
||||
- macOS-standalone
|
||||
- publish-android
|
||||
push:
|
||||
branches:
|
||||
- main-next
|
||||
- dev-next
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
calculate_version:
|
||||
name: Calculate version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.outputs.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
echo "version=${{ inputs.version }}"
|
||||
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Calculate version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||
- name: Set outputs
|
||||
id: outputs
|
||||
run: |-
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
build:
|
||||
name: Build binary
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
|
||||
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
|
||||
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
|
||||
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
|
||||
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
|
||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
|
||||
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
|
||||
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
|
||||
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
|
||||
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
|
||||
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
|
||||
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
|
||||
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
|
||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
|
||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
||||
|
||||
- { os: windows, arch: amd64 }
|
||||
- { os: windows, arch: amd64, legacy_go: true }
|
||||
- { os: windows, arch: "386" }
|
||||
- { os: windows, arch: "386", legacy_go: true }
|
||||
- { os: windows, arch: arm64 }
|
||||
|
||||
- { os: darwin, arch: amd64 }
|
||||
- { os: darwin, arch: arm64 }
|
||||
|
||||
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
|
||||
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
|
||||
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
|
||||
- { os: android, arch: "386", ndk: "i686-linux-android21" }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
if: ${{ ! matrix.legacy_go }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
- name: Cache Legacy Go
|
||||
if: matrix.require_legacy_go
|
||||
id: cache-legacy-go
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/go_legacy
|
||||
key: go_legacy_1236
|
||||
- name: Setup Legacy Go
|
||||
if: matrix.legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
||||
run: |-
|
||||
.github/setup_legacy_go.sh
|
||||
- name: Setup Legacy Go 2
|
||||
if: matrix.legacy_go
|
||||
run: |-
|
||||
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
|
||||
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
|
||||
- name: Setup Android NDK
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28
|
||||
local-cache: true
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
if: matrix.os != 'android'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GO386: ${{ matrix.go386 }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
GOMIPS64: ${{ matrix.gomips }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Android
|
||||
if: matrix.os == 'android'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
go install -v ./cmd/internal/build
|
||||
export CC='${{ matrix.ndk }}-clang'
|
||||
export CXX="${CC}++"
|
||||
mkdir -p dist
|
||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
BUILD_GOOS: ${{ matrix.os }}
|
||||
BUILD_GOARCH: ${{ matrix.arch }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set name
|
||||
run: |-
|
||||
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
if [[ -n "${{ matrix.goarm }}" ]]; then
|
||||
DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}"
|
||||
elif [[ -n "${{ matrix.go386 }}" && "${{ matrix.go386 }}" != 'sse2' ]]; then
|
||||
DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}"
|
||||
elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then
|
||||
DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}"
|
||||
elif [[ "${{ matrix.legacy_go }}" == 'true' ]]; then
|
||||
DIR_NAME="${DIR_NAME}-legacy"
|
||||
fi
|
||||
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||
- name: Package DEB
|
||||
if: matrix.debian != ''
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y debsigs
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t deb \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \
|
||||
--architecture ${{ matrix.debian }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||
rm -rf $HOME/.gnupg
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||
- name: Package RPM
|
||||
if: matrix.rpm != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t rpm \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.rpm }}.rpm" \
|
||||
--architecture ${{ matrix.rpm }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
cat > $HOME/.rpmmacros <<EOF
|
||||
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||
EOF
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
rpmsign --addsign dist/*.rpm
|
||||
- name: Package Pacman
|
||||
if: matrix.pacman != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libarchive-tools
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t pacman \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
||||
--architecture ${{ matrix.pacman }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
- name: Package OpenWrt
|
||||
if: matrix.openwrt != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .fpm_openwrt .fpm
|
||||
fpm -t deb \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/openwrt.deb" \
|
||||
--architecture all \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
for architecture in ${{ matrix.openwrt }}; do
|
||||
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
|
||||
done
|
||||
rm "dist/openwrt.deb"
|
||||
- name: Archive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
cd dist
|
||||
mkdir -p "${DIR_NAME}"
|
||||
cp ../LICENSE "${DIR_NAME}"
|
||||
if [ '${{ matrix.os }}' = 'windows' ]; then
|
||||
cp sing-box "${DIR_NAME}/sing-box.exe"
|
||||
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
|
||||
else
|
||||
cp sing-box "${DIR_NAME}"
|
||||
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
|
||||
fi
|
||||
rm -r "${DIR_NAME}"
|
||||
- name: Cleanup
|
||||
run: rm dist/sing-box
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||
path: "dist"
|
||||
build_android:
|
||||
name: Build Android
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28
|
||||
- name: Setup OpenJDK
|
||||
run: |-
|
||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Build library
|
||||
run: |-
|
||||
make lib_install
|
||||
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||
make lib_android
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
- name: Checkout main branch
|
||||
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
cd clients/android
|
||||
git checkout main
|
||||
- name: Checkout dev branch
|
||||
if: github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
cd clients/android
|
||||
git checkout dev
|
||||
- name: Gradle cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.gradle
|
||||
key: gradle-${{ hashFiles('**/*.gradle') }}
|
||||
- name: Update version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_android_version --ci
|
||||
- name: Update nightly version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_android_version --ci --nightly
|
||||
- name: Build
|
||||
run: |-
|
||||
mkdir clients/android/app/libs
|
||||
cp libbox.aar clients/android/app/libs
|
||||
cd clients/android
|
||||
./gradlew :app:assemblePlayRelease :app:assembleOtherRelease
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||
- name: Prepare upload
|
||||
run: |-
|
||||
mkdir -p dist
|
||||
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
|
||||
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-android-apks
|
||||
path: 'dist'
|
||||
publish_android:
|
||||
name: Publish Android
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28
|
||||
- name: Setup OpenJDK
|
||||
run: |-
|
||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Build library
|
||||
run: |-
|
||||
make lib_install
|
||||
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||
make lib_android
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
- name: Checkout main branch
|
||||
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
cd clients/android
|
||||
git checkout main
|
||||
- name: Checkout dev branch
|
||||
if: github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
cd clients/android
|
||||
git checkout dev
|
||||
- name: Gradle cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.gradle
|
||||
key: gradle-${{ hashFiles('**/*.gradle') }}
|
||||
- name: Build
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_android_version --ci
|
||||
mkdir clients/android/app/libs
|
||||
cp libbox.aar clients/android/app/libs
|
||||
cd clients/android
|
||||
echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json
|
||||
./gradlew :app:publishPlayReleaseBundle
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
|
||||
build_apple:
|
||||
name: Build Apple clients
|
||||
runs-on: macos-15
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: iOS
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'iOS' }}
|
||||
platform: ios
|
||||
scheme: SFI
|
||||
destination: 'generic/platform=iOS'
|
||||
archive: build/SFI.xcarchive
|
||||
upload: SFI/Upload.plist
|
||||
- name: macOS
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'macOS' }}
|
||||
platform: macos
|
||||
scheme: SFM
|
||||
destination: 'generic/platform=macOS'
|
||||
archive: build/SFM.xcarchive
|
||||
upload: SFI/Upload.plist
|
||||
- name: tvOS
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'tvOS' }}
|
||||
platform: tvos
|
||||
scheme: SFT
|
||||
destination: 'generic/platform=tvOS'
|
||||
archive: build/SFT.xcarchive
|
||||
upload: SFI/Upload.plist
|
||||
- name: macOS-standalone
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone' }}
|
||||
platform: macos
|
||||
scheme: SFM.System
|
||||
destination: 'generic/platform=macOS'
|
||||
archive: build/SFM.System.xcarchive
|
||||
export: SFM.System/Export.plist
|
||||
export_path: build/SFM.System
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: matrix.if
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Setup Go
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
- name: Setup Xcode stable
|
||||
if: matrix.if && github.ref == 'refs/heads/main-next'
|
||||
run: |-
|
||||
sudo xcode-select -s /Applications/Xcode_16.2.app
|
||||
- name: Setup Xcode beta
|
||||
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
sudo xcode-select -s /Applications/Xcode_16.2.app
|
||||
- name: Set tag
|
||||
if: matrix.if
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Checkout main branch
|
||||
if: matrix.if && github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
cd clients/apple
|
||||
git checkout main
|
||||
- name: Checkout dev branch
|
||||
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
cd clients/apple
|
||||
git checkout dev
|
||||
- name: Setup certificates
|
||||
if: matrix.if
|
||||
run: |-
|
||||
CERTIFICATE_PATH=$RUNNER_TEMP/Certificates.p12
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/certificates.keychain-db
|
||||
echo -n "$CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
|
||||
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
|
||||
|
||||
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
mkdir -p "$PROFILES_PATH"
|
||||
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
|
||||
|
||||
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
|
||||
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
|
||||
|
||||
xcrun notarytool store-credentials "notarytool-password" \
|
||||
--key $ASC_KEY_PATH \
|
||||
--key-id $ASC_KEY_ID \
|
||||
--issuer $ASC_KEY_ISSUER_ID
|
||||
|
||||
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
|
||||
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
|
||||
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
|
||||
env:
|
||||
CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
PROVISIONING_PROFILES: ${{ secrets.PROVISIONING_PROFILES }}
|
||||
ASC_KEY: ${{ secrets.ASC_KEY }}
|
||||
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
|
||||
- name: Build library
|
||||
if: matrix.if
|
||||
run: |-
|
||||
make lib_install
|
||||
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||
go run ./cmd/internal/build_libbox -target apple -platform ${{ matrix.platform }}
|
||||
mv Libbox.xcframework clients/apple
|
||||
- name: Update macOS version
|
||||
if: matrix.if && matrix.name == 'macOS' && github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
|
||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
|
||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
|
||||
- name: Update version
|
||||
if: matrix.if && matrix.name != 'iOS'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_apple_version --ci
|
||||
- name: Build
|
||||
if: matrix.if
|
||||
run: |-
|
||||
cd clients/apple
|
||||
xcodebuild archive \
|
||||
-scheme "${{ matrix.scheme }}" \
|
||||
-configuration Release \
|
||||
-destination "${{ matrix.destination }}" \
|
||||
-archivePath "${{ matrix.archive }}" \
|
||||
-allowProvisioningUpdates \
|
||||
-authenticationKeyPath $ASC_KEY_PATH \
|
||||
-authenticationKeyID $ASC_KEY_ID \
|
||||
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||
- name: Upload to App Store Connect
|
||||
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/app_store_connect cancel_app_store ${{ matrix.platform }}
|
||||
cd clients/apple
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "${{ matrix.archive }}" \
|
||||
-exportOptionsPlist ${{ matrix.upload }} \
|
||||
-allowProvisioningUpdates \
|
||||
-authenticationKeyPath $ASC_KEY_PATH \
|
||||
-authenticationKeyID $ASC_KEY_ID \
|
||||
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||
- name: Publish to TestFlight
|
||||
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/dev-next'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
|
||||
- name: Build image
|
||||
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
pushd clients/apple
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "${{ matrix.archive }}" \
|
||||
-exportOptionsPlist ${{ matrix.export }} \
|
||||
-exportPath "${{ matrix.export_path }}"
|
||||
brew install create-dmg
|
||||
create-dmg \
|
||||
--volname "sing-box" \
|
||||
--volicon "${{ matrix.export_path }}/SFM.app/Contents/Resources/AppIcon.icns" \
|
||||
--icon "SFM.app" 0 0 \
|
||||
--hide-extension "SFM.app" \
|
||||
--app-drop-link 0 0 \
|
||||
--skip-jenkins \
|
||||
SFM.dmg "${{ matrix.export_path }}/SFM.app"
|
||||
xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password"
|
||||
cd "${{ matrix.archive }}"
|
||||
zip -r SFM.dSYMs.zip dSYMs
|
||||
popd
|
||||
|
||||
mkdir -p dist
|
||||
cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg"
|
||||
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip"
|
||||
- name: Upload image
|
||||
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-macos-dmg
|
||||
path: 'dist'
|
||||
upload:
|
||||
name: Upload builds
|
||||
if: always() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
- build
|
||||
- build_android
|
||||
- build_apple
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Cache ghr
|
||||
uses: actions/cache@v4
|
||||
id: cache-ghr
|
||||
with:
|
||||
path: |
|
||||
~/go/bin/ghr
|
||||
key: ghr
|
||||
- name: Setup ghr
|
||||
if: steps.cache-ghr.outputs.cache-hit != 'true'
|
||||
run: |-
|
||||
cd $HOME
|
||||
git clone https://github.com/nekohasekai/ghr ghr
|
||||
cd ghr
|
||||
go install -v .
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Download builds
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Upload builds
|
||||
if: ${{ env.PUBLISHED == 'false' }}
|
||||
run: |-
|
||||
export PATH="$PATH:$HOME/go/bin"
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Replace builds
|
||||
if: ${{ env.PUBLISHED != 'false' }}
|
||||
run: |-
|
||||
export PATH="$PATH:$HOME/go/bin"
|
||||
ghr --replace -p 5 "v${VERSION}" dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
189
.github/workflows/debug.yml
vendored
Normal file
189
.github/workflows/debug.yml
vendored
Normal file
@ -0,0 +1,189 @@
|
||||
name: Debug build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/debug.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Debug build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel previous
|
||||
uses: styfle/cancel-workflow-action@0.7.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get latest go version
|
||||
id: version
|
||||
run: |
|
||||
echo ::set-output name=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')
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
- 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
|
||||
- name: Run Test
|
||||
run: |
|
||||
go test -v ./...
|
||||
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
|
||||
# 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
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get latest go version
|
||||
id: version
|
||||
run: |
|
||||
echo ::set-output name=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')
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
- name: Build
|
||||
id: build
|
||||
run: make
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: sing-box-${{ matrix.name }}
|
||||
path: sing-box*
|
133
.github/workflows/docker.yml
vendored
133
.github/workflows/docker.yml
vendored
@ -1,133 +0,0 @@
|
||||
name: Publish Docker Images
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "The tag version you want to build"
|
||||
|
||||
env:
|
||||
REGISTRY_IMAGE: ghcr.io/sagernet/sing-box
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
- linux/arm64
|
||||
- linux/386
|
||||
- linux/ppc64le
|
||||
- linux/riscv64
|
||||
- linux/s390x
|
||||
steps:
|
||||
- name: Get commit to build
|
||||
id: ref
|
||||
run: |-
|
||||
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
|
||||
ref="${{ github.ref_name }}"
|
||||
else
|
||||
ref="${{ github.event.inputs.tag }}"
|
||||
fi
|
||||
echo "ref=$ref"
|
||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-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 meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
context: .
|
||||
build-args: |
|
||||
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: Get commit to build
|
||||
id: ref
|
||||
run: |-
|
||||
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
|
||||
ref="${{ github.ref_name }}"
|
||||
else
|
||||
ref="${{ github.event.inputs.tag }}"
|
||||
fi
|
||||
echo "ref=$ref"
|
||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||
if [[ $ref == *"-"* ]]; then
|
||||
latest=latest-beta
|
||||
else
|
||||
latest=latest
|
||||
fi
|
||||
echo "latest=$latest"
|
||||
echo "latest=$latest" >> $GITHUB_OUTPUT
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-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: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
|
||||
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
40
.github/workflows/lint.yml
vendored
40
.github/workflows/lint.yml
vendored
@ -3,36 +3,46 @@ name: Lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- stable-next
|
||||
- main-next
|
||||
- dev-next
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/lint.yml'
|
||||
- '!.github/workflows/debug.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- stable-next
|
||||
- main-next
|
||||
- dev-next
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel previous
|
||||
uses: styfle/cancel-workflow-action@0.7.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get latest go version
|
||||
id: version
|
||||
run: |
|
||||
echo ::set-output name=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')
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go mod download -x
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=30m
|
||||
install-mode: binary
|
||||
verify: false
|
||||
version: latest
|
184
.github/workflows/linux.yml
vendored
184
.github/workflows/linux.yml
vendored
@ -1,184 +0,0 @@
|
||||
name: Build Linux Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version name"
|
||||
required: true
|
||||
type: string
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
calculate_version:
|
||||
name: Calculate version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.outputs.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
echo "version=${{ inputs.version }}"
|
||||
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Calculate version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||
- name: Set outputs
|
||||
id: outputs
|
||||
run: |-
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
build:
|
||||
name: Build binary
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
|
||||
- { os: linux, arch: "386", debian: i386, rpm: i386 }
|
||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
||||
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
||||
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
|
||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
|
||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.3
|
||||
- name: Setup Android NDK
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28
|
||||
local-cache: true
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set mtime
|
||||
run: |-
|
||||
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||
- name: Set name
|
||||
if: ${{ ! contains(needs.calculate_version.outputs.version, '-') }}
|
||||
run: |-
|
||||
echo "NAME=sing-box" >> "$GITHUB_ENV"
|
||||
- name: Set beta name
|
||||
if: contains(needs.calculate_version.outputs.version, '-')
|
||||
run: |-
|
||||
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||
- name: Set version
|
||||
run: |-
|
||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||
- name: Package DEB
|
||||
if: matrix.debian != ''
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get install -y debsigs
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t deb \
|
||||
--name "${NAME}" \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
||||
--architecture ${{ matrix.debian }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||
rm -rf $HOME/.gnupg
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||
- name: Package RPM
|
||||
if: matrix.rpm != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t rpm \
|
||||
--name "${NAME}" \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
||||
--architecture ${{ matrix.rpm }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
cat > $HOME/.rpmmacros <<EOF
|
||||
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||
EOF
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
rpmsign --addsign dist/*.rpm
|
||||
- name: Cleanup
|
||||
run: rm dist/sing-box
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||
path: "dist"
|
||||
upload:
|
||||
name: Upload builds
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
- build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Download builds
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Publish packages
|
||||
run: |-
|
||||
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/
|
18
.github/workflows/mkdocs.yml
vendored
Normal file
18
.github/workflows/mkdocs.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
name: Generate Documents
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- docs/**
|
||||
- .github/workflows/mkdocs.yml
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: pip install mkdocs-material
|
||||
- run: mkdocs gh-deploy -m "{sha}" --force --ignore-version --no-history
|
16
.github/workflows/stale.yml
vendored
16
.github/workflows/stale.yml
vendored
@ -1,16 +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
|
||||
exempt-issue-labels: 'bug,enhancement'
|
14
.gitignore
vendored
14
.gitignore
vendored
@ -1,18 +1,6 @@
|
||||
/.idea/
|
||||
/vendor/
|
||||
/*.json
|
||||
/*.srs
|
||||
/*.db
|
||||
/site/
|
||||
/bin/
|
||||
/dist/
|
||||
/sing-box
|
||||
/sing-box.exe
|
||||
/build/
|
||||
/*.jar
|
||||
/*.aar
|
||||
/*.xcframework/
|
||||
.DS_Store
|
||||
/config.d/
|
||||
/venv/
|
||||
|
||||
/bin/
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -1,6 +0,0 @@
|
||||
[submodule "clients/apple"]
|
||||
path = clients/apple
|
||||
url = https://github.com/SagerNet/sing-box-for-apple.git
|
||||
[submodule "clients/android"]
|
||||
path = clients/android
|
||||
url = https://github.com/SagerNet/sing-box-for-android.git
|
@ -6,31 +6,15 @@ linters:
|
||||
- gci
|
||||
- staticcheck
|
||||
- paralleltest
|
||||
- ineffassign
|
||||
|
||||
issues:
|
||||
fix: true
|
||||
|
||||
linters-settings:
|
||||
gci:
|
||||
custom-order: true
|
||||
sections:
|
||||
- standard
|
||||
- prefix(github.com/sagernet/)
|
||||
- default
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- -SA1003
|
||||
|
||||
run:
|
||||
go: "1.23"
|
||||
build-tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- transport/simple-obfs
|
||||
go: '1.19'
|
||||
|
@ -1,103 +0,0 @@
|
||||
project_name: sing-box
|
||||
builds:
|
||||
- id: main
|
||||
main: ./cmd/sing-box
|
||||
flags:
|
||||
- -v
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||
- -s
|
||||
- -buildid=
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- linux_386
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_arm_7
|
||||
- linux_s390x
|
||||
- linux_riscv64
|
||||
- linux_mips64le
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
snapshot:
|
||||
name_template: "{{ .Version }}.{{ .ShortCommit }}"
|
||||
nfpms:
|
||||
- &template
|
||||
id: package
|
||||
package_name: sing-box
|
||||
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
builds:
|
||||
- main
|
||||
homepage: https://sing-box.sagernet.org/
|
||||
maintainer: nekohasekai <contact-git@sekai.icu>
|
||||
description: The universal proxy platform.
|
||||
license: GPLv3 or later
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
priority: extra
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: "config|noreplace"
|
||||
|
||||
- src: release/config/sing-box.service
|
||||
dst: /usr/lib/systemd/system/sing-box.service
|
||||
- src: release/config/sing-box@.service
|
||||
dst: /usr/lib/systemd/system/sing-box@.service
|
||||
- src: release/config/sing-box.sysusers
|
||||
dst: /usr/lib/sysusers.d/sing-box.conf
|
||||
- src: release/config/sing-box.rules
|
||||
dst: /usr/share/polkit-1/rules.d/sing-box.rules
|
||||
- src: release/config/sing-box-split-dns.xml
|
||||
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||
|
||||
- src: release/completions/sing-box.bash
|
||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||
- src: release/completions/sing-box.fish
|
||||
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
- src: release/completions/sing-box.zsh
|
||||
dst: /usr/share/zsh/site-functions/_sing-box
|
||||
|
||||
- src: LICENSE
|
||||
dst: /usr/share/licenses/sing-box/LICENSE
|
||||
deb:
|
||||
signature:
|
||||
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||
fields:
|
||||
Bugs: https://github.com/SagerNet/sing-box/issues
|
||||
rpm:
|
||||
signature:
|
||||
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||
conflicts:
|
||||
- sing-box-beta
|
||||
- id: package_beta
|
||||
<<: *template
|
||||
package_name: sing-box-beta
|
||||
file_name_template: '{{ .ProjectName }}-beta_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
conflicts:
|
||||
- sing-box
|
||||
release:
|
||||
disable: true
|
||||
furies:
|
||||
- account: sagernet
|
||||
ids:
|
||||
- package
|
||||
disable: "{{ not (not .Prerelease) }}"
|
||||
- account: sagernet
|
||||
ids:
|
||||
- package_beta
|
||||
disable: "{{ not .Prerelease }}"
|
213
.goreleaser.yaml
213
.goreleaser.yaml
@ -1,213 +0,0 @@
|
||||
version: 2
|
||||
project_name: sing-box
|
||||
builds:
|
||||
- &template
|
||||
id: main
|
||||
main: ./cmd/sing-box
|
||||
flags:
|
||||
- -v
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||
- -s
|
||||
- -buildid=
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
targets:
|
||||
- linux_386
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_arm_6
|
||||
- linux_arm_7
|
||||
- linux_s390x
|
||||
- linux_riscv64
|
||||
- linux_mips64le
|
||||
- windows_amd64_v1
|
||||
- windows_386
|
||||
- windows_arm64
|
||||
- darwin_amd64_v1
|
||||
- darwin_arm64
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
- id: legacy
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOROOT={{ .Env.GOPATH }}/go_legacy
|
||||
tool: "{{ .Env.GOPATH }}/go_legacy/bin/go"
|
||||
targets:
|
||||
- windows_amd64_v1
|
||||
- windows_386
|
||||
- id: android
|
||||
<<: *template
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- GOTOOLCHAIN=local
|
||||
overrides:
|
||||
- goos: android
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
env:
|
||||
- CC=armv7a-linux-androideabi21-clang
|
||||
- CXX=armv7a-linux-androideabi21-clang++
|
||||
- goos: android
|
||||
goarch: arm64
|
||||
env:
|
||||
- CC=aarch64-linux-android21-clang
|
||||
- CXX=aarch64-linux-android21-clang++
|
||||
- goos: android
|
||||
goarch: 386
|
||||
env:
|
||||
- CC=i686-linux-android21-clang
|
||||
- CXX=i686-linux-android21-clang++
|
||||
- goos: android
|
||||
goarch: amd64
|
||||
goamd64: v1
|
||||
env:
|
||||
- CC=x86_64-linux-android21-clang
|
||||
- CXX=x86_64-linux-android21-clang++
|
||||
targets:
|
||||
- android_arm_7
|
||||
- android_arm64
|
||||
- android_386
|
||||
- android_amd64
|
||||
archives:
|
||||
- &template
|
||||
id: archive
|
||||
builds:
|
||||
- main
|
||||
- android
|
||||
formats:
|
||||
- tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats:
|
||||
- zip
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
- id: archive-legacy
|
||||
<<: *template
|
||||
builds:
|
||||
- legacy
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
|
||||
nfpms:
|
||||
- id: package
|
||||
package_name: sing-box
|
||||
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}_{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
builds:
|
||||
- main
|
||||
homepage: https://sing-box.sagernet.org/
|
||||
maintainer: nekohasekai <contact-git@sekai.icu>
|
||||
description: The universal proxy platform.
|
||||
license: GPLv3 or later
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
- archlinux
|
||||
# - apk
|
||||
# - ipk
|
||||
priority: extra
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: "config|noreplace"
|
||||
|
||||
- src: release/config/sing-box.service
|
||||
dst: /usr/lib/systemd/system/sing-box.service
|
||||
- src: release/config/sing-box@.service
|
||||
dst: /usr/lib/systemd/system/sing-box@.service
|
||||
- src: release/config/sing-box.sysusers
|
||||
dst: /usr/lib/sysusers.d/sing-box.conf
|
||||
- src: release/config/sing-box.rules
|
||||
dst: /usr/share/polkit-1/rules.d/sing-box.rules
|
||||
- src: release/config/sing-box-split-dns.xml
|
||||
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||
|
||||
- src: release/completions/sing-box.bash
|
||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||
- src: release/completions/sing-box.fish
|
||||
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
- src: release/completions/sing-box.zsh
|
||||
dst: /usr/share/zsh/site-functions/_sing-box
|
||||
|
||||
- src: LICENSE
|
||||
dst: /usr/share/licenses/sing-box/LICENSE
|
||||
deb:
|
||||
signature:
|
||||
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||
fields:
|
||||
Bugs: https://github.com/SagerNet/sing-box/issues
|
||||
rpm:
|
||||
signature:
|
||||
key_file: "{{ .Env.NFPM_KEY_PATH }}"
|
||||
overrides:
|
||||
apk:
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: config
|
||||
|
||||
- src: release/config/sing-box.initd
|
||||
dst: /etc/init.d/sing-box
|
||||
|
||||
- src: release/completions/sing-box.bash
|
||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||
- src: release/completions/sing-box.fish
|
||||
dst: /usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
- src: release/completions/sing-box.zsh
|
||||
dst: /usr/share/zsh/site-functions/_sing-box
|
||||
|
||||
- src: LICENSE
|
||||
dst: /usr/share/licenses/sing-box/LICENSE
|
||||
ipk:
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: config
|
||||
|
||||
- src: release/config/openwrt.init
|
||||
dst: /etc/init.d/sing-box
|
||||
- src: release/config/openwrt.conf
|
||||
dst: /etc/config/sing-box
|
||||
source:
|
||||
enabled: false
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
||||
prefix_template: '{{ .ProjectName }}-{{ .Version }}/'
|
||||
checksum:
|
||||
disable: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}.checksum'
|
||||
signs:
|
||||
- artifacts: checksum
|
||||
release:
|
||||
github:
|
||||
owner: SagerNet
|
||||
name: sing-box
|
||||
draft: true
|
||||
prerelease: auto
|
||||
mode: replace
|
||||
ids:
|
||||
- archive
|
||||
- package
|
||||
skip_upload: true
|
||||
partial:
|
||||
by: target
|
27
Dockerfile
27
Dockerfile
@ -1,27 +0,0 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
COPY . /go/src/github.com/sagernet/sing-box
|
||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||
ARG TARGETOS TARGETARCH
|
||||
ARG GOPROXY=""
|
||||
ENV GOPROXY ${GOPROXY}
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=$TARGETOS
|
||||
ENV GOARCH=$TARGETARCH
|
||||
RUN set -ex \
|
||||
&& apk add git build-base \
|
||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||
&& go build -v -trimpath -tags \
|
||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale" \
|
||||
-o /go/bin/sing-box \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
RUN set -ex \
|
||||
&& apk upgrade \
|
||||
&& apk add bash tzdata ca-certificates nftables \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
||||
ENTRYPOINT ["sing-box"]
|
5
LICENSE
5
LICENSE
@ -11,7 +11,4 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
In addition, no derivative work may use the name or imply association
|
||||
with this application without prior consent.
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
249
Makefile
249
Makefile
@ -1,248 +1,49 @@
|
||||
NAME = sing-box
|
||||
COMMIT = $(shell git rev-parse --short HEAD)
|
||||
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
|
||||
NAME=sing-box
|
||||
COMMIT=$(shell git rev-parse --short HEAD)
|
||||
PARAMS=-trimpath -tags '$(TAGS)' -ldflags \
|
||||
'-X "github.com/sagernet/sing-box/constant.Commit=$(COMMIT)" \
|
||||
-w -s -buildid='
|
||||
MAIN=./cmd/sing-box
|
||||
|
||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
|
||||
|
||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
|
||||
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
||||
MAIN = ./cmd/sing-box
|
||||
PREFIX ?= $(shell go env GOPATH)
|
||||
|
||||
.PHONY: test release docs build
|
||||
.PHONY: test
|
||||
|
||||
build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
go build $(PARAMS) $(MAIN)
|
||||
|
||||
ci_build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(PARAMS) $(MAIN) && \
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
generate_completions:
|
||||
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
|
||||
action_version: build
|
||||
echo "::set-output name=VERSION::`./sing-box version -n`"
|
||||
|
||||
install:
|
||||
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
fmt:
|
||||
@gofumpt -l -w .
|
||||
@gofmt -s -w .
|
||||
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
|
||||
go install $(PARAMS) $(MAIN)
|
||||
|
||||
fmt_install:
|
||||
go install -v mvdan.cc/gofumpt@latest
|
||||
go install -v github.com/daixiang0/gci@latest
|
||||
go install -v github.com/daixiang0/gci@v0.4.0
|
||||
|
||||
fmt:
|
||||
gofumpt -l -w .
|
||||
gofmt -s -w .
|
||||
gci write -s "standard,prefix(github.com/sagernet/),default" .
|
||||
|
||||
lint_install:
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
lint:
|
||||
GOOS=linux golangci-lint run ./...
|
||||
GOOS=android golangci-lint run ./...
|
||||
GOOS=windows golangci-lint run ./...
|
||||
GOOS=darwin golangci-lint run ./...
|
||||
GOOS=freebsd golangci-lint run ./...
|
||||
|
||||
lint_install:
|
||||
go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
proto:
|
||||
@go run ./cmd/internal/protogen
|
||||
@gofumpt -l -w .
|
||||
@gofumpt -l -w .
|
||||
|
||||
proto_install:
|
||||
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
|
||||
update_certificates:
|
||||
go run ./cmd/internal/update_certificates
|
||||
|
||||
release:
|
||||
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
||||
mkdir dist/release
|
||||
mv dist/*.tar.gz \
|
||||
dist/*.zip \
|
||||
dist/*.deb \
|
||||
dist/*.rpm \
|
||||
dist/*_amd64.pkg.tar.zst \
|
||||
dist/*_arm64.pkg.tar.zst \
|
||||
dist/release
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
rm -r dist/release
|
||||
|
||||
release_repo:
|
||||
go run ./cmd/internal/build goreleaser release -f .goreleaser.fury.yaml --clean
|
||||
|
||||
release_install:
|
||||
go install -v github.com/tcnksm/ghr@latest
|
||||
|
||||
update_android_version:
|
||||
go run ./cmd/internal/update_android_version
|
||||
|
||||
build_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease :app:assembleOtherRelease && ./gradlew --stop
|
||||
|
||||
upload_android:
|
||||
mkdir -p dist/release_android
|
||||
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android
|
||||
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*-universal.apk dist/release_android
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||
rm -rf dist/release_android
|
||||
|
||||
release_android: lib_android update_android_version build_android upload_android
|
||||
|
||||
publish_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
|
||||
|
||||
# TODO: find why and remove `-destination 'generic/platform=iOS'`
|
||||
# TODO: remove xcode clean when fix control widget fixed
|
||||
build_ios:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFI.xcarchive && \
|
||||
xcodebuild clean -scheme SFI && \
|
||||
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates
|
||||
|
||||
upload_ios_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||
|
||||
release_ios: build_ios upload_ios_app_store
|
||||
|
||||
build_macos:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFM.xcarchive && \
|
||||
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates
|
||||
|
||||
upload_macos_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||
|
||||
release_macos: build_macos upload_macos_app_store
|
||||
|
||||
build_macos_standalone:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFM.System.xcarchive && \
|
||||
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive -allowProvisioningUpdates
|
||||
|
||||
build_macos_dmg:
|
||||
rm -rf dist/SFM
|
||||
mkdir -p dist/SFM
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFM.System && \
|
||||
rm -rf build/SFM.dmg && \
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "build/SFM.System.xcarchive" \
|
||||
-exportOptionsPlist SFM.System/Export.plist -allowProvisioningUpdates \
|
||||
-exportPath "build/SFM.System" && \
|
||||
create-dmg \
|
||||
--volname "sing-box" \
|
||||
--volicon "build/SFM.System/SFM.app/Contents/Resources/AppIcon.icns" \
|
||||
--icon "SFM.app" 0 0 \
|
||||
--hide-extension "SFM.app" \
|
||||
--app-drop-link 0 0 \
|
||||
--skip-jenkins \
|
||||
"../sing-box/dist/SFM/SFM.dmg" "build/SFM.System/SFM.app"
|
||||
|
||||
notarize_macos_dmg:
|
||||
xcrun notarytool submit "dist/SFM/SFM.dmg" --wait \
|
||||
--keychain-profile "notarytool-password" \
|
||||
--no-s3-acceleration
|
||||
|
||||
upload_macos_dmg:
|
||||
cd dist/SFM && \
|
||||
cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dmg"
|
||||
|
||||
upload_macos_dsyms:
|
||||
pushd ../sing-box-for-apple/build/SFM.System.xcarchive && \
|
||||
zip -r SFM.dSYMs.zip dSYMs && \
|
||||
mv SFM.dSYMs.zip ../../../sing-box/dist/SFM && \
|
||||
popd && \
|
||||
cd dist/SFM && \
|
||||
cp SFM.dSYMs.zip "SFM-${VERSION}-universal.dSYMs.zip" && \
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dSYMs.zip"
|
||||
|
||||
release_macos_standalone: build_macos_standalone build_macos_dmg notarize_macos_dmg upload_macos_dmg upload_macos_dsyms
|
||||
|
||||
build_tvos:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFT.xcarchive && \
|
||||
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates
|
||||
|
||||
upload_tvos_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||
|
||||
release_tvos: build_tvos upload_tvos_app_store
|
||||
|
||||
update_apple_version:
|
||||
go run ./cmd/internal/update_apple_version
|
||||
|
||||
update_macos_version:
|
||||
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
|
||||
|
||||
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
|
||||
|
||||
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
||||
|
||||
publish_testflight:
|
||||
go run -v ./cmd/internal/app_store_connect publish_testflight
|
||||
|
||||
prepare_app_store:
|
||||
go run -v ./cmd/internal/app_store_connect prepare_app_store
|
||||
|
||||
publish_app_store:
|
||||
go run -v ./cmd/internal/app_store_connect publish_app_store
|
||||
|
||||
test:
|
||||
@go test -v ./... && \
|
||||
cd test && \
|
||||
go mod tidy && \
|
||||
go test -v -tags "$(TAGS_TEST)" .
|
||||
|
||||
test_stdio:
|
||||
@go test -v ./... && \
|
||||
cd test && \
|
||||
go mod tidy && \
|
||||
go test -v -tags "$(TAGS_TEST),force_stdio" .
|
||||
|
||||
lib_android:
|
||||
go run ./cmd/internal/build_libbox -target android
|
||||
|
||||
lib_android_debug:
|
||||
go run ./cmd/internal/build_libbox -target android -debug
|
||||
|
||||
lib_apple:
|
||||
go run ./cmd/internal/build_libbox -target apple
|
||||
|
||||
lib_ios:
|
||||
go run ./cmd/internal/build_libbox -target apple -platform ios -debug
|
||||
|
||||
lib:
|
||||
go run ./cmd/internal/build_libbox -target android
|
||||
go run ./cmd/internal/build_libbox -target ios
|
||||
|
||||
lib_install:
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.6
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.6
|
||||
|
||||
docs:
|
||||
venv/bin/mkdocs serve
|
||||
|
||||
publish_docs:
|
||||
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
||||
|
||||
docs_install:
|
||||
python -m venv venv
|
||||
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
|
||||
go test -v . && \
|
||||
pushd test && \
|
||||
go test -v . && \
|
||||
popd
|
||||
|
||||
clean:
|
||||
rm -rf bin dist sing-box
|
||||
rm -f $(shell go env GOPATH)/sing-box
|
||||
|
||||
update:
|
||||
git fetch
|
||||
git reset FETCH_HEAD --hard
|
||||
git clean -fdx
|
||||
git clean -fdx
|
@ -2,8 +2,6 @@
|
||||
|
||||
The universal proxy platform.
|
||||
|
||||
[](https://repology.org/project/sing-box/versions)
|
||||
|
||||
## Documentation
|
||||
|
||||
https://sing-box.sagernet.org
|
||||
@ -25,7 +23,4 @@ GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
In addition, no derivative work may use the name or imply association
|
||||
with this application without prior consent.
|
||||
```
|
@ -1,21 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type CertificateStore interface {
|
||||
LifecycleService
|
||||
Pool() *x509.CertPool
|
||||
}
|
||||
|
||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||
store := service.FromContext[CertificateStore](ctx)
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return store.Pool()
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ConnectionManager interface {
|
||||
Lifecycle
|
||||
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type DNSRouter interface {
|
||||
Lifecycle
|
||||
Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, domain string, options DNSQueryOptions) ([]netip.Addr, error)
|
||||
ClearCache()
|
||||
LookupReverseMapping(ip netip.Addr) (string, bool)
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
type DNSClient interface {
|
||||
Start()
|
||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
|
||||
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
|
||||
ClearCache()
|
||||
}
|
||||
|
||||
type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
}
|
||||
|
||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||
if options == nil {
|
||||
return &DNSQueryOptions{}, nil
|
||||
}
|
||||
transportManager := service.FromContext[DNSTransportManager](ctx)
|
||||
transport, loaded := transportManager.Transport(options.Server)
|
||||
if !loaded {
|
||||
return nil, E.New("domain resolver not found: " + options.Server)
|
||||
}
|
||||
return &DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
DisableCache: options.DisableCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type RDRCStore interface {
|
||||
LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
|
||||
SaveRDRC(transportName string, qName string, qType uint16) error
|
||||
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
||||
}
|
||||
|
||||
type DNSTransport interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
Dependencies() []string
|
||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||
}
|
||||
|
||||
type LegacyDNSTransport interface {
|
||||
LegacyStrategy() C.DomainStrategy
|
||||
LegacyClientSubnet() netip.Prefix
|
||||
}
|
||||
|
||||
type DNSTransportRegistry interface {
|
||||
option.DNSTransportOptionsRegistry
|
||||
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
||||
}
|
||||
|
||||
type DNSTransportManager interface {
|
||||
Lifecycle
|
||||
Transports() []DNSTransport
|
||||
Transport(tag string) (DNSTransport, bool)
|
||||
Default() DNSTransport
|
||||
FakeIP() FakeIPTransport
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type Endpoint interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
Outbound
|
||||
}
|
||||
|
||||
type EndpointRegistry interface {
|
||||
option.EndpointOptionsRegistry
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) (Endpoint, error)
|
||||
}
|
||||
|
||||
type EndpointManager interface {
|
||||
Lifecycle
|
||||
Endpoints() []Endpoint
|
||||
Get(tag string) (Endpoint, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) error
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import "github.com/sagernet/sing-box/option"
|
||||
|
||||
type Adapter struct {
|
||||
endpointType string
|
||||
endpointTag string
|
||||
network []string
|
||||
dependencies []string
|
||||
}
|
||||
|
||||
func NewAdapter(endpointType string, endpointTag string, network []string, dependencies []string) Adapter {
|
||||
return Adapter{
|
||||
endpointType: endpointType,
|
||||
endpointTag: endpointTag,
|
||||
network: network,
|
||||
dependencies: dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAdapterWithDialerOptions(endpointType string, endpointTag string, network []string, dialOptions option.DialerOptions) Adapter {
|
||||
var dependencies []string
|
||||
if dialOptions.Detour != "" {
|
||||
dependencies = []string{dialOptions.Detour}
|
||||
}
|
||||
return NewAdapter(endpointType, endpointTag, network, dependencies)
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.endpointType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.endpointTag
|
||||
}
|
||||
|
||||
func (a *Adapter) Network() []string {
|
||||
return a.network
|
||||
}
|
||||
|
||||
func (a *Adapter) Dependencies() []string {
|
||||
return a.dependencies
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.EndpointManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.EndpointRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
endpoints []adapter.Endpoint
|
||||
endpointByTag map[string]adapter.Endpoint
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpointByTag: make(map[string]adapter.Endpoint),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
if stage == adapter.StartStateStart {
|
||||
// started with outbound manager
|
||||
return nil
|
||||
}
|
||||
for _, endpoint := range m.endpoints {
|
||||
err := adapter.LegacyStart(endpoint, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
endpoints := m.endpoints
|
||||
m.endpoints = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, endpoint := range endpoints {
|
||||
monitor.Start("close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||
err = E.Append(err, endpoint.Close(), func(err error) error {
|
||||
return E.Cause(err, "close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Endpoints() []adapter.Endpoint {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.endpoints
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.Endpoint, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
endpoint, found := m.endpointByTag[tag]
|
||||
return endpoint, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
endpoint, found := m.endpointByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.endpointByTag, tag)
|
||||
index := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
|
||||
return it == endpoint
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid endpoint index")
|
||||
}
|
||||
m.endpoints = append(m.endpoints[:index], m.endpoints[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return endpoint.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
|
||||
endpoint, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(endpoint, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsEndpoint.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close endpoint/", existsEndpoint.Type(), "[", existsEndpoint.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
|
||||
return it == existsEndpoint
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid endpoint index")
|
||||
}
|
||||
m.endpoints = append(m.endpoints[:existsIndex], m.endpoints[existsIndex+1:]...)
|
||||
}
|
||||
m.endpoints = append(m.endpoints, endpoint)
|
||||
m.endpointByTag[tag] = endpoint
|
||||
return nil
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Endpoint, error)
|
||||
|
||||
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(outboundType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Endpoint, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.EndpointRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Endpoint, error)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
access sync.Mutex
|
||||
optionsType map[string]optionsConstructorFunc
|
||||
constructor map[string]constructorFunc
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
optionsType: make(map[string]optionsConstructorFunc),
|
||||
constructor: make(map[string]constructorFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
optionsConstructor, loaded := m.optionsType[outboundType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Endpoint, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
constructor, loaded := m.constructor[outboundType]
|
||||
if !loaded {
|
||||
return nil, E.New("outbound type not found: " + outboundType)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, options)
|
||||
}
|
||||
|
||||
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.optionsType[outboundType] = optionsConstructor
|
||||
m.constructor[outboundType] = constructor
|
||||
}
|
@ -1,121 +1,31 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"time"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ClashServer interface {
|
||||
LifecycleService
|
||||
ConnectionTracker
|
||||
Mode() string
|
||||
ModeList() []string
|
||||
HistoryStorage() URLTestHistoryStorage
|
||||
Service
|
||||
TrafficController
|
||||
}
|
||||
|
||||
type URLTestHistory struct {
|
||||
Time time.Time `json:"time"`
|
||||
Delay uint16 `json:"delay"`
|
||||
type Tracker interface {
|
||||
Leave()
|
||||
}
|
||||
|
||||
type URLTestHistoryStorage interface {
|
||||
SetHook(hook chan<- struct{})
|
||||
LoadURLTestHistory(tag string) *URLTestHistory
|
||||
DeleteURLTestHistory(tag string)
|
||||
StoreURLTestHistory(tag string, history *URLTestHistory)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type V2RayServer interface {
|
||||
LifecycleService
|
||||
StatsService() ConnectionTracker
|
||||
}
|
||||
|
||||
type CacheFile interface {
|
||||
LifecycleService
|
||||
|
||||
StoreFakeIP() bool
|
||||
FakeIPStorage
|
||||
|
||||
StoreRDRC() bool
|
||||
RDRCStore
|
||||
|
||||
LoadMode() string
|
||||
StoreMode(mode string) error
|
||||
LoadSelected(group string) string
|
||||
StoreSelected(group string, selected string) error
|
||||
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
||||
StoreGroupExpand(group string, expand bool) error
|
||||
LoadRuleSet(tag string) *SavedBinary
|
||||
SaveRuleSet(tag string, set *SavedBinary) error
|
||||
}
|
||||
|
||||
type SavedBinary struct {
|
||||
Content []byte
|
||||
LastUpdated time.Time
|
||||
LastEtag string
|
||||
}
|
||||
|
||||
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = varbin.Write(&buffer, binary.BigEndian, s.Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = binary.Write(&buffer, binary.BigEndian, s.LastUpdated.Unix())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
||||
reader := bytes.NewReader(data)
|
||||
var version uint8
|
||||
err := binary.Read(reader, binary.BigEndian, &version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = varbin.Read(reader, binary.BigEndian, &s.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var lastUpdated int64
|
||||
err = binary.Read(reader, binary.BigEndian, &lastUpdated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.LastUpdated = time.Unix(lastUpdated, 0)
|
||||
err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
type TrafficController interface {
|
||||
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
|
||||
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
|
||||
}
|
||||
|
||||
type OutboundGroup interface {
|
||||
Outbound
|
||||
Now() string
|
||||
All() []string
|
||||
}
|
||||
|
||||
type URLTestGroup interface {
|
||||
OutboundGroup
|
||||
URLTest(ctx context.Context) (map[string]uint16, error)
|
||||
}
|
||||
|
||||
func OutboundTag(detour Outbound) string {
|
||||
if group, isGroup := detour.(OutboundGroup); isGroup {
|
||||
return group.Now()
|
||||
|
@ -1,31 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
type FakeIPStore interface {
|
||||
SimpleLifecycle
|
||||
Contains(address netip.Addr) bool
|
||||
Create(domain string, isIPv6 bool) (netip.Addr, error)
|
||||
Lookup(address netip.Addr) (string, bool)
|
||||
Reset() error
|
||||
}
|
||||
|
||||
type FakeIPStorage interface {
|
||||
FakeIPMetadata() *FakeIPMetadata
|
||||
FakeIPSaveMetadata(metadata *FakeIPMetadata) error
|
||||
FakeIPSaveMetadataAsync(metadata *FakeIPMetadata)
|
||||
FakeIPStore(address netip.Addr, domain string) error
|
||||
FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger)
|
||||
FakeIPLoad(address netip.Addr) (string, bool)
|
||||
FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bool)
|
||||
FakeIPReset() error
|
||||
}
|
||||
|
||||
type FakeIPTransport interface {
|
||||
DNSTransport
|
||||
Store() FakeIPStore
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
type FakeIPMetadata struct {
|
||||
Inet4Range netip.Prefix
|
||||
Inet6Range netip.Prefix
|
||||
Inet4Current netip.Addr
|
||||
Inet6Current netip.Addr
|
||||
}
|
||||
|
||||
func (m *FakeIPMetadata) MarshalBinary() (data []byte, err error) {
|
||||
var buffer bytes.Buffer
|
||||
for _, marshaler := range []encoding.BinaryMarshaler{m.Inet4Range, m.Inet6Range, m.Inet4Current, m.Inet6Current} {
|
||||
data, err = marshaler.MarshalBinary()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
common.Must(binary.Write(&buffer, binary.BigEndian, uint16(len(data))))
|
||||
buffer.Write(data)
|
||||
}
|
||||
data = buffer.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func (m *FakeIPMetadata) UnmarshalBinary(data []byte) error {
|
||||
reader := bytes.NewReader(data)
|
||||
for _, unmarshaler := range []encoding.BinaryUnmarshaler{&m.Inet4Range, &m.Inet6Range, &m.Inet4Current, &m.Inet6Current} {
|
||||
var length uint16
|
||||
common.Must(binary.Read(reader, binary.BigEndian, &length))
|
||||
element := make([]byte, length)
|
||||
_, err := io.ReadFull(reader, element)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = unmarshaler.UnmarshalBinary(element)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -6,56 +6,27 @@ import (
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
// Deprecated
|
||||
type ConnectionHandler interface {
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type ConnectionHandlerEx interface {
|
||||
NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
// Deprecated: use PacketHandlerEx instead
|
||||
type PacketHandler interface {
|
||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type PacketHandlerEx interface {
|
||||
NewPacketEx(buffer *buf.Buffer, source M.Socksaddr)
|
||||
}
|
||||
|
||||
// Deprecated: use OOBPacketHandlerEx instead
|
||||
type OOBPacketHandler interface {
|
||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type OOBPacketHandlerEx interface {
|
||||
NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
type PacketConnectionHandler interface {
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type PacketConnectionHandlerEx interface {
|
||||
NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
// Deprecated: use TCPConnectionHandlerEx instead
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type UpstreamHandlerAdapter interface {
|
||||
N.TCPConnectionHandler
|
||||
N.UDPConnectionHandler
|
||||
E.Handler
|
||||
}
|
||||
|
||||
type UpstreamHandlerAdapterEx interface {
|
||||
N.TCPConnectionHandlerEx
|
||||
N.UDPConnectionHandlerEx
|
||||
}
|
||||
|
@ -3,112 +3,39 @@ package adapter
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/process"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-dns"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
type Inbound interface {
|
||||
Lifecycle
|
||||
Service
|
||||
Type() string
|
||||
Tag() string
|
||||
}
|
||||
|
||||
type TCPInjectableInbound interface {
|
||||
Inbound
|
||||
ConnectionHandlerEx
|
||||
}
|
||||
|
||||
type UDPInjectableInbound interface {
|
||||
Inbound
|
||||
PacketConnectionHandlerEx
|
||||
}
|
||||
|
||||
type InboundRegistry interface {
|
||||
option.InboundOptionsRegistry
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
|
||||
}
|
||||
|
||||
type InboundManager interface {
|
||||
Lifecycle
|
||||
Inbounds() []Inbound
|
||||
Get(tag string) (Inbound, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error
|
||||
}
|
||||
|
||||
type InboundContext struct {
|
||||
Inbound string
|
||||
InboundType string
|
||||
IPVersion uint8
|
||||
Network string
|
||||
Source M.Socksaddr
|
||||
Destination M.Socksaddr
|
||||
Domain string
|
||||
Protocol string
|
||||
User string
|
||||
Outbound string
|
||||
|
||||
// sniffer
|
||||
|
||||
Protocol string
|
||||
Domain string
|
||||
Client string
|
||||
SniffContext any
|
||||
PacketSniffError error
|
||||
|
||||
// cache
|
||||
|
||||
// Deprecated: implement in rule action
|
||||
InboundDetour string
|
||||
LastInbound string
|
||||
OriginDestination M.Socksaddr
|
||||
RouteOriginalDestination M.Socksaddr
|
||||
// Deprecated: to be removed
|
||||
//nolint:staticcheck
|
||||
InboundOptions option.InboundOptions
|
||||
UDPDisableDomainUnmapping bool
|
||||
UDPConnect bool
|
||||
UDPTimeout time.Duration
|
||||
TLSFragment bool
|
||||
TLSFragmentFallbackDelay time.Duration
|
||||
TLSRecordFragment bool
|
||||
DomainStrategy dns.DomainStrategy
|
||||
SniffEnabled bool
|
||||
SniffOverrideDestination bool
|
||||
DestinationAddresses []netip.Addr
|
||||
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
FallbackDelay time.Duration
|
||||
|
||||
DestinationAddresses []netip.Addr
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *process.Info
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
|
||||
// rule cache
|
||||
|
||||
IPCIDRMatchSource bool
|
||||
IPCIDRAcceptEmpty bool
|
||||
|
||||
SourceAddressMatch bool
|
||||
SourcePortMatch bool
|
||||
DestinationAddressMatch bool
|
||||
DestinationPortMatch bool
|
||||
DidMatch bool
|
||||
IgnoreDestinationIPCIDRMatch bool
|
||||
}
|
||||
|
||||
func (c *InboundContext) ResetRuleCache() {
|
||||
c.IPCIDRMatchSource = false
|
||||
c.IPCIDRAcceptEmpty = false
|
||||
c.SourceAddressMatch = false
|
||||
c.SourcePortMatch = false
|
||||
c.DestinationAddressMatch = false
|
||||
c.DestinationPortMatch = false
|
||||
c.DidMatch = false
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *process.Info
|
||||
}
|
||||
|
||||
type inboundContextKey struct{}
|
||||
@ -125,19 +52,11 @@ func ContextFrom(ctx context.Context) *InboundContext {
|
||||
return metadata.(*InboundContext)
|
||||
}
|
||||
|
||||
func ExtendContext(ctx context.Context) (context.Context, *InboundContext) {
|
||||
var newMetadata InboundContext
|
||||
if metadata := ContextFrom(ctx); metadata != nil {
|
||||
newMetadata = *metadata
|
||||
func AppendContext(ctx context.Context) (context.Context, *InboundContext) {
|
||||
metadata := ContextFrom(ctx)
|
||||
if metadata != nil {
|
||||
return ctx, metadata
|
||||
}
|
||||
return WithContext(ctx, &newMetadata), &newMetadata
|
||||
}
|
||||
|
||||
func OverrideContext(ctx context.Context) context.Context {
|
||||
if metadata := ContextFrom(ctx); metadata != nil {
|
||||
var newMetadata InboundContext
|
||||
newMetadata = *metadata
|
||||
return WithContext(ctx, &newMetadata)
|
||||
}
|
||||
return ctx
|
||||
metadata = new(InboundContext)
|
||||
return WithContext(ctx, metadata), metadata
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
package inbound
|
||||
|
||||
type Adapter struct {
|
||||
inboundType string
|
||||
inboundTag string
|
||||
}
|
||||
|
||||
func NewAdapter(inboundType string, inboundTag string) Adapter {
|
||||
return Adapter{
|
||||
inboundType: inboundType,
|
||||
inboundTag: inboundTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.inboundType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.inboundTag
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.InboundManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.InboundRegistry
|
||||
endpoint adapter.EndpointManager
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
inbounds []adapter.Inbound
|
||||
inboundByTag map[string]adapter.Inbound
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpoint: endpoint,
|
||||
inboundByTag: make(map[string]adapter.Inbound),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
inbounds := m.inbounds
|
||||
m.access.Unlock()
|
||||
for _, inbound := range inbounds {
|
||||
err := adapter.LegacyStart(inbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
inbounds := m.inbounds
|
||||
m.inbounds = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, inbound := range inbounds {
|
||||
monitor.Start("close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||
err = E.Append(err, inbound.Close(), func(err error) error {
|
||||
return E.Cause(err, "close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Inbounds() []adapter.Inbound {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.inbounds
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.Inbound, bool) {
|
||||
m.access.Lock()
|
||||
inbound, found := m.inboundByTag[tag]
|
||||
m.access.Unlock()
|
||||
if found {
|
||||
return inbound, true
|
||||
}
|
||||
return m.endpoint.Get(tag)
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
inbound, found := m.inboundByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.inboundByTag, tag)
|
||||
index := common.Index(m.inbounds, func(it adapter.Inbound) bool {
|
||||
return it == inbound
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return inbound.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
|
||||
inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(inbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsInbound, loaded := m.inboundByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsInbound.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool {
|
||||
return it == existsInbound
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...)
|
||||
}
|
||||
m.inbounds = append(m.inbounds, inbound)
|
||||
m.inboundByTag[tag] = inbound
|
||||
return nil
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Inbound, error)
|
||||
|
||||
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(outboundType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Inbound, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.InboundRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
access sync.Mutex
|
||||
optionsType map[string]optionsConstructorFunc
|
||||
constructor map[string]constructorFunc
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
optionsType: make(map[string]optionsConstructorFunc),
|
||||
constructor: make(map[string]constructorFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
optionsConstructor, loaded := m.optionsType[outboundType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
constructor, loaded := m.constructor[outboundType]
|
||||
if !loaded {
|
||||
return nil, E.New("outbound type not found: " + outboundType)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, options)
|
||||
}
|
||||
|
||||
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.optionsType[outboundType] = optionsConstructor
|
||||
m.constructor[outboundType] = constructor
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
type SimpleLifecycle interface {
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type StartStage uint8
|
||||
|
||||
const (
|
||||
StartStateInitialize StartStage = iota
|
||||
StartStateStart
|
||||
StartStatePostStart
|
||||
StartStateStarted
|
||||
)
|
||||
|
||||
var ListStartStages = []StartStage{
|
||||
StartStateInitialize,
|
||||
StartStateStart,
|
||||
StartStatePostStart,
|
||||
StartStateStarted,
|
||||
}
|
||||
|
||||
func (s StartStage) String() string {
|
||||
switch s {
|
||||
case StartStateInitialize:
|
||||
return "initialize"
|
||||
case StartStateStart:
|
||||
return "start"
|
||||
case StartStatePostStart:
|
||||
return "post-start"
|
||||
case StartStateStarted:
|
||||
return "finish-start"
|
||||
default:
|
||||
panic("unknown stage")
|
||||
}
|
||||
}
|
||||
|
||||
type Lifecycle interface {
|
||||
Start(stage StartStage) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type LifecycleService interface {
|
||||
Name() string
|
||||
Lifecycle
|
||||
}
|
||||
|
||||
func Start(stage StartStage, services ...Lifecycle) error {
|
||||
for _, service := range services {
|
||||
err := service.Start(stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func StartNamed(stage StartStage, services []LifecycleService) error {
|
||||
for _, service := range services {
|
||||
err := service.Start(stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage.String(), " ", service.Name())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package adapter
|
||||
|
||||
func LegacyStart(starter any, stage StartStage) error {
|
||||
if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle {
|
||||
return lifecycle.Start(stage)
|
||||
}
|
||||
switch stage {
|
||||
case StartStateInitialize:
|
||||
if preStarter, isPreStarter := starter.(interface {
|
||||
PreStart() error
|
||||
}); isPreStarter {
|
||||
return preStarter.PreStart()
|
||||
}
|
||||
case StartStateStart:
|
||||
if starter, isStarter := starter.(interface {
|
||||
Start() error
|
||||
}); isStarter {
|
||||
return starter.Start()
|
||||
}
|
||||
case StartStateStarted:
|
||||
if postStarter, isPostStarter := starter.(interface {
|
||||
PostStart() error
|
||||
}); isPostStarter {
|
||||
return postStarter.PostStart()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type lifecycleServiceWrapper struct {
|
||||
SimpleLifecycle
|
||||
name string
|
||||
}
|
||||
|
||||
func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
|
||||
return &lifecycleServiceWrapper{
|
||||
SimpleLifecycle: service,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Name() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
|
||||
return LegacyStart(l.SimpleLifecycle, stage)
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Close() error {
|
||||
return l.SimpleLifecycle.Close()
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
)
|
||||
|
||||
type NetworkManager interface {
|
||||
Lifecycle
|
||||
InterfaceFinder() control.InterfaceFinder
|
||||
UpdateInterfaces() error
|
||||
DefaultNetworkInterface() *NetworkInterface
|
||||
NetworkInterfaces() []NetworkInterface
|
||||
AutoDetectInterface() bool
|
||||
AutoDetectInterfaceFunc() control.Func
|
||||
ProtectFunc() control.Func
|
||||
DefaultOptions() NetworkOptions
|
||||
RegisterAutoRedirectOutputMark(mark uint32) error
|
||||
AutoRedirectOutputMark() uint32
|
||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||
PackageManager() tun.PackageManager
|
||||
WIFIState() WIFIState
|
||||
ResetNetwork()
|
||||
UpdateWIFIState()
|
||||
}
|
||||
|
||||
type NetworkOptions struct {
|
||||
BindInterface string
|
||||
RoutingMark uint32
|
||||
DomainResolver string
|
||||
DomainResolveOptions DNSQueryOptions
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
FallbackDelay time.Duration
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
InterfaceUpdated()
|
||||
}
|
||||
|
||||
type WIFIState struct {
|
||||
SSID string
|
||||
BSSID string
|
||||
}
|
||||
|
||||
type NetworkInterface struct {
|
||||
control.Interface
|
||||
Type C.InterfaceType
|
||||
DNSServers []string
|
||||
Expensive bool
|
||||
Constrained bool
|
||||
}
|
@ -2,9 +2,8 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
@ -14,20 +13,7 @@ type Outbound interface {
|
||||
Type() string
|
||||
Tag() string
|
||||
Network() []string
|
||||
Dependencies() []string
|
||||
N.Dialer
|
||||
}
|
||||
|
||||
type OutboundRegistry interface {
|
||||
option.OutboundOptionsRegistry
|
||||
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||
}
|
||||
|
||||
type OutboundManager interface {
|
||||
Lifecycle
|
||||
Outbounds() []Outbound
|
||||
Outbound(tag string) (Outbound, bool)
|
||||
Default() Outbound
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type Adapter struct {
|
||||
outboundType string
|
||||
outboundTag string
|
||||
network []string
|
||||
dependencies []string
|
||||
}
|
||||
|
||||
func NewAdapter(outboundType string, outboundTag string, network []string, dependencies []string) Adapter {
|
||||
return Adapter{
|
||||
outboundType: outboundType,
|
||||
outboundTag: outboundTag,
|
||||
network: network,
|
||||
dependencies: dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAdapterWithDialerOptions(outboundType string, outboundTag string, network []string, dialOptions option.DialerOptions) Adapter {
|
||||
var dependencies []string
|
||||
if dialOptions.Detour != "" {
|
||||
dependencies = []string{dialOptions.Detour}
|
||||
}
|
||||
return NewAdapter(outboundType, outboundTag, network, dependencies)
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.outboundType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.outboundTag
|
||||
}
|
||||
|
||||
func (a *Adapter) Network() []string {
|
||||
return a.network
|
||||
}
|
||||
|
||||
func (a *Adapter) Dependencies() []string {
|
||||
return a.dependencies
|
||||
}
|
@ -1,287 +0,0 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
var _ adapter.OutboundManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.OutboundRegistry
|
||||
endpoint adapter.EndpointManager
|
||||
defaultTag string
|
||||
access sync.RWMutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
outbounds []adapter.Outbound
|
||||
outboundByTag map[string]adapter.Outbound
|
||||
dependByTag map[string][]string
|
||||
defaultOutbound adapter.Outbound
|
||||
defaultOutboundFallback adapter.Outbound
|
||||
}
|
||||
|
||||
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpoint: endpoint,
|
||||
defaultTag: defaultTag,
|
||||
outboundByTag: make(map[string]adapter.Outbound),
|
||||
dependByTag: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) {
|
||||
m.defaultOutboundFallback = defaultOutboundFallback
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
outbounds := m.outbounds
|
||||
m.access.Unlock()
|
||||
if stage == adapter.StartStateStart {
|
||||
if m.defaultTag != "" && m.defaultOutbound == nil {
|
||||
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
|
||||
if !loaded {
|
||||
return E.New("default outbound not found: ", m.defaultTag)
|
||||
}
|
||||
m.defaultOutbound = defaultEndpoint
|
||||
}
|
||||
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
|
||||
} else {
|
||||
for _, outbound := range outbounds {
|
||||
err := adapter.LegacyStart(outbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
||||
monitor := taskmonitor.New(m.logger, C.StartTimeout)
|
||||
started := make(map[string]bool)
|
||||
for {
|
||||
canContinue := false
|
||||
startOne:
|
||||
for _, outboundToStart := range outbounds {
|
||||
outboundTag := outboundToStart.Tag()
|
||||
if started[outboundTag] {
|
||||
continue
|
||||
}
|
||||
dependencies := outboundToStart.Dependencies()
|
||||
for _, dependency := range dependencies {
|
||||
if !started[dependency] {
|
||||
continue startOne
|
||||
}
|
||||
}
|
||||
started[outboundTag] = true
|
||||
canContinue = true
|
||||
if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
|
||||
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
err := starter.Start(adapter.StartStateStart)
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
}
|
||||
} else if starter, isStarter := outboundToStart.(interface {
|
||||
Start() error
|
||||
}); isStarter {
|
||||
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
err := starter.Start()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(started) == len(outbounds) {
|
||||
break
|
||||
}
|
||||
if canContinue {
|
||||
continue
|
||||
}
|
||||
currentOutbound := common.Find(outbounds, func(it adapter.Outbound) bool {
|
||||
return !started[it.Tag()]
|
||||
})
|
||||
var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error
|
||||
lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error {
|
||||
problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
|
||||
return !started[it]
|
||||
})
|
||||
if common.Contains(oTree, problemOutboundTag) {
|
||||
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
|
||||
}
|
||||
m.access.Lock()
|
||||
problemOutbound := m.outboundByTag[problemOutboundTag]
|
||||
m.access.Unlock()
|
||||
if problemOutbound == nil {
|
||||
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]")
|
||||
}
|
||||
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
|
||||
}
|
||||
return lintOutbound([]string{currentOutbound.Tag()}, currentOutbound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
m.access.Lock()
|
||||
if !m.started {
|
||||
m.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
outbounds := m.outbounds
|
||||
m.outbounds = nil
|
||||
m.access.Unlock()
|
||||
var err error
|
||||
for _, outbound := range outbounds {
|
||||
if closer, isCloser := outbound.(io.Closer); isCloser {
|
||||
monitor.Start("close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
err = E.Append(err, closer.Close(), func(err error) error {
|
||||
return E.Cause(err, "close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Outbounds() []adapter.Outbound {
|
||||
m.access.RLock()
|
||||
defer m.access.RUnlock()
|
||||
return m.outbounds
|
||||
}
|
||||
|
||||
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
||||
m.access.RLock()
|
||||
outbound, found := m.outboundByTag[tag]
|
||||
m.access.RUnlock()
|
||||
if found {
|
||||
return outbound, true
|
||||
}
|
||||
return m.endpoint.Get(tag)
|
||||
}
|
||||
|
||||
func (m *Manager) Default() adapter.Outbound {
|
||||
m.access.RLock()
|
||||
defer m.access.RUnlock()
|
||||
if m.defaultOutbound != nil {
|
||||
return m.defaultOutbound
|
||||
} else {
|
||||
return m.defaultOutboundFallback
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
outbound, found := m.outboundByTag[tag]
|
||||
if !found {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.outboundByTag, tag)
|
||||
index := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it == outbound
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...)
|
||||
started := m.started
|
||||
if m.defaultOutbound == outbound {
|
||||
if len(m.outbounds) > 0 {
|
||||
m.defaultOutbound = m.outbounds[0]
|
||||
m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag())
|
||||
} else {
|
||||
m.defaultOutbound = nil
|
||||
}
|
||||
}
|
||||
dependBy := m.dependByTag[tag]
|
||||
if len(dependBy) > 0 {
|
||||
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
|
||||
}
|
||||
dependencies := outbound.Dependencies()
|
||||
for _, dependency := range dependencies {
|
||||
if len(m.dependByTag[dependency]) == 1 {
|
||||
delete(m.dependByTag, dependency)
|
||||
} else {
|
||||
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
|
||||
return it != tag
|
||||
})
|
||||
}
|
||||
}
|
||||
if started {
|
||||
return common.Close(outbound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error {
|
||||
if tag == "" {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
outbound, err := m.registry.CreateOutbound(ctx, router, logger, tag, inboundType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(outbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = common.Close(existsOutbound)
|
||||
if err != nil {
|
||||
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it == existsOutbound
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...)
|
||||
}
|
||||
m.outbounds = append(m.outbounds, outbound)
|
||||
m.outboundByTag[tag] = outbound
|
||||
dependencies := outbound.Dependencies()
|
||||
for _, dependency := range dependencies {
|
||||
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
|
||||
}
|
||||
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
|
||||
m.defaultOutbound = outbound
|
||||
if m.started {
|
||||
m.logger.Info("updated default outbound to ", outbound.Tag())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Outbound, error)
|
||||
|
||||
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(outboundType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Outbound, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.OutboundRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
access sync.Mutex
|
||||
optionsType map[string]optionsConstructorFunc
|
||||
constructors map[string]constructorFunc
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
optionsType: make(map[string]optionsConstructorFunc),
|
||||
constructors: make(map[string]constructorFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
optionsConstructor, loaded := r.optionsType[outboundType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
constructor, loaded := r.constructors[outboundType]
|
||||
if !loaded {
|
||||
return nil, E.New("outbound type not found: " + outboundType)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, options)
|
||||
}
|
||||
|
||||
func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
r.optionsType[outboundType] = optionsConstructor
|
||||
r.constructors[outboundType] = constructor
|
||||
}
|
@ -1 +0,0 @@
|
||||
package adapter
|
@ -2,111 +2,55 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"net/netip"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing-box/common/geoip"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
|
||||
"go4.org/netipx"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
)
|
||||
|
||||
type Router interface {
|
||||
Lifecycle
|
||||
ConnectionRouter
|
||||
PreMatch(metadata InboundContext) error
|
||||
ConnectionRouterEx
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
NeedWIFIState() bool
|
||||
Rules() []Rule
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
ResetNetwork()
|
||||
}
|
||||
Service
|
||||
|
||||
type ConnectionTracker interface {
|
||||
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) net.Conn
|
||||
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) N.PacketConn
|
||||
}
|
||||
Outbounds() []Outbound
|
||||
Outbound(tag string) (Outbound, bool)
|
||||
DefaultOutbound(network string) Outbound
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
type ConnectionRouter interface {
|
||||
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
|
||||
GeoIPReader() *geoip.Reader
|
||||
LoadGeosite(code string) (Rule, error)
|
||||
|
||||
Exchange(ctx context.Context, message *dnsmessage.Message) (*dnsmessage.Message, error)
|
||||
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
||||
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
||||
|
||||
InterfaceBindManager() control.BindManager
|
||||
DefaultInterface() string
|
||||
AutoDetectInterface() bool
|
||||
DefaultMark() int
|
||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||
Rules() []Rule
|
||||
SetTrafficController(controller TrafficController)
|
||||
}
|
||||
|
||||
type ConnectionRouterEx interface {
|
||||
ConnectionRouter
|
||||
RouteConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
type Rule interface {
|
||||
Service
|
||||
Type() string
|
||||
UpdateGeosite() error
|
||||
Match(metadata *InboundContext) bool
|
||||
Outbound() string
|
||||
String() string
|
||||
}
|
||||
|
||||
type RuleSet interface {
|
||||
Name() string
|
||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||
PostStart() error
|
||||
Metadata() RuleSetMetadata
|
||||
ExtractIPSet() []*netipx.IPSet
|
||||
IncRef()
|
||||
DecRef()
|
||||
Cleanup()
|
||||
RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback]
|
||||
UnregisterCallback(element *list.Element[RuleSetUpdateCallback])
|
||||
Close() error
|
||||
HeadlessRule
|
||||
}
|
||||
|
||||
type RuleSetUpdateCallback func(it RuleSet)
|
||||
|
||||
type RuleSetMetadata struct {
|
||||
ContainsProcessRule bool
|
||||
ContainsWIFIRule bool
|
||||
ContainsIPCIDRRule bool
|
||||
}
|
||||
type HTTPStartContext struct {
|
||||
ctx context.Context
|
||||
access sync.Mutex
|
||||
httpClientCache map[string]*http.Client
|
||||
}
|
||||
|
||||
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||
return &HTTPStartContext{
|
||||
ctx: ctx,
|
||||
httpClientCache: make(map[string]*http.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if httpClient, loaded := c.httpClientCache[detour]; loaded {
|
||||
return httpClient
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||
RootCAs: RootPoolFromContext(c.ctx),
|
||||
},
|
||||
},
|
||||
}
|
||||
c.httpClientCache[detour] = httpClient
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) Close() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
for _, client := range c.httpClientCache {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
DisableCache() bool
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
)
|
||||
|
||||
type HeadlessRule interface {
|
||||
Match(metadata *InboundContext) bool
|
||||
String() string
|
||||
}
|
||||
|
||||
type Rule interface {
|
||||
HeadlessRule
|
||||
SimpleLifecycle
|
||||
Type() string
|
||||
Action() RuleAction
|
||||
}
|
||||
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
WithAddressLimit() bool
|
||||
MatchAddressLimit(metadata *InboundContext) bool
|
||||
}
|
||||
|
||||
type RuleAction interface {
|
||||
Type() string
|
||||
String() string
|
||||
}
|
||||
|
||||
func IsFinalAction(action RuleAction) bool {
|
||||
switch action.Type() {
|
||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,27 +1,12 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
import "io"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
type Starter interface {
|
||||
Start() error
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
}
|
||||
|
||||
type ServiceRegistry interface {
|
||||
option.ServiceOptionsRegistry
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
|
||||
}
|
||||
|
||||
type ServiceManager interface {
|
||||
Lifecycle
|
||||
Services() []Service
|
||||
Get(tag string) (Service, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
|
||||
Starter
|
||||
io.Closer
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
package service
|
||||
|
||||
type Adapter struct {
|
||||
serviceType string
|
||||
serviceTag string
|
||||
}
|
||||
|
||||
func NewAdapter(serviceType string, serviceTag string) Adapter {
|
||||
return Adapter{
|
||||
serviceType: serviceType,
|
||||
serviceTag: serviceTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.serviceType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.serviceTag
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.ServiceManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.ServiceRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
services []adapter.Service
|
||||
serviceByTag map[string]adapter.Service
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
serviceByTag: make(map[string]adapter.Service),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
services := m.services
|
||||
m.access.Unlock()
|
||||
for _, service := range services {
|
||||
err := adapter.LegacyStart(service, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
services := m.services
|
||||
m.services = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, service := range services {
|
||||
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
|
||||
err = E.Append(err, service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Services() []adapter.Service {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.services
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.Service, bool) {
|
||||
m.access.Lock()
|
||||
service, found := m.serviceByTag[tag]
|
||||
m.access.Unlock()
|
||||
return service, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
service, found := m.serviceByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.serviceByTag, tag)
|
||||
index := common.Index(m.services, func(it adapter.Service) bool {
|
||||
return it == service
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid service index")
|
||||
}
|
||||
m.services = append(m.services[:index], m.services[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return service.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
|
||||
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(service, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsService, loaded := m.serviceByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsService.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
|
||||
return it == existsService
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid service index")
|
||||
}
|
||||
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
|
||||
}
|
||||
m.services = append(m.services, service)
|
||||
m.serviceByTag[tag] = service
|
||||
return nil
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.Service, error)
|
||||
|
||||
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(outboundType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.ServiceRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, error)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
access sync.Mutex
|
||||
optionsType map[string]optionsConstructorFunc
|
||||
constructor map[string]constructorFunc
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
optionsType: make(map[string]optionsConstructorFunc),
|
||||
constructor: make(map[string]constructorFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
optionsConstructor, loaded := m.optionsType[outboundType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
constructor, loaded := m.constructor[outboundType]
|
||||
if !loaded {
|
||||
return nil, E.New("outbound type not found: " + outboundType)
|
||||
}
|
||||
return constructor(ctx, logger, tag, options)
|
||||
}
|
||||
|
||||
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.optionsType[outboundType] = optionsConstructor
|
||||
m.constructor[outboundType] = constructor
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ManagedSSMServer interface {
|
||||
Inbound
|
||||
SetTracker(tracker SSMTracker)
|
||||
UpdateUsers(users []string, uPSKs []string) error
|
||||
}
|
||||
|
||||
type SSMTracker interface {
|
||||
TrackConnection(conn net.Conn, metadata InboundContext) net.Conn
|
||||
TrackPacketConnection(conn N.PacketConn, metadata InboundContext) N.PacketConn
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import "time"
|
||||
|
||||
type TimeService interface {
|
||||
SimpleLifecycle
|
||||
TimeFunc() func() time.Time
|
||||
}
|
@ -4,165 +4,90 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type (
|
||||
ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
)
|
||||
|
||||
func NewUpstreamHandlerEx(
|
||||
func NewUpstreamHandler(
|
||||
metadata InboundContext,
|
||||
connectionHandler ConnectionHandlerFuncEx,
|
||||
packetHandler PacketConnectionHandlerFuncEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &myUpstreamHandlerWrapperEx{
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamHandlerWrapper{
|
||||
metadata: metadata,
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil)
|
||||
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
|
||||
|
||||
type myUpstreamHandlerWrapperEx struct {
|
||||
type myUpstreamHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
connectionHandler ConnectionHandlerFuncEx
|
||||
packetHandler PacketConnectionHandlerFuncEx
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
myMetadata := w.metadata
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
}
|
||||
w.connectionHandler(ctx, conn, myMetadata, onClose)
|
||||
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
w.metadata.Destination = metadata.Destination
|
||||
return w.connectionHandler(ctx, conn, w.metadata)
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
myMetadata := w.metadata
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
}
|
||||
w.packetHandler(ctx, conn, myMetadata, onClose)
|
||||
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
w.metadata.Destination = metadata.Destination
|
||||
return w.packetHandler(ctx, conn, w.metadata)
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil)
|
||||
|
||||
type myUpstreamContextHandlerWrapperEx struct {
|
||||
connectionHandler ConnectionHandlerFuncEx
|
||||
packetHandler PacketConnectionHandlerFuncEx
|
||||
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
||||
func NewUpstreamContextHandlerEx(
|
||||
connectionHandler ConnectionHandlerFuncEx,
|
||||
packetHandler PacketConnectionHandlerFuncEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &myUpstreamContextHandlerWrapperEx{
|
||||
func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
||||
return M.Metadata{
|
||||
Source: metadata.Source,
|
||||
Destination: metadata.Destination,
|
||||
}
|
||||
}
|
||||
|
||||
type myUpstreamContextHandlerWrapper struct {
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
func NewUpstreamContextHandler(
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamContextHandlerWrapper{
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
}
|
||||
w.connectionHandler(ctx, conn, *myMetadata, onClose)
|
||||
myMetadata.Destination = metadata.Destination
|
||||
return w.connectionHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
}
|
||||
w.packetHandler(ctx, conn, *myMetadata, onClose)
|
||||
myMetadata.Destination = metadata.Destination
|
||||
return w.packetHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
func NewRouteHandlerEx(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouterEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &routeHandlerWrapperEx{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil)
|
||||
|
||||
type routeHandlerWrapperEx struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouterEx
|
||||
}
|
||||
|
||||
func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
if source.IsValid() {
|
||||
r.metadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
r.metadata.Destination = destination
|
||||
}
|
||||
r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose)
|
||||
}
|
||||
|
||||
func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
if source.IsValid() {
|
||||
r.metadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
r.metadata.Destination = destination
|
||||
}
|
||||
r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose)
|
||||
}
|
||||
|
||||
func NewRouteContextHandlerEx(
|
||||
router ConnectionRouterEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &routeContextHandlerWrapperEx{
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil)
|
||||
|
||||
type routeContextHandlerWrapperEx struct {
|
||||
router ConnectionRouterEx
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
metadata := ContextFrom(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
metadata.Destination = destination
|
||||
}
|
||||
r.router.RouteConnectionEx(ctx, conn, *metadata, onClose)
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
metadata := ContextFrom(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
metadata.Destination = destination
|
||||
}
|
||||
r.router.RoutePacketConnectionEx(ctx, conn, *metadata, onClose)
|
||||
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
@ -1,234 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type (
|
||||
// Deprecated
|
||||
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
// Deprecated
|
||||
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
)
|
||||
|
||||
// Deprecated
|
||||
//
|
||||
//nolint:staticcheck
|
||||
func NewUpstreamHandler(
|
||||
metadata InboundContext,
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamHandlerWrapper{
|
||||
metadata: metadata,
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type myUpstreamHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.connectionHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.packetHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
||||
// Deprecated: removed
|
||||
func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
||||
return M.Metadata{
|
||||
Source: metadata.Source,
|
||||
Destination: metadata.Destination,
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
type myUpstreamContextHandlerWrapper struct {
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func NewUpstreamContextHandler(
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamContextHandlerWrapper{
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.connectionHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.packetHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func NewRouteHandler(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeHandlerWrapper{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func NewRouteContextHandler(
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeContextHandlerWrapper{
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type routeHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RouteConnection(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RoutePacketConnection(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
type routeContextHandlerWrapper struct {
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RouteConnection(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type V2RayServerTransport interface {
|
||||
Network() []string
|
||||
Serve(listener net.Listener) error
|
||||
ServePacket(listener net.PacketConn) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type V2RayServerTransportHandler interface {
|
||||
N.TCPConnectionHandlerEx
|
||||
}
|
||||
|
||||
type V2RayClientTransport interface {
|
||||
DialContext(ctx context.Context) (net.Conn, error)
|
||||
Close() error
|
||||
}
|
572
box.go
572
box.go
@ -2,494 +2,194 @@ package box
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport/local"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/inbound"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
"github.com/sagernet/sing-box/outbound"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
)
|
||||
|
||||
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||
var _ adapter.Service = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
service *boxService.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
createdAt time.Time
|
||||
router adapter.Router
|
||||
inbounds []adapter.Inbound
|
||||
outbounds []adapter.Outbound
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
logFile *os.File
|
||||
clashServer adapter.ClashServer
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
option.Options
|
||||
Context context.Context
|
||||
PlatformLogWriter log.PlatformWriter
|
||||
}
|
||||
|
||||
func Context(
|
||||
ctx context.Context,
|
||||
inboundRegistry adapter.InboundRegistry,
|
||||
outboundRegistry adapter.OutboundRegistry,
|
||||
endpointRegistry adapter.EndpointRegistry,
|
||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||
serviceRegistry adapter.ServiceRegistry,
|
||||
) context.Context {
|
||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
|
||||
ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)
|
||||
}
|
||||
if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.OutboundRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry)
|
||||
ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
|
||||
}
|
||||
if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.EndpointRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
||||
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
||||
}
|
||||
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
||||
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
||||
}
|
||||
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func New(options Options) (*Box, error) {
|
||||
func New(ctx context.Context, options option.Options) (*Box, error) {
|
||||
createdAt := time.Now()
|
||||
ctx := options.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
ctx = service.ContextWithDefaultRegistry(ctx)
|
||||
logOptions := common.PtrValueOrDefault(options.Log)
|
||||
|
||||
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||
|
||||
if endpointRegistry == nil {
|
||||
return nil, E.New("missing endpoint registry in context")
|
||||
}
|
||||
if inboundRegistry == nil {
|
||||
return nil, E.New("missing inbound registry in context")
|
||||
}
|
||||
if outboundRegistry == nil {
|
||||
return nil, E.New("missing outbound registry in context")
|
||||
}
|
||||
if dnsTransportRegistry == nil {
|
||||
return nil, E.New("missing DNS transport registry in context")
|
||||
}
|
||||
if serviceRegistry == nil {
|
||||
return nil, E.New("missing service registry in context")
|
||||
}
|
||||
|
||||
ctx = pause.WithDefaultManager(ctx)
|
||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||
var needCacheFile bool
|
||||
var needClashAPI bool
|
||||
var needV2RayAPI bool
|
||||
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
|
||||
needCacheFile = true
|
||||
}
|
||||
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
|
||||
if options.Experimental != nil && options.Experimental.ClashAPI != nil && options.Experimental.ClashAPI.ExternalController != "" {
|
||||
needClashAPI = true
|
||||
}
|
||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||
needV2RayAPI = true
|
||||
}
|
||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||
var defaultLogWriter io.Writer
|
||||
if platformInterface != nil {
|
||||
defaultLogWriter = io.Discard
|
||||
}
|
||||
logFactory, err := log.New(log.Options{
|
||||
Context: ctx,
|
||||
Options: common.PtrValueOrDefault(options.Log),
|
||||
Observable: needClashAPI,
|
||||
DefaultWriter: defaultLogWriter,
|
||||
BaseTime: createdAt,
|
||||
PlatformWriter: options.PlatformLogWriter,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create log factory")
|
||||
|
||||
var logFactory log.Factory
|
||||
var observableLogFactory log.ObservableFactory
|
||||
var logFile *os.File
|
||||
if logOptions.Disabled {
|
||||
observableLogFactory = log.NewNOPFactory()
|
||||
logFactory = observableLogFactory
|
||||
} else {
|
||||
var logWriter io.Writer
|
||||
switch logOptions.Output {
|
||||
case "", "stderr":
|
||||
logWriter = os.Stderr
|
||||
case "stdout":
|
||||
logWriter = os.Stdout
|
||||
default:
|
||||
var err error
|
||||
logFile, err = os.OpenFile(logOptions.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
logFormatter := log.Formatter{
|
||||
BaseTime: createdAt,
|
||||
DisableColors: logOptions.DisableColor || logFile != nil,
|
||||
DisableTimestamp: !logOptions.Timestamp && logFile != nil,
|
||||
FullTimestamp: logOptions.Timestamp,
|
||||
TimestampFormat: "-0700 2006-01-02 15:04:05",
|
||||
}
|
||||
if needClashAPI {
|
||||
observableLogFactory = log.NewObservableFactory(logFormatter, logWriter)
|
||||
logFactory = observableLogFactory
|
||||
} else {
|
||||
logFactory = log.NewFactory(logFormatter, logWriter)
|
||||
}
|
||||
if logOptions.Level != "" {
|
||||
logLevel, err := log.ParseLevel(logOptions.Level)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse log level")
|
||||
}
|
||||
logFactory.SetLevel(logLevel)
|
||||
} else {
|
||||
logFactory.SetLevel(log.LevelTrace)
|
||||
}
|
||||
}
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
len(certificateOptions.CertificatePath) > 0 ||
|
||||
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
||||
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
internalServices = append(internalServices, certificateStore)
|
||||
}
|
||||
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
|
||||
router, err := route.NewRouter(
|
||||
ctx,
|
||||
logFactory.NewLogger("router"),
|
||||
logFactory.NewLogger("dns"),
|
||||
common.PtrValueOrDefault(options.Route),
|
||||
common.PtrValueOrDefault(options.DNS),
|
||||
options.Inbounds,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
}
|
||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||
service.MustRegister[adapter.Router](ctx, router)
|
||||
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize router")
|
||||
}
|
||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
if ntpOptions.Enabled {
|
||||
timeService = new(tls.TimeServiceWrapper)
|
||||
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||
}
|
||||
for i, transportOptions := range dnsOptions.Servers {
|
||||
var tag string
|
||||
if transportOptions.Tag != "" {
|
||||
tag = transportOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = dnsTransportManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
transportOptions.Type,
|
||||
transportOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize DNS server[", i, "]")
|
||||
}
|
||||
}
|
||||
err = dnsRouter.Initialize(dnsOptions.Rules)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize dns router")
|
||||
}
|
||||
for i, endpointOptions := range options.Endpoints {
|
||||
var tag string
|
||||
if endpointOptions.Tag != "" {
|
||||
tag = endpointOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
endpointCtx := ctx
|
||||
if tag != "" {
|
||||
// TODO: remove this
|
||||
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
|
||||
Outbound: tag,
|
||||
})
|
||||
}
|
||||
err = endpointManager.Create(
|
||||
endpointCtx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
endpointOptions.Type,
|
||||
endpointOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize endpoint[", i, "]")
|
||||
}
|
||||
return nil, E.Cause(err, "parse route options")
|
||||
}
|
||||
inbounds := make([]adapter.Inbound, 0, len(options.Inbounds))
|
||||
outbounds := make([]adapter.Outbound, 0, len(options.Outbounds))
|
||||
for i, inboundOptions := range options.Inbounds {
|
||||
var in adapter.Inbound
|
||||
var tag string
|
||||
if inboundOptions.Tag != "" {
|
||||
tag = inboundOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = inboundManager.Create(
|
||||
in, err = inbound.New(
|
||||
ctx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
inboundOptions.Type,
|
||||
inboundOptions.Options,
|
||||
inboundOptions,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
||||
return nil, E.Cause(err, "parse inbound[", i, "]")
|
||||
}
|
||||
inbounds = append(inbounds, in)
|
||||
}
|
||||
for i, outboundOptions := range options.Outbounds {
|
||||
var out adapter.Outbound
|
||||
var tag string
|
||||
if outboundOptions.Tag != "" {
|
||||
tag = outboundOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
outboundCtx := ctx
|
||||
if tag != "" {
|
||||
// TODO: remove this
|
||||
outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{
|
||||
Outbound: tag,
|
||||
})
|
||||
}
|
||||
err = outboundManager.Create(
|
||||
outboundCtx,
|
||||
out, err = outbound.New(
|
||||
ctx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
outboundOptions.Type,
|
||||
outboundOptions.Options,
|
||||
)
|
||||
outboundOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||
return nil, E.Cause(err, "parse outbound[", i, "]")
|
||||
}
|
||||
outbounds = append(outbounds, out)
|
||||
}
|
||||
for i, serviceOptions := range options.Services {
|
||||
var tag string
|
||||
if serviceOptions.Tag != "" {
|
||||
tag = serviceOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = serviceManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
serviceOptions.Type,
|
||||
serviceOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||
}
|
||||
}
|
||||
outboundManager.Initialize(common.Must1(
|
||||
direct.NewOutbound(
|
||||
ctx,
|
||||
router,
|
||||
logFactory.NewLogger("outbound/direct"),
|
||||
"direct",
|
||||
option.DirectOutboundOptions{},
|
||||
),
|
||||
))
|
||||
dnsTransportManager.Initialize(common.Must1(
|
||||
local.NewTransport(
|
||||
ctx,
|
||||
logFactory.NewLogger("dns/local"),
|
||||
"local",
|
||||
option.LocalDNSServerOptions{},
|
||||
)))
|
||||
if platformInterface != nil {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize platform interface")
|
||||
}
|
||||
}
|
||||
if needCacheFile {
|
||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
err = router.Initialize(outbounds, func() adapter.Outbound {
|
||||
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), option.Outbound{Type: "direct", Tag: "default"})
|
||||
common.Must(oErr)
|
||||
outbounds = append(outbounds, out)
|
||||
return out
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var clashServer adapter.ClashServer
|
||||
if needClashAPI {
|
||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions)
|
||||
clashServer, err = experimental.NewClashServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create clash-server")
|
||||
return nil, E.Cause(err, "create clash api server")
|
||||
}
|
||||
router.AppendTracker(clashServer)
|
||||
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||
internalServices = append(internalServices, clashServer)
|
||||
}
|
||||
if needV2RayAPI {
|
||||
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create v2ray-server")
|
||||
}
|
||||
if v2rayServer.StatsService() != nil {
|
||||
router.AppendTracker(v2rayServer.StatsService())
|
||||
internalServices = append(internalServices, v2rayServer)
|
||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||
}
|
||||
}
|
||||
if ntpOptions.Enabled {
|
||||
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create NTP service")
|
||||
}
|
||||
ntpService := ntp.NewService(ntp.Options{
|
||||
Context: ctx,
|
||||
Dialer: ntpDialer,
|
||||
Logger: logFactory.NewLogger("ntp"),
|
||||
Server: ntpOptions.ServerOptions.Build(),
|
||||
Interval: time.Duration(ntpOptions.Interval),
|
||||
WriteToSystem: ntpOptions.WriteToSystem,
|
||||
})
|
||||
timeService.TimeService = ntpService
|
||||
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||
router.SetTrafficController(clashServer)
|
||||
}
|
||||
return &Box{
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
done: make(chan struct{}),
|
||||
router: router,
|
||||
inbounds: inbounds,
|
||||
outbounds: outbounds,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.NewLogger(""),
|
||||
logFile: logFile,
|
||||
clashServer: clashServer,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Box) PreStart() error {
|
||||
err := s.preStart()
|
||||
func (s *Box) Start() error {
|
||||
err := s.router.Start()
|
||||
if err != nil {
|
||||
// TODO: remove catch error
|
||||
defer func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
println(err.Error())
|
||||
debug.PrintStack()
|
||||
panic("panic on early close: " + fmt.Sprint(v))
|
||||
}
|
||||
}()
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) Start() error {
|
||||
err := s.start()
|
||||
if err != nil {
|
||||
// TODO: remove catch error
|
||||
defer func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
println(err.Error())
|
||||
debug.PrintStack()
|
||||
println("panic on early start: " + fmt.Sprint(v))
|
||||
for i, in := range s.inbounds {
|
||||
err = in.Start()
|
||||
if err != nil {
|
||||
for g := 0; g < i; g++ {
|
||||
s.inbounds[g].Close()
|
||||
}
|
||||
}()
|
||||
s.Close()
|
||||
return err
|
||||
return err
|
||||
}
|
||||
}
|
||||
if s.clashServer != nil {
|
||||
err = s.clashServer.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start clash api server")
|
||||
}
|
||||
}
|
||||
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) preStart() error {
|
||||
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||
monitor.Start("start logger")
|
||||
err := s.logFactory.Start()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start logger")
|
||||
}
|
||||
err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) start() error {
|
||||
err := s.preStart()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) Close() error {
|
||||
select {
|
||||
case <-s.done:
|
||||
@ -497,32 +197,16 @@ func (s *Box) Close() error {
|
||||
default:
|
||||
close(s.done)
|
||||
}
|
||||
err := common.Close(
|
||||
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||
)
|
||||
for _, lifecycleService := range s.internalService {
|
||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", lifecycleService.Name())
|
||||
})
|
||||
for _, in := range s.inbounds {
|
||||
in.Close()
|
||||
}
|
||||
err = E.Append(err, s.logFactory.Close(), func(err error) error {
|
||||
return E.Cause(err, "close logger")
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Box) Network() adapter.NetworkManager {
|
||||
return s.network
|
||||
}
|
||||
|
||||
func (s *Box) Router() adapter.Router {
|
||||
return s.router
|
||||
}
|
||||
|
||||
func (s *Box) Inbound() adapter.InboundManager {
|
||||
return s.inbound
|
||||
}
|
||||
|
||||
func (s *Box) Outbound() adapter.OutboundManager {
|
||||
return s.outbound
|
||||
for _, out := range s.outbounds {
|
||||
common.Close(out)
|
||||
}
|
||||
return common.Close(
|
||||
s.router,
|
||||
s.logFactory,
|
||||
s.clashServer,
|
||||
common.PtrOrNil(s.logFile),
|
||||
)
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
Subproject commit 320170a1077ea5c93872b3e055b96b8836615ef0
|
@ -1 +0,0 @@
|
||||
Subproject commit ae5818ee5a24af965dc91f80bffa16e1e6c109c1
|
@ -1,450 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/asc-go/asc"
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
switch os.Args[1] {
|
||||
case "next_macos_project_version":
|
||||
err := fetchMacOSVersion(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "publish_testflight":
|
||||
err := publishTestflight(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "cancel_app_store":
|
||||
err := cancelAppStore(ctx, os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "prepare_app_store":
|
||||
err := prepareAppStore(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "publish_app_store":
|
||||
err := publishAppStore(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
default:
|
||||
log.Fatal("unknown action: ", os.Args[1])
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
appID = "6673731168"
|
||||
groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda"
|
||||
)
|
||||
|
||||
func createClient(expireDuration time.Duration) *asc.Client {
|
||||
privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), expireDuration, privateKey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return asc.NewClient(tokenConfig.Client())
|
||||
}
|
||||
|
||||
func fetchMacOSVersion(ctx context.Context) error {
|
||||
client := createClient(time.Minute)
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{"MAC_OS"},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var versionID string
|
||||
findVersion:
|
||||
for _, version := range versions.Data {
|
||||
switch *version.Attributes.AppStoreState {
|
||||
case asc.AppStoreVersionStateReadyForSale,
|
||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
versionID = version.ID
|
||||
break findVersion
|
||||
}
|
||||
}
|
||||
if versionID == "" {
|
||||
return E.New("no version found")
|
||||
}
|
||||
latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse version code")
|
||||
}
|
||||
os.Stdout.WriteString(F.ToString(versionInt+1, "\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishTestflight(ctx context.Context) error {
|
||||
tagVersion, err := build_shared.ReadTagVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag := tagVersion.VersionString()
|
||||
client := createClient(20 * time.Minute)
|
||||
|
||||
log.Info(tag, " list build IDs")
|
||||
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
|
||||
return it.ID
|
||||
})
|
||||
var platforms []asc.Platform
|
||||
if len(os.Args) == 3 {
|
||||
switch os.Args[2] {
|
||||
case "ios":
|
||||
platforms = []asc.Platform{asc.PlatformIOS}
|
||||
case "macos":
|
||||
platforms = []asc.Platform{asc.PlatformMACOS}
|
||||
case "tvos":
|
||||
platforms = []asc.Platform{asc.PlatformTVOS}
|
||||
default:
|
||||
return E.New("unknown platform: ", os.Args[2])
|
||||
}
|
||||
} else {
|
||||
platforms = []asc.Platform{
|
||||
asc.PlatformIOS,
|
||||
asc.PlatformMACOS,
|
||||
asc.PlatformTVOS,
|
||||
}
|
||||
}
|
||||
for _, platform := range platforms {
|
||||
log.Info(string(platform), " list builds")
|
||||
for {
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
build := builds.Data[0]
|
||||
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
|
||||
log.Info(string(platform), " ", tag, " waiting for process")
|
||||
time.Sleep(15 * time.Second)
|
||||
continue
|
||||
}
|
||||
if *build.Attributes.ProcessingState != "VALID" {
|
||||
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
||||
time.Sleep(15 * time.Second)
|
||||
continue
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " list localizations")
|
||||
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
|
||||
return *it.Attributes.Locale == "en-US"
|
||||
})
|
||||
if localization.ID == "" {
|
||||
log.Fatal(string(platform), " ", tag, " no en-US localization found")
|
||||
}
|
||||
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
||||
log.Info(string(platform), " ", tag, " update localization")
|
||||
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(
|
||||
F.ToString("sing-box ", tagVersion.String()),
|
||||
))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " publish")
|
||||
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
|
||||
if response != nil && response.StatusCode == http.StatusUnprocessableEntity {
|
||||
log.Info("waiting for process")
|
||||
time.Sleep(15 * time.Second)
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " list submissions")
|
||||
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
|
||||
FilterBuild: []string{build.ID},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(betaSubmissions.Data) == 0 {
|
||||
log.Info(string(platform), " ", tag, " create submission")
|
||||
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
|
||||
log.Error(err)
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cancelAppStore(ctx context.Context, platform string) error {
|
||||
switch platform {
|
||||
case "ios":
|
||||
platform = string(asc.PlatformIOS)
|
||||
case "macos":
|
||||
platform = string(asc.PlatformMACOS)
|
||||
case "tvos":
|
||||
platform = string(asc.PlatformTVOS)
|
||||
}
|
||||
tag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
for {
|
||||
log.Info(platform, " list versions")
|
||||
versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
})
|
||||
if isRetryable(response) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
||||
return *it.Attributes.VersionString == tag
|
||||
})
|
||||
if version.ID == "" {
|
||||
return nil
|
||||
}
|
||||
log.Info(platform, " ", tag, " get submission")
|
||||
submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
|
||||
if response != nil && response.StatusCode == http.StatusNotFound {
|
||||
return nil
|
||||
}
|
||||
if isRetryable(response) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(platform, " ", tag, " delete submission")
|
||||
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAppStore(ctx context.Context) error {
|
||||
tag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
for _, platform := range []asc.Platform{
|
||||
asc.PlatformIOS,
|
||||
asc.PlatformMACOS,
|
||||
asc.PlatformTVOS,
|
||||
} {
|
||||
log.Info(string(platform), " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
||||
return *it.Attributes.VersionString == tag
|
||||
})
|
||||
log.Info(string(platform), " ", tag, " list builds")
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(builds.Data) == 0 {
|
||||
log.Fatal(platform, " ", tag, " no build found")
|
||||
}
|
||||
buildID := common.Ptr(builds.Data[0].ID)
|
||||
if version.ID == "" {
|
||||
log.Info(string(platform), " ", tag, " create version")
|
||||
newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
|
||||
Platform: platform,
|
||||
VersionString: tag,
|
||||
}, appID, buildID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version = newVersion.Data
|
||||
|
||||
} else {
|
||||
log.Info(string(platform), " ", tag, " check build")
|
||||
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
|
||||
switch *version.Attributes.AppStoreState {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
||||
asc.AppStoreVersionStateRejected,
|
||||
asc.AppStoreVersionStateDeveloperRejected:
|
||||
case asc.AppStoreVersionStateWaitingForReview,
|
||||
asc.AppStoreVersionStateInReview,
|
||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if submission != nil {
|
||||
log.Info(string(platform), " ", tag, " delete submission")
|
||||
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
default:
|
||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " update build")
|
||||
response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != http.StatusNoContent {
|
||||
response.Write(os.Stderr)
|
||||
log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
|
||||
}
|
||||
} else {
|
||||
switch *version.Attributes.AppStoreState {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
||||
asc.AppStoreVersionStateRejected,
|
||||
asc.AppStoreVersionStateDeveloperRejected:
|
||||
case asc.AppStoreVersionStateWaitingForReview,
|
||||
asc.AppStoreVersionStateInReview,
|
||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
continue
|
||||
default:
|
||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " list localization")
|
||||
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
|
||||
return *it.Attributes.Locale == "en-US"
|
||||
})
|
||||
if localization.ID == "" {
|
||||
log.Info(string(platform), " ", tag, " no en-US localization found")
|
||||
}
|
||||
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
||||
log.Info(string(platform), " ", tag, " update localization")
|
||||
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
|
||||
PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
|
||||
WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " create submission")
|
||||
fixSubmit:
|
||||
for {
|
||||
_, response, err := client.Submission.CreateSubmission(ctx, version.ID)
|
||||
if err != nil {
|
||||
switch response.StatusCode {
|
||||
case http.StatusInternalServerError:
|
||||
continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch response.StatusCode {
|
||||
case http.StatusCreated:
|
||||
break fixSubmit
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishAppStore(ctx context.Context) error {
|
||||
tag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
for _, platform := range []asc.Platform{
|
||||
asc.PlatformIOS,
|
||||
asc.PlatformMACOS,
|
||||
asc.PlatformTVOS,
|
||||
} {
|
||||
log.Info(string(platform), " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
||||
return *it.Attributes.VersionString == tag
|
||||
})
|
||||
switch *version.Attributes.AppStoreState {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
|
||||
log.Fatal(string(platform), " ", tag, " not submitted")
|
||||
case asc.AppStoreVersionStateWaitingForReview,
|
||||
asc.AppStoreVersionStateInReview:
|
||||
log.Warn(string(platform), " ", tag, " waiting for review")
|
||||
continue
|
||||
case asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
default:
|
||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||
}
|
||||
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isRetryable(response *asc.Response) bool {
|
||||
if response == nil {
|
||||
return false
|
||||
}
|
||||
switch response.StatusCode {
|
||||
case http.StatusInternalServerError, http.StatusUnprocessableEntity:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
build_shared.FindSDK()
|
||||
|
||||
if os.Getenv("GOPATH") == "" {
|
||||
os.Setenv("GOPATH", build.Default.GOPATH)
|
||||
}
|
||||
|
||||
command := exec.Command(os.Args[1], os.Args[2:]...)
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
err := command.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
@ -1,187 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
_ "github.com/sagernet/gomobile"
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
"github.com/sagernet/sing/common/shell"
|
||||
)
|
||||
|
||||
var (
|
||||
debugEnabled bool
|
||||
target string
|
||||
platform string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
||||
flag.StringVar(&target, "target", "android", "target platform")
|
||||
flag.StringVar(&platform, "platform", "", "specify platform")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
build_shared.FindMobile()
|
||||
|
||||
switch target {
|
||||
case "android":
|
||||
buildAndroid()
|
||||
case "apple":
|
||||
buildApple()
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
sharedFlags []string
|
||||
debugFlags []string
|
||||
sharedTags []string
|
||||
iosTags []string
|
||||
memcTags []string
|
||||
debugTags []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
sharedFlags = append(sharedFlags, "-trimpath")
|
||||
sharedFlags = append(sharedFlags, "-buildvcs=false")
|
||||
currentTag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
currentTag = "unknown"
|
||||
}
|
||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
|
||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory")
|
||||
memcTags = append(memcTags, "with_tailscale")
|
||||
debugTags = append(debugTags, "debug")
|
||||
}
|
||||
|
||||
func buildAndroid() {
|
||||
build_shared.FindSDK()
|
||||
|
||||
var javaPath string
|
||||
javaHome := os.Getenv("JAVA_HOME")
|
||||
if javaHome == "" {
|
||||
javaPath = "java"
|
||||
} else {
|
||||
javaPath = filepath.Join(javaHome, "bin", "java")
|
||||
}
|
||||
|
||||
javaVersion, err := shell.Exec(javaPath, "--version").ReadOutput()
|
||||
if err != nil {
|
||||
log.Fatal(E.Cause(err, "check java version"))
|
||||
}
|
||||
if !strings.Contains(javaVersion, "openjdk 17") {
|
||||
log.Fatal("java version should be openjdk 17")
|
||||
}
|
||||
|
||||
var bindTarget string
|
||||
if platform != "" {
|
||||
bindTarget = platform
|
||||
} else if debugEnabled {
|
||||
bindTarget = "android/arm64"
|
||||
} else {
|
||||
bindTarget = "android"
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"bind",
|
||||
"-v",
|
||||
"-target", bindTarget,
|
||||
"-androidapi", "21",
|
||||
"-javapkg=io.nekohasekai",
|
||||
"-libname=box",
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
args = append(args, sharedFlags...)
|
||||
} else {
|
||||
args = append(args, debugFlags...)
|
||||
}
|
||||
|
||||
tags := append(sharedTags, memcTags...)
|
||||
if debugEnabled {
|
||||
tags = append(tags, debugTags...)
|
||||
}
|
||||
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
args = append(args, "./experimental/libbox")
|
||||
|
||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
err = command.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
const name = "libbox.aar"
|
||||
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
|
||||
if rw.IsDir(copyPath) {
|
||||
copyPath, _ = filepath.Abs(copyPath)
|
||||
err = rw.CopyFile(name, filepath.Join(copyPath, name))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Info("copied to ", copyPath)
|
||||
}
|
||||
}
|
||||
|
||||
func buildApple() {
|
||||
var bindTarget string
|
||||
if platform != "" {
|
||||
bindTarget = platform
|
||||
} else if debugEnabled {
|
||||
bindTarget = "ios"
|
||||
} else {
|
||||
bindTarget = "ios,tvos,macos"
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"bind",
|
||||
"-v",
|
||||
"-target", bindTarget,
|
||||
"-libname=box",
|
||||
"-tags-macos=" + strings.Join(memcTags, ","),
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
args = append(args, sharedFlags...)
|
||||
} else {
|
||||
args = append(args, debugFlags...)
|
||||
}
|
||||
|
||||
tags := append(sharedTags, iosTags...)
|
||||
if debugEnabled {
|
||||
tags = append(tags, debugTags...)
|
||||
}
|
||||
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
args = append(args, "./experimental/libbox")
|
||||
|
||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
err := command.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
copyPath := filepath.Join("..", "sing-box-for-apple")
|
||||
if rw.IsDir(copyPath) {
|
||||
targetDir := filepath.Join(copyPath, "Libbox.xcframework")
|
||||
targetDir, _ = filepath.Abs(targetDir)
|
||||
os.RemoveAll(targetDir)
|
||||
os.Rename("Libbox.xcframework", targetDir)
|
||||
log.Info("copied to ", targetDir)
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
package build_shared
|
||||
|
||||
import (
|
||||
"go/build"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
var (
|
||||
androidSDKPath string
|
||||
androidNDKPath string
|
||||
)
|
||||
|
||||
func FindSDK() {
|
||||
searchPath := []string{
|
||||
"$ANDROID_HOME",
|
||||
"$HOME/Android/Sdk",
|
||||
"$HOME/.local/lib/android/sdk",
|
||||
"$HOME/Library/Android/sdk",
|
||||
}
|
||||
for _, path := range searchPath {
|
||||
path = os.ExpandEnv(path)
|
||||
if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) {
|
||||
androidSDKPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
if androidSDKPath == "" {
|
||||
log.Fatal("android SDK not found")
|
||||
}
|
||||
if !findNDK() {
|
||||
log.Fatal("android NDK not found")
|
||||
}
|
||||
|
||||
os.Setenv("ANDROID_HOME", androidSDKPath)
|
||||
os.Setenv("ANDROID_SDK_HOME", androidSDKPath)
|
||||
os.Setenv("ANDROID_NDK_HOME", androidNDKPath)
|
||||
os.Setenv("NDK", androidNDKPath)
|
||||
os.Setenv("PATH", os.Getenv("PATH")+":"+filepath.Join(androidNDKPath, "toolchains", "llvm", "prebuilt", runtime.GOOS+"-x86_64", "bin"))
|
||||
}
|
||||
|
||||
func findNDK() bool {
|
||||
const fixedVersion = "28.0.13004108"
|
||||
const versionFile = "source.properties"
|
||||
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
|
||||
androidNDKPath = fixedPath
|
||||
return true
|
||||
}
|
||||
if ndkHomeEnv := os.Getenv("ANDROID_NDK_HOME"); rw.IsFile(filepath.Join(ndkHomeEnv, versionFile)) {
|
||||
androidNDKPath = ndkHomeEnv
|
||||
return true
|
||||
}
|
||||
ndkVersions, err := os.ReadDir(filepath.Join(androidSDKPath, "ndk"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
versionNames := common.Map(ndkVersions, os.DirEntry.Name)
|
||||
if len(versionNames) == 0 {
|
||||
return false
|
||||
}
|
||||
sort.Slice(versionNames, func(i, j int) bool {
|
||||
iVersions := strings.Split(versionNames[i], ".")
|
||||
jVersions := strings.Split(versionNames[j], ".")
|
||||
for k := 0; k < len(iVersions) && k < len(jVersions); k++ {
|
||||
iVersion, _ := strconv.Atoi(iVersions[k])
|
||||
jVersion, _ := strconv.Atoi(jVersions[k])
|
||||
if iVersion != jVersion {
|
||||
return iVersion > jVersion
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
for _, versionName := range versionNames {
|
||||
currentNDKPath := filepath.Join(androidSDKPath, "ndk", versionName)
|
||||
if rw.IsFile(filepath.Join(currentNDKPath, versionFile)) {
|
||||
androidNDKPath = currentNDKPath
|
||||
log.Warn("reproducibility warning: using NDK version " + versionName + " instead of " + fixedVersion)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var GoBinPath string
|
||||
|
||||
func FindMobile() {
|
||||
goBin := filepath.Join(build.Default.GOPATH, "bin")
|
||||
if runtime.GOOS == "windows" {
|
||||
if !rw.IsFile(filepath.Join(goBin, "gobind.exe")) {
|
||||
log.Fatal("missing gomobile installation")
|
||||
}
|
||||
} else {
|
||||
if !rw.IsFile(filepath.Join(goBin, "gobind")) {
|
||||
log.Fatal("missing gomobile installation")
|
||||
}
|
||||
}
|
||||
GoBinPath = goBin
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package build_shared
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/common/badversion"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/shell"
|
||||
)
|
||||
|
||||
func ReadTag() (string, error) {
|
||||
currentTag, err := shell.Exec("git", "describe", "--tags").ReadOutput()
|
||||
if err != nil {
|
||||
return currentTag, err
|
||||
}
|
||||
currentTagRev, _ := shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput()
|
||||
if currentTagRev == currentTag {
|
||||
return currentTag[1:], nil
|
||||
}
|
||||
shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput()
|
||||
version := badversion.Parse(currentTagRev[1:])
|
||||
return version.String() + "-" + shortCommit, nil
|
||||
}
|
||||
|
||||
func ReadTagVersionRev() (badversion.Version, error) {
|
||||
currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput())
|
||||
return badversion.Parse(currentTagRev[1:]), nil
|
||||
}
|
||||
|
||||
func ReadTagVersion() (badversion.Version, error) {
|
||||
currentTag := common.Must1(shell.Exec("git", "describe", "--tags").ReadOutput())
|
||||
currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput())
|
||||
version := badversion.Parse(currentTagRev[1:])
|
||||
if currentTagRev != currentTag {
|
||||
if version.PreReleaseIdentifier == "" {
|
||||
version.Patch++
|
||||
}
|
||||
}
|
||||
return version, nil
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// envFile returns the name of the Go environment configuration file.
|
||||
// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166
|
||||
func envFile() (string, error) {
|
||||
if file := os.Getenv("GOENV"); file != "" {
|
||||
if file == "off" {
|
||||
return "", fmt.Errorf("GOENV=off")
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
dir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if dir == "" {
|
||||
return "", fmt.Errorf("missing user-config dir")
|
||||
}
|
||||
return filepath.Join(dir, "go", "env"), nil
|
||||
}
|
||||
|
||||
// GetRuntimeEnv returns the value of runtime environment variable,
|
||||
// that is set by running following command: `go env -w key=value`.
|
||||
func GetRuntimeEnv(key string) (string, error) {
|
||||
file, err := envFile()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if file == "" {
|
||||
return "", fmt.Errorf("missing runtime env file")
|
||||
}
|
||||
var data []byte
|
||||
var runtimeEnv string
|
||||
data, readErr := os.ReadFile(file)
|
||||
if readErr != nil {
|
||||
return "", readErr
|
||||
}
|
||||
envStrings := strings.Split(string(data), "\n")
|
||||
for _, envItem := range envStrings {
|
||||
envItem = strings.TrimSuffix(envItem, "\r")
|
||||
envKeyValue := strings.Split(envItem, "=")
|
||||
if strings.EqualFold(strings.TrimSpace(envKeyValue[0]), key) {
|
||||
runtimeEnv = strings.TrimSpace(envKeyValue[1])
|
||||
}
|
||||
}
|
||||
return runtimeEnv, nil
|
||||
}
|
||||
|
||||
// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty.
|
||||
func GetGOBIN() string {
|
||||
// The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command`
|
||||
GOBIN := os.Getenv("GOBIN")
|
||||
if GOBIN == "" {
|
||||
var err error
|
||||
// The one set by user by running `go env -w GOBIN=/path`
|
||||
GOBIN, err = GetRuntimeEnv("GOBIN")
|
||||
if err != nil {
|
||||
// The default one that Golang uses
|
||||
return filepath.Join(build.Default.GOPATH, "bin")
|
||||
}
|
||||
if GOBIN == "" {
|
||||
return filepath.Join(build.Default.GOPATH, "bin")
|
||||
}
|
||||
return GOBIN
|
||||
}
|
||||
return GOBIN
|
||||
}
|
||||
|
||||
func main() {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Println("Can not get current working directory.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
GOBIN := GetGOBIN()
|
||||
binPath := os.Getenv("PATH")
|
||||
pathSlice := []string{pwd, GOBIN, binPath}
|
||||
binPath = strings.Join(pathSlice, string(os.PathListSeparator))
|
||||
os.Setenv("PATH", binPath)
|
||||
|
||||
suffix := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
suffix = ".exe"
|
||||
}
|
||||
|
||||
protoc := "protoc"
|
||||
|
||||
if linkPath, err := os.Readlink(protoc); err == nil {
|
||||
protoc = linkPath
|
||||
}
|
||||
|
||||
protoFilesMap := make(map[string][]string)
|
||||
walkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
filename := filepath.Base(path)
|
||||
if strings.HasSuffix(filename, ".proto") &&
|
||||
filename != "typed_message.proto" &&
|
||||
filename != "descriptor.proto" {
|
||||
protoFilesMap[dir] = append(protoFilesMap[dir], path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if walkErr != nil {
|
||||
fmt.Println(walkErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, files := range protoFilesMap {
|
||||
for _, relProtoFile := range files {
|
||||
args := []string{
|
||||
"-I", ".",
|
||||
"--go_out", pwd,
|
||||
"--go_opt", "paths=source_relative",
|
||||
"--go-grpc_out", pwd,
|
||||
"--go-grpc_opt", "paths=source_relative",
|
||||
"--plugin", "protoc-gen-go=" + filepath.Join(GOBIN, "protoc-gen-go"+suffix),
|
||||
"--plugin", "protoc-gen-go-grpc=" + filepath.Join(GOBIN, "protoc-gen-go-grpc"+suffix),
|
||||
}
|
||||
args = append(args, relProtoFile)
|
||||
cmd := exec.Command(protoc, args...)
|
||||
cmd.Env = append(cmd.Env, os.Environ()...)
|
||||
output, cmdErr := cmd.CombinedOutput()
|
||||
if len(output) > 0 {
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
if cmdErr != nil {
|
||||
fmt.Println(cmdErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
normalizeWalkErr := filepath.Walk("./", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
filename := filepath.Base(path)
|
||||
if strings.HasSuffix(filename, ".pb.go") &&
|
||||
path != "config.pb.go" {
|
||||
if err := NormalizeGeneratedProtoFile(path); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if normalizeWalkErr != nil {
|
||||
fmt.Println(normalizeWalkErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeGeneratedProtoFile(path string) error {
|
||||
fd, err := os.OpenFile(path, os.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fd.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out := bytes.NewBuffer(nil)
|
||||
scanner := bufio.NewScanner(fd)
|
||||
valid := false
|
||||
for scanner.Scan() {
|
||||
if !valid && !strings.HasPrefix(scanner.Text(), "package ") {
|
||||
continue
|
||||
}
|
||||
valid = true
|
||||
out.Write(scanner.Bytes())
|
||||
out.Write([]byte("\n"))
|
||||
}
|
||||
_, err = fd.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = fd.Truncate(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(fd, bytes.NewReader(out.Bytes()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/common/badversion"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
var (
|
||||
flagRunInCI bool
|
||||
flagRunNightly bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
var (
|
||||
versionStr string
|
||||
err error
|
||||
)
|
||||
if flagRunNightly {
|
||||
var version badversion.Version
|
||||
version, err = build_shared.ReadTagVersion()
|
||||
if err == nil {
|
||||
versionStr = version.String()
|
||||
}
|
||||
} else {
|
||||
versionStr, err = build_shared.ReadTag()
|
||||
}
|
||||
if flagRunInCI {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = setGitHubEnv("version", versionStr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
os.Stdout.WriteString("unknown\n")
|
||||
} else {
|
||||
os.Stdout.WriteString(versionStr + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setGitHubEnv(name string, value string) error {
|
||||
outputFile, err := os.OpenFile(os.Getenv("GITHUB_ENV"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = outputFile.WriteString(name + "=" + value + "\n")
|
||||
if err != nil {
|
||||
outputFile.Close()
|
||||
return err
|
||||
}
|
||||
err = outputFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stderr.WriteString(name + "=" + value + "\n")
|
||||
return nil
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
var (
|
||||
flagRunInCI bool
|
||||
flagRunNightly bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
newVersion := common.Must1(build_shared.ReadTag())
|
||||
var androidPath string
|
||||
if flagRunInCI {
|
||||
androidPath = "clients/android"
|
||||
} else {
|
||||
androidPath = "../sing-box-for-android"
|
||||
}
|
||||
androidPath, err := filepath.Abs(androidPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
common.Must(os.Chdir(androidPath))
|
||||
localProps := common.Must1(os.ReadFile("version.properties"))
|
||||
var propsList [][]string
|
||||
for _, propLine := range strings.Split(string(localProps), "\n") {
|
||||
propsList = append(propsList, strings.Split(propLine, "="))
|
||||
}
|
||||
var (
|
||||
versionUpdated bool
|
||||
goVersionUpdated bool
|
||||
)
|
||||
for _, propPair := range propsList {
|
||||
switch propPair[0] {
|
||||
case "VERSION_NAME":
|
||||
if propPair[1] != newVersion {
|
||||
log.Info("updated version from ", propPair[1], " to ", newVersion)
|
||||
versionUpdated = true
|
||||
propPair[1] = newVersion
|
||||
}
|
||||
case "GO_VERSION":
|
||||
if propPair[1] != runtime.Version() {
|
||||
log.Info("updated Go version from ", propPair[1], " to ", runtime.Version())
|
||||
goVersionUpdated = true
|
||||
propPair[1] = runtime.Version()
|
||||
}
|
||||
}
|
||||
}
|
||||
if !(versionUpdated || goVersionUpdated) {
|
||||
log.Info("version not changed")
|
||||
return
|
||||
} else if flagRunInCI && !flagRunNightly {
|
||||
log.Fatal("version changed, commit changes first.")
|
||||
}
|
||||
for _, propPair := range propsList {
|
||||
switch propPair[0] {
|
||||
case "VERSION_CODE":
|
||||
versionCode := common.Must1(strconv.ParseInt(propPair[1], 10, 64))
|
||||
propPair[1] = strconv.Itoa(int(versionCode + 1))
|
||||
log.Info("updated version code to ", propPair[1])
|
||||
}
|
||||
}
|
||||
var newProps []string
|
||||
for _, propPair := range propsList {
|
||||
newProps = append(newProps, strings.Join(propPair, "="))
|
||||
}
|
||||
common.Must(os.WriteFile("version.properties", []byte(strings.Join(newProps, "\n")), 0o644))
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
|
||||
"howett.net/plist"
|
||||
)
|
||||
|
||||
var flagRunInCI bool
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
newVersion := common.Must1(build_shared.ReadTagVersion())
|
||||
var applePath string
|
||||
if flagRunInCI {
|
||||
applePath = "clients/apple"
|
||||
} else {
|
||||
applePath = "../sing-box-for-apple"
|
||||
}
|
||||
applePath, err := filepath.Abs(applePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
common.Must(os.Chdir(applePath))
|
||||
projectFile := common.Must1(os.Open("sing-box.xcodeproj/project.pbxproj"))
|
||||
var project map[string]any
|
||||
decoder := plist.NewDecoder(projectFile)
|
||||
common.Must(decoder.Decode(&project))
|
||||
objectsMap := project["objects"].(map[string]any)
|
||||
projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj")))
|
||||
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfavt"}, newVersion.VersionString())
|
||||
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfavt.standalone", "io.nekohasekai.sfavt.system"}, newVersion.String())
|
||||
if updated0 || updated1 {
|
||||
log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")")
|
||||
}
|
||||
var updated2 bool
|
||||
if macProjectVersion := os.Getenv("MACOS_PROJECT_VERSION"); macProjectVersion != "" {
|
||||
newContent, updated2 = findAndReplaceProjectVersion(objectsMap, newContent, []string{"SFM"}, macProjectVersion)
|
||||
if updated2 {
|
||||
log.Info("updated macos project version to ", macProjectVersion)
|
||||
}
|
||||
}
|
||||
if updated0 || updated1 || updated2 {
|
||||
common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj", []byte(newContent), 0o644))
|
||||
}
|
||||
}
|
||||
|
||||
func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDList []string, newVersion string) (string, bool) {
|
||||
objectKeyList := findObjectKey(objectsMap, bundleIDList)
|
||||
var updated bool
|
||||
for _, objectKey := range objectKeyList {
|
||||
matchRegexp := common.Must1(regexp.Compile(objectKey + ".*= \\{"))
|
||||
indexes := matchRegexp.FindStringIndex(projectContent)
|
||||
if len(indexes) < 2 {
|
||||
println(projectContent)
|
||||
log.Fatal("failed to find object key ", objectKey, ": ", strings.Index(projectContent, objectKey))
|
||||
}
|
||||
indexStart := indexes[1]
|
||||
indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
|
||||
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20
|
||||
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
|
||||
version := projectContent[versionStart:versionEnd]
|
||||
if version == newVersion {
|
||||
continue
|
||||
}
|
||||
updated = true
|
||||
projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:]
|
||||
}
|
||||
return projectContent, updated
|
||||
}
|
||||
|
||||
func findAndReplaceProjectVersion(objectsMap map[string]any, projectContent string, directoryList []string, newVersion string) (string, bool) {
|
||||
objectKeyList := findObjectKeyByDirectory(objectsMap, directoryList)
|
||||
var updated bool
|
||||
for _, objectKey := range objectKeyList {
|
||||
matchRegexp := common.Must1(regexp.Compile(objectKey + ".*= \\{"))
|
||||
indexes := matchRegexp.FindStringIndex(projectContent)
|
||||
if len(indexes) < 2 {
|
||||
println(projectContent)
|
||||
log.Fatal("failed to find object key ", objectKey, ": ", strings.Index(projectContent, objectKey))
|
||||
}
|
||||
indexStart := indexes[1]
|
||||
indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
|
||||
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "CURRENT_PROJECT_VERSION = ") + 26
|
||||
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
|
||||
version := projectContent[versionStart:versionEnd]
|
||||
if version == newVersion {
|
||||
continue
|
||||
}
|
||||
updated = true
|
||||
projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:]
|
||||
}
|
||||
return projectContent, updated
|
||||
}
|
||||
|
||||
func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string {
|
||||
var objectKeyList []string
|
||||
for objectKey, object := range objectsMap {
|
||||
buildSettings := object.(map[string]any)["buildSettings"]
|
||||
if buildSettings == nil {
|
||||
continue
|
||||
}
|
||||
bundleIDObject := buildSettings.(map[string]any)["PRODUCT_BUNDLE_IDENTIFIER"]
|
||||
if bundleIDObject == nil {
|
||||
continue
|
||||
}
|
||||
if common.Contains(bundleIDList, bundleIDObject.(string)) {
|
||||
objectKeyList = append(objectKeyList, objectKey)
|
||||
}
|
||||
}
|
||||
return objectKeyList
|
||||
}
|
||||
|
||||
func findObjectKeyByDirectory(objectsMap map[string]any, directoryList []string) []string {
|
||||
var objectKeyList []string
|
||||
for objectKey, object := range objectsMap {
|
||||
buildSettings := object.(map[string]any)["buildSettings"]
|
||||
if buildSettings == nil {
|
||||
continue
|
||||
}
|
||||
infoPListFile := buildSettings.(map[string]any)["INFOPLIST_FILE"]
|
||||
if infoPListFile == nil {
|
||||
continue
|
||||
}
|
||||
for _, searchDirectory := range directoryList {
|
||||
if strings.HasPrefix(infoPListFile.(string), searchDirectory+"/") {
|
||||
objectKeyList = append(objectKeyList, objectKey)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return objectKeyList
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := updateMozillaIncludedRootCAs()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func updateMozillaIncludedRootCAs() error {
|
||||
response, err := http.Get("https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
reader := csv.NewReader(response.Body)
|
||||
header, err := reader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
geoIndex := slices.Index(header, "Geographic Focus")
|
||||
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||
certIndex := slices.Index(header, "PEM Info")
|
||||
|
||||
generated := strings.Builder{}
|
||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var mozillaIncluded *x509.CertPool
|
||||
|
||||
func init() {
|
||||
mozillaIncluded = x509.NewCertPool()
|
||||
`)
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if record[geoIndex] == "China" {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
generated.WriteString(record[nameIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes
|
||||
cert = cert[1 : len(cert)-1]
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
}
|
||||
generated.WriteString("}\n")
|
||||
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/include"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
globalCtx context.Context
|
||||
configPaths []string
|
||||
configDirectories []string
|
||||
workingDir string
|
||||
disableColor bool
|
||||
)
|
||||
|
||||
var mainCommand = &cobra.Command{
|
||||
Use: "sing-box",
|
||||
PersistentPreRun: preRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
mainCommand.PersistentFlags().StringArrayVarP(&configPaths, "config", "c", nil, "set configuration file path")
|
||||
mainCommand.PersistentFlags().StringArrayVarP(&configDirectories, "config-directory", "C", nil, "set configuration directory path")
|
||||
mainCommand.PersistentFlags().StringVarP(&workingDir, "directory", "D", "", "set working directory")
|
||||
mainCommand.PersistentFlags().BoolVarP(&disableColor, "disable-color", "", false, "disable color output")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
err = os.Chdir(workingDir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(configPaths) == 0 && len(configDirectories) == 0 {
|
||||
configPaths = append(configPaths, "config.json")
|
||||
}
|
||||
globalCtx = include.Context(service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())))
|
||||
}
|
@ -2,9 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -12,32 +15,24 @@ import (
|
||||
var commandCheck = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Check configuration",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := check()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
Args: cobra.NoArgs,
|
||||
Run: checkConfiguration,
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func init() {
|
||||
mainCommand.AddCommand(commandCheck)
|
||||
}
|
||||
|
||||
func check() error {
|
||||
options, err := readConfigAndMerge()
|
||||
func checkConfiguration(cmd *cobra.Command, args []string) {
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Fatal("read config: ", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
})
|
||||
if err == nil {
|
||||
instance.Close()
|
||||
var options option.Options
|
||||
err = json.Unmarshal(configContent, &options)
|
||||
if err != nil {
|
||||
log.Fatal("decode config: ", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
_, err = box.New(ctx, options)
|
||||
if err != nil {
|
||||
log.Fatal("create service: ", err)
|
||||
}
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
|
@ -5,10 +5,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -18,58 +17,47 @@ var commandFormatFlagWrite bool
|
||||
var commandFormat = &cobra.Command{
|
||||
Use: "format",
|
||||
Short: "Format configuration",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := format()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
Args: cobra.NoArgs,
|
||||
Run: formatConfiguration,
|
||||
Args: cobra.NoArgs,
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandFormat.Flags().BoolVarP(&commandFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
||||
mainCommand.AddCommand(commandFormat)
|
||||
}
|
||||
|
||||
func format() error {
|
||||
optionsList, err := readConfig()
|
||||
func formatConfiguration(cmd *cobra.Command, args []string) {
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Fatal("read config: ", err)
|
||||
}
|
||||
for _, optionsEntry := range optionsList {
|
||||
optionsEntry.options, err = badjson.Omitempty(globalCtx, optionsEntry.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(optionsEntry.options)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
outputPath, _ := filepath.Abs(optionsEntry.path)
|
||||
if !commandFormatFlagWrite {
|
||||
if len(optionsList) > 1 {
|
||||
os.Stdout.WriteString(outputPath + "\n")
|
||||
}
|
||||
os.Stdout.WriteString(buffer.String() + "\n")
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(optionsEntry.content, buffer.Bytes()) {
|
||||
continue
|
||||
}
|
||||
output, err := os.Create(optionsEntry.path)
|
||||
if err != nil {
|
||||
return E.Cause(err, "open output")
|
||||
}
|
||||
_, err = output.Write(buffer.Bytes())
|
||||
output.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "write output")
|
||||
}
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
var options option.Options
|
||||
err = json.Unmarshal(configContent, &options)
|
||||
if err != nil {
|
||||
log.Fatal("decode config: ", err)
|
||||
}
|
||||
return nil
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(options)
|
||||
if err != nil {
|
||||
log.Fatal("encode config: ", err)
|
||||
}
|
||||
if !commandFormatFlagWrite {
|
||||
os.Stdout.WriteString(buffer.String() + "\n")
|
||||
return
|
||||
}
|
||||
if bytes.Equal(configContent, buffer.Bytes()) {
|
||||
return
|
||||
}
|
||||
output, err := os.Create(configPath)
|
||||
if err != nil {
|
||||
log.Fatal("open output: ", err)
|
||||
}
|
||||
_, err = output.Write(buffer.Bytes())
|
||||
output.Close()
|
||||
if err != nil {
|
||||
log.Fatal("write output: ", err)
|
||||
}
|
||||
outputPath, _ := filepath.Abs(configPath)
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
}
|
||||
|
@ -1,92 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandGenerate = &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate things",
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerate.AddCommand(commandGenerateUUID)
|
||||
commandGenerate.AddCommand(commandGenerateRandom)
|
||||
|
||||
mainCommand.AddCommand(commandGenerate)
|
||||
}
|
||||
|
||||
var (
|
||||
outputBase64 bool
|
||||
outputHex bool
|
||||
)
|
||||
|
||||
var commandGenerateRandom = &cobra.Command{
|
||||
Use: "rand <length>",
|
||||
Short: "Generate random bytes",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateRandom(args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerateRandom.Flags().BoolVar(&outputBase64, "base64", false, "Generate base64 string")
|
||||
commandGenerateRandom.Flags().BoolVar(&outputHex, "hex", false, "Generate hex string")
|
||||
}
|
||||
|
||||
func generateRandom(args []string) error {
|
||||
length, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
randomBytes := make([]byte, length)
|
||||
_, err = rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputBase64 {
|
||||
_, err = os.Stdout.WriteString(base64.StdEncoding.EncodeToString(randomBytes) + "\n")
|
||||
} else if outputHex {
|
||||
_, err = os.Stdout.WriteString(hex.EncodeToString(randomBytes) + "\n")
|
||||
} else {
|
||||
_, err = os.Stdout.Write(randomBytes)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var commandGenerateUUID = &cobra.Command{
|
||||
Use: "uuid",
|
||||
Short: "Generate UUID string",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateUUID()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func generateUUID() error {
|
||||
newUUID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stdout.WriteString(newUUID.String() + "\n")
|
||||
return err
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandGenerateECHKeyPair = &cobra.Command{
|
||||
Use: "ech-keypair <plain_server_name>",
|
||||
Short: "Generate TLS ECH key pair",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateECHKeyPair(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerate.AddCommand(commandGenerateECHKeyPair)
|
||||
}
|
||||
|
||||
func generateECHKeyPair(serverName string) error {
|
||||
configPem, keyPem, err := tls.ECHKeygenDefault(serverName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.WriteString(configPem)
|
||||
os.Stdout.WriteString(keyPem)
|
||||
return nil
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var flagGenerateTLSKeyPairMonths int
|
||||
|
||||
var commandGenerateTLSKeyPair = &cobra.Command{
|
||||
Use: "tls-keypair <server_name>",
|
||||
Short: "Generate TLS self sign key pair",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateTLSKeyPair(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerateTLSKeyPair.Flags().IntVarP(&flagGenerateTLSKeyPairMonths, "months", "m", 1, "Valid months")
|
||||
commandGenerate.AddCommand(commandGenerateTLSKeyPair)
|
||||
}
|
||||
|
||||
func generateTLSKeyPair(serverName string) error {
|
||||
privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.WriteString(string(privateKeyPem) + "\n")
|
||||
os.Stdout.WriteString(string(publicKeyPem) + "\n")
|
||||
return nil
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
//go:build go1.20
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandGenerateVAPIDKeyPair = &cobra.Command{
|
||||
Use: "vapid-keypair",
|
||||
Short: "Generate VAPID key pair",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateVAPIDKeyPair()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerate.AddCommand(commandGenerateVAPIDKeyPair)
|
||||
}
|
||||
|
||||
func generateVAPIDKeyPair() error {
|
||||
privateKey, err := ecdh.P256().GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicKey := privateKey.PublicKey()
|
||||
os.Stdout.WriteString("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey.Bytes()) + "\n")
|
||||
os.Stdout.WriteString("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey.Bytes()) + "\n")
|
||||
return nil
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commandGenerate.AddCommand(commandGenerateWireGuardKeyPair)
|
||||
commandGenerate.AddCommand(commandGenerateRealityKeyPair)
|
||||
}
|
||||
|
||||
var commandGenerateWireGuardKeyPair = &cobra.Command{
|
||||
Use: "wg-keypair",
|
||||
Short: "Generate WireGuard key pair",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateWireGuardKey()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func generateWireGuardKey() error {
|
||||
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.WriteString("PrivateKey: " + privateKey.String() + "\n")
|
||||
os.Stdout.WriteString("PublicKey: " + privateKey.PublicKey().String() + "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
var commandGenerateRealityKeyPair = &cobra.Command{
|
||||
Use: "reality-keypair",
|
||||
Short: "Generate reality key pair",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateRealityKey()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func generateRealityKey() error {
|
||||
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicKey := privateKey.PublicKey()
|
||||
os.Stdout.WriteString("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]) + "\n")
|
||||
os.Stdout.WriteString("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:]) + "\n")
|
||||
return nil
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
geoipReader *maxminddb.Reader
|
||||
commandGeoIPFlagFile string
|
||||
)
|
||||
|
||||
var commandGeoip = &cobra.Command{
|
||||
Use: "geoip",
|
||||
Short: "GeoIP tools",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
err := geoipPreRun()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGeoip.PersistentFlags().StringVarP(&commandGeoIPFlagFile, "file", "f", "geoip.db", "geoip file")
|
||||
mainCommand.AddCommand(commandGeoip)
|
||||
}
|
||||
|
||||
func geoipPreRun() error {
|
||||
reader, err := maxminddb.Open(commandGeoIPFlagFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if reader.Metadata.DatabaseType != "sing-geoip" {
|
||||
reader.Close()
|
||||
return E.New("incorrect database type, expected sing-geoip, got ", reader.Metadata.DatabaseType)
|
||||
}
|
||||
geoipReader = reader
|
||||
return nil
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var flagGeoipExportOutput string
|
||||
|
||||
const flagGeoipExportDefaultOutput = "geoip-<country>.srs"
|
||||
|
||||
var commandGeoipExport = &cobra.Command{
|
||||
Use: "export <country>",
|
||||
Short: "Export geoip country as rule-set",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := geoipExport(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGeoipExport.Flags().StringVarP(&flagGeoipExportOutput, "output", "o", flagGeoipExportDefaultOutput, "Output path")
|
||||
commandGeoip.AddCommand(commandGeoipExport)
|
||||
}
|
||||
|
||||
func geoipExport(countryCode string) error {
|
||||
networks := geoipReader.Networks(maxminddb.SkipAliasedNetworks)
|
||||
countryMap := make(map[string][]*net.IPNet)
|
||||
var (
|
||||
ipNet *net.IPNet
|
||||
nextCountryCode string
|
||||
err error
|
||||
)
|
||||
for networks.Next() {
|
||||
ipNet, err = networks.Network(&nextCountryCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
countryMap[nextCountryCode] = append(countryMap[nextCountryCode], ipNet)
|
||||
}
|
||||
ipNets := countryMap[strings.ToLower(countryCode)]
|
||||
if len(ipNets) == 0 {
|
||||
return E.New("country code not found: ", countryCode)
|
||||
}
|
||||
|
||||
var (
|
||||
outputFile *os.File
|
||||
outputWriter io.Writer
|
||||
)
|
||||
if flagGeoipExportOutput == "stdout" {
|
||||
outputWriter = os.Stdout
|
||||
} else if flagGeoipExportOutput == flagGeoipExportDefaultOutput {
|
||||
outputFile, err = os.Create("geoip-" + countryCode + ".json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outputFile.Close()
|
||||
outputWriter = outputFile
|
||||
} else {
|
||||
outputFile, err = os.Create(flagGeoipExportOutput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outputFile.Close()
|
||||
outputWriter = outputFile
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(outputWriter)
|
||||
encoder.SetIndent("", " ")
|
||||
var headlessRule option.DefaultHeadlessRule
|
||||
headlessRule.IPCIDR = make([]string, 0, len(ipNets))
|
||||
for _, cidr := range ipNets {
|
||||
headlessRule.IPCIDR = append(headlessRule.IPCIDR, cidr.String())
|
||||
}
|
||||
var plainRuleSet option.PlainRuleSetCompat
|
||||
plainRuleSet.Version = C.RuleSetVersion2
|
||||
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
||||
{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: headlessRule,
|
||||
},
|
||||
}
|
||||
return encoder.Encode(plainRuleSet)
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandGeoipList = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List geoip country codes",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := listGeoip()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGeoip.AddCommand(commandGeoipList)
|
||||
}
|
||||
|
||||
func listGeoip() error {
|
||||
for _, code := range geoipReader.Metadata.Languages {
|
||||
os.Stdout.WriteString(code + "\n")
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandGeoipLookup = &cobra.Command{
|
||||
Use: "lookup <address>",
|
||||
Short: "Lookup if an IP address is contained in the GeoIP database",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := geoipLookup(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGeoip.AddCommand(commandGeoipLookup)
|
||||
}
|
||||
|
||||
func geoipLookup(address string) error {
|
||||
addr, err := netip.ParseAddr(address)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse address")
|
||||
}
|
||||
if !N.IsPublicAddr(addr) {
|
||||
os.Stdout.WriteString("private\n")
|
||||
return nil
|
||||
}
|
||||
var code string
|
||||
_ = geoipReader.Lookup(addr.AsSlice(), &code)
|
||||
if code != "" {
|
||||
os.Stdout.WriteString(code + "\n")
|
||||
return nil
|
||||
}
|
||||
os.Stdout.WriteString("unknown\n")
|
||||
return nil
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/common/geosite"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
commandGeoSiteFlagFile string
|
||||
geositeReader *geosite.Reader
|
||||
geositeCodeList []string
|
||||
)
|
||||
|
||||
var commandGeoSite = &cobra.Command{
|
||||
Use: "geosite",
|
||||
Short: "Geosite tools",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
err := geositePreRun()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGeoSite.PersistentFlags().StringVarP(&commandGeoSiteFlagFile, "file", "f", "geosite.db", "geosite file")
|
||||
mainCommand.AddCommand(commandGeoSite)
|
||||
}
|
||||
|
||||
func geositePreRun() error {
|
||||
reader, codeList, err := geosite.Open(commandGeoSiteFlagFile)
|
||||
if err != nil {
|
||||
return E.Cause(err, "open geosite file")
|
||||
}
|
||||
geositeReader = reader
|
||||
geositeCodeList = codeList
|
||||
return nil
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/common/geosite"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandGeositeExportOutput string
|
||||
|
||||
const commandGeositeExportDefaultOutput = "geosite-<category>.json"
|
||||
|
||||
var commandGeositeExport = &cobra.Command{
|
||||
Use: "export <category>",
|
||||
Short: "Export geosite category as rule-set",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := geositeExport(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGeositeExport.Flags().StringVarP(&commandGeositeExportOutput, "output", "o", commandGeositeExportDefaultOutput, "Output path")
|
||||
commandGeoSite.AddCommand(commandGeositeExport)
|
||||
}
|
||||
|
||||
func geositeExport(category string) error {
|
||||
sourceSet, err := geositeReader.Read(category)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
outputFile *os.File
|
||||
outputWriter io.Writer
|
||||
)
|
||||
if commandGeositeExportOutput == "stdout" {
|
||||
outputWriter = os.Stdout
|
||||
} else if commandGeositeExportOutput == commandGeositeExportDefaultOutput {
|
||||
outputFile, err = os.Create("geosite-" + category + ".json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outputFile.Close()
|
||||
outputWriter = outputFile
|
||||
} else {
|
||||
outputFile, err = os.Create(commandGeositeExportOutput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outputFile.Close()
|
||||
outputWriter = outputFile
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(outputWriter)
|
||||
encoder.SetIndent("", " ")
|
||||
var headlessRule option.DefaultHeadlessRule
|
||||
defaultRule := geosite.Compile(sourceSet)
|
||||
headlessRule.Domain = defaultRule.Domain
|
||||
headlessRule.DomainSuffix = defaultRule.DomainSuffix
|
||||
headlessRule.DomainKeyword = defaultRule.DomainKeyword
|
||||
headlessRule.DomainRegex = defaultRule.DomainRegex
|
||||
var plainRuleSet option.PlainRuleSetCompat
|
||||
plainRuleSet.Version = C.RuleSetVersion2
|
||||
plainRuleSet.Options.Rules = []option.HeadlessRule{
|
||||
{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: headlessRule,
|
||||
},
|
||||
}
|
||||
return encoder.Encode(plainRuleSet)
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandGeositeList = &cobra.Command{
|
||||
Use: "list <category>",
|
||||
Short: "List geosite categories",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := geositeList()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGeoSite.AddCommand(commandGeositeList)
|
||||
}
|
||||
|
||||
func geositeList() error {
|
||||
var geositeEntry []struct {
|
||||
category string
|
||||
items int
|
||||
}
|
||||
for _, category := range geositeCodeList {
|
||||
sourceSet, err := geositeReader.Read(category)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
geositeEntry = append(geositeEntry, struct {
|
||||
category string
|
||||
items int
|
||||
}{category, len(sourceSet)})
|
||||
}
|
||||
sort.SliceStable(geositeEntry, func(i, j int) bool {
|
||||
return geositeEntry[i].items < geositeEntry[j].items
|
||||
})
|
||||
for _, entry := range geositeEntry {
|
||||
os.Stdout.WriteString(F.ToString(entry.category, " (", entry.items, ")\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandGeositeLookup = &cobra.Command{
|
||||
Use: "lookup [category] <domain>",
|
||||
Short: "Check if a domain is in the geosite",
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var (
|
||||
source string
|
||||
target string
|
||||
)
|
||||
switch len(args) {
|
||||
case 1:
|
||||
target = args[0]
|
||||
case 2:
|
||||
source = args[0]
|
||||
target = args[1]
|
||||
}
|
||||
err := geositeLookup(source, target)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGeoSite.AddCommand(commandGeositeLookup)
|
||||
}
|
||||
|
||||
func geositeLookup(source string, target string) error {
|
||||
var sourceMatcherList []struct {
|
||||
code string
|
||||
matcher *searchGeositeMatcher
|
||||
}
|
||||
if source != "" {
|
||||
sourceSet, err := geositeReader.Read(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceMatcher, err := newSearchGeositeMatcher(sourceSet)
|
||||
if err != nil {
|
||||
return E.Cause(err, "compile code: "+source)
|
||||
}
|
||||
sourceMatcherList = []struct {
|
||||
code string
|
||||
matcher *searchGeositeMatcher
|
||||
}{
|
||||
{
|
||||
code: source,
|
||||
matcher: sourceMatcher,
|
||||
},
|
||||
}
|
||||
|
||||
} else {
|
||||
for _, code := range geositeCodeList {
|
||||
sourceSet, err := geositeReader.Read(code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceMatcher, err := newSearchGeositeMatcher(sourceSet)
|
||||
if err != nil {
|
||||
return E.Cause(err, "compile code: "+code)
|
||||
}
|
||||
sourceMatcherList = append(sourceMatcherList, struct {
|
||||
code string
|
||||
matcher *searchGeositeMatcher
|
||||
}{
|
||||
code: code,
|
||||
matcher: sourceMatcher,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(sourceMatcherList, func(i, j int) bool {
|
||||
return sourceMatcherList[i].code < sourceMatcherList[j].code
|
||||
})
|
||||
|
||||
for _, matcherItem := range sourceMatcherList {
|
||||
if matchRule := matcherItem.matcher.Match(target); matchRule != "" {
|
||||
os.Stdout.WriteString("Match code (")
|
||||
os.Stdout.WriteString(matcherItem.code)
|
||||
os.Stdout.WriteString(") ")
|
||||
os.Stdout.WriteString(matchRule)
|
||||
os.Stdout.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/geosite"
|
||||
)
|
||||
|
||||
type searchGeositeMatcher struct {
|
||||
domainMap map[string]bool
|
||||
suffixList []string
|
||||
keywordList []string
|
||||
regexList []string
|
||||
}
|
||||
|
||||
func newSearchGeositeMatcher(items []geosite.Item) (*searchGeositeMatcher, error) {
|
||||
options := geosite.Compile(items)
|
||||
domainMap := make(map[string]bool)
|
||||
for _, domain := range options.Domain {
|
||||
domainMap[domain] = true
|
||||
}
|
||||
rule := &searchGeositeMatcher{
|
||||
domainMap: domainMap,
|
||||
suffixList: options.DomainSuffix,
|
||||
keywordList: options.DomainKeyword,
|
||||
regexList: options.DomainRegex,
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func (r *searchGeositeMatcher) Match(domain string) string {
|
||||
if r.domainMap[domain] {
|
||||
return "domain=" + domain
|
||||
}
|
||||
for _, suffix := range r.suffixList {
|
||||
if strings.HasSuffix(domain, suffix) {
|
||||
return "domain_suffix=" + suffix
|
||||
}
|
||||
}
|
||||
for _, keyword := range r.keywordList {
|
||||
if strings.Contains(domain, keyword) {
|
||||
return "domain_keyword=" + keyword
|
||||
}
|
||||
}
|
||||
for _, regexStr := range r.regexList {
|
||||
regex, err := regexp.Compile(regexStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if regex.MatchString(domain) {
|
||||
return "domain_regex=" + regexStr
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandMerge = &cobra.Command{
|
||||
Use: "merge <output-path>",
|
||||
Short: "Merge configurations",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := merge(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
func init() {
|
||||
mainCommand.AddCommand(commandMerge)
|
||||
}
|
||||
|
||||
func merge(outputPath string) error {
|
||||
mergedOptions, err := readConfigAndMerge()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = mergePathResources(&mergedOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(mergedOptions)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
if existsContent, err := os.ReadFile(outputPath); err != nil {
|
||||
if string(existsContent) == buffer.String() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
err = rw.MkdirParent(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputPath, _ = filepath.Abs(outputPath)
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergePathResources(options *option.Options) error {
|
||||
for _, inbound := range options.Inbounds {
|
||||
if tlsOptions, containsTLSOptions := inbound.Options.(option.InboundTLSOptionsWrapper); containsTLSOptions {
|
||||
tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions()))
|
||||
}
|
||||
}
|
||||
for _, outbound := range options.Outbounds {
|
||||
switch outbound.Type {
|
||||
case C.TypeSSH:
|
||||
mergeSSHOutboundOptions(outbound.Options.(*option.SSHOutboundOptions))
|
||||
}
|
||||
if tlsOptions, containsTLSOptions := outbound.Options.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
|
||||
tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions()))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeTLSInboundOptions(options *option.InboundTLSOptions) *option.InboundTLSOptions {
|
||||
if options == nil {
|
||||
return nil
|
||||
}
|
||||
if options.CertificatePath != "" {
|
||||
if content, err := os.ReadFile(options.CertificatePath); err == nil {
|
||||
options.Certificate = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
if options.KeyPath != "" {
|
||||
if content, err := os.ReadFile(options.KeyPath); err == nil {
|
||||
options.Key = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
if options.ECH != nil {
|
||||
if options.ECH.KeyPath != "" {
|
||||
if content, err := os.ReadFile(options.ECH.KeyPath); err == nil {
|
||||
options.ECH.Key = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.OutboundTLSOptions {
|
||||
if options == nil {
|
||||
return nil
|
||||
}
|
||||
if options.CertificatePath != "" {
|
||||
if content, err := os.ReadFile(options.CertificatePath); err == nil {
|
||||
options.Certificate = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
if options.ECH != nil {
|
||||
if options.ECH.ConfigPath != "" {
|
||||
if content, err := os.ReadFile(options.ECH.ConfigPath); err == nil {
|
||||
options.ECH.Config = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func mergeSSHOutboundOptions(options *option.SSHOutboundOptions) {
|
||||
if options.PrivateKeyPath != "" {
|
||||
if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil {
|
||||
options.PrivateKey = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func trimStringArray(array []string) []string {
|
||||
return common.Filter(array, func(it string) bool {
|
||||
return strings.TrimSpace(it) != ""
|
||||
})
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandRuleSet = &cobra.Command{
|
||||
Use: "rule-set",
|
||||
Short: "Manage rule-sets",
|
||||
}
|
||||
|
||||
func init() {
|
||||
mainCommand.AddCommand(commandRuleSet)
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var flagRuleSetCompileOutput string
|
||||
|
||||
const flagRuleSetCompileDefaultOutput = "<file_name>.srs"
|
||||
|
||||
var commandRuleSetCompile = &cobra.Command{
|
||||
Use: "compile [source-path]",
|
||||
Short: "Compile rule-set json to binary",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := compileRuleSet(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSet.AddCommand(commandRuleSetCompile)
|
||||
commandRuleSetCompile.Flags().StringVarP(&flagRuleSetCompileOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
|
||||
}
|
||||
|
||||
func compileRuleSet(sourcePath string) error {
|
||||
var (
|
||||
reader io.Reader
|
||||
err error
|
||||
)
|
||||
if sourcePath == "stdin" {
|
||||
reader = os.Stdin
|
||||
} else {
|
||||
reader, err = os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var outputPath string
|
||||
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
|
||||
if strings.HasSuffix(sourcePath, ".json") {
|
||||
outputPath = sourcePath[:len(sourcePath)-5] + ".srs"
|
||||
} else {
|
||||
outputPath = sourcePath + ".srs"
|
||||
}
|
||||
} else {
|
||||
outputPath = flagRuleSetCompileOutput
|
||||
}
|
||||
outputFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version)
|
||||
if err != nil {
|
||||
outputFile.Close()
|
||||
os.Remove(outputPath)
|
||||
return err
|
||||
}
|
||||
outputFile.Close()
|
||||
return nil
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
flagRuleSetConvertType string
|
||||
flagRuleSetConvertOutput string
|
||||
)
|
||||
|
||||
var commandRuleSetConvert = &cobra.Command{
|
||||
Use: "convert [source-path]",
|
||||
Short: "Convert adguard DNS filter to rule-set",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := convertRuleSet(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSet.AddCommand(commandRuleSetConvert)
|
||||
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertType, "type", "t", "", "Source type, available: adguard")
|
||||
commandRuleSetConvert.Flags().StringVarP(&flagRuleSetConvertOutput, "output", "o", flagRuleSetCompileDefaultOutput, "Output file")
|
||||
}
|
||||
|
||||
func convertRuleSet(sourcePath string) error {
|
||||
var (
|
||||
reader io.Reader
|
||||
err error
|
||||
)
|
||||
if sourcePath == "stdin" {
|
||||
reader = os.Stdin
|
||||
} else {
|
||||
reader, err = os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var rules []option.HeadlessRule
|
||||
switch flagRuleSetConvertType {
|
||||
case "adguard":
|
||||
rules, err = adguard.Convert(reader)
|
||||
case "":
|
||||
return E.New("source type is required")
|
||||
default:
|
||||
return E.New("unsupported source type: ", flagRuleSetConvertType)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var outputPath string
|
||||
if flagRuleSetConvertOutput == flagRuleSetCompileDefaultOutput {
|
||||
if strings.HasSuffix(sourcePath, ".txt") {
|
||||
outputPath = sourcePath[:len(sourcePath)-4] + ".srs"
|
||||
} else {
|
||||
outputPath = sourcePath + ".srs"
|
||||
}
|
||||
} else {
|
||||
outputPath = flagRuleSetConvertOutput
|
||||
}
|
||||
outputFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outputFile.Close()
|
||||
err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, C.RuleSetVersion2)
|
||||
if err != nil {
|
||||
outputFile.Close()
|
||||
os.Remove(outputPath)
|
||||
return err
|
||||
}
|
||||
outputFile.Close()
|
||||
return nil
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var flagRuleSetDecompileOutput string
|
||||
|
||||
const flagRuleSetDecompileDefaultOutput = "<file_name>.json"
|
||||
|
||||
var commandRuleSetDecompile = &cobra.Command{
|
||||
Use: "decompile [binary-path]",
|
||||
Short: "Decompile rule-set binary to json",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := decompileRuleSet(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSet.AddCommand(commandRuleSetDecompile)
|
||||
commandRuleSetDecompile.Flags().StringVarP(&flagRuleSetDecompileOutput, "output", "o", flagRuleSetDecompileDefaultOutput, "Output file")
|
||||
}
|
||||
|
||||
func decompileRuleSet(sourcePath string) error {
|
||||
var (
|
||||
reader io.Reader
|
||||
err error
|
||||
)
|
||||
if sourcePath == "stdin" {
|
||||
reader = os.Stdin
|
||||
} else {
|
||||
reader, err = os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ruleSet, err := srs.Read(reader, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var outputPath string
|
||||
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
|
||||
if strings.HasSuffix(sourcePath, ".srs") {
|
||||
outputPath = sourcePath[:len(sourcePath)-4] + ".json"
|
||||
} else {
|
||||
outputPath = sourcePath + ".json"
|
||||
}
|
||||
} else {
|
||||
outputPath = flagRuleSetDecompileOutput
|
||||
}
|
||||
outputFile, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encoder := json.NewEncoder(outputFile)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(ruleSet)
|
||||
if err != nil {
|
||||
outputFile.Close()
|
||||
os.Remove(outputPath)
|
||||
return err
|
||||
}
|
||||
outputFile.Close()
|
||||
return nil
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandRuleSetFormatFlagWrite bool
|
||||
|
||||
var commandRuleSetFormat = &cobra.Command{
|
||||
Use: "format <source-path>",
|
||||
Short: "Format rule-set json",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := formatRuleSet(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSetFormat.Flags().BoolVarP(&commandRuleSetFormatFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
||||
commandRuleSet.AddCommand(commandRuleSetFormat)
|
||||
}
|
||||
|
||||
func formatRuleSet(sourcePath string) error {
|
||||
var (
|
||||
reader io.Reader
|
||||
err error
|
||||
)
|
||||
if sourcePath == "stdin" {
|
||||
reader = os.Stdin
|
||||
} else {
|
||||
reader, err = os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plainRuleSet, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(plainRuleSet)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
outputPath, _ := filepath.Abs(sourcePath)
|
||||
if !commandRuleSetFormatFlagWrite || sourcePath == "stdin" {
|
||||
os.Stdout.WriteString(buffer.String() + "\n")
|
||||
return nil
|
||||
}
|
||||
if bytes.Equal(content, buffer.Bytes()) {
|
||||
return nil
|
||||
}
|
||||
output, err := os.Create(sourcePath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "open output")
|
||||
}
|
||||
_, err = output.Write(buffer.Bytes())
|
||||
output.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "write output")
|
||||
}
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
return nil
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/route/rule"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var flagRuleSetMatchFormat string
|
||||
|
||||
var commandRuleSetMatch = &cobra.Command{
|
||||
Use: "match <rule-set path> <IP address/domain>",
|
||||
Short: "Check if an IP address or a domain matches the rule-set",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := ruleSetMatch(args[0], args[1])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSetMatch.Flags().StringVarP(&flagRuleSetMatchFormat, "format", "f", "source", "rule-set format")
|
||||
commandRuleSet.AddCommand(commandRuleSetMatch)
|
||||
}
|
||||
|
||||
func ruleSetMatch(sourcePath string, domain string) error {
|
||||
var (
|
||||
reader io.Reader
|
||||
err error
|
||||
)
|
||||
if sourcePath == "stdin" {
|
||||
reader = os.Stdin
|
||||
} else {
|
||||
reader, err = os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "read rule-set")
|
||||
}
|
||||
}
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return E.Cause(err, "read rule-set")
|
||||
}
|
||||
if flagRuleSetMatchFormat == "" {
|
||||
switch filepath.Ext(sourcePath) {
|
||||
case ".json":
|
||||
flagRuleSetMatchFormat = C.RuleSetFormatSource
|
||||
case ".srs":
|
||||
flagRuleSetMatchFormat = C.RuleSetFormatBinary
|
||||
}
|
||||
}
|
||||
var ruleSet option.PlainRuleSetCompat
|
||||
switch flagRuleSetMatchFormat {
|
||||
case C.RuleSetFormatSource:
|
||||
ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case C.RuleSetFormatBinary:
|
||||
ruleSet, err = srs.Read(bytes.NewReader(content), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return E.New("unknown rule-set format: ", flagRuleSetMatchFormat)
|
||||
}
|
||||
plainRuleSet, err := ruleSet.Upgrade()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ipAddress := M.ParseAddr(domain)
|
||||
var metadata adapter.InboundContext
|
||||
if ipAddress.IsValid() {
|
||||
metadata.Destination = M.SocksaddrFrom(ipAddress, 0)
|
||||
} else {
|
||||
metadata.Domain = domain
|
||||
}
|
||||
for i, ruleOptions := range plainRuleSet.Rules {
|
||||
var currentRule adapter.HeadlessRule
|
||||
currentRule, err = rule.NewHeadlessRule(context.Background(), ruleOptions)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||
}
|
||||
if currentRule.Match(&metadata) {
|
||||
println(F.ToString("match rules.[", i, "]: ", currentRule))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
ruleSetPaths []string
|
||||
ruleSetDirectories []string
|
||||
)
|
||||
|
||||
var commandRuleSetMerge = &cobra.Command{
|
||||
Use: "merge <output-path>",
|
||||
Short: "Merge rule-set source files",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := mergeRuleSet(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetPaths, "config", "c", nil, "set input rule-set file path")
|
||||
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetDirectories, "config-directory", "C", nil, "set input rule-set directory path")
|
||||
commandRuleSet.AddCommand(commandRuleSetMerge)
|
||||
}
|
||||
|
||||
type RuleSetEntry struct {
|
||||
content []byte
|
||||
path string
|
||||
options option.PlainRuleSetCompat
|
||||
}
|
||||
|
||||
func readRuleSetAt(path string) (*RuleSetEntry, error) {
|
||||
var (
|
||||
configContent []byte
|
||||
err error
|
||||
)
|
||||
if path == "stdin" {
|
||||
configContent, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
configContent, err = os.ReadFile(path)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read config at ", path)
|
||||
}
|
||||
options, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, configContent)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode config at ", path)
|
||||
}
|
||||
return &RuleSetEntry{
|
||||
content: configContent,
|
||||
path: path,
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readRuleSet() ([]*RuleSetEntry, error) {
|
||||
var optionsList []*RuleSetEntry
|
||||
for _, path := range ruleSetPaths {
|
||||
optionsEntry, err := readRuleSetAt(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
optionsList = append(optionsList, optionsEntry)
|
||||
}
|
||||
for _, directory := range ruleSetDirectories {
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read rule-set directory at ", directory)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
optionsEntry, err := readRuleSetAt(filepath.Join(directory, entry.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
optionsList = append(optionsList, optionsEntry)
|
||||
}
|
||||
}
|
||||
sort.Slice(optionsList, func(i, j int) bool {
|
||||
return optionsList[i].path < optionsList[j].path
|
||||
})
|
||||
return optionsList, nil
|
||||
}
|
||||
|
||||
func readRuleSetAndMerge() (option.PlainRuleSetCompat, error) {
|
||||
optionsList, err := readRuleSet()
|
||||
if err != nil {
|
||||
return option.PlainRuleSetCompat{}, err
|
||||
}
|
||||
if len(optionsList) == 1 {
|
||||
return optionsList[0].options, nil
|
||||
}
|
||||
var optionVersion uint8
|
||||
for _, options := range optionsList {
|
||||
if optionVersion < options.options.Version {
|
||||
optionVersion = options.options.Version
|
||||
}
|
||||
}
|
||||
var mergedMessage json.RawMessage
|
||||
for _, options := range optionsList {
|
||||
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
|
||||
if err != nil {
|
||||
return option.PlainRuleSetCompat{}, E.Cause(err, "merge config at ", options.path)
|
||||
}
|
||||
}
|
||||
mergedOptions, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, mergedMessage)
|
||||
if err != nil {
|
||||
return option.PlainRuleSetCompat{}, E.Cause(err, "unmarshal merged config")
|
||||
}
|
||||
mergedOptions.Version = optionVersion
|
||||
return mergedOptions, nil
|
||||
}
|
||||
|
||||
func mergeRuleSet(outputPath string) error {
|
||||
mergedOptions, err := readRuleSetAndMerge()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(mergedOptions)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
if existsContent, err := os.ReadFile(outputPath); err != nil {
|
||||
if string(existsContent) == buffer.String() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
err = rw.MkdirParent(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputPath, _ = filepath.Abs(outputPath)
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
return nil
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandRuleSetUpgradeFlagWrite bool
|
||||
|
||||
var commandRuleSetUpgrade = &cobra.Command{
|
||||
Use: "upgrade <source-path>",
|
||||
Short: "Upgrade rule-set json",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := upgradeRuleSet(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSetUpgrade.Flags().BoolVarP(&commandRuleSetUpgradeFlagWrite, "write", "w", false, "write result to (source) file instead of stdout")
|
||||
commandRuleSet.AddCommand(commandRuleSetUpgrade)
|
||||
}
|
||||
|
||||
func upgradeRuleSet(sourcePath string) error {
|
||||
var (
|
||||
reader io.Reader
|
||||
err error
|
||||
)
|
||||
if sourcePath == "stdin" {
|
||||
reader = os.Stdin
|
||||
} else {
|
||||
reader, err = os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plainRuleSetCompat, err := json.UnmarshalExtended[option.PlainRuleSetCompat](content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch plainRuleSetCompat.Version {
|
||||
case C.RuleSetVersion1:
|
||||
default:
|
||||
log.Info("already up-to-date")
|
||||
return nil
|
||||
}
|
||||
plainRuleSetCompat.Options, err = plainRuleSetCompat.Upgrade()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plainRuleSetCompat.Version = C.RuleSetVersionCurrent
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(plainRuleSetCompat)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
outputPath, _ := filepath.Abs(sourcePath)
|
||||
if !commandRuleSetUpgradeFlagWrite || sourcePath == "stdin" {
|
||||
os.Stdout.WriteString(buffer.String() + "\n")
|
||||
return nil
|
||||
}
|
||||
if bytes.Equal(content, buffer.Bytes()) {
|
||||
return nil
|
||||
}
|
||||
output, err := os.Create(sourcePath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "open output")
|
||||
}
|
||||
_, err = output.Write(buffer.Bytes())
|
||||
output.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "write output")
|
||||
}
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
return nil
|
||||
}
|
@ -2,23 +2,17 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
runtimeDebug "runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/common/json"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/debug"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -26,106 +20,25 @@ import (
|
||||
var commandRun = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Run service",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
Run: run,
|
||||
}
|
||||
|
||||
func init() {
|
||||
mainCommand.AddCommand(commandRun)
|
||||
}
|
||||
|
||||
type OptionsEntry struct {
|
||||
content []byte
|
||||
path string
|
||||
options option.Options
|
||||
}
|
||||
|
||||
func readConfigAt(path string) (*OptionsEntry, error) {
|
||||
var (
|
||||
configContent []byte
|
||||
err error
|
||||
)
|
||||
if path == "stdin" {
|
||||
configContent, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
configContent, err = os.ReadFile(path)
|
||||
}
|
||||
func run(cmd *cobra.Command, args []string) {
|
||||
err := run0()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read config at ", path)
|
||||
log.Fatal(err)
|
||||
}
|
||||
options, err := json.UnmarshalExtendedContext[option.Options](globalCtx, configContent)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode config at ", path)
|
||||
}
|
||||
return &OptionsEntry{
|
||||
content: configContent,
|
||||
path: path,
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readConfig() ([]*OptionsEntry, error) {
|
||||
var optionsList []*OptionsEntry
|
||||
for _, path := range configPaths {
|
||||
optionsEntry, err := readConfigAt(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
optionsList = append(optionsList, optionsEntry)
|
||||
}
|
||||
for _, directory := range configDirectories {
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read config directory at ", directory)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
optionsEntry, err := readConfigAt(filepath.Join(directory, entry.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
optionsList = append(optionsList, optionsEntry)
|
||||
}
|
||||
}
|
||||
sort.Slice(optionsList, func(i, j int) bool {
|
||||
return optionsList[i].path < optionsList[j].path
|
||||
})
|
||||
return optionsList, nil
|
||||
}
|
||||
|
||||
func readConfigAndMerge() (option.Options, error) {
|
||||
optionsList, err := readConfig()
|
||||
func run0() error {
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return option.Options{}, err
|
||||
return E.Cause(err, "read config")
|
||||
}
|
||||
if len(optionsList) == 1 {
|
||||
return optionsList[0].options, nil
|
||||
}
|
||||
var mergedMessage json.RawMessage
|
||||
for _, options := range optionsList {
|
||||
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "merge config at ", options.path)
|
||||
}
|
||||
}
|
||||
var mergedOptions option.Options
|
||||
err = mergedOptions.UnmarshalJSONContext(globalCtx, mergedMessage)
|
||||
var options option.Options
|
||||
err = json.Unmarshal(configContent, &options)
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "unmarshal merged config")
|
||||
}
|
||||
return mergedOptions, nil
|
||||
}
|
||||
|
||||
func create() (*box.Box, context.CancelFunc, error) {
|
||||
options, err := readConfigAndMerge()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return E.Cause(err, "decode config")
|
||||
}
|
||||
if disableColor {
|
||||
if options.Log == nil {
|
||||
@ -133,80 +46,27 @@ func create() (*box.Box, context.CancelFunc, error) {
|
||||
}
|
||||
options.Log.DisableColor = true
|
||||
}
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
})
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
instance, err := box.New(ctx, options)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, E.Cause(err, "create service")
|
||||
return E.Cause(err, "create service")
|
||||
}
|
||||
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer func() {
|
||||
signal.Stop(osSignals)
|
||||
close(osSignals)
|
||||
}()
|
||||
startCtx, finishStart := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
_, loaded := <-osSignals
|
||||
if loaded {
|
||||
cancel()
|
||||
closeMonitor(startCtx)
|
||||
}
|
||||
}()
|
||||
err = instance.Start()
|
||||
finishStart()
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, E.Cause(err, "start service")
|
||||
return E.Cause(err, "start service")
|
||||
}
|
||||
return instance, cancel, nil
|
||||
}
|
||||
|
||||
func run() error {
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer signal.Stop(osSignals)
|
||||
for {
|
||||
instance, cancel, err := create()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtimeDebug.FreeOSMemory()
|
||||
for {
|
||||
osSignal := <-osSignals
|
||||
if osSignal == syscall.SIGHUP {
|
||||
err = check()
|
||||
if err != nil {
|
||||
log.Error(E.Cause(err, "reload service"))
|
||||
continue
|
||||
}
|
||||
}
|
||||
if debug.Enabled {
|
||||
http.HandleFunc("/debug/close", func(writer http.ResponseWriter, request *http.Request) {
|
||||
cancel()
|
||||
closeCtx, closed := context.WithCancel(context.Background())
|
||||
go closeMonitor(closeCtx)
|
||||
err = instance.Close()
|
||||
closed()
|
||||
if osSignal != syscall.SIGHUP {
|
||||
if err != nil {
|
||||
log.Error(E.Cause(err, "sing-box did not closed properly"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
break
|
||||
}
|
||||
instance.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func closeMonitor(ctx context.Context) {
|
||||
time.Sleep(C.FatalStopTimeout)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
log.Fatal("sing-box did not close!")
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM)
|
||||
<-osSignals
|
||||
cancel()
|
||||
instance.Close()
|
||||
return nil
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user