mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-10-19 15:54:24 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3491a9a3e1 | ||
|
|
476d3c1aca | ||
|
|
bdf6fa5816 | ||
|
|
06487e073d | ||
|
|
09cf71cc2d | ||
|
|
274a147e89 |
@@ -1,35 +0,0 @@
|
||||
-s dir
|
||||
--name sing-box-extended
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform (extended)."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--no-deb-generate-changes
|
||||
|
||||
--provides sing-box
|
||||
--conflicts sing-box
|
||||
--replaces sing-box
|
||||
|
||||
--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
|
||||
--depends kmod-nft-queue
|
||||
|
||||
--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
|
||||
-23
@@ -1,23 +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>"
|
||||
--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,26 +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/"
|
||||
--vendor SagerNet
|
||||
--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 +0,0 @@
|
||||
0d28acc44093df24b2526dea3d6ffefd6b0a54f0
|
||||
@@ -1 +0,0 @@
|
||||
github: nekohasekai
|
||||
@@ -31,8 +31,6 @@ body:
|
||||
- sing-box for macOS Graphical Client
|
||||
- sing-box for Apple tvOS Graphical Client
|
||||
- sing-box for Android Graphical Client
|
||||
- sing-box for Windows Graphical Client
|
||||
- sing-box for Linux 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
|
||||
@@ -46,7 +44,12 @@ body:
|
||||
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
|
||||
value: |-
|
||||
<details>
|
||||
```console
|
||||
# Replace this line with the output
|
||||
```
|
||||
</details>
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
@@ -63,28 +66,12 @@ body:
|
||||
attributes:
|
||||
label: Logs
|
||||
description: |-
|
||||
In addition, if you encounter a crash with the graphical client, please also provide crash logs.
|
||||
If you encounter a crash with the graphical client, please provide crash logs.
|
||||
For Apple platform clients, please check `Settings - View Service Log` for crash logs.
|
||||
For 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
|
||||
attributes:
|
||||
label: Supporter
|
||||
options:
|
||||
- label: I am a [sponsor](https://github.com/sponsors/nekohasekai/)
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Integrity requirements
|
||||
description: |-
|
||||
Please check all of the following options to prove that you have read and understood the requirements, otherwise this issue will be closed.
|
||||
Sing-box is not a project aimed to please users who can't make any meaningful contributions and gain unethical influence. If you deceive here to deliberately waste the time of the developers, you will be permanently blocked.
|
||||
options:
|
||||
- label: I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values.
|
||||
required: true
|
||||
- label: I confirm that I have provided the server and client configuration files and process that can be reproduced locally, instead of a complicated client configuration file that has been stripped of sensitive data.
|
||||
required: true
|
||||
- label: I confirm that I have provided the simplest configuration that can be used to reproduce the error I reported, instead of depending on remote servers, TUN, graphical interface clients, or other closed-source software.
|
||||
required: true
|
||||
- label: I confirm that I have provided the complete configuration files and logs, rather than just providing parts I think are useful out of confidence in my own intelligence.
|
||||
required: true
|
||||
value: |-
|
||||
<details>
|
||||
```console
|
||||
# Replace this line with logs
|
||||
```
|
||||
</details>
|
||||
@@ -31,8 +31,6 @@ body:
|
||||
- sing-box for macOS 图形客户端程序
|
||||
- sing-box for Apple tvOS 图形客户端程序
|
||||
- sing-box for Android 图形客户端程序
|
||||
- sing-box for Windows 图形客户端程序
|
||||
- sing-box for Linux 图形客户端程序
|
||||
- 宣传使用 sing-box 的第三方图形客户端程序 (Windows)
|
||||
- 宣传使用 sing-box 的第三方图形客户端程序 (Android)
|
||||
- 其他
|
||||
@@ -46,7 +44,12 @@ body:
|
||||
attributes:
|
||||
label: 版本
|
||||
description: 如果您使用原始命令行程序,请提供 `sing-box version` 命令的输出。
|
||||
render: shell
|
||||
value: |-
|
||||
<details>
|
||||
```console
|
||||
# 使用输出内容覆盖此行
|
||||
```
|
||||
</details>
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 描述
|
||||
@@ -63,28 +66,12 @@ body:
|
||||
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
|
||||
value: |-
|
||||
<details>
|
||||
```console
|
||||
# 使用日志内容覆盖此行
|
||||
```
|
||||
</details>
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
prepare_apk_root() {
|
||||
# apk mkpkg resolves owner/group names through --root/etc/{passwd,group}.
|
||||
APK_ROOT_DIR=$(mktemp -d)
|
||||
mkdir -p "$APK_ROOT_DIR/etc"
|
||||
cat > "$APK_ROOT_DIR/etc/passwd" <<EOF
|
||||
root:x:$(id -u):$(id -g):root:/root:/sbin/nologin
|
||||
EOF
|
||||
cat > "$APK_ROOT_DIR/etc/group" <<EOF
|
||||
root:x:$(id -g):root
|
||||
EOF
|
||||
}
|
||||
|
||||
ARCHITECTURE="$1"
|
||||
VERSION="$2"
|
||||
BINARY_PATH="$3"
|
||||
OUTPUT_PATH="$4"
|
||||
|
||||
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
|
||||
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||
|
||||
# Convert version to APK format:
|
||||
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
|
||||
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
|
||||
# 1.13.0 -> 1.13.0-r0
|
||||
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
|
||||
APK_VERSION="${APK_VERSION}-r0"
|
||||
|
||||
ROOT_DIR=$(mktemp -d)
|
||||
prepare_apk_root
|
||||
trap 'rm -rf "$ROOT_DIR" "$APK_ROOT_DIR"' EXIT
|
||||
|
||||
# Binary
|
||||
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
|
||||
|
||||
# Config files
|
||||
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
|
||||
install -Dm755 "$PROJECT/release/config/sing-box.initd" "$ROOT_DIR/etc/init.d/sing-box"
|
||||
install -Dm644 "$PROJECT/release/config/sing-box.confd" "$ROOT_DIR/etc/conf.d/sing-box"
|
||||
|
||||
# Service files
|
||||
install -Dm644 "$PROJECT/release/config/sing-box.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box.service"
|
||||
install -Dm644 "$PROJECT/release/config/sing-box@.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box@.service"
|
||||
|
||||
# Completions
|
||||
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
|
||||
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
|
||||
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
|
||||
|
||||
# License
|
||||
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
|
||||
|
||||
# APK metadata
|
||||
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
|
||||
mkdir -p "$PACKAGES_DIR"
|
||||
|
||||
# .conffiles
|
||||
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
|
||||
/etc/conf.d/sing-box
|
||||
/etc/init.d/sing-box
|
||||
/etc/sing-box/config.json
|
||||
EOF
|
||||
|
||||
# .conffiles_static (sha256 checksums)
|
||||
while IFS= read -r conffile; do
|
||||
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
|
||||
echo "$conffile $sha256"
|
||||
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
|
||||
|
||||
# .list (all files, excluding lib/apk/packages/ metadata)
|
||||
(cd "$ROOT_DIR" && find . -type f -o -type l) \
|
||||
| sed 's|^\./|/|' \
|
||||
| grep -v '^/lib/apk/packages/' \
|
||||
| sort > "$PACKAGES_DIR/.list"
|
||||
|
||||
# Build APK
|
||||
apk --root "$APK_ROOT_DIR" mkpkg \
|
||||
--info "name:sing-box" \
|
||||
--info "version:${APK_VERSION}" \
|
||||
--info "description:The universal proxy platform." \
|
||||
--info "arch:${ARCHITECTURE}" \
|
||||
--info "license:GPL-3.0-or-later with name use or association addition" \
|
||||
--info "origin:sing-box" \
|
||||
--info "url:https://sing-box.sagernet.org/" \
|
||||
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
|
||||
--files "$ROOT_DIR" \
|
||||
--output "$OUTPUT_PATH"
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
prepare_apk_root() {
|
||||
# apk mkpkg resolves owner/group names through --root/etc/{passwd,group}.
|
||||
APK_ROOT_DIR=$(mktemp -d)
|
||||
mkdir -p "$APK_ROOT_DIR/etc"
|
||||
cat > "$APK_ROOT_DIR/etc/passwd" <<EOF
|
||||
root:x:$(id -u):$(id -g):root:/root:/sbin/nologin
|
||||
EOF
|
||||
cat > "$APK_ROOT_DIR/etc/group" <<EOF
|
||||
root:x:$(id -g):root
|
||||
EOF
|
||||
}
|
||||
|
||||
ARCHITECTURE="$1"
|
||||
VERSION="$2"
|
||||
BINARY_PATH="$3"
|
||||
OUTPUT_PATH="$4"
|
||||
|
||||
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
|
||||
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||
|
||||
# Convert version to APK format:
|
||||
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/' | sed -E 's/-[a-z]+-/./g')
|
||||
APK_VERSION="${APK_VERSION}-r0"
|
||||
|
||||
ROOT_DIR=$(mktemp -d)
|
||||
prepare_apk_root
|
||||
trap 'rm -rf "$ROOT_DIR" "$APK_ROOT_DIR"' EXIT
|
||||
|
||||
# Binary
|
||||
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
|
||||
|
||||
# Config files
|
||||
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
|
||||
install -Dm644 "$PROJECT/release/config/openwrt.conf" "$ROOT_DIR/etc/config/sing-box"
|
||||
install -Dm755 "$PROJECT/release/config/openwrt.init" "$ROOT_DIR/etc/init.d/sing-box"
|
||||
install -Dm644 "$PROJECT/release/config/openwrt.keep" "$ROOT_DIR/lib/upgrade/keep.d/sing-box"
|
||||
|
||||
# Completions
|
||||
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
|
||||
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
|
||||
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
|
||||
|
||||
# License
|
||||
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
|
||||
|
||||
# APK metadata
|
||||
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
|
||||
mkdir -p "$PACKAGES_DIR"
|
||||
|
||||
# .conffiles
|
||||
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
|
||||
/etc/config/sing-box
|
||||
/etc/sing-box/config.json
|
||||
EOF
|
||||
|
||||
# .conffiles_static (sha256 checksums)
|
||||
while IFS= read -r conffile; do
|
||||
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
|
||||
echo "$conffile $sha256"
|
||||
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
|
||||
|
||||
# .list (all files, excluding lib/apk/packages/ metadata)
|
||||
(cd "$ROOT_DIR" && find . -type f -o -type l) \
|
||||
| sed 's|^\./|/|' \
|
||||
| grep -v '^/lib/apk/packages/' \
|
||||
| sort > "$PACKAGES_DIR/.list"
|
||||
|
||||
# Build APK
|
||||
apk --root "$APK_ROOT_DIR" mkpkg \
|
||||
--info "name:sing-box-extended" \
|
||||
--info "version:${APK_VERSION}" \
|
||||
--info "description:The universal proxy platform (extended)." \
|
||||
--info "arch:${ARCHITECTURE}" \
|
||||
--info "license:GPL-3.0-or-later" \
|
||||
--info "origin:sing-box-extended" \
|
||||
--info "url:https://sing-box.sagernet.org/" \
|
||||
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
|
||||
--info "depends:ca-bundle kmod-inet-diag kmod-tun firewall4 kmod-nft-queue" \
|
||||
--info "provides:sing-box" \
|
||||
--info "provider-priority:100" \
|
||||
--script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \
|
||||
--files "$ROOT_DIR" \
|
||||
--output "$OUTPUT_PATH"
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e -o pipefail
|
||||
|
||||
VERSION="$1"
|
||||
TARGET="$2"
|
||||
BINARY_PATH="$3"
|
||||
|
||||
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||
DIST="$PROJECT/dist"
|
||||
|
||||
case "$TARGET" in
|
||||
linux_amd64*) ARCHITECTURES="x86_64" ;;
|
||||
linux_arm64*) ARCHITECTURES="aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" ;;
|
||||
linux_386*softfloat) ARCHITECTURES="i386_pentium-mmx" ;;
|
||||
linux_386*) ARCHITECTURES="i386_pentium4" ;;
|
||||
linux_arm_7*) ARCHITECTURES="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" ;;
|
||||
linux_arm_6*) ARCHITECTURES="arm_arm1176jzf-s_vfp" ;;
|
||||
linux_arm_5*) ARCHITECTURES="arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" ;;
|
||||
linux_mips64_*) ARCHITECTURES="mips64_mips64r2 mips64_octeonplus" ;;
|
||||
linux_mips64le*) ARCHITECTURES="mips64el_mips64r2" ;;
|
||||
linux_mipsle*hardfloat) ARCHITECTURES="mipsel_24kc_24kf" ;;
|
||||
linux_mipsle*) ARCHITECTURES="mipsel_24kc mipsel_74kc mipsel_mips32" ;;
|
||||
linux_mips_*) ARCHITECTURES="mips_24kc mips_4kec mips_mips32" ;;
|
||||
linux_riscv64*) ARCHITECTURES="riscv64_generic" ;;
|
||||
linux_loong64*) ARCHITECTURES="loongarch64_generic" ;;
|
||||
*) echo "Unknown target: $TARGET"; exit 1 ;;
|
||||
esac
|
||||
|
||||
PKG_VERSION="${VERSION//-/\~}"
|
||||
|
||||
FPM_DIR=$(mktemp -d)
|
||||
sed "s|release/|$PROJECT/release/|g;s|^LICENSE|$PROJECT/LICENSE|" "$PROJECT/.fpm_openwrt" > "$FPM_DIR/.fpm"
|
||||
trap 'rm -rf "$FPM_DIR"' EXIT
|
||||
|
||||
for ARCH in $ARCHITECTURES; do
|
||||
TMP_DEB=$(mktemp -p "$DIST" _openwrt_XXXXXX.deb)
|
||||
rm -f "$TMP_DEB"
|
||||
(cd "$FPM_DIR" && fpm -t deb \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "$TMP_DEB" \
|
||||
--architecture all \
|
||||
"$BINARY_PATH=/usr/bin/sing-box")
|
||||
|
||||
bash "$PROJECT/.github/deb2ipk.sh" \
|
||||
"$ARCH" \
|
||||
"$TMP_DEB" \
|
||||
"$DIST/sing-box-extended_${VERSION}_openwrt_${ARCH}.ipk"
|
||||
rm -f "$TMP_DEB"
|
||||
|
||||
if command -v apk &>/dev/null; then
|
||||
bash "$PROJECT/.github/build_openwrt_apk.sh" \
|
||||
"$ARCH" "$VERSION" "$BINARY_PATH" \
|
||||
"$DIST/sing-box-extended_${VERSION}_openwrt_${ARCH}.apk"
|
||||
fi
|
||||
|
||||
echo "Built: sing-box-extended_${VERSION}_openwrt_${ARCH} (.ipk/.apk)"
|
||||
done
|
||||
@@ -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
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
branches=$(git branch -r --contains HEAD)
|
||||
if echo "$branches" | grep -q 'origin/stable'; then
|
||||
track=stable
|
||||
elif echo "$branches" | grep -q 'origin/testing'; then
|
||||
track=testing
|
||||
elif echo "$branches" | grep -q 'origin/oldstable'; then
|
||||
track=oldstable
|
||||
else
|
||||
echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$track" == "stable" ]]; then
|
||||
tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
|
||||
if [[ -n "$tag" && "$tag" == *"-"* ]]; then
|
||||
track=beta
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$track" in
|
||||
stable) name=sing-box; docker_tag=latest ;;
|
||||
beta) name=sing-box-beta; docker_tag=latest-beta ;;
|
||||
testing) name=sing-box-testing; docker_tag=latest-testing ;;
|
||||
oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;;
|
||||
esac
|
||||
|
||||
echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2
|
||||
echo "TRACK=${track}" >> "$GITHUB_ENV"
|
||||
echo "NAME=${name}" >> "$GITHUB_ENV"
|
||||
echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV"
|
||||
@@ -1,28 +0,0 @@
|
||||
--- a/src/internal/cpu/cpu_arm64_ios.go
|
||||
+++ b/src/internal/cpu/cpu_arm64_ios.go
|
||||
@@ -0,0 +1,14 @@
|
||||
+// Copyright 2020 The Go Authors. All rights reserved.
|
||||
+// Use of this source code is governed by a BSD-style
|
||||
+// license that can be found in the LICENSE file.
|
||||
+
|
||||
+//go:build arm64 && ios
|
||||
+
|
||||
+package cpu
|
||||
+
|
||||
+func osInit() {
|
||||
+ ARM64.HasAES = true
|
||||
+ ARM64.HasPMULL = true
|
||||
+ ARM64.HasSHA1 = true
|
||||
+ ARM64.HasSHA2 = true
|
||||
+}
|
||||
--- a/src/internal/cpu/cpu_arm64_other.go
|
||||
+++ b/src/internal/cpu/cpu_arm64_other.go
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
-//go:build arm64 && !linux && !freebsd && !android && (!darwin || ios) && !openbsd
|
||||
+//go:build arm64 && !linux && !freebsd && !android && !darwin && !openbsd
|
||||
|
||||
package cpu
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Go's internal/cpu never detects ARM64 features on GOOS=ios, so crypto/aes,
|
||||
# AES-GCM and crypto/sha256 use their generic implementations on iOS and tvOS.
|
||||
# See https://github.com/SagerNet/sing-box/issues/4486
|
||||
#
|
||||
# Only the ARMv8.0 cryptographic extensions are asserted; ARMv8.1 atomics and
|
||||
# SHA-512 are absent on the A8/A9 devices still supported by the deployment targets.
|
||||
#
|
||||
# Remove once the Go release used by the Apple library build includes the fix.
|
||||
|
||||
export GOTOOLCHAIN=local
|
||||
|
||||
GOROOT="$(go env GOROOT)"
|
||||
PATCH_FILE="$(cd "$(dirname "$0")" && pwd)/go_ios_cpu_features.patch"
|
||||
|
||||
cd "$GOROOT"
|
||||
if [[ -f src/internal/cpu/cpu_arm64_ios.go ]]; then
|
||||
echo "already patched"
|
||||
else
|
||||
patch --verbose -p1 < "$PATCH_FILE"
|
||||
fi
|
||||
|
||||
CPU_FILES="$(GOOS=ios GOARCH=arm64 CGO_ENABLED=0 go list -f '{{.GoFiles}}' internal/cpu)"
|
||||
echo "internal/cpu files for ios/arm64: $CPU_FILES"
|
||||
case "$CPU_FILES" in
|
||||
*cpu_arm64_ios.go*) ;;
|
||||
*)
|
||||
echo "patch is not effective" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -6,7 +6,7 @@
|
||||
":disableRateLimiting"
|
||||
],
|
||||
"baseBranches": [
|
||||
"unstable"
|
||||
"dev-next"
|
||||
],
|
||||
"golang": {
|
||||
"enabled": false
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.26.8"
|
||||
PATCH_COMMITS=(
|
||||
"f080b0c6346eb690c0dc82497b35925f385b35ac"
|
||||
"2d9c12887c342fb9051d231aa5388743cb7e9cb6"
|
||||
"367663849c656612fc3c8875ef6e7f6b0c93dbd0"
|
||||
)
|
||||
CURL_ARGS=(
|
||||
-fL
|
||||
--silent
|
||||
--show-error
|
||||
)
|
||||
|
||||
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
|
||||
fi
|
||||
|
||||
mkdir -p "$HOME/go"
|
||||
cd "$HOME/go"
|
||||
wget "https://dl.google.com/go/go${VERSION}.darwin-arm64.tar.gz"
|
||||
tar -xzf "go${VERSION}.darwin-arm64.tar.gz"
|
||||
#cp -a go go_bootstrap
|
||||
mv go go_osx
|
||||
cd go_osx
|
||||
|
||||
# these patch URLs only work on golang1.26.x
|
||||
# that means after golang1.27 release it must be changed
|
||||
# see: https://github.com/SagerNet/go/commits/release-branch.go1.26/
|
||||
# revert:
|
||||
# 33d3f603c1: "cmd/link/internal/ld: use 12.0.0 OS/SDK versions for macOS linking"
|
||||
# 937368f84e: "crypto/x509: change how we retrieve chains on darwin"
|
||||
# d90a57ffe8: "cmd/link/internal/ld: unify OS/SDK versions for macOS linking"
|
||||
|
||||
for patch_commit in "${PATCH_COMMITS[@]}"; do
|
||||
curl "${CURL_ARGS[@]}" "https://github.com/SagerNet/go/commit/${patch_commit}.diff" | patch --verbose -p 1
|
||||
done
|
||||
|
||||
# Rebuild is not needed: we build with CGO_ENABLED=1, so Apple's external
|
||||
# linker handles LC_BUILD_VERSION via MACOSX_DEPLOYMENT_TARGET, and the
|
||||
# stdlib (crypto/x509) is compiled from patched src automatically.
|
||||
#cd src
|
||||
#GOROOT_BOOTSTRAP="$HOME/go/go_bootstrap" ./make.bash
|
||||
#cd ../..
|
||||
#rm -rf go_bootstrap "go${VERSION}.darwin-arm64.tar.gz"
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.26.8"
|
||||
PATCH_COMMITS=(
|
||||
"a4ae550aa148b04c9d4890e98bee63aede5c4b53"
|
||||
"95b851f661584711faa8115b3234a461044f4510"
|
||||
"b0bf0a863cf218b9bb0d6c013903ab160ae0c39b"
|
||||
"7c7a2a0d68920f8764d9a3aeae7ad1067a17b3fa"
|
||||
"ba34356e82f3c12a7303d2231e6ea0e42bbbb3bf"
|
||||
"472a88edbc2a42feb53fa31d29ab54d33840dca6"
|
||||
)
|
||||
CURL_ARGS=(
|
||||
-fL
|
||||
--silent
|
||||
--show-error
|
||||
)
|
||||
|
||||
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
|
||||
fi
|
||||
|
||||
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_win7
|
||||
cd go_win7
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# these patch URLs only work on golang1.26.x
|
||||
# that means after golang1.27 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.26/
|
||||
# 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"
|
||||
# fixes:
|
||||
# bed309eff415bcb3c77dd4bc3277b682b89a388d: "Fix os.RemoveAll not working on Windows7"
|
||||
# 34b899c2fb39b092db4fa67c4417e41dc046be4b: "Revert \"os: remove 5ms sleep on Windows in (*Process).Wait\""
|
||||
|
||||
for patch_commit in "${PATCH_COMMITS[@]}"; do
|
||||
curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1
|
||||
done
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PROJECTS=$(dirname "$0")/../..
|
||||
|
||||
function updateClient() {
|
||||
pushd clients/$1
|
||||
git fetch origin main
|
||||
git reset FETCH_HEAD --hard
|
||||
popd
|
||||
git add clients/$1
|
||||
}
|
||||
|
||||
updateClient "apple"
|
||||
updateClient "android"
|
||||
updateClient "desktop"
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
PROJECTS=$SCRIPT_DIR/../..
|
||||
|
||||
git -C $PROJECTS/cronet-go fetch origin main
|
||||
git -C $PROJECTS/cronet-go fetch origin go
|
||||
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
|
||||
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
|
||||
go mod tidy
|
||||
git -C $PROJECTS/cronet-go rev-parse origin/go > "$SCRIPT_DIR/CRONET_GO_VERSION"
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
PROJECTS=$SCRIPT_DIR/../..
|
||||
|
||||
git -C $PROJECTS/cronet-go fetch origin dev
|
||||
git -C $PROJECTS/cronet-go fetch origin go_dev
|
||||
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
|
||||
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
|
||||
go mod tidy
|
||||
git -C $PROJECTS/cronet-go rev-parse origin/go_dev > "$SCRIPT_DIR/CRONET_GO_VERSION"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,220 @@
|
||||
name: Debug build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main-next
|
||||
- dev-next
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/debug.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- main-next
|
||||
- dev-next
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Debug build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get latest go version
|
||||
id: version
|
||||
run: |
|
||||
echo go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') >> $GITHUB_OUTPUT
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: Add cache to Go proxy
|
||||
run: |
|
||||
version=`git rev-parse HEAD`
|
||||
mkdir build
|
||||
pushd build
|
||||
go mod init build
|
||||
go get -v github.com/sagernet/sing-box@$version
|
||||
popd
|
||||
continue-on-error: true
|
||||
- name: Run Test
|
||||
run: |
|
||||
go test -v ./...
|
||||
build_go118:
|
||||
name: Debug build (Go 1.18)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.10
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go118-${{ hashFiles('**/go.sum') }}
|
||||
- name: Run Test
|
||||
run: make ci_build_go118
|
||||
build_go120:
|
||||
name: Debug build (Go 1.20)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.7
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go118-${{ hashFiles('**/go.sum') }}
|
||||
- name: Run Test
|
||||
run: make ci_build
|
||||
cross:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# windows
|
||||
- name: windows-amd64
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
goamd64: v1
|
||||
- name: windows-amd64-v3
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
goamd64: v3
|
||||
- name: windows-386
|
||||
goos: windows
|
||||
goarch: 386
|
||||
- name: windows-arm64
|
||||
goos: windows
|
||||
goarch: arm64
|
||||
- name: windows-arm32v7
|
||||
goos: windows
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
|
||||
# linux
|
||||
- name: linux-amd64
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
goamd64: v1
|
||||
- name: linux-amd64-v3
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
goamd64: v3
|
||||
- name: linux-386
|
||||
goos: linux
|
||||
goarch: 386
|
||||
- name: linux-arm64
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
- name: linux-armv5
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: 5
|
||||
- name: linux-armv6
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
- name: linux-armv7
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
- name: linux-mips-softfloat
|
||||
goos: linux
|
||||
goarch: mips
|
||||
gomips: softfloat
|
||||
- name: linux-mips-hardfloat
|
||||
goos: linux
|
||||
goarch: mips
|
||||
gomips: hardfloat
|
||||
- name: linux-mipsel-softfloat
|
||||
goos: linux
|
||||
goarch: mipsle
|
||||
gomips: softfloat
|
||||
- name: linux-mipsel-hardfloat
|
||||
goos: linux
|
||||
goarch: mipsle
|
||||
gomips: hardfloat
|
||||
- name: linux-mips64
|
||||
goos: linux
|
||||
goarch: mips64
|
||||
- name: linux-mips64el
|
||||
goos: linux
|
||||
goarch: mips64le
|
||||
- name: linux-s390x
|
||||
goos: linux
|
||||
goarch: s390x
|
||||
# darwin
|
||||
- name: darwin-amd64
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
goamd64: v1
|
||||
- name: darwin-amd64-v3
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
goamd64: v3
|
||||
- name: darwin-arm64
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
# freebsd
|
||||
- name: freebsd-amd64
|
||||
goos: freebsd
|
||||
goarch: amd64
|
||||
goamd64: v1
|
||||
- name: freebsd-amd64-v3
|
||||
goos: freebsd
|
||||
goarch: amd64
|
||||
goamd64: v3
|
||||
- name: freebsd-386
|
||||
goos: freebsd
|
||||
goarch: 386
|
||||
- name: freebsd-arm64
|
||||
goos: freebsd
|
||||
goarch: arm64
|
||||
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOAMD64: ${{ matrix.goamd64 }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
CGO_ENABLED: 0
|
||||
TAGS: with_clash_api,with_quic
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get latest go version
|
||||
id: version
|
||||
run: |
|
||||
echo go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') >> $GITHUB_OUTPUT
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: Build
|
||||
id: build
|
||||
run: make
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: sing-box-${{ matrix.name }}
|
||||
path: sing-box*
|
||||
+24
-275
@@ -1,296 +1,45 @@
|
||||
name: Publish Docker Images
|
||||
|
||||
name: Build Docker Images
|
||||
on:
|
||||
#push:
|
||||
# branches:
|
||||
# - stable
|
||||
# - testing
|
||||
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_binary:
|
||||
name: Build binary
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
# Naive-enabled builds (musl)
|
||||
- { arch: amd64, naive: true, docker_platform: "linux/amd64" }
|
||||
- { arch: arm64, naive: true, docker_platform: "linux/arm64" }
|
||||
- { arch: "386", naive: true, docker_platform: "linux/386" }
|
||||
- { arch: arm, goarm: "7", naive: true, docker_platform: "linux/arm/v7" }
|
||||
- { arch: mipsle, gomips: softfloat, naive: true, docker_platform: "linux/mipsle" }
|
||||
- { arch: riscv64, naive: true, docker_platform: "linux/riscv64" }
|
||||
- { arch: loong64, naive: true, docker_platform: "linux/loong64" }
|
||||
# Non-naive builds
|
||||
- { arch: arm, goarm: "6", docker_platform: "linux/arm/v6" }
|
||||
- { arch: ppc64le, docker_platform: "linux/ppc64le" }
|
||||
- { arch: s390x, docker_platform: "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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.8
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
|
||||
git init ~/cronet-go
|
||||
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
|
||||
git -C ~/cronet-go sparse-checkout set --no-cone '/*' '!/lib'
|
||||
git -C ~/cronet-go fetch --depth=1 --filter=blob:none origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go checkout FETCH_HEAD
|
||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||
- name: Regenerate Debian keyring
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
|
||||
cd ~/cronet-go
|
||||
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
|
||||
- name: Cache Chromium toolchain
|
||||
if: matrix.naive
|
||||
id: cache-chromium-toolchain
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/cronet-go/naiveproxy/src/third_party/llvm-build/
|
||||
~/cronet-go/naiveproxy/src/gn/out/
|
||||
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
|
||||
~/cronet-go/naiveproxy/src/out/sysroot-build/
|
||||
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
||||
- name: Download Chromium toolchain
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
cd ~/cronet-go
|
||||
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
|
||||
- name: Set version
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
VERSION=$(go run ./cmd/internal/read_tag)
|
||||
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
|
||||
- name: Set Chromium toolchain environment
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
cd ~/cronet-go
|
||||
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
if [[ "${{ matrix.naive }}" == "true" ]]; then
|
||||
TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl"
|
||||
else
|
||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
||||
fi
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Set shared ldflags
|
||||
run: |
|
||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
|
||||
- name: Build (naive)
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
- name: Build (non-naive)
|
||||
if: ${{ ! matrix.naive }}
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
- name: Prepare artifact
|
||||
run: |
|
||||
platform=${{ matrix.docker_platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
# Rename binary to include arch info for Dockerfile.binary
|
||||
BINARY_NAME="sing-box-${{ matrix.arch }}"
|
||||
if [[ -n "${{ matrix.goarm }}" ]]; then
|
||||
BINARY_NAME="${BINARY_NAME}v${{ matrix.goarm }}"
|
||||
fi
|
||||
mv sing-box "${BINARY_NAME}"
|
||||
echo "BINARY_NAME=${BINARY_NAME}" >> $GITHUB_ENV
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ env.BINARY_NAME }}
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
build_docker:
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_binary
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- { platform: "linux/amd64" }
|
||||
- { platform: "linux/arm/v6" }
|
||||
- { platform: "linux/arm/v7" }
|
||||
- { platform: "linux/arm64" }
|
||||
- { platform: "linux/386" }
|
||||
# mipsle: no base Docker image available for this platform
|
||||
- { platform: "linux/ppc64le" }
|
||||
- { platform: "linux/riscv64" }
|
||||
- { platform: "linux/s390x" }
|
||||
- { platform: "linux/loong64", base_image: "ghcr.io/loong64/alpine:edge" }
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
- name: Download binary
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: binary-${{ env.PLATFORM_PAIR }}
|
||||
path: .
|
||||
- name: Prepare binary
|
||||
run: |
|
||||
# Find and make the binary executable
|
||||
chmod +x sing-box-*
|
||||
ls -la sing-box-*
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Setup QEMU for Docker Buildx
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
- name: Docker metadata
|
||||
id: metadata
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
context: .
|
||||
file: Dockerfile.binary
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ matrix.base_image || 'alpine' }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
- name: Export digest
|
||||
images: ghcr.io/sagernet/sing-box
|
||||
- name: Get tag to build
|
||||
id: tag
|
||||
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:
|
||||
if: github.event_name != 'push'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_docker
|
||||
steps:
|
||||
- name: Get commit to build
|
||||
id: ref
|
||||
run: |-
|
||||
echo "latest=ghcr.io/sagernet/sing-box:latest" >> $GITHUB_OUTPUT
|
||||
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
|
||||
ref="${{ github.ref_name }}"
|
||||
echo "versioned=ghcr.io/sagernet/sing-box:${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
ref="${{ github.event.inputs.tag }}"
|
||||
echo "versioned=ghcr.io/sagernet/sing-box:${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "ref=$ref"
|
||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- name: Build and release Docker images
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
- name: Detect track
|
||||
run: bash .github/detect_track.sh
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
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
|
||||
if: github.event_name != 'push'
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \
|
||||
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
- name: Inspect image
|
||||
if: github.event_name != 'push'
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
||||
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
|
||||
target: dist
|
||||
tags: |
|
||||
${{ steps.tag.outputs.latest }}
|
||||
${{ steps.tag.outputs.versioned }}
|
||||
push: true
|
||||
|
||||
+14
-56
@@ -3,77 +3,35 @@ name: Lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- oldstable
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
- main-next
|
||||
- dev-next
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/lint.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- oldstable
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
||||
cancel-in-progress: true
|
||||
- main-next
|
||||
- dev-next
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Lint ${{ matrix.goos }}/${{ matrix.goarch }}
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- goos: windows
|
||||
goarch: amd64
|
||||
- goos: windows
|
||||
goarch: '386'
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: linux
|
||||
goarch: arm
|
||||
- goos: linux
|
||||
goarch: '386'
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
- goos: android
|
||||
goarch: arm64
|
||||
# - goos: freebsd
|
||||
# goarch: amd64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get latest go version
|
||||
id: version
|
||||
run: |
|
||||
echo go_version=$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') >> $GITHUB_OUTPUT
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ^1.26
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=30m
|
||||
install-mode: binary
|
||||
verify: false
|
||||
version: latest
|
||||
@@ -1,243 +0,0 @@
|
||||
name: Build Linux Packages
|
||||
|
||||
on:
|
||||
#push:
|
||||
# branches:
|
||||
# - stable
|
||||
# - testing
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.8
|
||||
- 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-26.04
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Naive-enabled builds (musl)
|
||||
- { os: linux, arch: amd64, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64 }
|
||||
- { os: linux, arch: arm64, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
||||
- { os: linux, arch: "386", naive: true, debian: i386, rpm: i386 }
|
||||
- { os: linux, arch: arm, goarm: "7", naive: true, debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
||||
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, debian: mipsel, rpm: mipsel }
|
||||
- { os: linux, arch: riscv64, naive: true, debian: riscv64, rpm: riscv64 }
|
||||
- { os: linux, arch: loong64, naive: true, debian: loongarch64, rpm: loongarch64 }
|
||||
# Non-naive builds (unsupported architectures)
|
||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.8
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
|
||||
git init ~/cronet-go
|
||||
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
|
||||
git -C ~/cronet-go sparse-checkout set --no-cone '/*' '!/lib'
|
||||
git -C ~/cronet-go fetch --depth=1 --filter=blob:none origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go checkout FETCH_HEAD
|
||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||
- name: Regenerate Debian keyring
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
|
||||
cd ~/cronet-go
|
||||
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
|
||||
- name: Cache Chromium toolchain
|
||||
if: matrix.naive
|
||||
id: cache-chromium-toolchain
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/cronet-go/naiveproxy/src/third_party/llvm-build/
|
||||
~/cronet-go/naiveproxy/src/gn/out/
|
||||
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
|
||||
~/cronet-go/naiveproxy/src/out/sysroot-build/
|
||||
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
||||
- name: Download Chromium toolchain
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
cd ~/cronet-go
|
||||
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
|
||||
- name: Set Chromium toolchain environment
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
cd ~/cronet-go
|
||||
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env >> $GITHUB_ENV
|
||||
- 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
|
||||
if [[ "${{ matrix.naive }}" == "true" ]]; then
|
||||
TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl"
|
||||
else
|
||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
||||
fi
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Set shared ldflags
|
||||
run: |
|
||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
|
||||
- name: Build (naive)
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
GOMIPS64: ${{ matrix.gomips }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build (non-naive)
|
||||
if: ${{ ! matrix.naive }}
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
||||
./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: Detect track
|
||||
run: bash .github/detect_track.sh
|
||||
- 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
|
||||
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
|
||||
echo '%_rpmformat 4' > "$HOME/.rpmmacros"
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
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@v5
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Publish packages
|
||||
if: github.event_name != 'push'
|
||||
run: |-
|
||||
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/
|
||||
@@ -8,9 +8,8 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v8
|
||||
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'
|
||||
days-before-close: 5
|
||||
@@ -1,57 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- oldstable
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/test.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- oldstable
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
go:
|
||||
- ~1.25
|
||||
- ~1.26
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Set build tags and ldflags
|
||||
shell: bash
|
||||
run: |
|
||||
echo "BUILD_TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)" >> "$GITHUB_ENV"
|
||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "$GITHUB_ENV"
|
||||
- name: Test (unix)
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: go test -v -exec sudo -tags "$BUILD_TAGS" -ldflags "$LDFLAGS_SHARED" ./...
|
||||
- name: Test (windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: bash
|
||||
run: go test -v -tags "$BUILD_TAGS" -ldflags "$LDFLAGS_SHARED" ./...
|
||||
-17
@@ -1,7 +1,6 @@
|
||||
/.idea/
|
||||
/vendor/
|
||||
/*.json
|
||||
/*.srs
|
||||
/*.db
|
||||
/site/
|
||||
/bin/
|
||||
@@ -12,21 +11,5 @@
|
||||
/*.jar
|
||||
/*.aar
|
||||
/*.xcframework/
|
||||
/experimental/libbox/*.aar
|
||||
/experimental/libbox/*.xcframework/
|
||||
/experimental/libbox/*.nupkg
|
||||
.DS_Store
|
||||
/config.d/
|
||||
/venv/
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
/.claude/
|
||||
dist
|
||||
logs
|
||||
/*.so
|
||||
/*.log
|
||||
/*.db-shm
|
||||
/*.db-wal
|
||||
/*.db.backup.*
|
||||
/test_download.bin
|
||||
/wget-log
|
||||
+23
-53
@@ -1,56 +1,26 @@
|
||||
version: "2"
|
||||
run:
|
||||
go: "1.24"
|
||||
build-tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
linters:
|
||||
default: none
|
||||
disable-all: true
|
||||
enable:
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
- modernize
|
||||
settings:
|
||||
modernize:
|
||||
disable:
|
||||
- omitzero # nested struct omitempty -> omitzero changes JSON output semantics
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- -QF1008 # could remove embedded field "<interface>" from selector
|
||||
- -ST1003 # should not use ALL_CAPS in Go names; use CamelCase instead
|
||||
- -QF1001 # could apply De Morgan's law
|
||||
- -SA4023 # impossible nil comparison, false positive on platform-specific implementations
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
paths:
|
||||
- transport/simple-obfs
|
||||
- \.pb\.go$
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofumpt
|
||||
settings:
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- prefix(github.com/sagernet/)
|
||||
- default
|
||||
custom-order: true
|
||||
- govet
|
||||
- gci
|
||||
- staticcheck
|
||||
- paralleltest
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
- transport/simple-obfs
|
||||
- transport/clashssr
|
||||
- transport/cloudflaretls
|
||||
- transport/shadowtls/tls
|
||||
- transport/shadowtls/tls_go119
|
||||
|
||||
linters-settings:
|
||||
gci:
|
||||
custom-order: true
|
||||
sections:
|
||||
- standard
|
||||
- prefix(github.com/sagernet/)
|
||||
- default
|
||||
staticcheck:
|
||||
go: '1.20'
|
||||
|
||||
+67
-539
@@ -1,103 +1,69 @@
|
||||
version: 2
|
||||
project_name: sing-box
|
||||
builds:
|
||||
- &template
|
||||
id: main
|
||||
- id: main
|
||||
main: ./cmd/sing-box
|
||||
flags:
|
||||
- -v
|
||||
- -trimpath
|
||||
asmflags:
|
||||
- all=-trimpath={{.Env.GOPATH}}
|
||||
gcflags:
|
||||
- all=-trimpath={{.Env.GOPATH}}
|
||||
ldflags:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||
- -s
|
||||
- -buildid=
|
||||
- -checklinkname=0
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_ech
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_reality_server
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
targets:
|
||||
- linux_386
|
||||
- linux_amd64_v1
|
||||
- linux_amd64_v3
|
||||
- linux_arm64
|
||||
- linux_arm_6
|
||||
- linux_arm_7
|
||||
- linux_s390x
|
||||
- linux_riscv64
|
||||
- windows_amd64_v1
|
||||
- windows_amd64_v3
|
||||
- windows_386
|
||||
- windows_arm64
|
||||
- darwin_amd64_v1
|
||||
- darwin_amd64_v3
|
||||
- darwin_arm64
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
- id: mips
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
targets:
|
||||
- linux_mips
|
||||
- linux_mips_softfloat
|
||||
- linux_mipsle
|
||||
- linux_mipsle_softfloat
|
||||
- linux_mips64
|
||||
- linux_mips64le
|
||||
- id: android
|
||||
<<: *template
|
||||
main: ./cmd/sing-box
|
||||
flags:
|
||||
- -v
|
||||
- -trimpath
|
||||
asmflags:
|
||||
- all=-trimpath={{.Env.GOPATH}}
|
||||
gcflags:
|
||||
- all=-trimpath={{.Env.GOPATH}}
|
||||
ldflags:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_ech
|
||||
- with_utls
|
||||
- with_clash_api
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- GOTOOLCHAIN=local
|
||||
overrides:
|
||||
- goos: android
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
env:
|
||||
- CC=armv7a-linux-androideabi21-clang
|
||||
- CXX=armv7a-linux-androideabi21-clang++
|
||||
- CC=armv7a-linux-androideabi19-clang
|
||||
- CXX=armv7a-linux-androideabi19-clang++
|
||||
- goos: android
|
||||
goarch: arm64
|
||||
env:
|
||||
@@ -106,8 +72,8 @@ builds:
|
||||
- goos: android
|
||||
goarch: 386
|
||||
env:
|
||||
- CC=i686-linux-android21-clang
|
||||
- CXX=i686-linux-android21-clang++
|
||||
- CC=i686-linux-android19-clang
|
||||
- CXX=i686-linux-android19-clang++
|
||||
- goos: android
|
||||
goarch: amd64
|
||||
goamd64: v1
|
||||
@@ -119,469 +85,42 @@ builds:
|
||||
- android_arm64
|
||||
- android_386
|
||||
- android_amd64
|
||||
- id: naive-purego-linux-amd64
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
hooks:
|
||||
post:
|
||||
- cmd: go run -C {{ .Env.CRONET_GO_PATH }} ./cmd/build-naive extract-lib --target {{ .Os }}/{{ .Arch }} -o {{ dir .Path }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- linux_amd64_v1
|
||||
- id: naive-purego-linux-arm64
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
hooks:
|
||||
post:
|
||||
- cmd: go run -C {{ .Env.CRONET_GO_PATH }} ./cmd/build-naive extract-lib --target {{ .Os }}/{{ .Arch }} -o {{ dir .Path }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- linux_arm64
|
||||
- id: naive-purego-windows-amd64
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
hooks:
|
||||
post:
|
||||
- cmd: go run -C {{ .Env.CRONET_GO_PATH }} ./cmd/build-naive extract-lib --target {{ .Os }}/{{ .Arch }} -o {{ dir .Path }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- windows_amd64_v1
|
||||
- id: naive-purego-windows-arm64
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_purego
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
hooks:
|
||||
post:
|
||||
- cmd: go run -C {{ .Env.CRONET_GO_PATH }} ./cmd/build-naive extract-lib --target {{ .Os }}/{{ .Arch }} -o {{ dir .Path }}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
- windows_arm64
|
||||
- id: naive-glibc
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- GOTOOLCHAIN=local
|
||||
- CGO_LDFLAGS=-fuse-ld=lld -Wl,-z,notext -Wl,-z,execstack
|
||||
- CC_linux_amd64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=x86_64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_amd64_staging
|
||||
- CXX_linux_amd64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=x86_64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_amd64_staging
|
||||
- CC_linux_arm64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=aarch64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_arm64_staging
|
||||
- CXX_linux_arm64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=aarch64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_arm64_staging
|
||||
- CC_linux_386={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=i686-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_i386_staging
|
||||
- CXX_linux_386={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=i686-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_i386_staging
|
||||
- CC_linux_arm={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=arm-linux-gnueabihf --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_armhf_staging
|
||||
- CXX_linux_arm={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=arm-linux-gnueabihf --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_armhf_staging
|
||||
- CC_linux_mips64le={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=mips64el-linux-gnuabi64 --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_mips64el_staging
|
||||
- CXX_linux_mips64le={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=mips64el-linux-gnuabi64 --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/bullseye/bullseye_mips64el_staging
|
||||
- CC_linux_riscv64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=riscv64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/trixie/trixie_riscv64_staging
|
||||
- CXX_linux_riscv64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=riscv64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/trixie/trixie_riscv64_staging
|
||||
- CC_linux_loong64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=loongarch64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/sid/sid_loong64_staging
|
||||
- CXX_linux_loong64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=loongarch64-linux-gnu --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/sid/sid_loong64_staging
|
||||
- 'CC={{ index .Env (print "CC_" .Os "_" .Arch) }}'
|
||||
- 'CXX={{ index .Env (print "CXX_" .Os "_" .Arch) }}'
|
||||
targets:
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_386
|
||||
- linux_arm_7
|
||||
- linux_riscv64
|
||||
- linux_loong64
|
||||
- id: naive-musl
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
- with_naive_outbound
|
||||
- with_musl
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- GOTOOLCHAIN=local
|
||||
- CGO_LDFLAGS=-fuse-ld=lld -Wl,-z,notext -Wl,-z,execstack
|
||||
- CC_linux_amd64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=x86_64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/x86_64
|
||||
- CXX_linux_amd64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=x86_64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/x86_64
|
||||
- CC_linux_arm64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=aarch64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/aarch64
|
||||
- CXX_linux_arm64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=aarch64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/aarch64
|
||||
- CC_linux_386={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=i486-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/i386_pentium4
|
||||
- CXX_linux_386={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=i486-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/i386_pentium4
|
||||
- CC_linux_arm={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=arm-openwrt-linux-musleabi --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/arm_cortex-a15_neon-vfpv4
|
||||
- CXX_linux_arm={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=arm-openwrt-linux-musleabi --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/arm_cortex-a15_neon-vfpv4
|
||||
- CC_linux_riscv64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=riscv64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/riscv64
|
||||
- CXX_linux_riscv64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=riscv64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/23.05.5/riscv64
|
||||
- CC_linux_loong64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang --target=loongarch64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/24.10.5/loongarch64
|
||||
- CXX_linux_loong64={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/third_party/llvm-build/Release+Asserts/bin/clang++ --target=loongarch64-openwrt-linux-musl --sysroot={{ .Env.CRONET_GO_PATH }}/naiveproxy/src/out/sysroot-build/openwrt/24.10.5/loongarch64
|
||||
- 'CC={{ index .Env (print "CC_" .Os "_" .Arch) }}'
|
||||
- 'CXX={{ index .Env (print "CXX_" .Os "_" .Arch) }}'
|
||||
targets:
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_386
|
||||
- linux_arm_7
|
||||
- linux_riscv64
|
||||
- linux_loong64
|
||||
- id: compressed
|
||||
<<: *template
|
||||
targets:
|
||||
- linux_386
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_arm_6
|
||||
- linux_arm_7
|
||||
- linux_riscv64
|
||||
- id: compressed-mips
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
targets:
|
||||
- linux_mips
|
||||
- linux_mips_softfloat
|
||||
- linux_mipsle
|
||||
- linux_mipsle_softfloat
|
||||
- id: compressed-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
|
||||
- id: openwrt
|
||||
<<: *template
|
||||
hooks:
|
||||
post:
|
||||
- cmd: bash .github/build_openwrt_packages.sh "{{ .Version }}" "{{ .Target }}" "{{ .Path }}"
|
||||
targets:
|
||||
- linux_amd64_v1
|
||||
- linux_arm64
|
||||
- linux_386
|
||||
- linux_arm_7
|
||||
- linux_arm_6
|
||||
- linux_riscv64
|
||||
- linux_loong64
|
||||
- id: openwrt-mips
|
||||
<<: *template
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
hooks:
|
||||
post:
|
||||
- cmd: bash .github/build_openwrt_packages.sh "{{ .Version }}" "{{ .Target }}" "{{ .Path }}"
|
||||
targets:
|
||||
- linux_arm_5
|
||||
- linux_mips_softfloat
|
||||
- linux_mips64_softfloat
|
||||
- linux_mipsle_softfloat
|
||||
- linux_mipsle_hardfloat
|
||||
- linux_mips64le
|
||||
upx:
|
||||
- enabled: true
|
||||
ids:
|
||||
- compressed
|
||||
- compressed-mips
|
||||
- compressed-android
|
||||
compress: best
|
||||
lzma: true
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
snapshot:
|
||||
name_template: "{{ .Version }}.{{ .ShortCommit }}"
|
||||
archives:
|
||||
- &template
|
||||
id: archive
|
||||
builds:
|
||||
- main
|
||||
- mips
|
||||
- android
|
||||
formats:
|
||||
- tar.gz
|
||||
- id: archive
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats:
|
||||
- zip
|
||||
format: 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-naive-purego-linux-amd64
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-purego-linux-amd64
|
||||
files:
|
||||
- LICENSE
|
||||
- src: dist/naive-purego-linux-amd64_*/libcronet*
|
||||
strip_parent: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-purego'
|
||||
- id: archive-naive-purego-linux-arm64
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-purego-linux-arm64
|
||||
files:
|
||||
- LICENSE
|
||||
- src: dist/naive-purego-linux-arm64_*/libcronet*
|
||||
strip_parent: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-purego'
|
||||
- id: archive-naive-purego-windows-amd64
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-purego-windows-amd64
|
||||
files:
|
||||
- LICENSE
|
||||
- src: dist/naive-purego-windows-amd64_*/libcronet*
|
||||
strip_parent: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-purego'
|
||||
- id: archive-naive-purego-windows-arm64
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-purego-windows-arm64
|
||||
files:
|
||||
- LICENSE
|
||||
- src: dist/naive-purego-windows-arm64_*/libcronet*
|
||||
strip_parent: true
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-purego'
|
||||
- id: archive-naive-glibc
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-glibc
|
||||
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 }}-glibc'
|
||||
- id: archive-naive-musl
|
||||
<<: *template
|
||||
builds:
|
||||
- naive-musl
|
||||
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 }}-musl'
|
||||
- id: archive-compressed
|
||||
<<: *template
|
||||
builds:
|
||||
- compressed
|
||||
- compressed-mips
|
||||
- compressed-android
|
||||
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 }}-compressed'
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||
nfpms:
|
||||
- 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 }}'
|
||||
vendor: sagernet
|
||||
homepage: https://sing-box.sagernet.org/
|
||||
maintainer: nekohasekai <contact-git@sekai.icu>
|
||||
description: The universal proxy platform.
|
||||
license: GPLv3 or later
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
priority: extra
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: config
|
||||
- src: release/config/sing-box.service
|
||||
dst: /etc/systemd/system/sing-box.service
|
||||
- src: release/config/sing-box@.service
|
||||
dst: /etc/systemd/system/sing-box@.service
|
||||
- src: LICENSE
|
||||
dst: /usr/share/licenses/sing-box/LICENSE
|
||||
source:
|
||||
enabled: false
|
||||
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
||||
@@ -593,19 +132,8 @@ signs:
|
||||
- artifacts: checksum
|
||||
release:
|
||||
github:
|
||||
owner: shtorm-7
|
||||
name: sing-box-extended
|
||||
owner: SagerNet
|
||||
name: sing-box
|
||||
name_template: '{{ if .IsSnapshot }}{{ nightly }}{{ else }}{{ .Version }}{{ end }}'
|
||||
draft: true
|
||||
prerelease: auto
|
||||
mode: replace
|
||||
ids:
|
||||
- archive
|
||||
- archive-naive-glibc
|
||||
- archive-naive-musl
|
||||
- archive-naive-purego-linux-amd64
|
||||
- archive-naive-purego-linux-arm64
|
||||
- archive-naive-purego-windows-amd64
|
||||
- archive-naive-purego-windows-arm64
|
||||
- archive-compressed
|
||||
- archive-openwrt
|
||||
skip_upload: true
|
||||
mode: replace
|
||||
@@ -1,24 +0,0 @@
|
||||
# Support the project
|
||||
|
||||
If you want to support the project, you can donate to the following addresses.
|
||||
|
||||
### TRX (Tron)
|
||||
```
|
||||
TSWU6VUZ4FcUghYDmbbhK15gRVvhvBgW3F
|
||||
```
|
||||
### TON
|
||||
```
|
||||
UQAyD2UuT5kCP6lZQlhFL0hyNibDXNE4nIo_RSLVSYAtD7N1
|
||||
```
|
||||
### Solana
|
||||
```
|
||||
CJu8ickwRCwNE71uVFjYf1UveyCkRp9Xo44rhPcQpeFL
|
||||
```
|
||||
### Bitcoin
|
||||
```
|
||||
bc1qqx97p8k4dchqkyd47s4vf74hrqdfnmhqvcja7x
|
||||
```
|
||||
### Ethereum
|
||||
```
|
||||
0xAcc5919C22F2B3fAa0ec7E8BaD142da5B375FBF6
|
||||
```
|
||||
+15
-23
@@ -1,31 +1,23 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.26 AS builder
|
||||
LABEL maintainer="shtorm-7"
|
||||
FROM golang:1.21-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=""
|
||||
ARG CRONET_GO_PATH=/go/src/github.com/sagernet/sing-box/cronet-go
|
||||
ENV GOPROXY=${GOPROXY}
|
||||
ENV CGO_ENABLED=1
|
||||
ENV GOOS=$TARGETOS
|
||||
ENV GOARCH=$TARGETARCH
|
||||
ENV CRONET_GO_PATH=${CRONET_GO_PATH}
|
||||
ENV GOPROXY ${GOPROXY}
|
||||
ENV CGO_ENABLED=0
|
||||
RUN set -ex \
|
||||
&& git config --global --add safe.directory /go/src/github.com/sagernet/sing-box \
|
||||
&& export GOTOOLCHAIN=local \
|
||||
&& export CGO_ENABLED=1 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
||||
&& eval "$(CGO_ENABLED=0 GOOS= GOARCH= go run -C "$CRONET_GO_PATH" ./cmd/build-naive --target=$GOOS/$GOARCH --libc=musl env --export)" \
|
||||
&& export CGO_LDFLAGS="$CGO_LDFLAGS -Wl,-z,notext -Wl,-z,execstack" \
|
||||
&& export VERSION=$(CGO_ENABLED=0 GOOS= GOARCH= go run ./cmd/internal/read_tag) \
|
||||
&& export TAGS=$(cat release/DEFAULT_BUILD_TAGS_DOCKER) \
|
||||
&& export LDFLAGS_SHARED=$(cat release/LDFLAGS) \
|
||||
&& go build -v -trimpath -tags "$TAGS" \
|
||||
&& 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_ech,with_utls,with_reality_server,with_clash_api,with_acme \
|
||||
-o /go/bin/sing-box \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" $LDFLAGS_SHARED -s -w -buildid=" \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||
LABEL maintainer="shtorm-7"
|
||||
FROM alpine AS dist
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
RUN set -ex \
|
||||
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
||||
&& apk upgrade \
|
||||
&& apk add bash tzdata ca-certificates \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
||||
ENTRYPOINT ["sing-box"]
|
||||
ENTRYPOINT ["sing-box"]
|
||||
@@ -1,14 +0,0 @@
|
||||
ARG BASE_IMAGE=alpine
|
||||
FROM ${BASE_IMAGE}
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
RUN set -ex \
|
||||
&& if command -v apk > /dev/null; then \
|
||||
apk add --no-cache --upgrade bash tzdata ca-certificates nftables; \
|
||||
else \
|
||||
apt-get update && apt-get install -y --no-install-recommends bash tzdata ca-certificates nftables \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
COPY sing-box-${TARGETARCH}${TARGETVARIANT} /usr/local/bin/sing-box
|
||||
ENTRYPOINT ["sing-box"]
|
||||
@@ -1,295 +1,158 @@
|
||||
NAME = sing-box
|
||||
COMMIT = $(shell git rev-parse --short HEAD)
|
||||
TAGS ?= $(shell cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
||||
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api
|
||||
TAGS_GO120 = with_quic,with_ech
|
||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
|
||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server,with_shadowsocksr
|
||||
|
||||
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)
|
||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
|
||||
|
||||
LDFLAGS_SHARED = $(shell cat release/LDFLAGS)
|
||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' $(LDFLAGS_SHARED) -s -w -buildid="
|
||||
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
||||
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)
|
||||
SING_FFI ?= sing-ffi
|
||||
LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json
|
||||
|
||||
ADMIN_PANEL_DIR = service/admin_panel
|
||||
ADMIN_PANEL_WEB = $(ADMIN_PANEL_DIR)/web
|
||||
ADMIN_PANEL_DIST = $(ADMIN_PANEL_DIR)/dist
|
||||
ADMIN_PANEL_TAGS = $(TAGS),with_admin_panel
|
||||
|
||||
DOCKER_IMAGE ?= shtorm7/sing-box-extended
|
||||
DOCKER_PLATFORMS ?= linux/amd64,linux/arm64
|
||||
|
||||
CRONET_GO_PATH ?= $(shell pwd)/cronet-go
|
||||
|
||||
.PHONY: test release docs build schema
|
||||
.PHONY: test release
|
||||
|
||||
build:
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
build_admin_panel:
|
||||
cd $(ADMIN_PANEL_WEB) && \
|
||||
npm install --no-fund --no-audit && \
|
||||
npm run build
|
||||
go run ./cmd/internal/admin_panel_pack \
|
||||
-dir $(ADMIN_PANEL_DIST)
|
||||
|
||||
build_naive:
|
||||
cd $(CRONET_GO_PATH) && \
|
||||
for arch in amd64 arm64 386 arm mipsle mips64le riscv64 loong64; do \
|
||||
go run ./cmd/build-naive --target=linux/$$arch download-toolchain; \
|
||||
done && \
|
||||
for arch in amd64 arm64 386 arm mipsle riscv64 loong64; do \
|
||||
go run ./cmd/build-naive --target=linux/$$arch --libc=musl download-toolchain; \
|
||||
done
|
||||
|
||||
race:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build -race $(MAIN_PARAMS) $(MAIN)
|
||||
ci_build_go118:
|
||||
go build $(PARAMS) $(MAIN)
|
||||
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
|
||||
|
||||
ci_build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(PARAMS) $(MAIN) && \
|
||||
go build $(PARAMS) $(MAIN)
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
generate_completions:
|
||||
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
|
||||
|
||||
schema:
|
||||
go run -ldflags "$(LDFLAGS_SHARED)" --tags "$(TAGS)" $(MAIN) schema -o docs/schema.json
|
||||
|
||||
install:
|
||||
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
||||
go build -o $(PREFIX)/bin/$(NAME) $(PARAMS) $(MAIN)
|
||||
|
||||
fmt:
|
||||
@golangci-lint fmt
|
||||
@gofumpt -l -w .
|
||||
@gofmt -s -w .
|
||||
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
|
||||
|
||||
fmt_docs:
|
||||
go run ./cmd/internal/format_docs
|
||||
fmt_install:
|
||||
go install -v mvdan.cc/gofumpt@latest
|
||||
go install -v github.com/daixiang0/gci@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 ./...
|
||||
GOOS=freebsd golangci-lint run ./...
|
||||
|
||||
lint_install:
|
||||
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
proto:
|
||||
@go run ./cmd/internal/protogen
|
||||
@golangci-lint fmt
|
||||
@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: build_admin_panel build_naive
|
||||
CRONET_GO_PATH=$(CRONET_GO_PATH) go run ./cmd/internal/build goreleaser release --skip=validate --clean -p 3 --skip publish
|
||||
release:
|
||||
go run ./cmd/internal/build goreleaser release --clean --skip-publish || exit 1
|
||||
mkdir dist/release
|
||||
mv dist/*.tar.gz \
|
||||
dist/*.zip \
|
||||
dist/*.ipk \
|
||||
dist/*.apk \
|
||||
dist/release
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
./codeberg-release.sh --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
|
||||
mv dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/release
|
||||
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
|
||||
rm -r dist
|
||||
|
||||
release_install:
|
||||
go install -v github.com/goreleaser/goreleaser@latest
|
||||
go install -v github.com/tcnksm/ghr@latest
|
||||
|
||||
release_docker:
|
||||
sudo docker buildx build \
|
||||
--platform $(DOCKER_PLATFORMS) \
|
||||
-t $(DOCKER_IMAGE):latest \
|
||||
-t $(DOCKER_IMAGE):$(VERSION) \
|
||||
--push \
|
||||
--network=host \
|
||||
.
|
||||
|
||||
update_android_version:
|
||||
go run ./cmd/internal/update_android_version
|
||||
|
||||
update_desktop_version:
|
||||
go run ./cmd/internal/update_desktop_version
|
||||
|
||||
build_android:
|
||||
cd ../sing-box-for-android && ./gradlew :app:clean :app:assembleOtherRelease && ./gradlew --stop
|
||||
cd ../sing-box-for-android && ./gradlew :app:assembleRelease
|
||||
|
||||
upload_android:
|
||||
mkdir -p dist/release_android
|
||||
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*.apk dist/release_android
|
||||
VERSION_CODE=$$(grep VERSION_CODE ../sing-box-for-android/version.properties | cut -d= -f2); \
|
||||
VERSION_NAME=$$(grep VERSION_NAME ../sing-box-for-android/version.properties | cut -d= -f2); \
|
||||
printf '{\n "version_code": %s,\n "version_name": "%s"\n}\n' "$$VERSION_CODE" "$$VERSION_NAME" > dist/release_android/SFA-version-metadata.json
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||
cp ../sing-box-for-android/app/build/outputs/apk/release/*.apk dist/release_android
|
||||
ghr --replace --draft --prerelease -p 3 "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
|
||||
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadRelease
|
||||
|
||||
build_desktop_windows:
|
||||
cd ../sing-box-for-desktop && pnpm install --frozen-lockfile && pnpm run package:win
|
||||
|
||||
build_desktop_linux:
|
||||
cd ../sing-box-for-desktop && pnpm install --frozen-lockfile && pnpm run package:linux
|
||||
|
||||
upload_desktop:
|
||||
mkdir -p dist/release_desktop
|
||||
cp ../sing-box-for-desktop/release/SFW-*.exe dist/release_desktop 2>/dev/null || true
|
||||
cp ../sing-box-for-desktop/release/SFL-*.deb ../sing-box-for-desktop/release/SFL-*.rpm ../sing-box-for-desktop/release/SFL-*.pkg.tar.zst dist/release_desktop 2>/dev/null || true
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_desktop
|
||||
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_desktop
|
||||
rm -rf dist/release_desktop
|
||||
|
||||
release_desktop: update_desktop_version build_desktop_windows build_desktop_linux upload_desktop
|
||||
|
||||
# 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 -derivedDataPath build/SFI.dd && \
|
||||
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -derivedDataPath build/SFI.dd -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
xcodebuild archive -scheme SFI -configuration Release -archivePath build/SFI.xcarchive
|
||||
|
||||
upload_ios_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||
|
||||
build_ios_deb:
|
||||
$(MAKE) -C ../sing-box-for-apple build_ios_deb
|
||||
|
||||
upload_ios_deb:
|
||||
ghr --replace --draft --prerelease "v${VERSION}" ../sing-box-for-apple/build/jailbreak/"SFI-${VERSION}-iphoneos-arm64.deb"
|
||||
ghr --replace --draft --prerelease "v${VERSION}" ../sing-box-for-apple/build/jailbreak/"SFI-${VERSION}-iphoneos-arm64e.deb"
|
||||
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist
|
||||
|
||||
release_ios: build_ios upload_ios_app_store
|
||||
|
||||
release_ios_deb: build_ios_deb upload_ios_deb
|
||||
|
||||
build_macos:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFM.xcarchive && \
|
||||
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -derivedDataPath build/SFM.dd -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive
|
||||
|
||||
upload_macos_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||
xcodebuild -exportArchive -archivePath build/SFM.xcarchive -exportOptionsPlist SFI/Upload.plist
|
||||
|
||||
release_macos: build_macos upload_macos_app_store
|
||||
|
||||
build_macos_standalone:
|
||||
$(MAKE) -C ../sing-box-for-apple archive_macos_standalone
|
||||
build_macos_independent:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFT.System.xcarchive && \
|
||||
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive
|
||||
|
||||
build_macos_dmg:
|
||||
$(MAKE) -C ../sing-box-for-apple build_macos_dmg
|
||||
notarize_macos_independent:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath "build/SFM.System.xcarchive" -exportOptionsPlist SFM.System/Upload.plist
|
||||
|
||||
build_macos_pkg:
|
||||
$(MAKE) -C ../sing-box-for-apple build_macos_pkg
|
||||
wait_notarize_macos_independent:
|
||||
sleep 60
|
||||
|
||||
notarize_macos_dmg:
|
||||
$(MAKE) -C ../sing-box-for-apple notarize_macos_dmg
|
||||
|
||||
notarize_macos_pkg:
|
||||
$(MAKE) -C ../sing-box-for-apple notarize_macos_pkg
|
||||
|
||||
upload_macos_dmg:
|
||||
export_macos_independent:
|
||||
rm -rf dist/SFM
|
||||
mkdir -p dist/SFM
|
||||
cp ../sing-box-for-apple/build/SFM-Apple.dmg "dist/SFM/SFM-${VERSION}-Apple.dmg"
|
||||
cp ../sing-box-for-apple/build/SFM-Intel.dmg "dist/SFM/SFM-${VERSION}-Intel.dmg"
|
||||
cp ../sing-box-for-apple/build/SFM-Universal.dmg "dist/SFM/SFM-${VERSION}-Universal.dmg"
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.dmg"
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.dmg"
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.dmg"
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportNotarizedApp -archivePath build/SFM.System.xcarchive -exportPath "../sing-box/dist/SFM"
|
||||
|
||||
upload_macos_pkg:
|
||||
mkdir -p dist/SFM
|
||||
cp ../sing-box-for-apple/build/SFM-Apple.pkg "dist/SFM/SFM-${VERSION}-Apple.pkg"
|
||||
cp ../sing-box-for-apple/build/SFM-Intel.pkg "dist/SFM/SFM-${VERSION}-Intel.pkg"
|
||||
cp ../sing-box-for-apple/build/SFM-Universal.pkg "dist/SFM/SFM-${VERSION}-Universal.pkg"
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.pkg"
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.pkg"
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.pkg"
|
||||
upload_macos_independent:
|
||||
cd dist/SFM && \
|
||||
rm -f *.zip && \
|
||||
zip -ry "SFM-${VERSION}-universal.zip" SFM.app && \
|
||||
ghr --replace --draft --prerelease "v${VERSION}" *.zip
|
||||
|
||||
replace_macos_pkg:
|
||||
mkdir -p dist/SFM
|
||||
cp ../sing-box-for-apple/build/SFM-Apple.pkg "dist/SFM/SFM-${VERSION}-Apple.pkg"
|
||||
cp ../sing-box-for-apple/build/SFM-Intel.pkg "dist/SFM/SFM-${VERSION}-Intel.pkg"
|
||||
cp ../sing-box-for-apple/build/SFM-Universal.pkg "dist/SFM/SFM-${VERSION}-Universal.pkg"
|
||||
ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.pkg"
|
||||
ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.pkg"
|
||||
ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.pkg"
|
||||
|
||||
upload_macos_dsyms:
|
||||
mkdir -p dist/SFM
|
||||
cd ../sing-box-for-apple/build/SFM.System-universal.xcarchive && zip -r SFM.dSYMs.zip dSYMs
|
||||
cp ../sing-box-for-apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM/SFM-${VERSION}.dSYMs.zip"
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}.dSYMs.zip"
|
||||
|
||||
replace_macos_dsyms:
|
||||
mkdir -p dist/SFM
|
||||
cd ../sing-box-for-apple/build/SFM.System-universal.xcarchive && zip -r SFM.dSYMs.zip dSYMs
|
||||
cp ../sing-box-for-apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM/SFM-${VERSION}.dSYMs.zip"
|
||||
ghr --replace "v${VERSION}" "dist/SFM/SFM-${VERSION}.dSYMs.zip"
|
||||
|
||||
release_macos_standalone: build_macos_pkg notarize_macos_pkg upload_macos_pkg upload_macos_dsyms
|
||||
|
||||
replace_macos_standalone: build_macos_pkg notarize_macos_pkg upload_macos_pkg upload_macos_dsyms
|
||||
release_macos_independent: build_macos_independent notarize_macos_independent wait_notarize_macos_independent export_macos_independent upload_macos_independent
|
||||
|
||||
build_tvos:
|
||||
cd ../sing-box-for-apple && \
|
||||
rm -rf build/SFT.xcarchive && \
|
||||
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -derivedDataPath build/SFT.dd -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive
|
||||
|
||||
upload_tvos_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||
|
||||
export_tvos_ipa:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFT && \
|
||||
cp build/SFT/sing-box.ipa dist/SFT.ipa
|
||||
|
||||
upload_tvos_ipa:
|
||||
cd dist && \
|
||||
cp SFT.ipa "SFT-${VERSION}.ipa" && \
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "SFT-${VERSION}.ipa"
|
||||
xcodebuild -exportArchive -archivePath "build/SFT.xcarchive" -exportOptionsPlist SFI/Upload.plist
|
||||
|
||||
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 ./cmd/internal/app_store_connect next_project_version macos) go run ./cmd/internal/update_apple_version
|
||||
|
||||
release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone
|
||||
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_independent
|
||||
rm -rf dist
|
||||
|
||||
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
||||
|
||||
publish_testflight:
|
||||
go run -v ./cmd/internal/app_store_connect publish_testflight $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
submit_app_store:
|
||||
go run -v ./cmd/internal/app_store_connect submit_app_store $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
release_app_store: release_ios release_tvos submit_app_store
|
||||
|
||||
publish_app_store:
|
||||
go run -v ./cmd/internal/app_store_connect publish_app_store $(filter-out $@,$(MAKECMDGOALS))
|
||||
rm -rf dist
|
||||
|
||||
test:
|
||||
@go test -v ./... && \
|
||||
@@ -306,31 +169,17 @@ test_stdio:
|
||||
lib_android:
|
||||
go run ./cmd/internal/build_libbox -target android
|
||||
|
||||
lib_apple:
|
||||
go run ./cmd/internal/build_libbox -target apple
|
||||
lib_ios:
|
||||
go run ./cmd/internal/build_libbox -target ios
|
||||
|
||||
lib_windows:
|
||||
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type csharp
|
||||
|
||||
lib_android_new:
|
||||
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type android
|
||||
|
||||
lib_apple_new:
|
||||
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple
|
||||
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.13
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.13
|
||||
|
||||
docs:
|
||||
venv/bin/mkdocs serve
|
||||
|
||||
publish_docs:
|
||||
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
||||
|
||||
docs_install:
|
||||
python3 -m venv venv
|
||||
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.7.2" mkdocs-static-i18n=="1.2.*"
|
||||
go get -v -d
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230915142329-c6740b6d2950
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230915142329-c6740b6d2950
|
||||
|
||||
clean:
|
||||
rm -rf bin dist sing-box
|
||||
@@ -339,7 +188,4 @@ clean:
|
||||
update:
|
||||
git fetch
|
||||
git reset FETCH_HEAD --hard
|
||||
git clean -fdx
|
||||
|
||||
%:
|
||||
@:
|
||||
git clean -fdx
|
||||
@@ -1,97 +1,16 @@
|
||||
# sing-box-extended
|
||||
# sing-box
|
||||
|
||||
[](LICENSE)
|
||||
[](go.mod)
|
||||
[](https://codeberg.org/shtorm-7/sing-box-extended)
|
||||
[](https://t.me/sing_box_extended)
|
||||
The universal proxy platform.
|
||||
|
||||
Sing-box with extended features.
|
||||
[](https://repology.org/project/sing-box/versions)
|
||||
|
||||
## 🔥 Features
|
||||
## Documentation
|
||||
|
||||
### Protocols
|
||||
- **WARP** — Cloudflare WARP integration through WireGuard
|
||||
- **MASQUE** — Cloudflare MASQUE proxy over QUIC / HTTP-2
|
||||
- **MTProxy** — Telegram MTProxy server with FakeTLS and domain fronting
|
||||
- **Mieru** — Secure, hard to classify, hard to probe network protocol
|
||||
- **TrustTunnel** — AdGuard's obfuscated VPN protocol, indistinguishable from HTTPS traffic
|
||||
- **Sudoku** — Traffic obfuscation protocol based on 4×4 Sudoku puzzles with low-entropy fingerprints
|
||||
- **SSH** — SSH client and server with certificate authentication and upstream fallback
|
||||
- **Call** — Traffic tunneling through video-call platforms (VK, Dion, Telemost, WBStream)
|
||||
- **VPN** — Routed tunnel over any sing-box protocol
|
||||
- **Bond** — Link aggregation for increasing throughput
|
||||
- **Fallback** — Outbound group with priority-based switching
|
||||
- **Failover** — Automatic outbound switching with session recovery for high availability
|
||||
https://sing-box.sagernet.org
|
||||
|
||||
### DNS
|
||||
- **SDNS (DNSCrypt)** — Encrypted DNS queries for enhanced privacy
|
||||
- **DNS Fallback** — Sequential / parallel queries across upstream resolvers
|
||||
## Support
|
||||
|
||||
### Limiters
|
||||
- **Bandwidth Limiter** — Upload / download / bidirectional rate limiting
|
||||
- **Connection Limiter** — Concurrent connection control
|
||||
- **Traffic Limiter** — Per-user traffic quotas
|
||||
- **Rate Limiter** — Request rate limiting
|
||||
|
||||
### Encryption & Obfuscation
|
||||
- **Amnezia 3.1** — WireGuard traffic obfuscation
|
||||
- **VLESS encryption** — XRAY encryption for VLESS protocol
|
||||
|
||||
### Transports
|
||||
- **mKCP** — Reliable UDP-based transport
|
||||
- **XHTTP** — Modern XRAY transport
|
||||
- **Rmux** — Improved smux multiplexer
|
||||
|
||||
### Services
|
||||
- **Admin Panel** — Web-based management interface
|
||||
- **Manager** — Management service for configuring users, nodes, limiters
|
||||
- **Manager API (HTTP/gRPC)** — HTTP and gRPC API for the Manager
|
||||
- **Node Manager API** — Service for connecting nodes to remote manager
|
||||
|
||||
### Miscellaneous
|
||||
- **Providers** — Outbound subscriptions from local files, inline lists, or remote URLs (sing-box JSON, Clash YAML, SIP008, share links)
|
||||
- **Link Parser** — Outbound configured from a share link (VLESS, VMess, Shadowsocks, Trojan, Hysteria, Hysteria2, TUIC, AnyTLS)
|
||||
- **Extended WireGuard options** — Advanced configuration capabilities
|
||||
- **Unified Delay** — Unified latency measurement
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
Configuration examples are available here:
|
||||
|
||||
https://github.com/shtorm-7/sing-box-extended/tree/extended/examples
|
||||
|
||||
## Support the Project
|
||||
|
||||
If you want to support the project, you can donate to the following addresses.
|
||||
|
||||
#### Tribute
|
||||
|
||||
**[RUB Donate](https://web.tribute.tg/d/JxY)**
|
||||
|
||||
**[EUR Donate](https://web.tribute.tg/d/JxZ)**
|
||||
|
||||
**[USD Donate](https://web.tribute.tg/d/Jy1)**
|
||||
|
||||
#### TRX (Tron)
|
||||
```
|
||||
TSWU6VUZ4FcUghYDmbbhK15gRVvhvBgW3F
|
||||
```
|
||||
#### TON
|
||||
```
|
||||
UQAyD2UuT5kCP6lZQlhFL0hyNibDXNE4nIo_RSLVSYAtD7N1
|
||||
```
|
||||
#### Solana
|
||||
```
|
||||
CJu8ickwRCwNE71uVFjYf1UveyCkRp9Xo44rhPcQpeFL
|
||||
```
|
||||
#### Bitcoin
|
||||
```
|
||||
bc1qqx97p8k4dchqkyd47s4vf74hrqdfnmhqvcja7x
|
||||
```
|
||||
#### Ethereum
|
||||
```
|
||||
0xAcc5919C22F2B3fAa0ec7E8BaD142da5B375FBF6
|
||||
```
|
||||
https://community.sagernet.org/c/sing-box/
|
||||
|
||||
## License
|
||||
|
||||
@@ -113,4 +32,4 @@ 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,22 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type CertificateStore interface {
|
||||
LifecycleService
|
||||
Pool() *x509.CertPool
|
||||
ExclusiveAnchors() bool
|
||||
}
|
||||
|
||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||
store := service.FromContext[CertificateStore](ctx)
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return store.Pool()
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package certificate
|
||||
|
||||
type Adapter struct {
|
||||
providerType string
|
||||
providerTag string
|
||||
}
|
||||
|
||||
func NewAdapter(providerType string, providerTag string) Adapter {
|
||||
return Adapter{
|
||||
providerType: providerType,
|
||||
providerTag: providerTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.providerType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.providerTag
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.CertificateProviderManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
registry adapter.CertificateProviderRegistry
|
||||
access sync.Mutex
|
||||
providers []adapter.CertificateProviderService
|
||||
providerByTag map[string]adapter.CertificateProviderService
|
||||
}
|
||||
|
||||
func NewManager(registry adapter.CertificateProviderRegistry) *Manager {
|
||||
return &Manager{
|
||||
registry: registry,
|
||||
providerByTag: make(map[string]adapter.CertificateProviderService),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.access.Lock()
|
||||
providers := m.providers
|
||||
m.access.Unlock()
|
||||
for _, provider := range providers {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
err := scope.Start(name, provider, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) CertificateProviders() []adapter.CertificateProviderService {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.providers
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) {
|
||||
m.access.Lock()
|
||||
provider, found := m.providerByTag[tag]
|
||||
m.access.Unlock()
|
||||
return provider, found
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error {
|
||||
provider, err := m.registry.Create(ctx, logger, tag, providerType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
_, loaded := m.providerByTag[tag]
|
||||
if loaded {
|
||||
return E.New("duplicate certificate provider tag: ", tag)
|
||||
}
|
||||
m.providers = append(m.providers, provider)
|
||||
m.providerByTag[tag] = provider
|
||||
return nil
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"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.CertificateProviderService, error)
|
||||
|
||||
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(providerType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.CertificateProviderService, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.CertificateProviderRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.CertificateProviderService, 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) OptionTypes() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return slices.Sorted(maps.Keys(m.optionsType))
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(providerType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
optionsConstructor, loaded := m.optionsType[providerType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (adapter.CertificateProviderService, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
constructor, loaded := m.constructor[providerType]
|
||||
if !loaded {
|
||||
return nil, E.New("certificate provider type not found: " + providerType)
|
||||
}
|
||||
return constructor(ctx, logger, tag, options)
|
||||
}
|
||||
|
||||
func (m *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.optionsType[providerType] = optionsConstructor
|
||||
m.constructor[providerType] = constructor
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package adapter
|
||||
|
||||
import "unsafe"
|
||||
|
||||
type AppleAnchors interface {
|
||||
Retain() AppleAnchors
|
||||
Release()
|
||||
// Ref returns the underlying CFArrayRef, or nil if the anchor set is empty.
|
||||
Ref() unsafe.Pointer
|
||||
}
|
||||
|
||||
type AppleCertificateStore interface {
|
||||
CertificateStore
|
||||
AppleAnchors() AppleAnchors
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type CertificateProvider interface {
|
||||
GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||
}
|
||||
|
||||
type ACMECertificateProvider interface {
|
||||
CertificateProvider
|
||||
GetACMENextProtos() []string
|
||||
}
|
||||
|
||||
type CertificateProviderService interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
CertificateProvider
|
||||
}
|
||||
|
||||
type CertificateProviderRegistry interface {
|
||||
option.CertificateProviderOptionsRegistry
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (CertificateProviderService, error)
|
||||
}
|
||||
|
||||
type CertificateProviderManager interface {
|
||||
Lifecycle
|
||||
CertificateProviders() []CertificateProviderService
|
||||
Get(tag string) (CertificateProviderService, bool)
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ConnectionManager interface {
|
||||
Lifecycle
|
||||
Count() int
|
||||
CloseAll()
|
||||
TrackConn(conn net.Conn) net.Conn
|
||||
TrackPacketConn(conn net.PacketConn) net.PacketConn
|
||||
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,93 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type DNSRouter interface {
|
||||
Lifecycle
|
||||
Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error)
|
||||
ExchangeAsync(ctx context.Context, message *dns.Msg, options DNSQueryOptions, callback func(response *dns.Msg, err 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(response *dns.Msg) bool) (*dns.Msg, error)
|
||||
ExchangeAsync(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool, callback func(response *dns.Msg, err error))
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
|
||||
ClearCache()
|
||||
}
|
||||
|
||||
type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
DisableOptimisticCache bool
|
||||
RewriteTTL *uint32
|
||||
Timeout time.Duration
|
||||
ClientSubnet netip.Prefix
|
||||
RemoveClientSubnet bool
|
||||
}
|
||||
|
||||
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 DNSCacheStore interface {
|
||||
LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool)
|
||||
SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error
|
||||
SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger)
|
||||
ClearDNSCache() error
|
||||
}
|
||||
|
||||
type DNSTransport interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
Dependencies() []string
|
||||
// Reset closes the transport's existing connections so later requests use fresh connections.
|
||||
// Exchanges that are currently using those connections may fail.
|
||||
Reset()
|
||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||
ExchangeAsync(ctx context.Context, message *dns.Msg, callback func(response *dns.Msg, err error))
|
||||
}
|
||||
|
||||
type DNSTransportWithPreferredDomain interface {
|
||||
DNSTransport
|
||||
PreferredDomain(domain string) bool
|
||||
}
|
||||
|
||||
type DNSTransportWithEnvironment interface {
|
||||
DNSTransport
|
||||
Environment() []string
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
StartEndpoint(endpoint Endpoint) error
|
||||
Endpoints() []Endpoint
|
||||
Get(tag string) (Endpoint, bool)
|
||||
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,81 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.EndpointManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
registry adapter.EndpointRegistry
|
||||
access sync.Mutex
|
||||
scope *adapter.Scope
|
||||
endpoints []adapter.Endpoint
|
||||
endpointByTag map[string]adapter.Endpoint
|
||||
}
|
||||
|
||||
func NewManager(registry adapter.EndpointRegistry) *Manager {
|
||||
return &Manager{
|
||||
registry: registry,
|
||||
endpointByTag: make(map[string]adapter.Endpoint),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.access.Lock()
|
||||
if stage == adapter.StartStateInitialize {
|
||||
m.scope = scope
|
||||
}
|
||||
if stage == adapter.StartStateStart {
|
||||
m.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
endpoints := m.endpoints
|
||||
m.access.Unlock()
|
||||
for _, endpoint := range endpoints {
|
||||
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
|
||||
err := scope.Start(name, endpoint, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) StartEndpoint(endpoint adapter.Endpoint) error {
|
||||
return m.scope.Start("endpoint/"+endpoint.Type()+"["+endpoint.Tag()+"]", endpoint, adapter.StartStateStart)
|
||||
}
|
||||
|
||||
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) 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()
|
||||
_, loaded := m.endpointByTag[tag]
|
||||
if loaded {
|
||||
return E.New("duplicate endpoint tag: ", tag)
|
||||
}
|
||||
m.endpoints = append(m.endpoints, endpoint)
|
||||
m.endpointByTag[tag] = endpoint
|
||||
return nil
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"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) OptionTypes() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return slices.Sorted(maps.Keys(m.optionsType))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
+36
-143
@@ -1,161 +1,38 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"time"
|
||||
"net"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type URLTestHistory struct {
|
||||
Time time.Time `json:"time"`
|
||||
Delay uint16 `json:"delay"`
|
||||
}
|
||||
|
||||
type V2RayServer interface {
|
||||
LifecycleService
|
||||
StatsService() ConnectionTracker
|
||||
}
|
||||
|
||||
type CacheFile interface {
|
||||
LifecycleService
|
||||
|
||||
CacheID() string
|
||||
|
||||
type ClashServer interface {
|
||||
Service
|
||||
PreStarter
|
||||
Mode() string
|
||||
ModeList() []string
|
||||
StoreSelected() bool
|
||||
StoreFakeIP() bool
|
||||
FakeIPStorage
|
||||
|
||||
StoreRDRC() bool
|
||||
RDRCStore
|
||||
|
||||
StoreWARPConfig() bool
|
||||
StoreMASQUEConfig() bool
|
||||
StoreSubscriptions() bool
|
||||
|
||||
StoreDNS() bool
|
||||
DNSCacheStore
|
||||
|
||||
SetDisableExpire(disableExpire bool)
|
||||
SetOptimisticTimeout(timeout time.Duration)
|
||||
CacheFile() ClashCacheFile
|
||||
HistoryStorage() *urltest.HistoryStorage
|
||||
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 ClashCacheFile interface {
|
||||
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
|
||||
LoadWARPConfig(tag string) *SavedBinary
|
||||
SaveWARPConfig(tag string, set *SavedBinary) error
|
||||
LoadMASQUEConfig(tag string) *SavedBinary
|
||||
SaveMASQUEConfig(tag string, set *SavedBinary) error
|
||||
LoadSubscription(tag string) *SavedBinary
|
||||
SaveSubscription(tag string, sub *SavedBinary) error
|
||||
FakeIPStorage
|
||||
}
|
||||
|
||||
type SavedBinary struct {
|
||||
Content []byte
|
||||
LastUpdated time.Time
|
||||
LastEtag string
|
||||
URLHash []byte
|
||||
}
|
||||
|
||||
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
err := binary.Write(&buffer, binary.BigEndian, uint8(2))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.Content)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = buffer.Write(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.WriteUvarint(&buffer, uint64(len(s.LastEtag)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = buffer.WriteString(s.LastEtag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.URLHash)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = buffer.Write(s.URLHash)
|
||||
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
|
||||
}
|
||||
contentLength, err := binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if contentLength > uint64(reader.Len()) {
|
||||
return E.New("invalid content length: ", contentLength)
|
||||
}
|
||||
s.Content = make([]byte, contentLength)
|
||||
_, err = io.ReadFull(reader, s.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var lastUpdated int64
|
||||
err = binary.Read(reader, binary.BigEndian, &lastUpdated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.LastUpdated = time.Unix(lastUpdated, 0)
|
||||
etagLength, err := binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if etagLength > uint64(reader.Len()) {
|
||||
return E.New("invalid etag length: ", etagLength)
|
||||
}
|
||||
etagBytes := make([]byte, etagLength)
|
||||
_, err = io.ReadFull(reader, etagBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.LastEtag = string(etagBytes)
|
||||
if version < 2 {
|
||||
return nil
|
||||
}
|
||||
urlHashLength, err := binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if urlHashLength > uint64(reader.Len()) {
|
||||
return E.New("invalid url hash length: ", urlHashLength)
|
||||
}
|
||||
s.URLHash = make([]byte, urlHashLength)
|
||||
_, err = io.ReadFull(reader, s.URLHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
type Tracker interface {
|
||||
Leave()
|
||||
}
|
||||
|
||||
type OutboundGroup interface {
|
||||
@@ -166,6 +43,22 @@ type OutboundGroup interface {
|
||||
|
||||
type URLTestGroup interface {
|
||||
OutboundGroup
|
||||
URLTest(ctx context.Context) (map[string]uint16, error)
|
||||
PerformUpdateCheck()
|
||||
URLTest(ctx context.Context, url string) (map[string]uint16, error)
|
||||
}
|
||||
|
||||
func OutboundTag(detour Outbound) string {
|
||||
if group, isGroup := detour.(OutboundGroup); isGroup {
|
||||
return group.Now()
|
||||
}
|
||||
return detour.Tag()
|
||||
}
|
||||
|
||||
type V2RayServer interface {
|
||||
Service
|
||||
StatsService() V2RayStatsService
|
||||
}
|
||||
|
||||
type V2RayStatsService interface {
|
||||
RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn
|
||||
RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn
|
||||
}
|
||||
|
||||
+3
-2
@@ -3,11 +3,12 @@ package adapter
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
type FakeIPStore interface {
|
||||
SimpleLifecycle
|
||||
Service
|
||||
Contains(address netip.Addr) bool
|
||||
Create(domain string, isIPv6 bool) (netip.Addr, error)
|
||||
Lookup(address netip.Addr) (string, bool)
|
||||
@@ -26,6 +27,6 @@ type FakeIPStorage interface {
|
||||
}
|
||||
|
||||
type FakeIPTransport interface {
|
||||
DNSTransport
|
||||
dns.Transport
|
||||
Store() FakeIPStore
|
||||
}
|
||||
|
||||
+8
-11
@@ -5,31 +5,28 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ConnectionHandler interface {
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type PacketHandler interface {
|
||||
NewPacket(buffer *buf.Buffer, source M.Socksaddr)
|
||||
}
|
||||
|
||||
type PacketBatchHandler interface {
|
||||
NewPacketBatch(buffers []*buf.Buffer, sources []M.Socksaddr)
|
||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type OOBPacketHandler interface {
|
||||
NewPacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
|
||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type PacketConnectionHandler interface {
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type UpstreamHandlerAdapter interface {
|
||||
N.TCPConnectionHandlerEx
|
||||
N.UDPConnectionHandlerEx
|
||||
N.TCPConnectionHandler
|
||||
N.UDPConnectionHandler
|
||||
E.Handler
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
type HTTPTransport interface {
|
||||
http.RoundTripper
|
||||
CloseIdleConnections()
|
||||
Reset()
|
||||
}
|
||||
|
||||
type HTTPClientManager interface {
|
||||
ResolveTransport(ctx context.Context, logger logger.ContextLogger, options option.HTTPClientOptions) (HTTPTransport, error)
|
||||
DefaultTransport() HTTPTransport
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
type HTTPStartContext struct {
|
||||
access sync.Mutex
|
||||
transports []HTTPTransport
|
||||
}
|
||||
|
||||
func NewHTTPStartContext() *HTTPStartContext {
|
||||
return &HTTPStartContext{}
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) Register(transport HTTPTransport) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
c.transports = append(c.transports, transport)
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) Close() {
|
||||
for _, transport := range c.transports {
|
||||
transport.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
+26
-178
@@ -4,44 +4,24 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/tlsspoof"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/common/process"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type Inbound interface {
|
||||
Lifecycle
|
||||
Service
|
||||
Type() string
|
||||
Tag() string
|
||||
}
|
||||
|
||||
type TCPInjectableInbound interface {
|
||||
type InjectableInbound interface {
|
||||
Inbound
|
||||
ConnectionHandler
|
||||
}
|
||||
|
||||
type UDPInjectableInbound interface {
|
||||
Inbound
|
||||
PacketConnectionHandler
|
||||
}
|
||||
|
||||
type InboundRegistry interface {
|
||||
option.InboundOptionsRegistry
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
|
||||
UnsafeCreate(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)
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error
|
||||
Network() []string
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type InboundContext struct {
|
||||
@@ -51,155 +31,30 @@ type InboundContext struct {
|
||||
Network string
|
||||
Source M.Socksaddr
|
||||
Destination M.Socksaddr
|
||||
Gateway *netip.Addr
|
||||
Domain string
|
||||
Protocol string
|
||||
User string
|
||||
Outbound string
|
||||
|
||||
// power report
|
||||
|
||||
RouteRule string
|
||||
RouteOutbound string
|
||||
|
||||
// sniffer
|
||||
|
||||
Protocol string
|
||||
Domain string
|
||||
Client string
|
||||
SniffContext any
|
||||
SnifferNames []string
|
||||
SniffError error
|
||||
|
||||
// cache
|
||||
|
||||
// Deprecated: implement in rule action
|
||||
InboundDetour string
|
||||
LastInbound string
|
||||
OriginDestination M.Socksaddr
|
||||
RouteOriginalDestination M.Socksaddr
|
||||
UDPDisableDomainUnmapping bool
|
||||
UDPConnect bool
|
||||
UDPTimeout time.Duration
|
||||
TLSFragment bool
|
||||
TLSFragmentFallbackDelay time.Duration
|
||||
TLSRecordFragment bool
|
||||
TLSSpoof string
|
||||
TLSSpoofMethod tlsspoof.Method
|
||||
InboundDetour string
|
||||
LastInbound string
|
||||
OriginDestination M.Socksaddr
|
||||
InboundOptions option.InboundOptions
|
||||
DestinationAddresses []netip.Addr
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *process.Info
|
||||
FakeIP bool
|
||||
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
FallbackDelay time.Duration
|
||||
// dns cache
|
||||
|
||||
DestinationAddresses []netip.Addr
|
||||
DNSResponse *dns.Msg
|
||||
NamedDNSResponses map[string]*dns.Msg
|
||||
DestinationAddressMatchFromResponse bool
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
SourceMACAddress net.HardwareAddr
|
||||
SourceHostname string
|
||||
QueryType uint16
|
||||
QueryClientSubnet netip.Prefix
|
||||
QueryDNSSEC bool
|
||||
FakeIP bool
|
||||
PreMatch bool
|
||||
|
||||
// rule cache
|
||||
|
||||
IPCIDRMatchSource bool
|
||||
IPCIDRAcceptEmpty bool
|
||||
|
||||
SourceAddressMatch bool
|
||||
SourcePortMatch bool
|
||||
DestinationAddressMatch bool
|
||||
DestinationPortMatch bool
|
||||
DeferredIPCIDRMatchGroups uint8
|
||||
IgnoreDestinationIPCIDRMatch bool
|
||||
}
|
||||
|
||||
func (c *InboundContext) ResetRuleCache() {
|
||||
c.IPCIDRMatchSource = false
|
||||
c.IPCIDRAcceptEmpty = false
|
||||
c.ResetRuleMatchCache()
|
||||
}
|
||||
|
||||
func (c *InboundContext) ResetRuleMatchCache() {
|
||||
c.SourceAddressMatch = false
|
||||
c.SourcePortMatch = false
|
||||
c.DestinationAddressMatch = false
|
||||
c.DestinationPortMatch = false
|
||||
c.DeferredIPCIDRMatchGroups = 0
|
||||
}
|
||||
|
||||
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
|
||||
return DNSResponseAddresses(c.DNSResponse)
|
||||
}
|
||||
|
||||
func DNSResponseAddresses(response *dns.Msg) []netip.Addr {
|
||||
if response == nil || response.Rcode != dns.RcodeSuccess {
|
||||
return nil
|
||||
}
|
||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||
for _, rawRecord := range response.Answer {
|
||||
switch record := rawRecord.(type) {
|
||||
case *dns.A:
|
||||
addr := M.AddrFromIP(record.A)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
case *dns.AAAA:
|
||||
addr := M.AddrFromIP(record.AAAA)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
case *dns.HTTPS:
|
||||
for _, value := range record.SVCB.Value {
|
||||
switch hint := value.(type) {
|
||||
case *dns.SVCBIPv4Hint:
|
||||
for _, ip := range hint.Hint {
|
||||
addr := M.AddrFromIP(ip).Unmap()
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
}
|
||||
case *dns.SVCBIPv6Hint:
|
||||
for _, ip := range hint.Hint {
|
||||
addr := M.AddrFromIP(ip)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return addresses
|
||||
QueryType uint16
|
||||
}
|
||||
|
||||
type inboundContextKey struct{}
|
||||
|
||||
type dnsTransportTagKey struct{}
|
||||
|
||||
func ContextWithDNSTransportTag(ctx context.Context, transportTag string) context.Context {
|
||||
return context.WithValue(ctx, (*dnsTransportTagKey)(nil), transportTag)
|
||||
}
|
||||
|
||||
func DNSTransportTagFromContext(ctx context.Context) (string, bool) {
|
||||
transportTag, loaded := ctx.Value((*dnsTransportTagKey)(nil)).(string)
|
||||
return transportTag, loaded
|
||||
}
|
||||
|
||||
func ContextForMultiplexSession(ctx context.Context) context.Context {
|
||||
var sessionContext InboundContext
|
||||
metadata := ContextFrom(ctx)
|
||||
if metadata != nil {
|
||||
sessionContext.Outbound = metadata.Outbound
|
||||
}
|
||||
ctx = ContextWithDNSTransportTag(ctx, "")
|
||||
return WithContext(ctx, &sessionContext)
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
|
||||
return context.WithValue(ctx, (*inboundContextKey)(nil), inboundContext)
|
||||
}
|
||||
@@ -212,18 +67,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 {
|
||||
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,74 +0,0 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.InboundManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
registry adapter.InboundRegistry
|
||||
endpoint adapter.EndpointManager
|
||||
access sync.Mutex
|
||||
inbounds []adapter.Inbound
|
||||
inboundByTag map[string]adapter.Inbound
|
||||
}
|
||||
|
||||
func NewManager(registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
|
||||
return &Manager{
|
||||
registry: registry,
|
||||
endpoint: endpoint,
|
||||
inboundByTag: make(map[string]adapter.Inbound),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.access.Lock()
|
||||
inbounds := m.inbounds
|
||||
m.access.Unlock()
|
||||
for _, inbound := range inbounds {
|
||||
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
|
||||
err := scope.Start(name, inbound, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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) 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()
|
||||
_, loaded := m.inboundByTag[tag]
|
||||
if loaded {
|
||||
return E.New("duplicate inbound tag: ", tag)
|
||||
}
|
||||
m.inbounds = append(m.inbounds, inbound)
|
||||
m.inboundByTag[tag] = inbound
|
||||
return nil
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"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) OptionTypes() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return slices.Sorted(maps.Keys(m.optionsType))
|
||||
}
|
||||
|
||||
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()
|
||||
return m.UnsafeCreate(ctx, router, logger, tag, outboundType, options)
|
||||
}
|
||||
|
||||
func (m *Registry) UnsafeCreate(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
|
||||
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,45 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ipv4Hint := net.ParseIP("1.1.1.1")
|
||||
require.NotNil(t, ipv4Hint)
|
||||
|
||||
response := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Response: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.HTTPS{
|
||||
SVCB: dns.SVCB{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: dns.Fqdn("example.com"),
|
||||
Rrtype: dns.TypeHTTPS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 60,
|
||||
},
|
||||
Priority: 1,
|
||||
Target: ".",
|
||||
Value: []dns.SVCBKeyValue{
|
||||
&dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
addresses := DNSResponseAddresses(response)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses)
|
||||
require.True(t, addresses[0].Is4())
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
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, scope *Scope) error
|
||||
}
|
||||
|
||||
type LifecycleService interface {
|
||||
Name() string
|
||||
Lifecycle
|
||||
}
|
||||
|
||||
type Scope struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
logger log.ContextLogger
|
||||
access sync.Mutex
|
||||
cleanups list.List[func() error]
|
||||
children map[Lifecycle]*scopeChild
|
||||
}
|
||||
|
||||
type scopeChild struct {
|
||||
scope *Scope
|
||||
cleanup *list.Element[func() error]
|
||||
}
|
||||
|
||||
func NewScope(ctx context.Context, logger log.ContextLogger) *Scope {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &Scope{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logger: logger,
|
||||
children: make(map[Lifecycle]*scopeChild),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scope) Context() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
|
||||
func (s *Scope) Add(cleanup func() error) {
|
||||
s.access.Lock()
|
||||
s.cleanups.PushBack(cleanup)
|
||||
s.access.Unlock()
|
||||
}
|
||||
|
||||
func (s *Scope) Start(name string, component Lifecycle, stage StartStage) error {
|
||||
s.access.Lock()
|
||||
err := s.ctx.Err()
|
||||
if err != nil {
|
||||
s.access.Unlock()
|
||||
return err
|
||||
}
|
||||
child, loaded := s.children[component]
|
||||
if !loaded {
|
||||
childScope := NewScope(s.ctx, s.logger)
|
||||
child = &scopeChild{scope: childScope}
|
||||
child.cleanup = s.cleanups.PushBack(func() error {
|
||||
done := LogElapsed(s.logger, "close ", name)
|
||||
monitor := taskmonitor.New(s.logger, C.StopTimeout)
|
||||
monitor.Start("close ", name)
|
||||
closeErr := childScope.Close()
|
||||
monitor.Finish()
|
||||
done()
|
||||
if closeErr != nil {
|
||||
return E.Cause(closeErr, "close ", name)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
s.children[component] = child
|
||||
}
|
||||
s.access.Unlock()
|
||||
done := LogElapsed(s.logger, stage, " ", name)
|
||||
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||
monitor.Start(stage, " ", name)
|
||||
err = component.Start(stage, child.scope)
|
||||
monitor.Finish()
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scope) Remove(component Lifecycle) error {
|
||||
s.access.Lock()
|
||||
child, loaded := s.children[component]
|
||||
if !loaded {
|
||||
s.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
delete(s.children, component)
|
||||
cleanup := s.cleanups.Remove(child.cleanup)
|
||||
s.access.Unlock()
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
func (s *Scope) Close() error {
|
||||
s.access.Lock()
|
||||
s.cancel()
|
||||
var cleanups []func() error
|
||||
for element := s.cleanups.Back(); element != nil; element = element.Prev() {
|
||||
cleanups = append(cleanups, element.Value)
|
||||
}
|
||||
s.cleanups.Init()
|
||||
s.children = nil
|
||||
s.access.Unlock()
|
||||
var err error
|
||||
for _, cleanup := range cleanups {
|
||||
err = E.Errors(err, cleanup())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func LogElapsed(logger log.ContextLogger, description ...any) func() {
|
||||
prefix := F.ToString(description...)
|
||||
startTime := time.Now()
|
||||
timer := time.AfterFunc(time.Second, func() {
|
||||
logger.Trace(prefix, "...")
|
||||
})
|
||||
return func() {
|
||||
if timer.Stop() {
|
||||
return
|
||||
}
|
||||
logger.Trace(prefix, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type NeighborEntry struct {
|
||||
Address netip.Addr
|
||||
MACAddress net.HardwareAddr
|
||||
Hostname string
|
||||
}
|
||||
|
||||
type NeighborResolver interface {
|
||||
LookupMAC(address netip.Addr) (net.HardwareAddr, bool)
|
||||
LookupHostname(address netip.Addr) (string, bool)
|
||||
LookupAddresses(hostname string) []netip.Addr
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type NeighborUpdateListener interface {
|
||||
UpdateNeighborTable(entries []NeighborEntry)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package adapter
|
||||
|
||||
type NetworkNamespaceManager interface {
|
||||
ResolvePath(nameOrPath string) string
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
)
|
||||
|
||||
type NetworkManager interface {
|
||||
Lifecycle
|
||||
Initialize(ruleSets []RuleSet)
|
||||
InterfaceFinder() control.InterfaceFinder
|
||||
UpdateInterfaces() error
|
||||
DefaultNetworkInterface() *NetworkInterface
|
||||
NetworkInterfaces() []NetworkInterface
|
||||
NetworkEnvironment() uint64
|
||||
AutoDetectInterface() bool
|
||||
AutoDetectInterfaceFunc() control.Func
|
||||
ProtectFunc() control.Func
|
||||
DefaultOptions() NetworkOptions
|
||||
RegisterAutoRedirectOutputMark(mark uint32) error
|
||||
AutoRedirectOutputMark() uint32
|
||||
AutoRedirectOutputMarkFunc() control.Func
|
||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||
PackageManager() tun.PackageManager
|
||||
NeedWIFIState() bool
|
||||
WIFIState() WIFIState
|
||||
UpdateWIFIState(ctx context.Context)
|
||||
ResetNetwork(ctx context.Context)
|
||||
}
|
||||
|
||||
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(ctx context.Context)
|
||||
}
|
||||
|
||||
type WIFIState struct {
|
||||
SSID string
|
||||
BSSID string
|
||||
}
|
||||
|
||||
func NormalizeWIFIBSSID(bssid string) string {
|
||||
bssid = strings.TrimSpace(bssid)
|
||||
if bssid == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err := net.ParseMAC(bssid)
|
||||
if err == nil && len(parsed) == 6 {
|
||||
return parsed.String()
|
||||
}
|
||||
if len(bssid) == 12 {
|
||||
decoded, err := hex.DecodeString(bssid)
|
||||
if err == nil {
|
||||
return net.HardwareAddr(decoded).String()
|
||||
}
|
||||
}
|
||||
return bssid
|
||||
}
|
||||
|
||||
type NetworkInterface struct {
|
||||
control.Interface
|
||||
Type C.InterfaceType
|
||||
DNSServers []string
|
||||
Gateways []netip.Addr
|
||||
Expensive bool
|
||||
Constrained bool
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
OpenConnectStateConnecting = "connecting"
|
||||
OpenConnectStateAuthPending = "auth-pending"
|
||||
OpenConnectStateConnected = "connected"
|
||||
OpenConnectStateError = "error"
|
||||
)
|
||||
|
||||
type OpenConnectEndpoint interface {
|
||||
Endpoint
|
||||
OpenConnectStatus() OpenConnectStatus
|
||||
StatusUpdated() <-chan struct{}
|
||||
CompleteAuthChallenge(challengeID string, response OpenConnectAuthResponse) error
|
||||
CancelAuthChallenge(challengeID string) error
|
||||
}
|
||||
|
||||
type OpenConnectStatus struct {
|
||||
State string
|
||||
AuthChallenge *OpenConnectAuthChallenge
|
||||
Error string
|
||||
TunnelInfo *OpenConnectTunnelInfo
|
||||
}
|
||||
|
||||
type OpenConnectTunnelInfo struct {
|
||||
Server string
|
||||
Flavor string
|
||||
Transport string
|
||||
IPv4 []netip.Prefix
|
||||
IPv6 []netip.Prefix
|
||||
DNS []netip.Addr
|
||||
MTU uint32
|
||||
ConnectedSince time.Time
|
||||
}
|
||||
|
||||
type OpenConnectAuthChallenge struct {
|
||||
ID string
|
||||
Banner string
|
||||
Message string
|
||||
Error string
|
||||
Form *OpenConnectAuthForm
|
||||
Browser *OpenConnectBrowserRequest
|
||||
}
|
||||
|
||||
type OpenConnectAuthForm struct {
|
||||
Fields []OpenConnectAuthFormField
|
||||
}
|
||||
|
||||
type OpenConnectBrowserRequest struct {
|
||||
URL string
|
||||
FinalURL string
|
||||
CookieNames []string
|
||||
EarlyCookieNames []string
|
||||
HeaderNames []string
|
||||
CallbackURLPrefixes []string
|
||||
CacheID string
|
||||
}
|
||||
|
||||
type OpenConnectBrowserCookie struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
type OpenConnectBrowserHeader struct {
|
||||
Name string
|
||||
Values []string
|
||||
}
|
||||
|
||||
type OpenConnectAuthResponse struct {
|
||||
Form *OpenConnectAuthFormResponse
|
||||
Browser *OpenConnectBrowserResult
|
||||
}
|
||||
|
||||
type OpenConnectAuthFormResponse struct {
|
||||
Values map[string]string
|
||||
}
|
||||
|
||||
type OpenConnectBrowserResult struct {
|
||||
FinalURL string
|
||||
Cookies []OpenConnectBrowserCookie
|
||||
Headers []OpenConnectBrowserHeader
|
||||
}
|
||||
|
||||
type OpenConnectAuthFormField struct {
|
||||
SubmissionKey string
|
||||
Name string
|
||||
Label string
|
||||
Kind string
|
||||
Value string
|
||||
Options []OpenConnectAuthFormChoice
|
||||
}
|
||||
|
||||
type OpenConnectAuthFormChoice struct {
|
||||
Value string
|
||||
Label string
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
OpenVPNStateConnecting = "connecting"
|
||||
OpenVPNStateAuthPending = "auth-pending"
|
||||
OpenVPNStateConnected = "connected"
|
||||
OpenVPNStateError = "error"
|
||||
)
|
||||
|
||||
type OpenVPNEndpoint interface {
|
||||
Endpoint
|
||||
OpenVPNStatus() OpenVPNStatus
|
||||
StatusUpdated() <-chan struct{}
|
||||
CompleteChallenge(challengeID string, response OpenVPNChallengeResponse) error
|
||||
CancelChallenge(challengeID string) error
|
||||
}
|
||||
|
||||
type OpenVPNStatus struct {
|
||||
State string
|
||||
Challenge *OpenVPNChallenge
|
||||
Error string
|
||||
TunnelInfo *OpenVPNTunnelInfo
|
||||
}
|
||||
|
||||
type OpenVPNTunnelInfo struct {
|
||||
Server string
|
||||
Network string
|
||||
Cipher string
|
||||
IPv4 []netip.Prefix
|
||||
IPv6 []netip.Prefix
|
||||
DNS []netip.Addr
|
||||
MTU uint32
|
||||
ConnectedSince time.Time
|
||||
}
|
||||
|
||||
type OpenVPNChallenge struct {
|
||||
ID string
|
||||
Kind string
|
||||
Username string
|
||||
Message string
|
||||
URL string
|
||||
SecretMessage string
|
||||
Echo bool
|
||||
PreviousError string
|
||||
Deadline time.Time
|
||||
}
|
||||
|
||||
type OpenVPNChallengeResponse struct {
|
||||
Username string
|
||||
Password string
|
||||
Secret string
|
||||
}
|
||||
+3
-45
@@ -2,11 +2,8 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
@@ -18,45 +15,6 @@ type Outbound interface {
|
||||
Network() []string
|
||||
Dependencies() []string
|
||||
N.Dialer
|
||||
}
|
||||
|
||||
type OutboundWithPreferredRoutes interface {
|
||||
Outbound
|
||||
PreferredDomain(metadata *InboundContext, domain string) bool
|
||||
PreferredAddress(metadata *InboundContext, address netip.Addr) bool
|
||||
}
|
||||
|
||||
type OutboundWithMultiplex interface {
|
||||
Outbound
|
||||
MultiplexEnabled() bool
|
||||
}
|
||||
|
||||
type FlowOutbound interface {
|
||||
Outbound
|
||||
tun.Port
|
||||
PreMatchFlow(network string, destination netip.Addr) PreMatchAction
|
||||
}
|
||||
|
||||
type OutboundRegistry interface {
|
||||
option.OutboundOptionsRegistry
|
||||
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||
UnsafeCreateOutbound(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
|
||||
}
|
||||
|
||||
type IdleConnectionKeeper interface {
|
||||
SetKeepIdleConnections(keep bool)
|
||||
CloseIdleConnections()
|
||||
}
|
||||
|
||||
type Referrer interface {
|
||||
References() []string
|
||||
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,280 +0,0 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"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"
|
||||
)
|
||||
|
||||
var _ adapter.OutboundManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
registry adapter.OutboundRegistry
|
||||
endpoint adapter.EndpointManager
|
||||
defaultTag string
|
||||
access sync.RWMutex
|
||||
startAccess sync.Mutex
|
||||
scope *adapter.Scope
|
||||
stage adapter.StartStage
|
||||
outbounds []adapter.Outbound
|
||||
outboundByTag map[string]adapter.Outbound
|
||||
dependByTag map[string][]string
|
||||
defaultOutbound adapter.Outbound
|
||||
defaultOutboundFallback func() (adapter.Outbound, error)
|
||||
}
|
||||
|
||||
func NewManager(registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
|
||||
return &Manager{
|
||||
registry: registry,
|
||||
endpoint: endpoint,
|
||||
defaultTag: defaultTag,
|
||||
outboundByTag: make(map[string]adapter.Outbound),
|
||||
dependByTag: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, error)) {
|
||||
m.defaultOutboundFallback = defaultOutboundFallback
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
m.scope = scope
|
||||
m.stage = stage
|
||||
if stage == adapter.StartStateInitialize {
|
||||
if m.defaultTag != "" && m.defaultOutbound == nil {
|
||||
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
|
||||
if !loaded {
|
||||
m.access.Unlock()
|
||||
return E.New("default outbound not found: ", m.defaultTag)
|
||||
}
|
||||
m.defaultOutbound = defaultEndpoint
|
||||
}
|
||||
if m.defaultOutbound == nil {
|
||||
directOutbound, err := m.defaultOutboundFallback()
|
||||
if err != nil {
|
||||
m.access.Unlock()
|
||||
return E.Cause(err, "create direct outbound for fallback")
|
||||
}
|
||||
m.outbounds = append(m.outbounds, directOutbound)
|
||||
m.outboundByTag[directOutbound.Tag()] = directOutbound
|
||||
m.defaultOutbound = directOutbound
|
||||
}
|
||||
}
|
||||
outbounds := m.outbounds
|
||||
m.access.Unlock()
|
||||
if stage == adapter.StartStateStart {
|
||||
return m.startOutbounds(scope, append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
|
||||
}
|
||||
for _, outbound := range outbounds {
|
||||
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
|
||||
if !isLifecycle {
|
||||
continue
|
||||
}
|
||||
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
||||
err := scope.Start(name, lifecycle, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) startOutbounds(scope *adapter.Scope, outbounds []adapter.Outbound) error {
|
||||
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 endpoint, isEndpoint := outboundToStart.(adapter.Endpoint); isEndpoint {
|
||||
err := m.endpoint.StartEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
lifecycle, isLifecycle := outboundToStart.(adapter.Lifecycle)
|
||||
if !isLifecycle {
|
||||
continue
|
||||
}
|
||||
name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]"
|
||||
err := scope.Start(name, lifecycle, adapter.StartStateStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
problemOutbound := common.Find(outbounds, func(it adapter.Outbound) bool {
|
||||
return it.Tag() == problemOutboundTag
|
||||
})
|
||||
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) 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()
|
||||
return m.defaultOutbound
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
outbound, found := m.outboundByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
dependBy := m.dependByTag[tag]
|
||||
if len(dependBy) > 0 {
|
||||
m.access.Unlock()
|
||||
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
|
||||
}
|
||||
delete(m.outboundByTag, tag)
|
||||
m.outbounds = common.Filter(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it != outbound
|
||||
})
|
||||
m.removeDependencies(outbound)
|
||||
if m.defaultOutbound == outbound {
|
||||
if len(m.outbounds) > 0 {
|
||||
m.defaultOutbound = m.outbounds[0]
|
||||
} else {
|
||||
m.defaultOutbound = nil
|
||||
}
|
||||
}
|
||||
scope := m.scope
|
||||
m.access.Unlock()
|
||||
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
|
||||
if scope == nil || !isLifecycle {
|
||||
return nil
|
||||
}
|
||||
return scope.Remove(lifecycle)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.RLock()
|
||||
scope := m.scope
|
||||
stage := m.stage
|
||||
m.access.RUnlock()
|
||||
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
|
||||
if scope != nil && isLifecycle {
|
||||
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
||||
for _, startStage := range adapter.ListStartStages[:stage+1] {
|
||||
err = scope.Start(name, lifecycle, startStage)
|
||||
if err != nil {
|
||||
return E.Errors(err, scope.Remove(lifecycle))
|
||||
}
|
||||
}
|
||||
}
|
||||
m.access.Lock()
|
||||
existsOutbound, loaded := m.outboundByTag[tag]
|
||||
if loaded {
|
||||
m.outbounds = common.Filter(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it != existsOutbound
|
||||
})
|
||||
m.removeDependencies(existsOutbound)
|
||||
}
|
||||
m.outbounds = append(m.outbounds, outbound)
|
||||
m.outboundByTag[tag] = outbound
|
||||
for _, dependency := range outbound.Dependencies() {
|
||||
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
|
||||
}
|
||||
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) || (loaded && m.defaultOutbound == existsOutbound) {
|
||||
m.defaultOutbound = outbound
|
||||
}
|
||||
m.access.Unlock()
|
||||
if !loaded || scope == nil {
|
||||
return nil
|
||||
}
|
||||
existsLifecycle, isExistsLifecycle := existsOutbound.(adapter.Lifecycle)
|
||||
if !isExistsLifecycle {
|
||||
return nil
|
||||
}
|
||||
err = scope.Remove(existsLifecycle)
|
||||
if err != nil {
|
||||
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) removeDependencies(outbound adapter.Outbound) {
|
||||
tag := outbound.Tag()
|
||||
for _, dependency := range outbound.Dependencies() {
|
||||
dependBy := common.Filter(m.dependByTag[dependency], func(it string) bool {
|
||||
return it != tag
|
||||
})
|
||||
if len(dependBy) == 0 {
|
||||
delete(m.dependByTag, dependency)
|
||||
} else {
|
||||
m.dependByTag[dependency] = dependBy
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"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) OptionTypes() []string {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
return slices.Sorted(maps.Keys(r.optionsType))
|
||||
}
|
||||
|
||||
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()
|
||||
return r.UnsafeCreateOutbound(ctx, router, logger, tag, outboundType, options)
|
||||
}
|
||||
|
||||
func (r *Registry) UnsafeCreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
|
||||
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,119 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
type PlatformInterface interface {
|
||||
Initialize(networkManager NetworkManager) error
|
||||
|
||||
UsePlatformAutoDetectInterfaceControl() bool
|
||||
AutoDetectInterfaceControl(fd int) error
|
||||
BindInterfaceControl(fd int, interfaceName string) error
|
||||
|
||||
UsePlatformInterface() bool
|
||||
OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
|
||||
ProcessPlatformOptions(options option.TunPlatformOptions) error
|
||||
|
||||
UsePlatformDefaultInterfaceMonitor() bool
|
||||
CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
|
||||
|
||||
UsePlatformNetworkInterfaces() bool
|
||||
NetworkInterfaces() ([]NetworkInterface, error)
|
||||
|
||||
UnderNetworkExtension() bool
|
||||
NetworkExtensionIncludeAllNetworks() bool
|
||||
|
||||
ClearDNSCache()
|
||||
RequestPermissionForWIFIState() error
|
||||
ReadWIFIState(ctx context.Context) WIFIState
|
||||
|
||||
UsePlatformConnectionOwnerFinder() bool
|
||||
FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error)
|
||||
|
||||
UsePlatformWIFIMonitor() bool
|
||||
|
||||
UsePlatformNotification() bool
|
||||
SendNotification(notification *Notification) error
|
||||
CancelNotification(identifier string, typeID int32) error
|
||||
|
||||
MyInterfaceAddress() []netip.Addr
|
||||
|
||||
UsePlatformNeighborResolver() bool
|
||||
StartNeighborMonitor(listener NeighborUpdateListener) error
|
||||
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
||||
|
||||
UsePlatformShell() bool
|
||||
CheckPlatformShell() error
|
||||
OpenShellSession(user *PlatformUser, command string, env []string, term string, rows int32, cols int32) (ShellSession, error)
|
||||
LookupUser(username string) (*PlatformUser, error)
|
||||
LookupSFTPServer() (string, error)
|
||||
ReadSystemSSHHostKey() ([]byte, error)
|
||||
TailscaleHostname() string
|
||||
|
||||
UsePlatformBridge() bool
|
||||
CreateBridge(options BridgeOptions) (BridgeSession, error)
|
||||
}
|
||||
|
||||
type BridgeOptions struct {
|
||||
BridgeName string
|
||||
MTU uint32
|
||||
Inet4Port netip.Addr
|
||||
Inet6Port netip.Addr
|
||||
Interface string
|
||||
RuleIndex int
|
||||
RouteTable int
|
||||
}
|
||||
|
||||
type BridgeSession interface {
|
||||
FileDescriptor() int
|
||||
Name() string
|
||||
Inet6Active() bool
|
||||
SetEgress(interfaceName string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type PlatformUser struct {
|
||||
Username string
|
||||
Uid int
|
||||
Gid int
|
||||
HomeDir string
|
||||
Shell string
|
||||
Groups []int
|
||||
}
|
||||
|
||||
type FindConnectionOwnerRequest struct {
|
||||
IpProtocol int32
|
||||
SourceAddress string
|
||||
SourcePort int32
|
||||
DestinationAddress string
|
||||
DestinationPort int32
|
||||
}
|
||||
|
||||
type ConnectionOwner struct {
|
||||
ProcessID uint32
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
AndroidPackageNames []string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
Identifier string
|
||||
TypeName string
|
||||
TypeID int32
|
||||
Title string
|
||||
Subtitle string
|
||||
Body string
|
||||
OpenURL string
|
||||
}
|
||||
|
||||
type SystemProxyStatus struct {
|
||||
Available bool
|
||||
Enabled bool
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
package adapter
|
||||
|
||||
type PreStarter interface {
|
||||
PreStart() error
|
||||
}
|
||||
|
||||
type PostStarter interface {
|
||||
PostStart() error
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
Outbounds() []Outbound
|
||||
Outbound(tag string) (Outbound, bool)
|
||||
UpdatedAt() time.Time
|
||||
HealthCheck(ctx context.Context) (map[string]uint16, error)
|
||||
RegisterCallback(callback ProviderUpdateCallback) *list.Element[ProviderUpdateCallback]
|
||||
UnregisterCallback(element *list.Element[ProviderUpdateCallback])
|
||||
}
|
||||
|
||||
type ProviderUpdater interface {
|
||||
Update() error
|
||||
}
|
||||
|
||||
type ProviderSubscriptionInfo interface {
|
||||
SubscriptionInfo() SubscriptionInfo
|
||||
}
|
||||
|
||||
type ProviderRegistry interface {
|
||||
option.ProviderOptionsRegistry
|
||||
CreateProvider(ctx context.Context, router Router, logFactory log.Factory, tag string, providerType string, options any) (Provider, error)
|
||||
}
|
||||
|
||||
type ProviderManager interface {
|
||||
Lifecycle
|
||||
Providers() []Provider
|
||||
Get(tag string) (Provider, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, router Router, logFactory log.Factory, tag string, providerType string, options any) error
|
||||
}
|
||||
|
||||
type SubscriptionInfo struct {
|
||||
Upload int64
|
||||
Download int64
|
||||
Total int64
|
||||
Expire int64
|
||||
}
|
||||
|
||||
type ProviderUpdateCallback = func(tag string) error
|
||||
@@ -1,326 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/batch"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type Adapter struct {
|
||||
ctx context.Context
|
||||
outbound adapter.OutboundManager
|
||||
router adapter.Router
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
providerType string
|
||||
providerTag string
|
||||
outboundAccess sync.RWMutex
|
||||
outbounds []adapter.Outbound
|
||||
outboundsByTag map[string]adapter.Outbound
|
||||
ticker *time.Ticker
|
||||
checking atomic.Bool
|
||||
history *urltest.HistoryStorage
|
||||
callbackAccess sync.Mutex
|
||||
callbacks list.List[adapter.ProviderUpdateCallback]
|
||||
|
||||
link string
|
||||
enabled bool
|
||||
removeEmojis bool
|
||||
overrideDialerOptions *option.DialerOptions
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.OutboundManager, logFactory log.Factory, logger log.ContextLogger, providerTag string, providerType string, options option.ProviderHealthCheckOptions) Adapter {
|
||||
timeout := time.Duration(options.Timeout)
|
||||
if timeout == 0 {
|
||||
timeout = 3 * time.Second
|
||||
}
|
||||
interval := time.Duration(options.Interval)
|
||||
if interval == 0 {
|
||||
interval = 10 * time.Minute
|
||||
}
|
||||
if interval < time.Minute {
|
||||
interval = time.Minute
|
||||
}
|
||||
return Adapter{
|
||||
ctx: ctx,
|
||||
outbound: outbound,
|
||||
router: router,
|
||||
logFactory: logFactory,
|
||||
logger: logger,
|
||||
providerType: providerType,
|
||||
providerTag: providerTag,
|
||||
|
||||
enabled: options.Enabled,
|
||||
link: options.URL,
|
||||
timeout: timeout,
|
||||
interval: interval,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) SetRemoveEmojis(remove bool) {
|
||||
a.removeEmojis = remove
|
||||
}
|
||||
|
||||
func (a *Adapter) SetOverrideDialerOptions(options *option.DialerOptions) {
|
||||
a.overrideDialerOptions = options
|
||||
}
|
||||
|
||||
func (a *Adapter) Start() error {
|
||||
a.history = service.PtrFromContext[urltest.HistoryStorage](a.ctx)
|
||||
if a.history == nil {
|
||||
a.history = urltest.NewHistoryStorage()
|
||||
}
|
||||
if a.enabled {
|
||||
a.ticker = time.NewTicker(a.interval)
|
||||
go a.loopCheck()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.providerType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.providerTag
|
||||
}
|
||||
|
||||
func (a *Adapter) Outbounds() []adapter.Outbound {
|
||||
a.outboundAccess.RLock()
|
||||
defer a.outboundAccess.RUnlock()
|
||||
return a.outbounds
|
||||
}
|
||||
|
||||
func (a *Adapter) Outbound(tag string) (adapter.Outbound, bool) {
|
||||
a.outboundAccess.RLock()
|
||||
defer a.outboundAccess.RUnlock()
|
||||
detour, ok := a.outboundsByTag[tag]
|
||||
return detour, ok
|
||||
}
|
||||
|
||||
func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Outbound) {
|
||||
if a.removeEmojis {
|
||||
removeEmojisFromTags(newOpts)
|
||||
}
|
||||
uniquifyTags(newOpts)
|
||||
a.removeUseless(newOpts)
|
||||
var (
|
||||
oldOptByTag = make(map[string]option.Outbound)
|
||||
outbounds = make([]adapter.Outbound, 0, len(newOpts))
|
||||
outboundsByTag = make(map[string]adapter.Outbound)
|
||||
)
|
||||
for _, opt := range oldOpts {
|
||||
oldOptByTag[opt.Tag] = opt
|
||||
}
|
||||
for i, opt := range newOpts {
|
||||
if a.overrideDialerOptions != nil {
|
||||
if wrapper, ok := opt.Options.(option.DialerOptionsWrapper); ok {
|
||||
wrapper.ReplaceDialerOptions(*a.overrideDialerOptions)
|
||||
}
|
||||
}
|
||||
var tag string
|
||||
if opt.Tag != "" {
|
||||
tag = F.ToString(a.providerTag, "/", opt.Tag)
|
||||
} else {
|
||||
tag = F.ToString(a.providerTag, "/", i)
|
||||
}
|
||||
outbound, exist := a.outbound.Outbound(tag)
|
||||
if !exist || !reflect.DeepEqual(opt, oldOptByTag[opt.Tag]) {
|
||||
err := a.outbound.Create(
|
||||
adapter.WithContext(a.ctx, &adapter.InboundContext{
|
||||
Outbound: tag,
|
||||
}),
|
||||
a.router,
|
||||
a.logFactory.NewLogger(F.ToString("outbound/", opt.Type, "[", tag, "]")),
|
||||
tag,
|
||||
opt.Type,
|
||||
opt.Options,
|
||||
)
|
||||
if err != nil {
|
||||
a.logger.Warn(err, " in ", tag, ", skip create this outbound")
|
||||
continue
|
||||
}
|
||||
outbound, _ = a.outbound.Outbound(tag)
|
||||
}
|
||||
outbounds = append(outbounds, outbound)
|
||||
outboundsByTag[tag] = outbound
|
||||
}
|
||||
a.outboundAccess.Lock()
|
||||
a.outbounds = outbounds
|
||||
a.outboundsByTag = outboundsByTag
|
||||
a.outboundAccess.Unlock()
|
||||
if a.enabled && a.history != nil {
|
||||
go a.HealthCheck(a.ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) HealthCheck(ctx context.Context) (map[string]uint16, error) {
|
||||
if a.ticker != nil {
|
||||
a.ticker.Reset(a.interval)
|
||||
}
|
||||
return a.healthcheck(ctx)
|
||||
}
|
||||
|
||||
func (a *Adapter) RegisterCallback(callback adapter.ProviderUpdateCallback) *list.Element[adapter.ProviderUpdateCallback] {
|
||||
a.callbackAccess.Lock()
|
||||
defer a.callbackAccess.Unlock()
|
||||
return a.callbacks.PushBack(callback)
|
||||
}
|
||||
|
||||
func (a *Adapter) UnregisterCallback(element *list.Element[adapter.ProviderUpdateCallback]) {
|
||||
a.callbackAccess.Lock()
|
||||
defer a.callbackAccess.Unlock()
|
||||
a.callbacks.Remove(element)
|
||||
}
|
||||
|
||||
func (a *Adapter) UpdateGroups() {
|
||||
for element := a.callbacks.Front(); element != nil; element = element.Next() {
|
||||
element.Value(a.providerTag)
|
||||
}
|
||||
history := service.PtrFromContext[urltest.HistoryStorage](a.ctx)
|
||||
if history != nil {
|
||||
history.NotifyUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Close() error {
|
||||
if a.ticker != nil {
|
||||
a.ticker.Stop()
|
||||
}
|
||||
a.outboundAccess.Lock()
|
||||
outbounds := a.outbounds
|
||||
a.outbounds = nil
|
||||
a.outboundsByTag = nil
|
||||
a.outboundAccess.Unlock()
|
||||
var err error
|
||||
for _, ob := range outbounds {
|
||||
if err2 := a.outbound.Remove(ob.Tag()); err2 != nil {
|
||||
err = E.Append(err, err2, func(err error) error {
|
||||
return E.Cause(err, "close outbound [", ob.Tag(), "]")
|
||||
})
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *Adapter) loopCheck() {
|
||||
a.healthcheck(a.ctx)
|
||||
for {
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
return
|
||||
case <-a.ticker.C:
|
||||
a.healthcheck(a.ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) healthcheck(ctx context.Context) (map[string]uint16, error) {
|
||||
result := make(map[string]uint16)
|
||||
if a.checking.Swap(true) {
|
||||
return result, nil
|
||||
}
|
||||
defer a.checking.Store(false)
|
||||
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
|
||||
var resultAccess sync.Mutex
|
||||
checked := make(map[string]bool)
|
||||
for _, detour := range a.Outbounds() {
|
||||
tag := detour.Tag()
|
||||
if checked[tag] {
|
||||
continue
|
||||
}
|
||||
checked[tag] = true
|
||||
b.Go(tag, func() (any, error) {
|
||||
ctx, cancel := context.WithTimeout(a.ctx, a.timeout)
|
||||
defer cancel()
|
||||
t, err := urltest.URLTest(ctx, a.link, detour)
|
||||
if err != nil {
|
||||
a.logger.Debug("outbound ", tag, " unavailable: ", err)
|
||||
a.history.DeleteURLTestHistory(tag)
|
||||
} else {
|
||||
a.logger.Debug("outbound ", tag, " available: ", t, "ms")
|
||||
a.history.StoreURLTestHistory(tag, &adapter.URLTestHistory{
|
||||
Time: time.Now(),
|
||||
Delay: t,
|
||||
})
|
||||
resultAccess.Lock()
|
||||
result[tag] = t
|
||||
resultAccess.Unlock()
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
b.Wait()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *Adapter) removeUseless(newOpts []option.Outbound) {
|
||||
outbounds := a.Outbounds()
|
||||
if len(outbounds) == 0 {
|
||||
return
|
||||
}
|
||||
exists := make(map[string]bool)
|
||||
for i, opt := range newOpts {
|
||||
var tag string
|
||||
if opt.Tag != "" {
|
||||
tag = F.ToString(a.providerTag, "/", opt.Tag)
|
||||
} else {
|
||||
tag = F.ToString(a.providerTag, "/", i)
|
||||
}
|
||||
exists[tag] = true
|
||||
}
|
||||
for _, opt := range outbounds {
|
||||
if !exists[opt.Tag()] {
|
||||
if err := a.outbound.Remove(opt.Tag()); err != nil {
|
||||
a.logger.Error(err, "close outbound [", opt.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uniquifyTags(opts []option.Outbound) {
|
||||
count := make(map[string]int)
|
||||
for i, opt := range opts {
|
||||
count[opt.Tag]++
|
||||
if count[opt.Tag] > 1 {
|
||||
opts[i].Tag = F.ToString(opt.Tag, " #", count[opt.Tag])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeEmojisFromTags(opts []option.Outbound) {
|
||||
for i, opt := range opts {
|
||||
cleaned := flagRegex.ReplaceAllStringFunc(opt.Tag, flagToCountryCode)
|
||||
cleaned = emojiRegex.ReplaceAllString(cleaned, "")
|
||||
cleaned = multiSpaceRegex.ReplaceAllString(cleaned, " ")
|
||||
opts[i].Tag = strings.TrimSpace(cleaned)
|
||||
}
|
||||
}
|
||||
|
||||
func flagToCountryCode(flag string) string {
|
||||
runes := []rune(flag)
|
||||
if len(runes) == 2 {
|
||||
return string(rune(runes[0]-0x1F1E6+'A')) + string(rune(runes[1]-0x1F1E6+'A')) + " "
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var flagRegex = regexp.MustCompile(`[\x{1F1E6}-\x{1F1FF}]{2}`)
|
||||
var emojiRegex = regexp.MustCompile(`[\x{1F1E0}-\x{1F1FF}\x{1F300}-\x{1F9FF}\x{2600}-\x{27BF}\x{FE00}-\x{FE0F}\x{200D}]+`)
|
||||
var multiSpaceRegex = regexp.MustCompile(`\s{2,}`)
|
||||
@@ -1,45 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
func TestFlagToCountryCodeAllFlags(t *testing.T) {
|
||||
for first := 'A'; first <= 'Z'; first++ {
|
||||
for second := 'A'; second <= 'Z'; second++ {
|
||||
flag := string(rune(0x1F1E6+(first-'A'))) + string(rune(0x1F1E6+(second-'A')))
|
||||
expected := string(first) + string(second)
|
||||
result := flagToCountryCode(flag)
|
||||
// flagToCountryCode appends a space
|
||||
if result != expected+" " {
|
||||
t.Errorf("flagToCountryCode(%q) = %q, want %q", expected, result, expected+" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveEmojisFromTags(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"🇺🇸 United States", "US United States"},
|
||||
{"🇷🇺 Россия", "RU Россия"},
|
||||
{"🇩🇪 Germany 🚀", "DE Germany"},
|
||||
{"🇫🇷🇬🇧 France-UK", "FR GB France-UK"},
|
||||
{"No emojis here", "No emojis here"},
|
||||
{"🌍 World", "World"},
|
||||
{"🇯🇵 Tokyo ⚡ Fast", "JP Tokyo Fast"},
|
||||
{"Germany 🇩🇪", "Germany DE"},
|
||||
{"Server 🇺🇸 Node", "Server US Node"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
opts := []option.Outbound{{Tag: tt.input}}
|
||||
removeEmojisFromTags(opts)
|
||||
if opts[0].Tag != tt.expected {
|
||||
t.Errorf("removeEmojisFromTags(%q) = %q, want %q", tt.input, opts[0].Tag, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"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"
|
||||
)
|
||||
|
||||
var _ adapter.ProviderManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
registry adapter.ProviderRegistry
|
||||
access sync.Mutex
|
||||
startAccess sync.Mutex
|
||||
scope *adapter.Scope
|
||||
stage adapter.StartStage
|
||||
providers []adapter.Provider
|
||||
providerByTag map[string]adapter.Provider
|
||||
}
|
||||
|
||||
func NewManager(registry adapter.ProviderRegistry) *Manager {
|
||||
return &Manager{
|
||||
registry: registry,
|
||||
providerByTag: make(map[string]adapter.Provider),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
m.scope = scope
|
||||
m.stage = stage
|
||||
providers := m.providers
|
||||
m.access.Unlock()
|
||||
for _, provider := range providers {
|
||||
err := scope.Start("provider/"+provider.Type()+"["+provider.Tag()+"]", provider, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Providers() []adapter.Provider {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.providers
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.Provider, bool) {
|
||||
m.access.Lock()
|
||||
provider, found := m.providerByTag[tag]
|
||||
m.access.Unlock()
|
||||
return provider, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
provider, found := m.providerByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.providerByTag, tag)
|
||||
m.providers = common.Filter(m.providers, func(it adapter.Provider) bool {
|
||||
return it != provider
|
||||
})
|
||||
scope := m.scope
|
||||
m.access.Unlock()
|
||||
if scope == nil {
|
||||
return nil
|
||||
}
|
||||
return scope.Remove(provider)
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) error {
|
||||
if tag == "" {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
provider, err := m.registry.CreateProvider(ctx, router, logFactory, tag, providerType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
scope := m.scope
|
||||
stage := m.stage
|
||||
m.access.Unlock()
|
||||
if scope != nil {
|
||||
name := "provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
for _, startStage := range adapter.ListStartStages[:stage+1] {
|
||||
err = scope.Start(name, provider, startStage)
|
||||
if err != nil {
|
||||
return E.Errors(err, scope.Remove(provider))
|
||||
}
|
||||
}
|
||||
}
|
||||
m.access.Lock()
|
||||
existsProvider, loaded := m.providerByTag[tag]
|
||||
if loaded {
|
||||
m.providers = common.Filter(m.providers, func(it adapter.Provider) bool {
|
||||
return it != existsProvider
|
||||
})
|
||||
}
|
||||
m.providers = append(m.providers, provider)
|
||||
m.providerByTag[tag] = provider
|
||||
m.access.Unlock()
|
||||
if !loaded || scope == nil {
|
||||
return nil
|
||||
}
|
||||
err = scope.Remove(existsProvider)
|
||||
if err != nil {
|
||||
return E.Cause(err, "close provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package provider
|
||||
|
||||
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, logFactory log.Factory, tag string, options T) (adapter.Provider, error)
|
||||
|
||||
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(providerType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, rawOptions any) (adapter.Provider, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, router, logFactory, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.ProviderRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options any) (adapter.Provider, 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(providerType string) (any, bool) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
optionsConstructor, loaded := r.optionsType[providerType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (r *Registry) CreateProvider(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) (adapter.Provider, error) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
constructor, loaded := r.constructors[providerType]
|
||||
if !loaded {
|
||||
return nil, E.New("provider type not found: '" + providerType + "'")
|
||||
}
|
||||
return constructor(ctx, router, logFactory, tag, options)
|
||||
}
|
||||
|
||||
func (r *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
r.optionsType[providerType] = optionsConstructor
|
||||
r.constructors[providerType] = constructor
|
||||
}
|
||||
+56
-131
@@ -4,155 +4,80 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/geoip"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing-tun/gtcpip/header"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"go4.org/netipx"
|
||||
mdns "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type Router interface {
|
||||
Lifecycle
|
||||
ConnectionRouter
|
||||
PreMatch(metadata InboundContext, firstPacket []byte) PreMatchResult
|
||||
HijackDNSPacket(ctx context.Context, payload []byte, writer N.PacketWriter, metadata InboundContext)
|
||||
ConnectionRouterEx
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
Rules() []Rule
|
||||
NeedFindProcess() bool
|
||||
NeedFindNeighbor() bool
|
||||
NeighborResolver() NeighborResolver
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
ResetNetwork()
|
||||
}
|
||||
Service
|
||||
|
||||
type PreMatchAction uint8
|
||||
Outbounds() []Outbound
|
||||
Outbound(tag string) (Outbound, bool)
|
||||
DefaultOutbound(network string) Outbound
|
||||
|
||||
const (
|
||||
PreMatchContinue PreMatchAction = iota
|
||||
PreMatchFlow
|
||||
PreMatchReject
|
||||
PreMatchDrop
|
||||
PreMatchBypass
|
||||
PreMatchHijackDNS
|
||||
)
|
||||
FakeIPStore() FakeIPStore
|
||||
|
||||
type PreMatchResult struct {
|
||||
Action PreMatchAction
|
||||
Outbound Outbound
|
||||
Destination netip.AddrPort
|
||||
UDPTimeout time.Duration
|
||||
NewTracker func() tun.FlowTracker
|
||||
}
|
||||
|
||||
func JudgeFlow(router Router, inbound string, inboundType string, network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
|
||||
var networkName string
|
||||
switch network {
|
||||
case uint8(header.TCPProtocolNumber):
|
||||
networkName = N.NetworkTCP
|
||||
case uint8(header.UDPProtocolNumber):
|
||||
networkName = N.NetworkUDP
|
||||
case uint8(header.ICMPv4ProtocolNumber), uint8(header.ICMPv6ProtocolNumber):
|
||||
networkName = N.NetworkICMP
|
||||
default:
|
||||
return tun.FlowVerdict{Action: tun.ActionAccept}
|
||||
}
|
||||
metadata := InboundContext{
|
||||
Inbound: inbound,
|
||||
InboundType: inboundType,
|
||||
Network: networkName,
|
||||
Source: M.SocksaddrFromNetIP(source),
|
||||
Destination: M.SocksaddrFromNetIP(destination),
|
||||
}
|
||||
if networkName == N.NetworkICMP {
|
||||
metadata.Source.Port = 0
|
||||
metadata.Destination.Port = 0
|
||||
}
|
||||
result := router.PreMatch(metadata, firstPacket)
|
||||
switch result.Action {
|
||||
case PreMatchFlow:
|
||||
port, isPort := result.Outbound.(tun.Port)
|
||||
if !isPort {
|
||||
return tun.FlowVerdict{Action: tun.ActionAccept}
|
||||
}
|
||||
verdict := tun.FlowVerdict{Action: tun.ActionFlow, Port: port, UDPTimeout: result.UDPTimeout, NewTracker: result.NewTracker}
|
||||
if result.Destination.IsValid() {
|
||||
destinationPort := result.Destination.Port()
|
||||
if networkName == N.NetworkICMP {
|
||||
destinationPort = destination.Port()
|
||||
}
|
||||
verdict.Destination = netip.AddrPortFrom(result.Destination.Addr(), destinationPort)
|
||||
}
|
||||
return verdict
|
||||
case PreMatchReject:
|
||||
return tun.FlowVerdict{Action: tun.ActionReject}
|
||||
case PreMatchDrop:
|
||||
return tun.FlowVerdict{Action: tun.ActionDrop}
|
||||
case PreMatchBypass:
|
||||
port, isPort := result.Outbound.(tun.Port)
|
||||
if !isPort {
|
||||
return tun.FlowVerdict{Action: tun.ActionBypass}
|
||||
}
|
||||
return tun.FlowVerdict{Action: tun.ActionBypass, Port: port, UDPTimeout: result.UDPTimeout, NewTracker: result.NewTracker}
|
||||
case PreMatchHijackDNS:
|
||||
return tun.FlowVerdict{Action: tun.ActionHijackDNS}
|
||||
default:
|
||||
return tun.FlowVerdict{Action: tun.ActionAccept}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
RoutedFlow(ctx context.Context, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) tun.FlowTracker
|
||||
}
|
||||
|
||||
// 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 *mdns.Msg) (*mdns.Msg, error)
|
||||
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
||||
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
||||
ClearDNSCache()
|
||||
|
||||
InterfaceFinder() control.InterfaceFinder
|
||||
UpdateInterfaces() error
|
||||
DefaultInterface() string
|
||||
AutoDetectInterface() bool
|
||||
AutoDetectInterfaceFunc() control.Func
|
||||
DefaultMark() int
|
||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||
PackageManager() tun.PackageManager
|
||||
Rules() []Rule
|
||||
|
||||
ClashServer() ClashServer
|
||||
SetClashServer(server ClashServer)
|
||||
|
||||
V2RayServer() V2RayServer
|
||||
SetV2RayServer(server V2RayServer)
|
||||
|
||||
ResetNetwork() error
|
||||
}
|
||||
|
||||
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)
|
||||
func ContextWithRouter(ctx context.Context, router Router) context.Context {
|
||||
return service.ContextWith(ctx, router)
|
||||
}
|
||||
|
||||
type RuleSet interface {
|
||||
Name() string
|
||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||
Metadata() RuleSetMetadata
|
||||
ExtractIPSet() []*netipx.IPSet
|
||||
IncRef()
|
||||
DecRef()
|
||||
Cleanup()
|
||||
RegisterCallback(callback RuleSetUpdateCallback) *list.Element[RuleSetUpdateCallback]
|
||||
UnregisterCallback(element *list.Element[RuleSetUpdateCallback])
|
||||
Close() error
|
||||
HeadlessRule
|
||||
func RouterFromContext(ctx context.Context) Router {
|
||||
return service.FromContext[Router](ctx)
|
||||
}
|
||||
|
||||
type RuleSetUpdateCallback func(it RuleSet)
|
||||
|
||||
type DNSRuleSetUpdateValidator interface {
|
||||
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
|
||||
type Rule interface {
|
||||
Service
|
||||
Type() string
|
||||
UpdateGeosite() error
|
||||
Match(metadata *InboundContext) bool
|
||||
Outbound() string
|
||||
String() string
|
||||
}
|
||||
|
||||
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
|
||||
type RuleSetMetadata struct {
|
||||
ContainsProcessRule bool
|
||||
ContainsWIFIRule bool
|
||||
ContainsIPCIDRRule bool
|
||||
ContainsDNSQueryTypeRule bool
|
||||
// ContainsNonIPCIDRRule signals that the rule-set carries at least one sub-rule
|
||||
// with a predicate other than destination ip_cidr / ip_set, so it can contribute
|
||||
// to DNS pre-response matching. A rule-set where this is false and
|
||||
// ContainsIPCIDRRule is true is "pure-IP" and matches nothing before a DNS
|
||||
// response is available.
|
||||
ContainsNonIPCIDRRule bool
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
DisableCache() bool
|
||||
RewriteTTL() *uint32
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
InterfaceUpdated()
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type HeadlessRule interface {
|
||||
Match(metadata *InboundContext) bool
|
||||
String() string
|
||||
}
|
||||
|
||||
type Rule interface {
|
||||
HeadlessRule
|
||||
SimpleLifecycle
|
||||
Type() string
|
||||
Action() RuleAction
|
||||
}
|
||||
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
LegacyPreMatch(metadata *InboundContext) bool
|
||||
WithAddressLimit() bool
|
||||
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
|
||||
MatchResponseTag() string
|
||||
MatchResponseTags() []string
|
||||
MatchResponseAnonymous() bool
|
||||
Race() bool
|
||||
}
|
||||
|
||||
type RuleAction interface {
|
||||
Type() string
|
||||
String() string
|
||||
}
|
||||
|
||||
func IsFinalAction(action RuleAction) bool {
|
||||
switch action.Type() {
|
||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type SecurityPolicy interface {
|
||||
CheckFeature(ctx context.Context, feature string) error
|
||||
}
|
||||
|
||||
func CheckSecurityFeature(ctx context.Context, feature string) error {
|
||||
policy := service.FromContext[SecurityPolicy](ctx)
|
||||
if policy == nil {
|
||||
return nil
|
||||
}
|
||||
return policy.CheckFeature(ctx, feature)
|
||||
}
|
||||
+2
-22
@@ -1,26 +1,6 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
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)
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -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,69 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.ServiceManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
registry adapter.ServiceRegistry
|
||||
access sync.Mutex
|
||||
services []adapter.Service
|
||||
serviceByTag map[string]adapter.Service
|
||||
}
|
||||
|
||||
func NewManager(registry adapter.ServiceRegistry) *Manager {
|
||||
return &Manager{
|
||||
registry: registry,
|
||||
serviceByTag: make(map[string]adapter.Service),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.access.Lock()
|
||||
services := m.services
|
||||
m.access.Unlock()
|
||||
for _, service := range services {
|
||||
name := "service/" + service.Type() + "[" + service.Tag() + "]"
|
||||
err := scope.Start(name, service, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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) 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()
|
||||
_, loaded := m.serviceByTag[tag]
|
||||
if loaded {
|
||||
return E.New("duplicate service tag: ", tag)
|
||||
}
|
||||
m.services = append(m.services, service)
|
||||
m.serviceByTag[tag] = service
|
||||
return nil
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"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) OptionTypes() []string {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return slices.Sorted(maps.Keys(m.optionsType))
|
||||
}
|
||||
|
||||
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,14 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import "context"
|
||||
|
||||
type keepSessionKey struct{}
|
||||
|
||||
func ContextWithKeepSession(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, (*keepSessionKey)(nil), true)
|
||||
}
|
||||
|
||||
func KeepSessionFromContext(ctx context.Context) bool {
|
||||
keep, _ := ctx.Value((*keepSessionKey)(nil)).(bool)
|
||||
return keep
|
||||
}
|
||||
@@ -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,104 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TailscaleEndpoint interface {
|
||||
SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error
|
||||
StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error
|
||||
SetTailscaleExitNode(ctx context.Context, stableID string) error
|
||||
Logout(ctx context.Context) error
|
||||
GetTailscaleCertificate(ctx context.Context, domain string, minValidity time.Duration) (certificatePEM []byte, privateKeyPEM []byte, err error)
|
||||
SubscribeTaildropInbox(ctx context.Context, fn func(*TaildropInbox)) error
|
||||
MarkTaildropInboxRead() error
|
||||
SendTaildropFile(ctx context.Context, peerStableID string, fileName string, size int64, content io.Reader, progress func(sentBytes int64)) error
|
||||
OpenTaildropFile(fileName string) (io.ReadCloser, int64, error)
|
||||
DeleteTaildropFile(fileName string) error
|
||||
CancelTaildropReceiving(senderID string, fileName string) error
|
||||
}
|
||||
|
||||
type TaildropInbox struct {
|
||||
Files []*TaildropFile
|
||||
Receiving []*TaildropReceivingFile
|
||||
}
|
||||
|
||||
type TaildropFile struct {
|
||||
Name string
|
||||
Size int64
|
||||
SenderName string
|
||||
ModifiedAt int64
|
||||
}
|
||||
|
||||
type TaildropReceivingFile struct {
|
||||
Name string
|
||||
Size int64
|
||||
ReceivedBytes int64
|
||||
SenderID string
|
||||
SenderName string
|
||||
}
|
||||
|
||||
type TailscalePingResult struct {
|
||||
LatencyMs float64
|
||||
IsDirect bool
|
||||
Endpoint string
|
||||
PeerRelay string
|
||||
DERPRegionID int32
|
||||
DERPRegionCode string
|
||||
Error string
|
||||
}
|
||||
|
||||
type TailscaleEndpointStatus struct {
|
||||
BackendState string
|
||||
AuthURL string
|
||||
NetworkName string
|
||||
MagicDNSSuffix string
|
||||
Self *TailscalePeer
|
||||
ExitNode *TailscalePeer
|
||||
UserGroups []*TailscaleUserGroup
|
||||
KeyAuth bool
|
||||
CanShareFiles bool
|
||||
WaitingFileCount int32
|
||||
ReceivingFileCount int32
|
||||
UnreadFileCount int32
|
||||
CertDomains []string
|
||||
}
|
||||
|
||||
type TailscaleUserGroup struct {
|
||||
UserID int64
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Peers []*TailscalePeer
|
||||
}
|
||||
|
||||
type TailscalePeer struct {
|
||||
StableID string
|
||||
HostName string
|
||||
DNSName string
|
||||
OS string
|
||||
TailscaleIPs []string
|
||||
SSHHostKeys []string
|
||||
Online bool
|
||||
ExitNode bool
|
||||
ExitNodeOption bool
|
||||
ShareeNode bool
|
||||
Expired bool
|
||||
Active bool
|
||||
CanReceiveFiles bool
|
||||
RxBytes int64
|
||||
TxBytes int64
|
||||
UserID int64
|
||||
KeyExpiry int64
|
||||
LastSeen int64
|
||||
}
|
||||
|
||||
type ShellSession interface {
|
||||
MasterFD() int32
|
||||
Resize(rows int32, cols int32) error
|
||||
Signal(signal int32) error
|
||||
WaitExit() (int32, error)
|
||||
Close() error
|
||||
}
|
||||
+1
-1
@@ -3,6 +3,6 @@ package adapter
|
||||
import "time"
|
||||
|
||||
type TimeService interface {
|
||||
SimpleLifecycle
|
||||
Service
|
||||
TimeFunc() func() time.Time
|
||||
}
|
||||
|
||||
+47
-100
@@ -4,24 +4,27 @@ 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 (
|
||||
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
PacketConnectionHandlerFunc = 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 NewUpstreamHandler(
|
||||
metadata InboundContext,
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamHandlerWrapper{
|
||||
metadata: metadata,
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,138 +34,82 @@ type myUpstreamHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
w.connectionHandler(ctx, conn, myMetadata, onClose)
|
||||
return w.connectionHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
w.packetHandler(ctx, conn, myMetadata, onClose)
|
||||
return w.packetHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*myUpstreamContextHandlerWrapper)(nil)
|
||||
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
||||
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 *myUpstreamContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, myMetadata := ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
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 destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
w.connectionHandler(ctx, conn, *myMetadata, onClose)
|
||||
return w.connectionHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, myMetadata := ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
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 destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
w.packetHandler(ctx, conn, *myMetadata, onClose)
|
||||
return w.packetHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
func NewRouteHandler(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouterEx,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeHandlerWrapper{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
|
||||
|
||||
type routeHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouterEx
|
||||
}
|
||||
|
||||
func (r *routeHandlerWrapper) 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 *routeHandlerWrapper) 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 NewRouteContextHandler(
|
||||
router ConnectionRouterEx,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeContextHandlerWrapper{
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
|
||||
|
||||
type routeContextHandlerWrapper struct {
|
||||
router ConnectionRouterEx
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, metadata := ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
metadata.Destination = destination
|
||||
}
|
||||
r.router.RouteConnectionEx(ctx, conn, *metadata, onClose)
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, metadata := ExtendContext(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,243 +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
|
||||
LegacyConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
// Deprecated
|
||||
LegacyPacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
)
|
||||
|
||||
// Deprecated
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type LegacyUpstreamHandlerAdapter interface {
|
||||
N.TCPConnectionHandler
|
||||
N.UDPConnectionHandler
|
||||
E.Handler
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
//
|
||||
//nolint:staticcheck
|
||||
func NewLegacyUpstreamHandler(
|
||||
metadata InboundContext,
|
||||
connectionHandler LegacyConnectionHandlerFunc,
|
||||
packetHandler LegacyPacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyUpstreamHandlerWrapper{
|
||||
metadata: metadata,
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
var _ LegacyUpstreamHandlerAdapter = (*legacyUpstreamHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: use NewUpstreamHandler instead.
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type legacyUpstreamHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
connectionHandler LegacyConnectionHandlerFunc
|
||||
packetHandler LegacyPacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
// Deprecated: use NewUpstreamHandler instead.
|
||||
func (w *legacyUpstreamHandlerWrapper) 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 NewUpstreamHandler instead.
|
||||
func (w *legacyUpstreamHandlerWrapper) 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 NewUpstreamHandler instead.
|
||||
func (w *legacyUpstreamHandlerWrapper) 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.Unwrap(),
|
||||
Destination: metadata.Destination.Unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
type legacyUpstreamContextHandlerWrapper struct {
|
||||
connectionHandler LegacyConnectionHandlerFunc
|
||||
packetHandler LegacyPacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
func NewLegacyUpstreamContextHandler(
|
||||
connectionHandler LegacyConnectionHandlerFunc,
|
||||
packetHandler LegacyPacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyUpstreamContextHandlerWrapper{
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
func (w *legacyUpstreamContextHandlerWrapper) 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 NewUpstreamContextHandler instead.
|
||||
func (w *legacyUpstreamContextHandlerWrapper) 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 NewUpstreamContextHandler instead.
|
||||
func (w *legacyUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func NewLegacyRouteHandler(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyRouteHandlerWrapper{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func NewLegacyRouteContextHandler(
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyRouteContextHandlerWrapper{
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
var _ LegacyUpstreamHandlerAdapter = (*legacyRouteHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type legacyRouteHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *legacyRouteHandlerWrapper) 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 *legacyRouteHandlerWrapper) 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 *legacyRouteHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
|
||||
var _ LegacyUpstreamHandlerAdapter = (*legacyRouteContextHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
type legacyRouteContextHandlerWrapper struct {
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *legacyRouteContextHandlerWrapper) 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 *legacyRouteContextHandlerWrapper) 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 *legacyRouteContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//go:build with_usbip && (linux || (darwin && cgo) || windows)
|
||||
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-usbip"
|
||||
)
|
||||
|
||||
type USBIPDynamicServer interface {
|
||||
AddDevice(info usbip.ProvidedDeviceInfo, transport usbip.DeviceTransport) (string, error)
|
||||
RemoveDevice(busID string)
|
||||
SubscribeDevices(ctx context.Context, listener func([]usbip.ControlDeviceInfo))
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
|
||||
|
||||
package adapter
|
||||
|
||||
type USBIPDynamicServer interface {
|
||||
usbipNotIncluded()
|
||||
}
|
||||
+5
-7
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
@@ -15,15 +17,11 @@ type V2RayServerTransport interface {
|
||||
}
|
||||
|
||||
type V2RayServerTransportHandler interface {
|
||||
N.TCPConnectionHandlerEx
|
||||
N.TCPConnectionHandler
|
||||
E.Handler
|
||||
FallbackConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error
|
||||
}
|
||||
|
||||
type V2RayClientTransport interface {
|
||||
DialContext(ctx context.Context) (net.Conn, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type V2RayMultiplexClientTransport interface {
|
||||
V2RayClientTransport
|
||||
MultiplexEnabled() bool
|
||||
}
|
||||
|
||||
@@ -2,630 +2,234 @@ package box
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxCertificate "github.com/sagernet/sing-box/adapter/certificate"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/adapter/provider"
|
||||
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/httpclient"
|
||||
"github.com/sagernet/sing-box/common/netns"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
"github.com/sagernet/sing-box/common/trafficcontrol"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/clashmode"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"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
|
||||
debugOptions option.DebugOptions
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
provider *provider.Manager
|
||||
service *boxService.Manager
|
||||
certificateProvider *boxCertificate.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
referenceManager *route.ReferenceManager
|
||||
httpClientService adapter.LifecycleService
|
||||
internalService []adapter.LifecycleService
|
||||
ntpService *ntp.Service
|
||||
scope *adapter.Scope
|
||||
createdAt time.Time
|
||||
router adapter.Router
|
||||
inbounds []adapter.Inbound
|
||||
outbounds []adapter.Outbound
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
preServices map[string]adapter.Service
|
||||
postServices map[string]adapter.Service
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
option.Options
|
||||
Context context.Context
|
||||
PlatformLogWriter log.PlatformWriter
|
||||
NetworkNamespaceHolderArgs []string
|
||||
}
|
||||
|
||||
func Context(
|
||||
ctx context.Context,
|
||||
inboundRegistry adapter.InboundRegistry,
|
||||
outboundRegistry adapter.OutboundRegistry,
|
||||
endpointRegistry adapter.EndpointRegistry,
|
||||
providerRegistry adapter.ProviderRegistry,
|
||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||
serviceRegistry adapter.ServiceRegistry,
|
||||
certificateProviderRegistry adapter.CertificateProviderRegistry,
|
||||
) 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[option.ProviderOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.ProviderRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.ProviderOptionsRegistry](ctx, providerRegistry)
|
||||
ctx = service.ContextWith[adapter.ProviderRegistry](ctx, providerRegistry)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry)
|
||||
ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry)
|
||||
}
|
||||
return ctx
|
||||
Context context.Context
|
||||
PlatformInterface platform.Interface
|
||||
}
|
||||
|
||||
func New(options Options) (*Box, error) {
|
||||
createdAt := time.Now()
|
||||
ctx := options.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
ctx = service.ContextWithDefaultRegistry(ctx)
|
||||
|
||||
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||
providerRegistry := service.FromContext[adapter.ProviderRegistry](ctx)
|
||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||
certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](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 providerRegistry == nil {
|
||||
return nil, E.New("missing provider 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")
|
||||
}
|
||||
if certificateProviderRegistry == nil {
|
||||
return nil, E.New("missing certificate provider registry in context")
|
||||
}
|
||||
|
||||
ctx = pause.WithDefaultManager(ctx)
|
||||
ctx = pause.ContextWithDefaultManager(ctx)
|
||||
createdAt := time.Now()
|
||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||
debugOptions := common.PtrValueOrDefault(experimentalOptions.Debug)
|
||||
err := checkDebugOptions(debugOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var needCacheFile bool
|
||||
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||
var needClashAPI bool
|
||||
var needV2RayAPI bool
|
||||
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
|
||||
needCacheFile = true
|
||||
}
|
||||
if experimentalOptions.ClashAPI != nil {
|
||||
if experimentalOptions.ClashAPI != nil || options.PlatformInterface != nil {
|
||||
needClashAPI = true
|
||||
}
|
||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||
needV2RayAPI = true
|
||||
}
|
||||
if experimentalOptions.UnifiedDelay != nil && experimentalOptions.UnifiedDelay.Enabled {
|
||||
ctx = urltest.ContextWithIsUnifiedDelay(ctx)
|
||||
}
|
||||
needAPIService := common.Any(options.Services, func(it option.Service) bool {
|
||||
return it.Type == C.TypeAPI
|
||||
})
|
||||
if service.PtrFromContext[urltest.HistoryStorage](ctx) == nil {
|
||||
ctx = service.ContextWithPtr(ctx, urltest.NewHistoryStorage())
|
||||
}
|
||||
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||
var defaultLogWriter io.Writer
|
||||
if platformInterface != nil {
|
||||
if options.PlatformInterface != nil {
|
||||
defaultLogWriter = io.Discard
|
||||
}
|
||||
logFactory, err := log.New(log.Options{
|
||||
Context: ctx,
|
||||
Options: common.PtrValueOrDefault(options.Log),
|
||||
Observable: needClashAPI && experimentalOptions.ClashAPI.ExternalController != "",
|
||||
Observable: needClashAPI,
|
||||
DefaultWriter: defaultLogWriter,
|
||||
BaseTime: createdAt,
|
||||
PlatformWriter: options.PlatformLogWriter,
|
||||
PlatformWriter: options.PlatformInterface,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create log factory")
|
||||
}
|
||||
service.MustRegister[log.Factory](ctx, logFactory)
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
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)
|
||||
}
|
||||
netnsManager, err := netns.NewManager(logFactory.NewLogger("netns"), options.NetworkNamespaces, options.NetworkNamespaceHolderArgs)
|
||||
router, err := route.NewRouter(
|
||||
ctx,
|
||||
logFactory,
|
||||
common.PtrValueOrDefault(options.Route),
|
||||
common.PtrValueOrDefault(options.DNS),
|
||||
common.PtrValueOrDefault(options.NTP),
|
||||
options.Inbounds,
|
||||
options.PlatformInterface,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.MustRegister[adapter.NetworkNamespaceManager](ctx, netnsManager)
|
||||
internalServices = append(internalServices, netnsManager)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(endpointRegistry)
|
||||
inboundManager := inbound.NewManager(inboundRegistry, endpointManager)
|
||||
outboundManager := outbound.NewManager(outboundRegistry, endpointManager, routeOptions.Final)
|
||||
providerManager := provider.NewManager(providerRegistry)
|
||||
dnsTransportManager := dns.NewTransportManager(dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||
serviceManager := boxService.NewManager(serviceRegistry)
|
||||
certificateProviderManager := boxCertificate.NewManager(certificateProviderRegistry)
|
||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||
service.MustRegister[adapter.ProviderManager](ctx, providerManager)
|
||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
||||
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize DNS router")
|
||||
}
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
}
|
||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||
// Must register after ConnectionManager: the Apple HTTP engine's proxy bridge reads it from the context when Manager.Start resolves the default client.
|
||||
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
||||
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
||||
httpClientService := adapter.LifecycleService(httpClientManager)
|
||||
router := route.NewRouter(ctx, logFactory, "router", 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")
|
||||
}
|
||||
if needClashAPI || needAPIService || options.PlatformLogWriter != nil {
|
||||
trafficManager := trafficcontrol.NewManager(outboundManager)
|
||||
service.MustRegisterPtr(ctx, trafficManager)
|
||||
router.AppendTracker(trafficManager)
|
||||
internalServices = append(internalServices, trafficManager)
|
||||
var clashDefaultMode string
|
||||
if experimentalOptions.ClashAPI != nil {
|
||||
clashDefaultMode = experimentalOptions.ClashAPI.DefaultMode
|
||||
}
|
||||
clashMode := clashmode.NewManager(ctx, logFactory.NewLogger("clash-mode"), clashDefaultMode, clashmode.CalculateModeList(options.Options))
|
||||
service.MustRegisterPtr(ctx, clashMode)
|
||||
internalServices = append(internalServices, clashMode)
|
||||
}
|
||||
referenceManager := route.NewReferenceManager(ctx, logFactory.NewLogger("reference"), options.Options)
|
||||
internalServices = append(internalServices, referenceManager)
|
||||
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,
|
||||
options.PlatformInterface,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
||||
}
|
||||
}
|
||||
options.Outbounds = append(options.Outbounds, option.Outbound{
|
||||
Tag: "Compatible",
|
||||
Type: C.TypeDirect,
|
||||
})
|
||||
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, "]")
|
||||
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, providerOptions := range options.Providers {
|
||||
var tag string
|
||||
if providerOptions.Tag != "" {
|
||||
tag = providerOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = providerManager.Create(
|
||||
ctx,
|
||||
router,
|
||||
logFactory,
|
||||
tag,
|
||||
providerOptions.Type,
|
||||
providerOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize provider[", i, "]")
|
||||
}
|
||||
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
|
||||
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"})
|
||||
common.Must(oErr)
|
||||
outbounds = append(outbounds, out)
|
||||
return out
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, certificateProviderOptions := range options.CertificateProviders {
|
||||
var tag string
|
||||
if certificateProviderOptions.Tag != "" {
|
||||
tag = certificateProviderOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = certificateProviderManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
certificateProviderOptions.Type,
|
||||
certificateProviderOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize certificate provider[", i, "]")
|
||||
}
|
||||
}
|
||||
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||
return direct.NewOutbound(
|
||||
ctx,
|
||||
router,
|
||||
logFactory.NewLogger("outbound/direct"),
|
||||
"direct",
|
||||
option.DirectOutboundOptions{},
|
||||
)
|
||||
})
|
||||
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||
return dnsTransportRegistry.CreateDNSTransport(
|
||||
ctx,
|
||||
logFactory.NewLogger("dns/local"),
|
||||
"local",
|
||||
C.DNSTypeLocal,
|
||||
&option.LocalDNSServerOptions{},
|
||||
)
|
||||
})
|
||||
httpClientManager.Initialize(func() (*httpclient.ManagedTransport, error) {
|
||||
deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient)
|
||||
var httpClientOptions option.HTTPClientOptions
|
||||
httpClientOptions.DefaultOutbound = true
|
||||
return httpclient.NewTransport(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
|
||||
})
|
||||
if platformInterface != nil {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
if options.PlatformInterface != nil {
|
||||
err = options.PlatformInterface.Initialize(ctx, router)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize platform interface")
|
||||
}
|
||||
}
|
||||
if needCacheFile {
|
||||
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
}
|
||||
preServices := make(map[string]adapter.Service)
|
||||
postServices := make(map[string]adapter.Service)
|
||||
if needClashAPI {
|
||||
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(experimentalOptions.ClashAPI))
|
||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), clashAPIOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create clash-server")
|
||||
return nil, E.Cause(err, "create clash api server")
|
||||
}
|
||||
internalServices = append(internalServices, clashServer)
|
||||
router.SetClashServer(clashServer)
|
||||
preServices["clash api"] = 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")
|
||||
return nil, E.Cause(err, "create v2ray api server")
|
||||
}
|
||||
if v2rayServer.StatsService() != nil {
|
||||
router.AppendTracker(v2rayServer.StatsService())
|
||||
internalServices = append(internalServices, v2rayServer)
|
||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||
}
|
||||
}
|
||||
var ntpService *ntp.Service
|
||||
if ntpOptions.Enabled {
|
||||
if ntpOptions.WriteToSystem {
|
||||
err = adapter.CheckSecurityFeature(ctx, "NTP `write_to_system`")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
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
|
||||
router.SetV2RayServer(v2rayServer)
|
||||
preServices["v2ray api"] = v2rayServer
|
||||
}
|
||||
return &Box{
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
provider: providerManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
certificateProvider: certificateProviderManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
referenceManager: referenceManager,
|
||||
httpClientService: httpClientService,
|
||||
createdAt: createdAt,
|
||||
debugOptions: debugOptions,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
ntpService: ntpService,
|
||||
scope: adapter.NewScope(ctx, logFactory.Logger()),
|
||||
router: router,
|
||||
inbounds: inbounds,
|
||||
outbounds: outbounds,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
preServices: preServices,
|
||||
postServices: postServices,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Box) PreStart() error {
|
||||
err := s.preStart()
|
||||
if err != nil {
|
||||
// TODO: remove catch error
|
||||
defer func() {
|
||||
v := recover()
|
||||
if v != nil {
|
||||
log.Error(E.Cause(err, "origin error"))
|
||||
debug.PrintStack()
|
||||
panic("panic on early close: " + fmt.Sprint(v))
|
||||
}
|
||||
}()
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
s.logger.Notice("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
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 {
|
||||
log.Error(E.Cause(err, "origin error"))
|
||||
debug.PrintStack()
|
||||
panic("panic on early close: " + fmt.Sprint(v))
|
||||
}
|
||||
}()
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
s.logger.Notice("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
||||
type boxComponent struct {
|
||||
name string
|
||||
lifecycle adapter.Lifecycle
|
||||
}
|
||||
|
||||
func (s *Box) startComponents(stage adapter.StartStage, components ...boxComponent) error {
|
||||
for _, component := range components {
|
||||
err := s.scope.Start(component.name, component.lifecycle, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) startInternalServices(stage adapter.StartStage) error {
|
||||
for _, lifecycleService := range s.internalService {
|
||||
err := s.scope.Start(lifecycleService.Name(), lifecycleService, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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")
|
||||
for serviceName, service := range s.preServices {
|
||||
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
||||
s.logger.Trace("pre-start ", serviceName)
|
||||
err := preService.PreStart()
|
||||
if err != nil {
|
||||
return E.Cause(err, "pre-starting ", serviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.scope.Add(s.logFactory.Close)
|
||||
applyDebugOptions(s.debugOptions)
|
||||
debugHTTPServer, err := startDebugHTTPServer(s.debugOptions)
|
||||
err := s.startOutbounds()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if debugHTTPServer != nil {
|
||||
s.scope.Add(debugHTTPServer.Close)
|
||||
}
|
||||
err = s.startInternalServices(adapter.StartStateInitialize) // cache-file clash-api v2ray-api
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.startComponents(adapter.StartStateInitialize,
|
||||
boxComponent{s.httpClientService.Name(), s.httpClientService},
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"endpoint", s.endpoint},
|
||||
boxComponent{"certificate-provider", s.certificateProvider},
|
||||
boxComponent{"inbound", s.inbound},
|
||||
boxComponent{"service", s.service},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.startComponents(adapter.StartStateStart,
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{s.httpClientService.Name(), s.httpClientService},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return s.router.Start()
|
||||
}
|
||||
|
||||
func (s *Box) start() error {
|
||||
@@ -633,103 +237,96 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.startInternalServices(adapter.StartStateStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.ntpService != nil {
|
||||
done := adapter.LogElapsed(s.logger, "start ntp service")
|
||||
err = s.ntpService.Start()
|
||||
done()
|
||||
for serviceName, service := range s.preServices {
|
||||
s.logger.Trace("starting ", serviceName)
|
||||
err = service.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start ntp service")
|
||||
return E.Cause(err, "start ", serviceName)
|
||||
}
|
||||
s.scope.Add(s.ntpService.Close)
|
||||
}
|
||||
err = s.startComponents(adapter.StartStateStart,
|
||||
boxComponent{"endpoint", s.endpoint},
|
||||
boxComponent{"certificate-provider", s.certificateProvider},
|
||||
boxComponent{"inbound", s.inbound},
|
||||
boxComponent{"service", s.service},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
for i, in := range s.inbounds {
|
||||
var tag string
|
||||
if in.Tag() == "" {
|
||||
tag = F.ToString(i)
|
||||
} else {
|
||||
tag = in.Tag()
|
||||
}
|
||||
s.logger.Trace("initializing inbound/", in.Type(), "[", tag, "]")
|
||||
err = in.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
||||
}
|
||||
}
|
||||
err = s.startComponents(adapter.StartStatePostStart,
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"endpoint", s.endpoint},
|
||||
boxComponent{"certificate-provider", s.certificateProvider},
|
||||
boxComponent{"inbound", s.inbound},
|
||||
boxComponent{"service", s.service},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) postStart() error {
|
||||
for serviceName, service := range s.postServices {
|
||||
s.logger.Trace("starting ", service)
|
||||
err := service.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start ", serviceName)
|
||||
}
|
||||
}
|
||||
err = s.startInternalServices(adapter.StartStatePostStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.startComponents(adapter.StartStateStarted,
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"endpoint", s.endpoint},
|
||||
boxComponent{"certificate-provider", s.certificateProvider},
|
||||
boxComponent{"inbound", s.inbound},
|
||||
boxComponent{"service", s.service},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.startInternalServices(adapter.StartStateStarted)
|
||||
if err != nil {
|
||||
return err
|
||||
for serviceName, service := range s.outbounds {
|
||||
if lateService, isLateService := service.(adapter.PostStarter); isLateService {
|
||||
s.logger.Trace("post-starting ", service)
|
||||
err := lateService.PostStart()
|
||||
if err != nil {
|
||||
return E.Cause(err, "post-start ", serviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) Close() error {
|
||||
return s.scope.Close()
|
||||
}
|
||||
|
||||
func (s *Box) Network() adapter.NetworkManager {
|
||||
return s.network
|
||||
select {
|
||||
case <-s.done:
|
||||
return os.ErrClosed
|
||||
default:
|
||||
close(s.done)
|
||||
}
|
||||
var errors error
|
||||
for serviceName, service := range s.postServices {
|
||||
s.logger.Trace("closing ", serviceName)
|
||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", serviceName)
|
||||
})
|
||||
}
|
||||
for i, in := range s.inbounds {
|
||||
s.logger.Trace("closing inbound/", in.Type(), "[", i, "]")
|
||||
errors = E.Append(errors, in.Close(), func(err error) error {
|
||||
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
|
||||
})
|
||||
}
|
||||
for i, out := range s.outbounds {
|
||||
s.logger.Trace("closing outbound/", out.Type(), "[", i, "]")
|
||||
errors = E.Append(errors, common.Close(out), func(err error) error {
|
||||
return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
|
||||
})
|
||||
}
|
||||
s.logger.Trace("closing router")
|
||||
if err := common.Close(s.router); err != nil {
|
||||
errors = E.Append(errors, err, func(err error) error {
|
||||
return E.Cause(err, "close router")
|
||||
})
|
||||
}
|
||||
for serviceName, service := range s.preServices {
|
||||
s.logger.Trace("closing ", serviceName)
|
||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", serviceName)
|
||||
})
|
||||
}
|
||||
s.logger.Trace("closing log factory")
|
||||
if err := common.Close(s.logFactory); err != nil {
|
||||
errors = E.Append(errors, err, func(err error) error {
|
||||
return E.Cause(err, "close log factory")
|
||||
})
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (s *Box) Endpoint() adapter.EndpointManager {
|
||||
return s.endpoint
|
||||
}
|
||||
|
||||
func (s *Box) CreatedAt() time.Time {
|
||||
return s.createdAt
|
||||
}
|
||||
|
||||
func (s *Box) CloseIdleConnections() {
|
||||
s.referenceManager.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func (s *Box) LogFactory() log.Factory {
|
||||
return s.logFactory
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package box
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
func (s *Box) startOutbounds() error {
|
||||
outboundTags := make(map[adapter.Outbound]string)
|
||||
outbounds := make(map[string]adapter.Outbound)
|
||||
for i, outboundToStart := range s.outbounds {
|
||||
var outboundTag string
|
||||
if outboundToStart.Tag() == "" {
|
||||
outboundTag = F.ToString(i)
|
||||
} else {
|
||||
outboundTag = outboundToStart.Tag()
|
||||
}
|
||||
if _, exists := outbounds[outboundTag]; exists {
|
||||
return E.New("outbound tag ", outboundTag, " duplicated")
|
||||
}
|
||||
outboundTags[outboundToStart] = outboundTag
|
||||
outbounds[outboundTag] = outboundToStart
|
||||
}
|
||||
started := make(map[string]bool)
|
||||
for {
|
||||
canContinue := false
|
||||
startOne:
|
||||
for _, outboundToStart := range s.outbounds {
|
||||
outboundTag := outboundTags[outboundToStart]
|
||||
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.(common.Starter); isStarter {
|
||||
s.logger.Trace("initializing outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
err := starter.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(started) == len(s.outbounds) {
|
||||
break
|
||||
}
|
||||
if canContinue {
|
||||
continue
|
||||
}
|
||||
currentOutbound := common.Find(s.outbounds, func(it adapter.Outbound) bool {
|
||||
return !started[outboundTags[it]]
|
||||
})
|
||||
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)
|
||||
}
|
||||
problemOutbound := outbounds[problemOutboundTag]
|
||||
if problemOutbound == nil {
|
||||
return E.New("dependency[", problemOutbound, "] not found for outbound[", outboundTags[oCurrent], "]")
|
||||
}
|
||||
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
|
||||
}
|
||||
return lintOutbound([]string{outboundTags[currentOutbound]}, currentOutbound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Submodule clients/desktop deleted from 50f5038491
@@ -1,194 +0,0 @@
|
||||
// Command admin_panel_pack post-processes a directory of built SPA
|
||||
// assets so it can be served straight from the Go binary via //go:embed.
|
||||
// It does *not* generate any Go source any more — that responsibility
|
||||
// moved to the embed directive in service/admin_panel/service.go.
|
||||
//
|
||||
// Three transformations happen here, all in-place inside the supplied
|
||||
// directory:
|
||||
//
|
||||
// 1. Legacy WOFF (1.0) fallback fonts are deleted. Every browser made
|
||||
// after 2014 reads WOFF2 natively, so shipping both formats roughly
|
||||
// doubles the embedded font payload for no real-world benefit. The
|
||||
// matching `,url(*.woff) format("woff")` segments are stripped from
|
||||
// the bundled CSS in step (2) so the @font-face rules don't reference
|
||||
// files that aren't shipped.
|
||||
// 2. Bundled CSS is rewritten to drop those WOFF URL fragments.
|
||||
// 3. Compressible text assets (.html, .css, .js, .svg, .json, .map) are
|
||||
// pre-gzipped as companion `*.gz` files. The HTTP handler then either
|
||||
// passes those bytes through verbatim with Content-Encoding: gzip or
|
||||
// falls back to the raw file for the rare client that does not
|
||||
// advertise gzip support — no on-line compression, no surprises.
|
||||
// Already-compressed formats (.woff2, fonts, images) are skipped: gzip
|
||||
// can't shrink them and the duplicate would only inflate the binary.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dir := flag.String("dir", "service/admin_panel/dist", "directory of built SPA assets to post-process in place")
|
||||
flag.Parse()
|
||||
|
||||
woffDropped, err := pruneWoff(*dir)
|
||||
if err != nil {
|
||||
fail("prune woff: %v", err)
|
||||
}
|
||||
cssRewritten, err := rewriteCSS(*dir)
|
||||
if err != nil {
|
||||
fail("rewrite css: %v", err)
|
||||
}
|
||||
stats, err := gzipText(*dir)
|
||||
if err != nil {
|
||||
fail("gzip text: %v", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "post-processed %s: dropped %d woff, rewrote %d css, gzipped %d files (%d→%d bytes)\n",
|
||||
*dir, woffDropped, cssRewritten, stats.gzipped, stats.totalRaw, stats.totalGz)
|
||||
}
|
||||
|
||||
// pruneWoff deletes every legacy *.woff (WOFF 1.0) font under dir. The
|
||||
// bundled CSS still references them on entry; rewriteCSS drops those
|
||||
// references in a separate pass so the two operations stay independently
|
||||
// testable.
|
||||
func pruneWoff(dir string) (int, error) {
|
||||
var n int
|
||||
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if d.IsDir() || !strings.EqualFold(filepath.Ext(p), ".woff") {
|
||||
return nil
|
||||
}
|
||||
if err := os.Remove(p); err != nil {
|
||||
return err
|
||||
}
|
||||
n++
|
||||
return nil
|
||||
})
|
||||
return n, err
|
||||
}
|
||||
|
||||
// woffRefAfterRE / woffRefBeforeRE match a single WOFF 1.0 entry inside a
|
||||
// Vite-bundled CSS `src:` declaration. Vite minifies the rule to
|
||||
// `src:url(./X.woff2) format("woff2"),url(./X.woff) format("woff");` so the
|
||||
// "after" regex (the common case) eats the *leading* comma + woff entry,
|
||||
// leaving only the woff2 source. We also handle the rare reverse ordering
|
||||
// in a second pass.
|
||||
var (
|
||||
woffRefAfterRE = regexp.MustCompile(`,\s*url\([^)]*\.woff\)\s*format\(["']woff["']\)`)
|
||||
woffRefBeforeRE = regexp.MustCompile(`url\([^)]*\.woff\)\s*format\(["']woff["']\)\s*,\s*`)
|
||||
)
|
||||
|
||||
// rewriteCSS drops every reference to a *.woff URL from every *.css file
|
||||
// under dir. Pairs naturally with pruneWoff: after both passes, no font
|
||||
// URL in the bundle points at a file that isn't shipped.
|
||||
func rewriteCSS(dir string) (int, error) {
|
||||
var n int
|
||||
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if d.IsDir() || !strings.EqualFold(filepath.Ext(p), ".css") {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out := woffRefAfterRE.ReplaceAll(data, nil)
|
||||
out = woffRefBeforeRE.ReplaceAll(out, nil)
|
||||
if bytes.Equal(out, data) {
|
||||
return nil
|
||||
}
|
||||
if err := os.WriteFile(p, out, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
n++
|
||||
return nil
|
||||
})
|
||||
return n, err
|
||||
}
|
||||
|
||||
// gzipExts is the set of file extensions for which a `.gz` companion is
|
||||
// generated. Anything not on this list is left alone — woff2/png/jpeg/etc.
|
||||
// are already compressed, so gzip can only inflate them slightly while
|
||||
// doubling the embedded payload.
|
||||
var gzipExts = map[string]bool{
|
||||
".html": true,
|
||||
".css": true,
|
||||
".js": true,
|
||||
".mjs": true,
|
||||
".svg": true,
|
||||
".json": true,
|
||||
".map": true,
|
||||
".txt": true,
|
||||
".xml": true,
|
||||
".wasm": true,
|
||||
}
|
||||
|
||||
type gzipStats struct {
|
||||
gzipped int
|
||||
totalRaw int64
|
||||
totalGz int64
|
||||
}
|
||||
|
||||
// gzipText produces a `<file>.gz` companion next to every text-like asset
|
||||
// in dir, using gzip.BestCompression. The companion is dropped if the
|
||||
// compressed bytes don't save at least 10 % over the raw file — same
|
||||
// heuristic we used in the previous (Go-source-emitting) generation, just
|
||||
// applied to disk files now.
|
||||
func gzipText(dir string) (gzipStats, error) {
|
||||
var stats gzipStats
|
||||
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(p))
|
||||
if ext == ".gz" || !gzipExts[ext] {
|
||||
return nil
|
||||
}
|
||||
raw, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
w, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Reproducible: no mtime, no OS marker.
|
||||
w.ModTime = time.Time{}
|
||||
w.OS = 0xff
|
||||
if _, err := w.Write(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if buf.Len() > len(raw)*9/10 {
|
||||
return nil
|
||||
}
|
||||
stats.gzipped++
|
||||
stats.totalRaw += int64(len(raw))
|
||||
stats.totalGz += int64(buf.Len())
|
||||
return os.WriteFile(p+".gz", buf.Bytes(), 0o644)
|
||||
})
|
||||
return stats, err
|
||||
}
|
||||
|
||||
func fail(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "admin_panel_pack: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -1,658 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/asc-go/asc"
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/common/badversion"
|
||||
"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_project_version":
|
||||
if len(os.Args) < 3 {
|
||||
log.Fatal("platform required: ios, macos, or tvos")
|
||||
}
|
||||
err := fetchNextProjectVersion(ctx, os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "publish_testflight":
|
||||
err := publishTestflight(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "list_builds":
|
||||
if len(os.Args) < 3 {
|
||||
log.Fatal("platform required: ios, macos, or tvos")
|
||||
}
|
||||
err := listBuilds(ctx, os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "remove_testflight":
|
||||
if len(os.Args) < 5 {
|
||||
log.Fatal("usage: remove_testflight <platform> <version> <build>")
|
||||
}
|
||||
err := removeTestflight(ctx, os.Args[2], os.Args[3], os.Args[4])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "cancel_app_store":
|
||||
if len(os.Args) < 3 {
|
||||
log.Fatal("platform required: ios, macos, or tvos")
|
||||
}
|
||||
err := cancelAppStore(ctx, os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "submit_app_store":
|
||||
err := submitAppStore(ctx, os.Args[2:])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "publish_app_store":
|
||||
err := publishAppStore(ctx, os.Args[2:])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
default:
|
||||
log.Fatal("unknown action: ", os.Args[1])
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
appID = "6785326793"
|
||||
groupID = "39f9ebdc-05d4-421f-9595-dae71df227c4"
|
||||
|
||||
applePath = "../sing-box-for-apple"
|
||||
appStorePromotionalText = "The universal proxy platform."
|
||||
appStoreWhatsNew = "Fixes and improvements"
|
||||
buildPollInterval = 15 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
appStorePlatforms = []string{"ios", "tvos"}
|
||||
platformDirectories = map[asc.Platform]string{
|
||||
asc.PlatformIOS: "SFI",
|
||||
asc.PlatformMACOS: "SFM",
|
||||
asc.PlatformTVOS: "SFT",
|
||||
}
|
||||
)
|
||||
|
||||
func parsePlatform(name string) (asc.Platform, error) {
|
||||
switch name {
|
||||
case "ios":
|
||||
return asc.PlatformIOS, nil
|
||||
case "macos":
|
||||
return asc.PlatformMACOS, nil
|
||||
case "tvos":
|
||||
return asc.PlatformTVOS, nil
|
||||
default:
|
||||
return "", E.New("unknown platform: ", name)
|
||||
}
|
||||
}
|
||||
|
||||
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 fetchNextProjectVersion(ctx context.Context, platformName string) error {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
Limit: 200,
|
||||
}
|
||||
if platform != asc.PlatformMACOS {
|
||||
tagVersion, err := build_shared.ReadTagVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query.FilterPreReleaseVersionVersion = []string{build_shared.TestFlightVersion(tagVersion)}
|
||||
}
|
||||
|
||||
client := createClient(time.Minute)
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextProjectVersion := 1
|
||||
var projectVersion int
|
||||
for _, build := range builds.Data {
|
||||
projectVersion, err = strconv.Atoi(*build.Attributes.Version)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse version code")
|
||||
}
|
||||
if projectVersion >= nextProjectVersion {
|
||||
nextProjectVersion = projectVersion + 1
|
||||
}
|
||||
}
|
||||
os.Stdout.WriteString(F.ToString(nextProjectVersion, "\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func listBuilds(ctx context.Context, platformName string) error {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
betaBuildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
|
||||
return it.ID
|
||||
})
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
Include: []string{"preReleaseVersion"},
|
||||
Sort: []string{"-uploadedDate"},
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, build := range builds.Data {
|
||||
var betaGroup string
|
||||
if slices.Contains(betaBuildIDs, build.ID) {
|
||||
betaGroup = " testflight"
|
||||
}
|
||||
var versionString string
|
||||
if build.Relationships != nil && build.Relationships.PreReleaseVersion != nil && build.Relationships.PreReleaseVersion.Data != nil {
|
||||
for _, included := range builds.Included {
|
||||
preReleaseVersion := included.PrereleaseVersion()
|
||||
if preReleaseVersion != nil && preReleaseVersion.ID == build.Relationships.PreReleaseVersion.Data.ID {
|
||||
versionString = *preReleaseVersion.Attributes.Version
|
||||
}
|
||||
}
|
||||
}
|
||||
os.Stdout.WriteString(F.ToString(versionString, " (", *build.Attributes.Version, ") ", *build.Attributes.ProcessingState, " ", build.Attributes.UploadedDate.Time.Format(time.RFC3339), betaGroup, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishTestflight(ctx context.Context) error {
|
||||
if len(os.Args) < 3 {
|
||||
return E.New("platform required: ios, macos, or tvos")
|
||||
}
|
||||
platform, err := parsePlatform(os.Args[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tagVersion, err := build_shared.ReadTagVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag := tagVersion.VersionString()
|
||||
testFlightVersion := build_shared.TestFlightVersion(tagVersion)
|
||||
projectVersion := os.Getenv(strings.ToUpper(os.Args[2]) + "_PROJECT_VERSION")
|
||||
|
||||
releaseNotes := F.ToString("sing-box ", tagVersion.String())
|
||||
if len(os.Args) >= 4 {
|
||||
releaseNotes = strings.Join(os.Args[3:], " ")
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
query := &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
FilterPreReleaseVersionVersion: []string{testFlightVersion},
|
||||
Sort: []string{"-uploadedDate"},
|
||||
Limit: 1,
|
||||
}
|
||||
if projectVersion != "" {
|
||||
query.FilterVersion = []string{projectVersion}
|
||||
}
|
||||
log.Info(string(platform), " ", testFlightVersion, " (", projectVersion, ") list builds")
|
||||
for {
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(builds.Data) == 0 {
|
||||
log.Info(string(platform), " ", testFlightVersion, " waiting for build upload")
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
build := builds.Data[0]
|
||||
log.Info(string(platform), " ", testFlightVersion, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
|
||||
if projectVersion == "" && common.Contains(buildIDs, build.ID) {
|
||||
log.Info(string(platform), " ", testFlightVersion, " build ", *build.Attributes.Version, " already published, waiting for new upload")
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
if *build.Attributes.ProcessingState != "VALID" {
|
||||
log.Info(string(platform), " ", testFlightVersion, " waiting for process: ", *build.Attributes.ProcessingState)
|
||||
time.Sleep(buildPollInterval)
|
||||
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(releaseNotes))
|
||||
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 || response.StatusCode == http.StatusNotFound) {
|
||||
log.Info("waiting for process")
|
||||
time.Sleep(buildPollInterval)
|
||||
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 removeTestflight(ctx context.Context, platformName string, versionString string, buildNumber string) error {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
FilterPreReleaseVersionVersion: []string{versionString},
|
||||
FilterVersion: []string{buildNumber},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(builds.Data) != 1 {
|
||||
return E.New(string(platform), " ", versionString, " (", buildNumber, ") matched ", len(builds.Data), " builds")
|
||||
}
|
||||
build := builds.Data[0]
|
||||
log.Info(string(platform), " ", versionString, " (", buildNumber, ") remove from beta group")
|
||||
_, err = client.Builds.RemoveAccessForBetaGroupsFromBuild(ctx, build.ID, []string{groupID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cancelAppStore(ctx context.Context, platformName string) error {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
log.Info(string(platform), " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterVersionString: []string{tag},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(versions.Data) == 0 {
|
||||
return nil
|
||||
}
|
||||
versionID := versions.Data[0].ID
|
||||
log.Info(string(platform), " ", tag, " list review submissions")
|
||||
submissions, _, err := client.Submission.ListReviewSubmissions(ctx, &asc.ListReviewSubmissionsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterState: []string{
|
||||
string(asc.ReviewSubmissionStateReadyForReview),
|
||||
string(asc.ReviewSubmissionStateWaitingForReview),
|
||||
string(asc.ReviewSubmissionStateInReview),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, submission := range submissions.Data {
|
||||
containsVersion, err := reviewSubmissionContainsVersion(ctx, client, submission.ID, versionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !containsVersion {
|
||||
continue
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " cancel review submission ", submission.ID)
|
||||
_, _, err = client.Submission.UpdateReviewSubmission(ctx, submission.ID, nil, common.Ptr(true))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func reviewSubmissionContainsVersion(ctx context.Context, client *asc.Client, submissionID string, versionID string) (bool, error) {
|
||||
items, _, err := client.Submission.ListItemsForReviewSubmission(ctx, submissionID, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return slices.ContainsFunc(items.Data, func(it asc.ReviewSubmissionItem) bool {
|
||||
return it.Relationships != nil && it.Relationships.AppStoreVersion != nil &&
|
||||
it.Relationships.AppStoreVersion.Data != nil && it.Relationships.AppStoreVersion.Data.ID == versionID
|
||||
}), nil
|
||||
}
|
||||
|
||||
func submitAppStore(ctx context.Context, platformNames []string) error {
|
||||
if len(platformNames) == 0 {
|
||||
platformNames = appStorePlatforms
|
||||
}
|
||||
client := createClient(20 * time.Minute)
|
||||
for _, platformName := range platformNames {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
versionString, err := build_shared.ReadAppleMarketingVersion(applePath, platformDirectories[platform])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = submitAppStoreVersion(ctx, client, platform, versionString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func submitAppStoreVersion(ctx context.Context, client *asc.Client, platform asc.Platform, versionString string) error {
|
||||
log.Info(string(platform), " ", versionString, " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localVersion := badversion.Parse(versionString)
|
||||
var version asc.AppStoreVersion
|
||||
for _, remoteVersion := range versions.Data {
|
||||
remoteVersionString := *remoteVersion.Attributes.VersionString
|
||||
if remoteVersionString == versionString {
|
||||
version = remoteVersion
|
||||
continue
|
||||
}
|
||||
if !badversion.Parse(remoteVersionString).LessThan(localVersion) {
|
||||
return E.New(string(platform), " ", versionString, " is not newer than App Store version ", remoteVersionString)
|
||||
}
|
||||
}
|
||||
if version.ID != "" {
|
||||
state := *version.Attributes.AppStoreState
|
||||
switch state {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
||||
asc.AppStoreVersionStateRejected,
|
||||
asc.AppStoreVersionStateDeveloperRejected,
|
||||
asc.AppStoreVersionStateMetadataRejected,
|
||||
asc.AppStoreVersionStateInvalidBinary:
|
||||
log.Info(string(platform), " ", versionString, " found version in state ", string(state))
|
||||
case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview:
|
||||
log.Info(string(platform), " ", versionString, " already submitted: ", string(state))
|
||||
return nil
|
||||
case asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
log.Info(string(platform), " ", versionString, " approved, run publish_app_store to release")
|
||||
return nil
|
||||
default:
|
||||
return E.New(string(platform), " ", versionString, " already released: ", string(state))
|
||||
}
|
||||
} else {
|
||||
log.Info(string(platform), " ", versionString, " create version")
|
||||
created, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
|
||||
Platform: platform,
|
||||
ReleaseType: common.Ptr("MANUAL"),
|
||||
VersionString: versionString,
|
||||
}, appID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version = created.Data
|
||||
}
|
||||
|
||||
log.Info(string(platform), " ", versionString, " list localizations")
|
||||
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), " ", versionString, " create en-US localization")
|
||||
_, _, err = client.Apps.CreateAppStoreVersionLocalization(ctx, asc.AppStoreVersionLocalizationCreateRequestAttributes{
|
||||
Locale: "en-US",
|
||||
PromotionalText: common.Ptr(appStorePromotionalText),
|
||||
WhatsNew: common.Ptr(appStoreWhatsNew),
|
||||
}, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if localization.Attributes.PromotionalText == nil || *localization.Attributes.PromotionalText != appStorePromotionalText ||
|
||||
localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew != appStoreWhatsNew {
|
||||
log.Info(string(platform), " ", versionString, " update en-US localization")
|
||||
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
|
||||
PromotionalText: common.Ptr(appStorePromotionalText),
|
||||
WhatsNew: common.Ptr(appStoreWhatsNew),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var build asc.Build
|
||||
for {
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
FilterPreReleaseVersionVersion: []string{versionString},
|
||||
Sort: []string{"-uploadedDate"},
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(builds.Data) == 0 {
|
||||
log.Info(string(platform), " ", versionString, " waiting for build upload")
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
build = builds.Data[0]
|
||||
processingState := *build.Attributes.ProcessingState
|
||||
if processingState == "VALID" {
|
||||
break
|
||||
}
|
||||
if processingState == "PROCESSING" {
|
||||
log.Info(string(platform), " ", versionString, " waiting for build ", *build.Attributes.Version, ": ", processingState)
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
return E.New(string(platform), " ", versionString, " build ", *build.Attributes.Version, " ", processingState)
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " found build ", *build.Attributes.Version)
|
||||
|
||||
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != build.ID {
|
||||
log.Info(string(platform), " ", versionString, " attach build ", *build.Attributes.Version)
|
||||
_, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, common.Ptr(build.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info(string(platform), " ", versionString, " list review submissions")
|
||||
submissions, _, err := client.Submission.ListReviewSubmissions(ctx, &asc.ListReviewSubmissionsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterState: []string{
|
||||
string(asc.ReviewSubmissionStateReadyForReview),
|
||||
string(asc.ReviewSubmissionStateWaitingForReview),
|
||||
string(asc.ReviewSubmissionStateInReview),
|
||||
string(asc.ReviewSubmissionStateUnresolvedIssues),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
submission asc.ReviewSubmission
|
||||
submissionHasItem bool
|
||||
)
|
||||
for _, remoteSubmission := range submissions.Data {
|
||||
containsVersion, err := reviewSubmissionContainsVersion(ctx, client, remoteSubmission.ID, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state := *remoteSubmission.Attributes.State
|
||||
switch state {
|
||||
case asc.ReviewSubmissionStateReadyForReview:
|
||||
submission = remoteSubmission
|
||||
submissionHasItem = containsVersion
|
||||
case asc.ReviewSubmissionStateWaitingForReview, asc.ReviewSubmissionStateInReview:
|
||||
if containsVersion {
|
||||
log.Info(string(platform), " ", versionString, " already submitted: ", string(state))
|
||||
return nil
|
||||
}
|
||||
return E.New(string(platform), " review submission ", remoteSubmission.ID, " with another version is ", string(state))
|
||||
default:
|
||||
return E.New(string(platform), " review submission ", remoteSubmission.ID, " is ", string(state))
|
||||
}
|
||||
}
|
||||
if submission.ID == "" {
|
||||
log.Info(string(platform), " ", versionString, " create review submission")
|
||||
created, _, err := client.Submission.CreateReviewSubmission(ctx, appID, common.Ptr(platform))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
submission = created.Data
|
||||
}
|
||||
if !submissionHasItem {
|
||||
log.Info(string(platform), " ", versionString, " add version to review submission ", submission.ID)
|
||||
_, _, err = client.Submission.CreateReviewSubmissionItemForAppStoreVersion(ctx, submission.ID, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " submit review submission ", submission.ID)
|
||||
_, _, err = client.Submission.UpdateReviewSubmission(ctx, submission.ID, common.Ptr(true), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishAppStore(ctx context.Context, platformNames []string) error {
|
||||
if len(platformNames) == 0 {
|
||||
platformNames = appStorePlatforms
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
for _, platformName := range platformNames {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
versionString, err := build_shared.ReadAppleMarketingVersion(applePath, platformDirectories[platform])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterVersionString: []string{versionString},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(versions.Data) == 0 {
|
||||
return E.New(string(platform), " ", versionString, " not found")
|
||||
}
|
||||
version := versions.Data[0]
|
||||
state := *version.Attributes.AppStoreState
|
||||
switch state {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
|
||||
return E.New(string(platform), " ", versionString, " not submitted")
|
||||
case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview:
|
||||
log.Warn(string(platform), " ", versionString, " waiting for review")
|
||||
continue
|
||||
case asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
default:
|
||||
return E.New(string(platform), " ", versionString, " unknown state ", string(state))
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " release")
|
||||
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func main() {
|
||||
build_shared.FindSDK()
|
||||
|
||||
if os.Getenv("GOPATH") == "" {
|
||||
if os.Getenv("build.Default.GOPATH") == "" {
|
||||
os.Setenv("GOPATH", build.Default.GOPATH)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/common/windivert"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-usbip/driverassets"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var (
|
||||
debugEnabled bool
|
||||
outputPath string
|
||||
target string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
||||
flag.StringVar(&outputPath, "output", "", "output path")
|
||||
flag.StringVar(&target, "target", runtime.GOOS+"/"+runtime.GOARCH, "target platform")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
err := build()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func build() error {
|
||||
targetParts := strings.Split(target, "/")
|
||||
if len(targetParts) != 2 || targetParts[0] == "" || targetParts[1] == "" {
|
||||
return E.New("invalid target: ", target)
|
||||
}
|
||||
operatingSystem := targetParts[0]
|
||||
architecture := targetParts[1]
|
||||
if outputPath == "" {
|
||||
outputPath = "sing-box-daemon"
|
||||
if operatingSystem == "windows" {
|
||||
outputPath += ".exe"
|
||||
}
|
||||
}
|
||||
absoluteOutputPath, err := filepath.Abs(outputPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "resolve output path")
|
||||
}
|
||||
err = os.MkdirAll(filepath.Dir(absoluteOutputPath), 0o755)
|
||||
if err != nil {
|
||||
return E.Cause(err, "create output directory")
|
||||
}
|
||||
version, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
return E.Cause(err, "read version")
|
||||
}
|
||||
cgoEnabled := operatingSystem != "windows" && os.Getenv("CC") != ""
|
||||
tags, err := buildTags(operatingSystem, architecture, cgoEnabled)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
arguments := []string{
|
||||
"build",
|
||||
"-v",
|
||||
"-trimpath",
|
||||
"-buildvcs=false",
|
||||
"-tags", strings.Join(tags, ","),
|
||||
"-ldflags", build_shared.LinkerFlags(version, debugEnabled),
|
||||
"-o", absoluteOutputPath,
|
||||
}
|
||||
if operatingSystem == "windows" && architecture == "386" {
|
||||
arguments = append(arguments, "-gcflags=net=-l")
|
||||
}
|
||||
arguments = append(arguments, "./experimental/boxdd")
|
||||
command := exec.Command("go", arguments...)
|
||||
cgoEnabledValue := "0"
|
||||
if cgoEnabled {
|
||||
cgoEnabledValue = "1"
|
||||
}
|
||||
command.Env = append(os.Environ(),
|
||||
"CGO_ENABLED="+cgoEnabledValue,
|
||||
"GOOS="+operatingSystem,
|
||||
"GOARCH="+architecture,
|
||||
"GOTOOLCHAIN=local",
|
||||
)
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
err = command.Run()
|
||||
if err != nil {
|
||||
return E.Cause(err, "build sing-box daemon")
|
||||
}
|
||||
if operatingSystem == "windows" {
|
||||
err = stageWinDivertDriver(architecture, filepath.Dir(absoluteOutputPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = stageUSBIPDrivers(architecture, filepath.Dir(absoluteOutputPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stageUSBIPDrivers(architecture string, outputDirectory string) error {
|
||||
driverPackages := []struct {
|
||||
assets map[string]driverassets.Package
|
||||
assetDir string
|
||||
}{
|
||||
{driverassets.VBoxUSB, filepath.Join("internal", "vboxusb", "assets")},
|
||||
{driverassets.VHCI, filepath.Join("internal", "usbipvhci", "assets")},
|
||||
}
|
||||
var moduleDirectory string
|
||||
for _, driverPackage := range driverPackages {
|
||||
staged := driverPackage.assets[architecture]
|
||||
for _, architecturePackage := range driverPackage.assets {
|
||||
for _, file := range architecturePackage.Files {
|
||||
if slices.ContainsFunc(staged.Files, func(stagedFile driverassets.File) bool {
|
||||
return stagedFile.Name == file.Name
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
err := os.Remove(filepath.Join(outputDirectory, file.Name))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return E.Cause(err, "remove stale ", file.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(staged.Files) == 0 {
|
||||
continue
|
||||
}
|
||||
if moduleDirectory == "" {
|
||||
listOutput, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "github.com/sagernet/sing-usbip").Output()
|
||||
if err != nil {
|
||||
return E.Cause(err, "locate sing-usbip module directory")
|
||||
}
|
||||
moduleDirectory = strings.TrimSpace(string(listOutput))
|
||||
}
|
||||
for _, file := range staged.Files {
|
||||
content, err := os.ReadFile(filepath.Join(moduleDirectory, driverPackage.assetDir, architecture, file.Name))
|
||||
if err != nil {
|
||||
return E.Cause(err, "read ", file.Name)
|
||||
}
|
||||
checksum := sha256.Sum256(content)
|
||||
if hex.EncodeToString(checksum[:]) != file.SHA256 {
|
||||
return E.New(file.Name, " does not match the digest declared in sing-usbip/driverassets")
|
||||
}
|
||||
targetPath := filepath.Join(outputDirectory, file.Name)
|
||||
stagedContent, err := os.ReadFile(targetPath)
|
||||
if err == nil && bytes.Equal(stagedContent, content) {
|
||||
continue
|
||||
}
|
||||
err = os.WriteFile(targetPath, content, 0o644)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write ", file.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stageWinDivertDriver(architecture string, outputDirectory string) error {
|
||||
var assetName, assetDigest string
|
||||
switch architecture {
|
||||
case "amd64":
|
||||
assetName, assetDigest = windivert.Asset64Name, windivert.Asset64SHA256
|
||||
case "386":
|
||||
assetName, assetDigest = windivert.Asset32Name, windivert.Asset32SHA256
|
||||
}
|
||||
for _, name := range []string{windivert.Asset64Name, windivert.Asset32Name} {
|
||||
if name == assetName {
|
||||
continue
|
||||
}
|
||||
err := os.Remove(filepath.Join(outputDirectory, name))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return E.Cause(err, "remove stale ", name)
|
||||
}
|
||||
}
|
||||
if assetName == "" {
|
||||
return nil
|
||||
}
|
||||
assetDirectory := filepath.Join("common", "windivert", "assets")
|
||||
content, err := os.ReadFile(filepath.Join(assetDirectory, assetName))
|
||||
if err != nil {
|
||||
return E.Cause(err, "read ", assetName)
|
||||
}
|
||||
checksum := sha256.Sum256(content)
|
||||
if hex.EncodeToString(checksum[:]) != assetDigest {
|
||||
return E.New(assetName, " does not match the digest declared in common/windivert")
|
||||
}
|
||||
targetPath := filepath.Join(outputDirectory, assetName)
|
||||
staged, err := os.ReadFile(targetPath)
|
||||
if err == nil && bytes.Equal(staged, content) {
|
||||
return nil
|
||||
}
|
||||
err = os.WriteFile(targetPath, content, 0o644)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write ", assetName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildTags(operatingSystem string, architecture string, cgoEnabled bool) ([]string, error) {
|
||||
tagsFile := "release/DEFAULT_BUILD_TAGS"
|
||||
if operatingSystem == "windows" {
|
||||
if architecture == "386" {
|
||||
tagsFile = "release/DEFAULT_BUILD_TAGS_OTHERS"
|
||||
} else {
|
||||
tagsFile = "release/DEFAULT_BUILD_TAGS_WINDOWS"
|
||||
}
|
||||
} else if !cgoEnabled {
|
||||
tagsFile = "release/DEFAULT_BUILD_TAGS_OTHERS"
|
||||
}
|
||||
content, err := os.ReadFile(tagsFile)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read build tags")
|
||||
}
|
||||
tags := strings.Split(strings.TrimSpace(string(content)), ",")
|
||||
if operatingSystem == "windows" {
|
||||
tags = append(tags, "with_external_windivert", "with_external_usbip_drivers")
|
||||
}
|
||||
if debugEnabled {
|
||||
tags = append(tags, "debug")
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
@@ -5,29 +5,22 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "github.com/sagernet/gomobile"
|
||||
_ "github.com/sagernet/gomobile/event/key"
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/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
|
||||
// withTailscale bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
||||
flag.StringVar(&target, "target", "android", "target platform")
|
||||
flag.StringVar(&platform, "platform", "", "specify platform")
|
||||
// flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -38,8 +31,8 @@ func main() {
|
||||
switch target {
|
||||
case "android":
|
||||
buildAndroid()
|
||||
case "apple":
|
||||
buildApple()
|
||||
case "ios":
|
||||
buildiOS()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,95 +40,47 @@ var (
|
||||
sharedFlags []string
|
||||
debugFlags []string
|
||||
sharedTags []string
|
||||
darwinTags []string
|
||||
// memcTags []string
|
||||
notMemcTags []string
|
||||
iosTags []string
|
||||
debugTags []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
sharedFlags = append(sharedFlags, "-trimpath")
|
||||
sharedFlags = append(sharedFlags, "-buildvcs=false")
|
||||
sharedFlags = append(sharedFlags, "-ldflags")
|
||||
currentTag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
currentTag = "unknown"
|
||||
}
|
||||
sharedFlags = append(sharedFlags, "-ldflags", build_shared.LinkerFlags(currentTag, false))
|
||||
debugFlags = append(debugFlags, "-ldflags", build_shared.LinkerFlags(currentTag, true))
|
||||
sharedFlags = append(sharedFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||
debugFlags = append(debugFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_masque", "with_mtproxy", "with_trusttunnel", "with_call", "with_sudoku", "with_utls", "with_naive_outbound", "with_clash_api", "with_usbip", "with_openvpn", "with_openconnect", "badlinkname", "tfogo_checklinkname0")
|
||||
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
|
||||
// memcTags = append(memcTags, "with_tailscale")
|
||||
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
|
||||
notMemcTags = append(notMemcTags, "with_low_memory")
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
|
||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
||||
debugTags = append(debugTags, "debug")
|
||||
}
|
||||
|
||||
type AndroidBuildConfig struct {
|
||||
AndroidAPI int
|
||||
OutputName string
|
||||
Tags []string
|
||||
}
|
||||
func buildAndroid() {
|
||||
build_shared.FindSDK()
|
||||
|
||||
func filterTags(tags []string, exclude ...string) []string {
|
||||
excludeMap := make(map[string]bool)
|
||||
for _, tag := range exclude {
|
||||
excludeMap[tag] = true
|
||||
}
|
||||
var result []string
|
||||
for _, tag := range tags {
|
||||
if !excludeMap[tag] {
|
||||
result = append(result, tag)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func checkJavaVersion() {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func getAndroidBindTarget() string {
|
||||
if platform != "" {
|
||||
return platform
|
||||
} else if debugEnabled {
|
||||
return "android/arm64"
|
||||
}
|
||||
return "android"
|
||||
}
|
||||
|
||||
func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) {
|
||||
args := []string{
|
||||
"bind",
|
||||
"-v",
|
||||
"-o", config.OutputName,
|
||||
"-target", bindTarget,
|
||||
"-androidapi", strconv.Itoa(config.AndroidAPI),
|
||||
"-androidapi", "21",
|
||||
"-javapkg=io.nekohasekai",
|
||||
"-libname=box",
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
args = append(args, sharedFlags...)
|
||||
} else {
|
||||
args = append(args, debugFlags...)
|
||||
}
|
||||
|
||||
args = append(args, "-tags", strings.Join(config.Tags, ","))
|
||||
args = append(args, "-tags")
|
||||
if !debugEnabled {
|
||||
args = append(args, strings.Join(sharedTags, ","))
|
||||
} else {
|
||||
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
|
||||
}
|
||||
args = append(args, "./experimental/libbox")
|
||||
|
||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||
@@ -146,87 +91,38 @@ func buildAndroidVariant(config AndroidBuildConfig, bindTarget string) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
const name = "libbox.aar"
|
||||
copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs")
|
||||
if rw.IsDir(copyPath) {
|
||||
if rw.FileExists(copyPath) {
|
||||
copyPath, _ = filepath.Abs(copyPath)
|
||||
err = rw.CopyFile(config.OutputName, filepath.Join(copyPath, config.OutputName))
|
||||
err = rw.CopyFile(name, filepath.Join(copyPath, name))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Info("copied ", config.OutputName, " to ", copyPath)
|
||||
log.Info("copied to ", copyPath)
|
||||
}
|
||||
}
|
||||
|
||||
func buildAndroid() {
|
||||
build_shared.FindSDK()
|
||||
checkJavaVersion()
|
||||
|
||||
bindTarget := getAndroidBindTarget()
|
||||
|
||||
// Build main variant (SDK 24)
|
||||
mainTags := append([]string{}, sharedTags...)
|
||||
// mainTags = append(mainTags, memcTags...)
|
||||
if debugEnabled {
|
||||
mainTags = append(mainTags, debugTags...)
|
||||
}
|
||||
buildAndroidVariant(AndroidBuildConfig{
|
||||
AndroidAPI: 24,
|
||||
OutputName: "libbox.aar",
|
||||
Tags: mainTags,
|
||||
}, bindTarget)
|
||||
|
||||
// Build legacy variant (SDK 21, no naive outbound)
|
||||
legacyTags := filterTags(sharedTags, "with_naive_outbound")
|
||||
// legacyTags = append(legacyTags, memcTags...)
|
||||
if debugEnabled {
|
||||
legacyTags = append(legacyTags, debugTags...)
|
||||
}
|
||||
buildAndroidVariant(AndroidBuildConfig{
|
||||
AndroidAPI: 21,
|
||||
OutputName: "libbox-legacy.aar",
|
||||
Tags: legacyTags,
|
||||
}, bindTarget)
|
||||
}
|
||||
|
||||
func buildApple() {
|
||||
var bindTarget string
|
||||
if platform != "" {
|
||||
bindTarget = platform
|
||||
} else if debugEnabled {
|
||||
bindTarget = "ios"
|
||||
} else {
|
||||
bindTarget = "ios,iossimulator,tvos,tvossimulator,macos"
|
||||
}
|
||||
|
||||
func buildiOS() {
|
||||
args := []string{
|
||||
"bind",
|
||||
"-v",
|
||||
"-target", bindTarget,
|
||||
"-target", "ios,iossimulator,tvos,tvossimulator,macos",
|
||||
"-libname=box",
|
||||
"-tags-not-macos=with_low_memory",
|
||||
"-iosversion=15.0",
|
||||
"-macosversion=13.0",
|
||||
"-tvosversion=17.0",
|
||||
}
|
||||
//if !withTailscale {
|
||||
// args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
|
||||
//}
|
||||
|
||||
if !debugEnabled {
|
||||
args = append(args, sharedFlags...)
|
||||
} else {
|
||||
args = append(args, debugFlags...)
|
||||
}
|
||||
|
||||
tags := append(sharedTags, darwinTags...)
|
||||
//if withTailscale {
|
||||
// tags = append(tags, memcTags...)
|
||||
//}
|
||||
if debugEnabled {
|
||||
tags = append(tags, debugTags...)
|
||||
tags := append(sharedTags, iosTags...)
|
||||
args = append(args, "-tags")
|
||||
if !debugEnabled {
|
||||
args = append(args, strings.Join(tags, ","))
|
||||
} else {
|
||||
args = append(args, strings.Join(append(tags, debugTags...), ","))
|
||||
}
|
||||
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
args = append(args, "./experimental/libbox")
|
||||
|
||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||
@@ -238,7 +134,7 @@ func buildApple() {
|
||||
}
|
||||
|
||||
copyPath := filepath.Join("..", "sing-box-for-apple")
|
||||
if rw.IsDir(copyPath) {
|
||||
if rw.FileExists(copyPath) {
|
||||
targetDir := filepath.Join(copyPath, "Libbox.xcframework")
|
||||
targetDir, _ = filepath.Abs(targetDir)
|
||||
os.RemoveAll(targetDir)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package build_shared
|
||||
|
||||
import "strings"
|
||||
|
||||
func LinkerFlags(version string, debug bool) string {
|
||||
flags := []string{
|
||||
"-X github.com/sagernet/sing-box/constant.Version=" + version,
|
||||
"-X runtime.godebugDefault=multipathtcp=0,tlssha1=1",
|
||||
"-checklinkname=0",
|
||||
}
|
||||
if !debug {
|
||||
flags = append(flags, "-s", "-w", "-buildid=")
|
||||
}
|
||||
return strings.Join(flags, " ")
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func FindSDK() {
|
||||
}
|
||||
for _, path := range searchPath {
|
||||
path = os.ExpandEnv(path)
|
||||
if rw.IsFile(filepath.Join(path, "licenses", "android-sdk-license")) {
|
||||
if rw.FileExists(path + "/licenses/android-sdk-license") {
|
||||
androidSDKPath = path
|
||||
break
|
||||
}
|
||||
@@ -48,17 +48,11 @@ func FindSDK() {
|
||||
}
|
||||
|
||||
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
|
||||
if rw.FileExists(androidSDKPath + "/ndk/25.1.8937393") {
|
||||
androidNDKPath = androidSDKPath + "/ndk/25.1.8937393"
|
||||
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"))
|
||||
ndkVersions, err := os.ReadDir(androidSDKPath + "/ndk")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -79,10 +73,8 @@ func findNDK() bool {
|
||||
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)
|
||||
if rw.FileExists(androidSDKPath + "/ndk/" + versionName) {
|
||||
androidNDKPath = androidSDKPath + "/ndk/" + versionName
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -93,14 +85,8 @@ 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")
|
||||
}
|
||||
if !rw.FileExists(goBin + "/" + "gobind") {
|
||||
log.Fatal("missing gomobile installation")
|
||||
}
|
||||
GoBinPath = goBin
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user