Compare commits

..
Author SHA1 Message Date
Sergei Maklagin ca0b484d7a Merge tag 'v1.13.12-extended-2.1.3' into extended 2026-05-29 14:41:56 +03:00
Sergei Maklagin 6c1d568876 Remove reserved 2026-05-29 14:34:11 +03:00
Sergei Maklagin 41040ba1e2 Update release pipeline 2026-05-29 04:38:59 +03:00
Sergei Maklagin a47bdbe2ae Update wireguard 2026-05-29 03:55:48 +03:00
Sergei Maklagin 8e8eca01fe Resolve conflicts 2026-05-29 01:33:34 +03:00
Sergei Maklagin 41e815e18b Update sing-box core, refactor MASQUE, update XHTTP 2026-05-29 01:31:57 +03:00
Sergei Maklagin b586c4f313 Remove unused file 2026-05-11 02:18:20 +03:00
Sergei Maklagin 3bd162ed6f Add new admin panel, failover, dns fallback, providers, limiters. Update XHTTP 2026-05-11 00:59:35 +03:00
Sergei Maklagin 652e0baf57 Fix go.sum 2026-05-05 12:59:07 +03:00
Sergei Maklagin 8be5c8fabe Fix legacy build 2026-05-05 12:58:01 +03:00
Sergei Maklagin eb36c934a7 Fix examples 2026-05-03 04:14:59 +03:00
Sergei Maklagin 52edfdb059 Remove IPBlocklist and IPAllowlist 2026-05-03 04:04:16 +03:00
Sergei Maklagin eecab479fa Fix Dockerfile 2026-05-02 15:27:46 +03:00
Sergei Maklagin d8670a742a Merge branch 'extended' of https://github.com/shtorm-7/sing-box-extended into extended 2026-05-02 15:00:57 +03:00
Sergei Maklagin e2ef7d83a1 Fix creating config for WARP 2026-05-02 14:54:26 +03:00
ShtormandGitHub af75b8074f Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-04-30 23:39:22 +03:00
Sergei Maklagin 2e0306ae41 Disable Wireguard ICMP 2026-04-30 19:20:23 +03:00
Sergei Maklagin ee2945ac8f Fix OverrideGateway 2026-04-30 19:19:40 +03:00
Sergei Maklagin b2503ca860 Update chi 2026-04-30 19:18:50 +03:00
Sergei Maklagin dcb1da8683 Fix examples 2026-04-30 13:20:45 +03:00
Sergei Maklagin 22aeb48794 Merge branch 'extended' of https://github.com/shtorm-7/sing-box-extended into extended 2026-04-30 13:14:37 +03:00
Sergei Maklagin 019103587b Update examples 2026-04-30 13:14:17 +03:00
Sergei Maklagin 3139f18bf1 Update android build tags 2026-04-30 12:17:37 +03:00
ShtormandGitHub 493538c743 Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-04-30 11:15:15 +03:00
ShtormandGitHub 578bc159fb Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-04-30 01:15:21 +03:00
ShtormandGitHub f0c317cb0b Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-04-30 01:12:28 +03:00
ShtormandGitHub 32bc1a9fce Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-04-30 01:08:36 +03:00
Sergei Maklagin 41922ba731 Update README.md 2026-04-29 22:40:15 +03:00
Sergei Maklagin bf8fe79a7a Fix go.mod 2026-04-29 22:16:10 +03:00
Sergei Maklagin 398b6387ab Resolve conflicts 2026-04-29 22:14:30 +03:00
Sergei Maklagin 04908a6a67 Add MTProxy, MASQUE, VPN, Link parser. Update AmneziaWG. Remove Tunneling 2026-04-29 22:11:30 +03:00
Sergei Maklagin 88a80e961b Fix README.md 2026-04-27 11:38:09 +03:00
Sergei Maklagin 05b82f63da Update tailscale 2026-04-26 21:31:13 +03:00
Sergei Maklagin 00c08995c7 Update sing-box core 2026-04-26 21:11:31 +03:00
Sergei Maklagin 09f9f114aa Update sing-box core 2026-04-22 19:23:23 +03:00
Sergei Maklagin 1995ba4279 Update sing-box core 2026-04-17 01:10:31 +03:00
Sergei Maklagin 2d33dee415 Update sing-box core 2026-04-06 20:54:24 +03:00
Sergei Maklagin cb3131b3d4 Update dependencies 2026-03-11 20:10:06 +03:00
Sergei Maklagin 20bf40e822 Update dependencies 2026-03-11 17:32:12 +03:00
Sergei Maklagin 861aff60f0 Update dependencies 2026-03-11 06:45:24 +03:00
Sergei Maklagin 47a62927f0 Merge branch 'extended' of https://github.com/shtorm-7/sing-box-extended into extended 2026-03-10 09:06:57 +03:00
Sergei Maklagin 2df1cffb0f Resolve conflicts 2026-03-10 08:43:01 +03:00
Sergei Maklagin f683fcb3cb Update dependencies 2026-03-10 08:37:04 +03:00
Sergei Maklagin bc6ca6e2ea Update sing-box core 2026-03-10 04:50:32 +03:00
Sergei Maklagin 6cd8b832fe Update sing-box core 2026-03-10 04:25:01 +03:00
世界 c0d45aebfa Bump version 2026-03-07 15:55:42 +08:00
Sergei Maklagin 0503006f48 Add Kmutex 2026-03-03 00:51:20 +03:00
Sergei Maklagin 517f5152e7 Add migrate 2026-03-03 00:51:04 +03:00
Sergei Maklagin b1b7aa81cd Update Amnezia H1-H4 format 2026-03-02 21:48:54 +03:00
Sergei Maklagin 195e941c35 Fix typo 2026-03-02 21:27:47 +03:00
Sergei Maklagin 35bc351564 Fix failover 2026-03-02 19:48:06 +03:00
Sergei Maklagin 290dbed7b8 Fix failover 2026-03-02 19:33:59 +03:00
Sergei Maklagin d7a8207f44 Update AmneziaWG 2026-03-02 19:33:07 +03:00
Sergei Maklagin 57c5ca13eb Fix bond outbound 2026-03-02 19:31:23 +03:00
Sergei Maklagin 7fc33134fb Update AmneziaWG 2026-03-01 16:42:21 +03:00
Sergei Maklagin 881ab6d436 Fix examples 2026-02-27 00:47:22 +03:00
Sergei Maklagin 0443b93328 Update README.md 2026-02-27 00:19:37 +03:00
Sergei Maklagin 75557830a8 Merge branch 'extended' into extended-next 2026-02-26 22:58:59 +03:00
Sergei Maklagin 9d5273ba1e Resolve conflicts 2026-02-26 22:58:45 +03:00
Sergei Maklagin 5f2a65f01b Add examples 2026-02-26 22:57:44 +03:00
Sergei Maklagin 06a519db27 Resolve conflicts 2026-02-26 22:57:25 +03:00
Sergei Maklagin 65e73fe817 Add examples 2026-02-26 22:55:24 +03:00
Sergei Maklagin c0aa3480c5 Add admin panel, manager, node_manager, bandwidth limiter, connection limiter, bonding, failover, vless encryption, mkcp transport 2026-02-26 22:44:31 +03:00
Sergei Maklagin 69f6c75dd7 Add vless encryption 2026-02-26 18:03:59 +03:00
ShtormandGitHub f95b34a8a7 Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-02-26 02:43:48 +03:00
Sergei Maklagin b62000e924 Update README.md 2026-02-25 14:10:09 +03:00
Sergei Maklagin a03af44c61 Add DONATE.md 2026-02-25 14:03:36 +03:00
Sergei Maklagin aa103fdfc6 Fix examples 2026-02-22 18:10:15 +03:00
Sergei Maklagin c82e613c52 Fix typo 2026-02-22 17:59:02 +03:00
Sergei Maklagin 3d16078651 Fix padding 2026-02-22 16:22:52 +03:00
Sergei Maklagin 18b1101fbe Update Dockerfile 2026-02-22 15:50:39 +03:00
Sergei Maklagin 4ebe870306 Update xhttp examples 2026-02-22 15:50:00 +03:00
Sergei Maklagin 50c5e9df0d Fix xhttp options 2026-02-22 15:46:12 +03:00
Sergei Maklagin c8a993834e Fix Range 2026-02-22 15:45:53 +03:00
Sergei Maklagin 260bbbfb45 Fix examples 2026-02-22 14:51:16 +03:00
Sergei Maklagin 82337299b9 Update xhttp 2026-02-22 14:48:52 +03:00
Sergei Maklagin c229c79dcc Update sing-box core 2026-02-22 14:46:42 +03:00
世界 f63091d14d Bump version 2026-02-15 21:05:34 +08:00
世界 1c4a01ee90 Fix matching multi predefined 2026-02-15 19:20:31 +08:00
世界 4d7f99310c Fix matching rule-set invert 2026-02-15 19:20:11 +08:00
世界 6fc511f56e wireguard: Fix missing fallback for gso 2026-02-15 19:20:03 +08:00
世界 d18d2b352a Bump version 2026-02-09 13:57:18 +08:00
世界 534128bba9 tuic: Fix udp context 2026-02-09 13:55:09 +08:00
世界 736a7368c6 Fix naive padding 2026-02-09 13:53:32 +08:00
世界 e7a9c90213 Fix DNS cache lock goroutine leak
The cache deduplication in Client.Exchange uses a channel-based lock
per DNS question. Waiting goroutines blocked on <-cond without context
awareness, causing them to accumulate indefinitely when the owning
goroutine's transport call stalls. Add select on ctx.Done() so waiters
respect context cancellation and timeouts.
2026-02-06 22:28:30 +08:00
世界 0f3774e501 Bump version 2026-02-05 17:13:38 +08:00
世界 2f8e656522 Update Go to 1.25.7 2026-02-05 17:12:42 +08:00
世界 3ba30e3f00 Fix route_address_set duplicated IP sets causing route creation failure
The FlatMap calls pre-populated routeAddressSet and routeExcludeAddressSet
before the for-loops which appended the same IP sets again, doubling every
entry. On Windows this caused CreateIpForwardEntry2 to return
ERROR_OBJECT_ALREADY_EXISTS.

Fixes #3725
2026-02-02 17:29:21 +08:00
世界 f2639a5829 Fix random iproute2 table index was incorrectly removed 2026-02-02 14:13:49 +08:00
世界 69bebbda82 Bump version 2026-02-01 10:19:35 +08:00
世界 00b2c042ee Disable rp filter atomically 2026-02-01 10:17:34 +08:00
世界 d9eb8f3ab6 Fix varbin serialization 2026-02-01 10:11:15 +08:00
世界 58025a01f8 Fix auto_redirect fallback rule 2026-01-29 12:07:15 +08:00
世界 99cad72ea8 Bump version 2026-01-28 16:56:08 +08:00
世界 6e96d620fe Minor fixes 2026-01-28 16:56:08 +08:00
Sergei Maklagin 596291567f Update AmneziaWG 2026-01-25 21:24:43 +03:00
Sergei Maklagin a2a5f46cb6 Resolve conflicts 2026-01-18 21:53:22 +03:00
Sergei Maklagin f6da8e52b4 Fix logger 2026-01-18 21:51:30 +03:00
世界 b27d707668 Bump version 2026-01-17 04:54:24 +08:00
Sergei Maklagin 287fe834db Update XHTTP 2025-12-11 02:46:57 +03:00
Sergei Maklagin d7f0cea4ff Fix typo 2025-12-08 23:18:51 +03:00
Sergei Maklagin d8b470d1ba Add new wireguard options 2025-12-08 22:32:33 +03:00
Sergei Maklagin 984fc295b3 Fix XHTTP TLS 2025-12-08 22:30:58 +03:00
ShtormandGitHub 6e4b7ed744 Merge pull request #6 from starifly/extended
fix(xhttp): use download request URL for down leg
2025-11-03 19:13:46 +03:00
starifly 855d400654 fix(xhttp): use download request URL for down leg 2025-11-03 23:18:03 +08:00
Sergei Maklagin 4c5e2c6645 Remove direct detour checking 2025-11-02 17:59:12 +03:00
Sergei Maklagin 725eccdea8 Fix Range 2025-11-02 17:55:18 +03:00
Sergei Maklagin 2ff042abd2 Resolve unnecessary logger 2025-11-02 17:41:18 +03:00
Sergei Maklagin ffb282e47e Resolve conflicts 2025-11-02 17:39:38 +03:00
Sergei Maklagin 91f9134379 Update README.md 2025-09-15 01:25:46 +03:00
Sergei Maklagin 5a4de7b242 Integrate Amnezia 1.5 2025-09-15 01:25:19 +03:00
Sergei Maklagin bc91312a73 Update go.sum 2025-09-14 23:45:55 +03:00
Sergei Maklagin 1d603e24fd Resolve conflicts 2025-09-14 23:44:29 +03:00
Sergei Maklagin 9dc526ea1f Resolve conflicts 2025-08-15 12:57:47 +03:00
Sergei Maklagin 93eb435e26 Resolve conflicts 2025-08-15 12:56:52 +03:00
Sergei Maklagin e22416f0d9 Merge branch 'extended' of https://github.com/shtorm-7/sing-box-extended into extended 2025-07-14 13:27:54 +03:00
Sergei Maklagin 89497dbfd5 Update dependencies 2025-07-13 21:48:31 +03:00
Sergei Maklagin 8388abbb77 Resolve conflicts 2025-07-13 21:44:23 +03:00
Sergei Maklagin 180b7c4134 Fix tunnel client 2025-07-13 21:41:41 +03:00
Sergei Maklagin deda2cca5e Fix xhttp transport 2025-07-13 21:40:34 +03:00
ShtormandGitHub 691ecd45a9 Update README.md
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2025-07-06 21:16:54 +03:00
Sergei Maklagin 209b89a4a3 Add tunnel 2025-07-06 18:31:06 +03:00
Sergei Maklagin 765111a552 Merge tag 'v1.11.14' into HEAD 2025-06-19 20:18:51 +03:00
Sergei Maklagin 824eac453b Add new examples 2025-06-15 22:23:42 +03:00
Sergei Maklagin 85a1a8a53b Format examples 2025-06-15 22:23:25 +03:00
Sergei Maklagin ae9e7aa5f4 Fix Range 2025-06-15 22:21:58 +03:00
Sergei Maklagin 52b71c6f00 Add sdns transport 2025-06-15 20:47:05 +03:00
Sergei Maklagin 5d04673783 Fix XHTTP Fqdn 2025-06-15 19:41:22 +03:00
Sergei Maklagin 307c429715 Fix WARP endpoint error 2025-06-15 19:40:42 +03:00
Sergei Maklagin 6801b58d96 Fix interrupt_exist_connections 2025-06-15 19:32:14 +03:00
Sergei Maklagin 6272596ebc Add unified delay 2025-06-15 19:24:09 +03:00
Sergei Maklagin 7c4c2d5ca8 Add mieru protocol 2025-06-15 18:04:27 +03:00
Sergei Maklagin 6768c77fa0 Merge tag 'v1.11.13' into HEAD 2025-06-08 22:43:21 +03:00
Sergei Maklagin 0dff811977 Add examples 2025-06-08 22:38:32 +03:00
Sergei Maklagin e1a58d12fe Resolve conflicts 2025-06-08 19:54:17 +03:00
Sergei Maklagin 4e74a4108d refactor: WARP 2025-06-08 19:51:17 +03:00
Sergei Maklagin 0238c54261 Add xhttp transport 2025-06-08 19:35:59 +03:00
Sergei Maklagin 5c911c97d8 Added WARP endpoint 2025-06-01 22:06:34 +03:00
Sergei Maklagin 2cfc8092ad Fix endpoint manager locks 2025-05-29 22:48:15 +03:00
Sergei Maklagin 26bd698462 Integrate AmneziaWG 2025-05-24 23:54:15 +03:00
1415 changed files with 17254 additions and 190573 deletions
+2 -6
View File
@@ -1,16 +1,12 @@
-s dir
--name sing-box-extended
--name sing-box
--category net
--license GPL-3.0-or-later
--description "The universal proxy platform (extended)."
--description "The universal proxy platform."
--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
+1 -1
View File
@@ -1 +1 @@
0d28acc44093df24b2526dea3d6ffefd6b0a54f0
2faf34666c2cc8234f10f2ab6d4c4d6104d34ae2
-2
View File
@@ -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
-2
View File
@@ -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)
- 其他
+7 -5
View File
@@ -27,7 +27,10 @@ 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')
# 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)
@@ -75,16 +78,15 @@ done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
# Build APK
apk --root "$APK_ROOT_DIR" mkpkg \
--info "name:sing-box-extended" \
--info "name:sing-box" \
--info "version:${APK_VERSION}" \
--info "description:The universal proxy platform (extended)." \
--info "description:The universal proxy platform." \
--info "arch:${ARCHITECTURE}" \
--info "license:GPL-3.0-or-later" \
--info "origin:sing-box-extended" \
--info "origin:sing-box" \
--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" \
-57
View File
@@ -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
-28
View File
@@ -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
-34
View File
@@ -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 -8
View File
@@ -2,11 +2,10 @@
set -euo pipefail
VERSION="1.26.8"
VERSION="1.25.9"
PATCH_COMMITS=(
"f080b0c6346eb690c0dc82497b35925f385b35ac"
"2d9c12887c342fb9051d231aa5388743cb7e9cb6"
"367663849c656612fc3c8875ef6e7f6b0c93dbd0"
"afe69d3cec1c6dcf0f1797b20546795730850070"
"1ed289b0cf87dc5aae9c6fe1aa5f200a83412938"
)
CURL_ARGS=(
-fL
@@ -26,13 +25,12 @@ tar -xzf "go${VERSION}.darwin-arm64.tar.gz"
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/
# these patch URLs only work on golang1.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/SagerNet/go/commits/release-branch.go1.25/
# 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
+10 -10
View File
@@ -2,14 +2,14 @@
set -euo pipefail
VERSION="1.26.8"
VERSION="1.25.9"
PATCH_COMMITS=(
"a4ae550aa148b04c9d4890e98bee63aede5c4b53"
"95b851f661584711faa8115b3234a461044f4510"
"b0bf0a863cf218b9bb0d6c013903ab160ae0c39b"
"7c7a2a0d68920f8764d9a3aeae7ad1067a17b3fa"
"ba34356e82f3c12a7303d2231e6ea0e42bbbb3bf"
"472a88edbc2a42feb53fa31d29ab54d33840dca6"
"466f6c7a29bc098b0d4c987b803c779222894a11"
"1bdabae205052afe1dadb2ad6f1ba612cdbc532a"
"a90777dcf692dd2168577853ba743b4338721b06"
"f6bddda4e8ff58a957462a1a09562924d5f3d05c"
"bed309eff415bcb3c77dd4bc3277b682b89a388d"
"34b899c2fb39b092db4fa67c4417e41dc046be4b"
)
CURL_ARGS=(
-fL
@@ -29,9 +29,9 @@ 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/
# these patch URLs only work on golang1.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
# revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
+1 -2
View File
@@ -4,7 +4,7 @@ PROJECTS=$(dirname "$0")/../..
function updateClient() {
pushd clients/$1
git fetch origin main
git fetch
git reset FETCH_HEAD --hard
popd
git add clients/$1
@@ -12,4 +12,3 @@ function updateClient() {
updateClient "apple"
updateClient "android"
updateClient "desktop"
+1 -1
View File
@@ -10,4 +10,4 @@ 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"
git -C $PROJECTS/cronet-go rev-parse origin/dev > "$SCRIPT_DIR/CRONET_GO_VERSION"
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -55,7 +55,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.26.8
go-version: ~1.25.9
- name: Clone cronet-go
if: matrix.naive
run: |
@@ -63,8 +63,7 @@ jobs:
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 fetch --depth=1 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
+1 -1
View File
@@ -60,7 +60,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.26
go-version: ^1.25
- name: Cache go module
uses: actions/cache@v4
with:
+6 -6
View File
@@ -29,7 +29,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.26.8
go-version: ~1.25.9
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -45,7 +45,7 @@ jobs:
echo "version=$version" >> "$GITHUB_OUTPUT"
build:
name: Build binary
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs:
- calculate_version
strategy:
@@ -72,7 +72,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.26.8
go-version: ~1.25.9
- name: Clone cronet-go
if: matrix.naive
run: |
@@ -80,8 +80,7 @@ jobs:
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 fetch --depth=1 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
@@ -183,6 +182,8 @@ jobs:
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
--architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
rm -rf $HOME/.gnupg
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
${{ secrets.GPG_KEY }}
@@ -193,7 +194,6 @@ jobs:
run: |-
set -xeuo pipefail
sudo gem install fpm
echo '%_rpmformat 4' > "$HOME/.rpmmacros"
cp .fpm_systemd .fpm
fpm -t rpm \
--name "${NAME}" \
-57
View File
@@ -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" ./...
+1 -8
View File
@@ -22,11 +22,4 @@ CLAUDE.md
AGENTS.md
/.claude/
dist
logs
/*.so
/*.log
/*.db-shm
/*.db-wal
/*.db.backup.*
/test_download.bin
/wget-log
logs
+1 -1
View File
@@ -18,6 +18,7 @@ linters:
default: none
enable:
- ineffassign
- paralleltest
- staticcheck
- unused
- modernize
@@ -31,7 +32,6 @@ linters:
- -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:
+4 -401
View File
@@ -11,32 +11,19 @@ builds:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
- -checklinkname=0
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
env:
- CGO_ENABLED=0
- GOTOOLCHAIN=local
@@ -62,23 +49,11 @@ builds:
- 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
@@ -119,271 +94,6 @@ 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:
@@ -401,23 +111,11 @@ builds:
- 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
@@ -456,54 +154,6 @@ builds:
- 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:
@@ -529,52 +179,11 @@ archives:
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
- id: archive-legacy
<<: *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'
- legacy
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
- id: archive-compressed
<<: *template
builds:
@@ -600,12 +209,6 @@ release:
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
- package
skip_upload: true
+7 -12
View File
@@ -1,23 +1,18 @@
FROM --platform=$BUILDPLATFORM golang:1.26 AS builder
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
LABEL maintainer="shtorm-7"
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 GOPROXY ${GOPROXY}
ENV CGO_ENABLED=0
ENV GOOS=$TARGETOS
ENV GOARCH=$TARGETARCH
ENV CRONET_GO_PATH=${CRONET_GO_PATH}
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) \
&& apk add git build-base \
&& export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \
&& export TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) \
&& export LDFLAGS_SHARED=$(cat release/LDFLAGS) \
&& go build -v -trimpath -tags "$TAGS" \
-o /go/bin/sing-box \
+26 -64
View File
@@ -22,11 +22,10 @@ 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 docs build
build:
export GOTOOLCHAIN=local && \
go build $(MAIN_PARAMS) $(MAIN)
build_admin_panel:
@@ -36,15 +35,6 @@ build_admin_panel:
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)
@@ -57,9 +47,6 @@ ci_build:
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)
@@ -81,7 +68,8 @@ lint_install:
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
@@ -90,16 +78,13 @@ proto_install:
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: build_admin_panel
go run ./cmd/internal/build goreleaser release --skip=validate --clean -p 3 --skip publish
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:
@@ -120,70 +105,49 @@ release_docker:
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
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
cp ../sing-box-for-android/app/build/outputs/apk/otherLegacy/release/*.apk dist/release_android
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
./codeberg-release.sh --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
rm -rf dist/release_android
release_android: lib_android update_android_version build_android upload_android
release_android: lib_android update_android_version build_android
publish_android:
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
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 clean -scheme SFI && \
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
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
export_ios_ipa:
cd ../sing-box-for-apple && \
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Export.plist -allowProvisioningUpdates -exportPath build/SFI && \
cp build/SFI/sing-box.ipa dist/SFI.ipa
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"
upload_ios_ipa:
cd dist && \
cp SFI.ipa "SFI-${VERSION}.ipa" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFI-${VERSION}.ipa"
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 -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
upload_macos_app_store:
cd ../sing-box-for-apple && \
@@ -252,7 +216,7 @@ replace_macos_standalone: build_macos_pkg notarize_macos_pkg upload_macos_pkg up
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 -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
upload_tvos_app_store:
cd ../sing-box-for-apple && \
@@ -274,7 +238,7 @@ 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
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone
@@ -283,13 +247,11 @@ 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
prepare_app_store:
go run -v ./cmd/internal/app_store_connect prepare_app_store
publish_app_store:
go run -v ./cmd/internal/app_store_connect publish_app_store $(filter-out $@,$(MAKECMDGOALS))
go run -v ./cmd/internal/app_store_connect publish_app_store
test:
@go test -v ./... && \
@@ -319,8 +281,8 @@ lib_apple_new:
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple
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
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.12
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12
docs:
venv/bin/mkdocs serve
+4 -14
View File
@@ -1,24 +1,15 @@
# sing-box-extended
[![license](https://img.shields.io/badge/license-GPLv3-blue.svg)](LICENSE)
[![go](https://img.shields.io/badge/go-1.26-00ADD8.svg)](go.mod)
[![codeberg](https://img.shields.io/badge/mirror-codeberg-2185D0.svg)](https://codeberg.org/shtorm-7/sing-box-extended)
[![telegram](https://img.shields.io/badge/telegram-chat-26A5E4.svg?logo=telegram)](https://t.me/sing_box_extended)
Sing-box with extended features.
## 🔥 Features
### Protocols
### Outbounds
- **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
- **VPN** — Routed tunnel over any TCP 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
@@ -34,13 +25,12 @@ Sing-box with extended features.
- **Rate Limiter** — Request rate limiting
### Encryption & Obfuscation
- **Amnezia 3.1** — WireGuard traffic obfuscation
- **Amnezia 2.0** — 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
@@ -50,7 +40,7 @@ Sing-box with extended features.
### 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)
- **Link Parser** — Outbound configured from a share link (VLESS, VMess, Shadowsocks, Trojan, Hysteria, Hysteria2, TUIC)
- **Extended WireGuard options** — Advanced configuration capabilities
- **Unified Delay** — Unified latency measurement
-1
View File
@@ -10,7 +10,6 @@ import (
type CertificateStore interface {
LifecycleService
Pool() *x509.CertPool
ExclusiveAnchors() bool
}
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
-21
View File
@@ -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
}
-69
View File
@@ -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
}
-80
View File
@@ -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
}
-17
View File
@@ -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
}
-37
View File
@@ -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
}
+32 -30
View File
@@ -3,12 +3,13 @@ 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"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/miekg/dns"
)
@@ -16,7 +17,6 @@ import (
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)
@@ -25,22 +25,36 @@ type DNSRouter interface {
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)
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
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
Transport DNSTransport
Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
}
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
if options == nil {
return &DNSQueryOptions{}, nil
}
transportManager := service.FromContext[DNSTransportManager](ctx)
transport, loaded := transportManager.Transport(options.Server)
if !loaded {
return nil, E.New("domain resolver not found: " + options.Server)
}
return &DNSQueryOptions{
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache,
RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
}, nil
}
type RDRCStore interface {
@@ -49,13 +63,6 @@ type RDRCStore interface {
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
@@ -65,17 +72,11 @@ type DNSTransport interface {
// 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 LegacyDNSTransport interface {
LegacyStrategy() C.DomainStrategy
LegacyClientSubnet() netip.Prefix
}
type DNSTransportRegistry interface {
@@ -89,5 +90,6 @@ type DNSTransportManager interface {
Transport(tag string) (DNSTransport, bool)
Default() DNSTransport
FakeIP() FakeIPTransport
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
}
+1 -1
View File
@@ -21,8 +21,8 @@ type EndpointRegistry interface {
type EndpointManager interface {
Lifecycle
StartEndpoint(endpoint Endpoint) error
Endpoints() []Endpoint
Get(tag string) (Endpoint, bool)
Remove(tag string) error
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) error
}
+89 -12
View File
@@ -2,36 +2,46 @@ package endpoint
import (
"context"
"os"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
var _ adapter.EndpointManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.EndpointRegistry
access sync.Mutex
scope *adapter.Scope
started bool
stage adapter.StartStage
endpoints []adapter.Endpoint
endpointByTag map[string]adapter.Endpoint
}
func NewManager(registry adapter.EndpointRegistry) *Manager {
func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
endpointByTag: make(map[string]adapter.Endpoint),
}
}
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if stage == adapter.StartStateInitialize {
m.scope = scope
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
if stage == adapter.StartStateStart {
// started with outbound manager
m.access.Unlock()
return nil
}
@@ -39,16 +49,38 @@ func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
m.access.Unlock()
for _, endpoint := range endpoints {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
err := scope.Start(name, endpoint, stage)
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(endpoint, stage)
done()
if err != nil {
return err
return E.Cause(err, stage, " ", name)
}
}
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) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
endpoints := m.endpoints
m.endpoints = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, endpoint := range endpoints {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, endpoint.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
return nil
}
func (m *Manager) Endpoints() []adapter.Endpoint {
@@ -64,6 +96,29 @@ func (m *Manager) Get(tag string) (adapter.Endpoint, bool) {
return endpoint, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
endpoint, found := m.endpointByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.endpointByTag, tag)
index := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
return it == endpoint
})
if index == -1 {
panic("invalid endpoint index")
}
m.endpoints = append(m.endpoints[:index], m.endpoints[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return endpoint.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
endpoint, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
if err != nil {
@@ -71,9 +126,31 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
}
m.access.Lock()
defer m.access.Unlock()
_, loaded := m.endpointByTag[tag]
if loaded {
return E.New("duplicate endpoint tag: ", tag)
if m.started {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(endpoint, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {
if m.started {
err = existsEndpoint.Close()
if err != nil {
return E.Cause(err, "close endpoint/", existsEndpoint.Type(), "[", existsEndpoint.Tag(), "]")
}
}
existsIndex := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
return it == existsEndpoint
})
if existsIndex == -1 {
panic("invalid endpoint index")
}
m.endpoints = append(m.endpoints[:existsIndex], m.endpoints[existsIndex+1:]...)
}
m.endpoints = append(m.endpoints, endpoint)
m.endpointByTag[tag] = endpoint
-8
View File
@@ -2,8 +2,6 @@ package endpoint
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -46,12 +44,6 @@ func NewRegistry() *Registry {
}
}
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()
+26 -42
View File
@@ -7,15 +7,32 @@ import (
"io"
"time"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/common/varbin"
)
type ClashServer interface {
LifecycleService
ConnectionTracker
Mode() string
ModeList() []string
SetModeUpdateHook(hook *observable.Subscriber[struct{}])
HistoryStorage() URLTestHistoryStorage
}
type URLTestHistory struct {
Time time.Time `json:"time"`
Delay uint16 `json:"delay"`
}
type URLTestHistoryStorage interface {
SetHook(hook *observable.Subscriber[struct{}])
LoadURLTestHistory(tag string) *URLTestHistory
DeleteURLTestHistory(tag string)
StoreURLTestHistory(tag string, history *URLTestHistory)
Close() error
}
type V2RayServer interface {
LifecycleService
StatsService() ConnectionTracker
@@ -24,8 +41,6 @@ type V2RayServer interface {
type CacheFile interface {
LifecycleService
CacheID() string
StoreFakeIP() bool
FakeIPStorage
@@ -34,13 +49,6 @@ type CacheFile interface {
StoreWARPConfig() bool
StoreMASQUEConfig() bool
StoreSubscriptions() bool
StoreDNS() bool
DNSCacheStore
SetDisableExpire(disableExpire bool)
SetOptimisticTimeout(timeout time.Duration)
LoadMode() string
StoreMode(mode string) error
@@ -62,12 +70,11 @@ 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))
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
if err != nil {
return nil, err
}
@@ -91,14 +98,6 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
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
}
@@ -113,9 +112,6 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
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 {
@@ -131,30 +127,12 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
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
}
@@ -167,5 +145,11 @@ type OutboundGroup interface {
type URLTestGroup interface {
OutboundGroup
URLTest(ctx context.Context) (map[string]uint16, error)
PerformUpdateCheck()
}
func OutboundTag(detour Outbound) string {
if group, isGroup := detour.(OutboundGroup); isGroup {
return group.Now()
}
return detour.Tag()
}
+32 -6
View File
@@ -5,31 +5,57 @@ import (
"net"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
// Deprecated
type ConnectionHandler interface {
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
}
type ConnectionHandlerEx interface {
NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
// Deprecated: use PacketHandlerEx instead
type PacketHandler interface {
NewPacket(buffer *buf.Buffer, source M.Socksaddr)
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
}
type PacketBatchHandler interface {
NewPacketBatch(buffers []*buf.Buffer, sources []M.Socksaddr)
type PacketHandlerEx interface {
NewPacketEx(buffer *buf.Buffer, source M.Socksaddr)
}
// Deprecated: use OOBPacketHandlerEx instead
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 OOBPacketHandlerEx interface {
NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
}
// Deprecated
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 PacketConnectionHandlerEx interface {
NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
// Deprecated: use TCPConnectionHandlerEx instead
//
//nolint:staticcheck
type UpstreamHandlerAdapter interface {
N.TCPConnectionHandler
N.UDPConnectionHandler
E.Handler
}
type UpstreamHandlerAdapterEx interface {
N.TCPConnectionHandlerEx
N.UDPConnectionHandlerEx
}
-43
View File
@@ -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()
}
}
+11 -95
View File
@@ -2,17 +2,13 @@ package adapter
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/option"
M "github.com/sagernet/sing/common/metadata"
"github.com/miekg/dns"
)
type Inbound interface {
@@ -23,12 +19,12 @@ type Inbound interface {
type TCPInjectableInbound interface {
Inbound
ConnectionHandler
ConnectionHandlerEx
}
type UDPInjectableInbound interface {
Inbound
PacketConnectionHandler
PacketConnectionHandlerEx
}
type InboundRegistry interface {
@@ -41,6 +37,7 @@ type InboundManager interface {
Lifecycle
Inbounds() []Inbound
Get(tag string) (Inbound, bool)
Remove(tag string) error
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error
}
@@ -55,11 +52,6 @@ type InboundContext struct {
User string
Outbound string
// power report
RouteRule string
RouteOutbound string
// sniffer
Protocol string
@@ -82,28 +74,18 @@ type InboundContext struct {
TLSFragment bool
TLSFragmentFallbackDelay time.Duration
TLSRecordFragment bool
TLSSpoof string
TLSSpoofMethod tlsspoof.Method
NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
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
DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
QueryType uint16
FakeIP bool
// rule cache
@@ -114,7 +96,7 @@ type InboundContext struct {
SourcePortMatch bool
DestinationAddressMatch bool
DestinationPortMatch bool
DeferredIPCIDRMatchGroups uint8
DidMatch bool
IgnoreDestinationIPCIDRMatch bool
}
@@ -129,77 +111,11 @@ func (c *InboundContext) ResetRuleMatchCache() {
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
c.DidMatch = false
}
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)
}
+91 -7
View File
@@ -2,45 +2,84 @@ package inbound
import (
"context"
"os"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
var _ adapter.InboundManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.InboundRegistry
endpoint adapter.EndpointManager
access sync.Mutex
started bool
stage adapter.StartStage
inbounds []adapter.Inbound
inboundByTag map[string]adapter.Inbound
}
func NewManager(registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
return &Manager{
logger: logger,
registry: registry,
endpoint: endpoint,
inboundByTag: make(map[string]adapter.Inbound),
}
}
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
inbounds := m.inbounds
m.access.Unlock()
for _, inbound := range inbounds {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
err := scope.Start(name, inbound, stage)
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(inbound, stage)
done()
if err != nil {
return err
return E.Cause(err, stage, " ", name)
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
inbounds := m.inbounds
m.inbounds = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, inbound := range inbounds {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, inbound.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
return nil
}
func (m *Manager) Inbounds() []adapter.Inbound {
m.access.Lock()
defer m.access.Unlock()
@@ -57,6 +96,29 @@ func (m *Manager) Get(tag string) (adapter.Inbound, bool) {
return m.endpoint.Get(tag)
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
inbound, found := m.inboundByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.inboundByTag, tag)
index := common.Index(m.inbounds, func(it adapter.Inbound) bool {
return it == inbound
})
if index == -1 {
panic("invalid inbound index")
}
m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return inbound.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
if err != nil {
@@ -64,9 +126,31 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
}
m.access.Lock()
defer m.access.Unlock()
_, loaded := m.inboundByTag[tag]
if loaded {
return E.New("duplicate inbound tag: ", tag)
if m.started {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(inbound, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
if existsInbound, loaded := m.inboundByTag[tag]; loaded {
if m.started {
err = existsInbound.Close()
if err != nil {
return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]")
}
}
existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool {
return it == existsInbound
})
if existsIndex == -1 {
panic("invalid inbound index")
}
m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...)
}
m.inbounds = append(m.inbounds, inbound)
m.inboundByTag[tag] = inbound
-8
View File
@@ -2,8 +2,6 @@ package inbound
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -46,12 +44,6 @@ func NewRegistry() *Registry {
}
}
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()
-45
View File
@@ -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())
}
+38 -99
View File
@@ -1,16 +1,13 @@
package adapter
import (
"context"
"sync"
"reflect"
"strings"
"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 {
@@ -50,7 +47,8 @@ func (s StartStage) String() string {
}
type Lifecycle interface {
Start(stage StartStage, scope *Scope) error
Start(stage StartStage) error
Close() error
}
type LifecycleService interface {
@@ -58,106 +56,47 @@ type LifecycleService interface {
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 getServiceName(service any) string {
if named, ok := service.(interface {
Type() string
Tag() string
}); ok {
tag := named.Tag()
if tag != "" {
return named.Type() + "[" + tag + "]"
}
return named.Type()
}
}
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
t := reflect.TypeOf(service)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
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 strings.ToLower(t.Name())
}
func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) error {
for _, service := range services {
name := getServiceName(service)
done := LogElapsed(logger, stage, " ", name)
err := service.Start(stage)
done()
if err != nil {
return err
}
}
return nil
}
func (s *Scope) Remove(component Lifecycle) error {
s.access.Lock()
child, loaded := s.children[component]
if !loaded {
s.access.Unlock()
return nil
func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error {
for _, service := range services {
done := LogElapsed(logger, stage, " ", service.Name())
err := service.Start(stage)
done()
if err != nil {
return E.Cause(err, stage.String(), " ", service.Name())
}
}
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
return nil
}
func LogElapsed(logger log.ContextLogger, description ...any) func() {
+52
View File
@@ -0,0 +1,52 @@
package adapter
func LegacyStart(starter any, stage StartStage) error {
if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle {
return lifecycle.Start(stage)
}
switch stage {
case StartStateInitialize:
if preStarter, isPreStarter := starter.(interface {
PreStart() error
}); isPreStarter {
return preStarter.PreStart()
}
case StartStateStart:
if starter, isStarter := starter.(interface {
Start() error
}); isStarter {
return starter.Start()
}
case StartStateStarted:
if postStarter, isPostStarter := starter.(interface {
PostStart() error
}); isPostStarter {
return postStarter.PostStart()
}
}
return nil
}
type lifecycleServiceWrapper struct {
SimpleLifecycle
name string
}
func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
return &lifecycleServiceWrapper{
SimpleLifecycle: service,
name: name,
}
}
func (l *lifecycleServiceWrapper) Name() string {
return l.name
}
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
return LegacyStart(l.SimpleLifecycle, stage)
}
func (l *lifecycleServiceWrapper) Close() error {
return l.SimpleLifecycle.Close()
}
-24
View File
@@ -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)
}
-5
View File
@@ -1,5 +0,0 @@
package adapter
type NetworkNamespaceManager interface {
ResolvePath(nameOrPath string) string
}
+3 -7
View File
@@ -1,10 +1,8 @@
package adapter
import (
"context"
"encoding/hex"
"net"
"net/netip"
"strings"
"time"
@@ -20,7 +18,6 @@ type NetworkManager interface {
UpdateInterfaces() error
DefaultNetworkInterface() *NetworkInterface
NetworkInterfaces() []NetworkInterface
NetworkEnvironment() uint64
AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func
ProtectFunc() control.Func
@@ -33,8 +30,8 @@ type NetworkManager interface {
PackageManager() tun.PackageManager
NeedWIFIState() bool
WIFIState() WIFIState
UpdateWIFIState(ctx context.Context)
ResetNetwork(ctx context.Context)
UpdateWIFIState()
ResetNetwork()
}
type NetworkOptions struct {
@@ -49,7 +46,7 @@ type NetworkOptions struct {
}
type InterfaceUpdateListener interface {
InterfaceUpdated(ctx context.Context)
InterfaceUpdated()
}
type WIFIState struct {
@@ -79,7 +76,6 @@ type NetworkInterface struct {
control.Interface
Type C.InterfaceType
DNSServers []string
Gateways []netip.Addr
Expensive bool
Constrained bool
}
-101
View File
@@ -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
}
-57
View File
@@ -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
}
+5 -19
View File
@@ -3,6 +3,7 @@ package adapter
import (
"context"
"net/netip"
"time"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
@@ -22,19 +23,13 @@ type Outbound interface {
type OutboundWithPreferredRoutes interface {
Outbound
PreferredDomain(metadata *InboundContext, domain string) bool
PreferredAddress(metadata *InboundContext, address netip.Addr) bool
PreferredDomain(domain string) bool
PreferredAddress(address netip.Addr) bool
}
type OutboundWithMultiplex interface {
type DirectRouteOutbound interface {
Outbound
MultiplexEnabled() bool
}
type FlowOutbound interface {
Outbound
tun.Port
PreMatchFlow(network string, destination netip.Addr) PreMatchAction
NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
}
type OutboundRegistry interface {
@@ -51,12 +46,3 @@ type OutboundManager interface {
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
}
+132 -102
View File
@@ -2,25 +2,29 @@ package outbound
import (
"context"
"io"
"os"
"strings"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
var _ adapter.OutboundManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.OutboundRegistry
endpoint adapter.EndpointManager
defaultTag string
access sync.RWMutex
startAccess sync.Mutex
scope *adapter.Scope
started bool
stage adapter.StartStage
outbounds []adapter.Outbound
outboundByTag map[string]adapter.Outbound
@@ -29,8 +33,9 @@ type Manager struct {
defaultOutboundFallback func() (adapter.Outbound, error)
}
func NewManager(registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
return &Manager{
logger: logger,
registry: registry,
endpoint: endpoint,
defaultTag: defaultTag,
@@ -43,13 +48,14 @@ func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, e
m.defaultOutboundFallback = defaultOutboundFallback
}
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
m.startAccess.Lock()
defer m.startAccess.Unlock()
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
m.scope = scope
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
if stage == adapter.StartStateInitialize {
if stage == adapter.StartStateStart {
if m.defaultTag != "" && m.defaultOutbound == nil {
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
if !loaded {
@@ -68,27 +74,27 @@ func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
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
outbounds := m.outbounds
m.access.Unlock()
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
} else {
outbounds := m.outbounds
m.access.Unlock()
for _, outbound := range outbounds {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(outbound, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
return nil
}
func (m *Manager) startOutbounds(scope *adapter.Scope, outbounds []adapter.Outbound) error {
func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
monitor := taskmonitor.New(m.logger, C.StartTimeout)
started := make(map[string]bool)
for {
canContinue := false
@@ -106,21 +112,27 @@ func (m *Manager) startOutbounds(scope *adapter.Scope, outbounds []adapter.Outbo
}
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 starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
done := adapter.LogElapsed(m.logger, "start ", name)
monitor.Start("start ", name)
err := starter.Start(adapter.StartStateStart)
monitor.Finish()
done()
if err != nil {
return E.Cause(err, "start ", name)
}
} else if starter, isStarter := outboundToStart.(interface {
Start() error
}); isStarter {
done := adapter.LogElapsed(m.logger, "start ", name)
monitor.Start("start ", name)
err := starter.Start()
monitor.Finish()
done()
if err != nil {
return E.Cause(err, "start ", name)
}
}
}
if len(started) == len(outbounds) {
@@ -140,9 +152,9 @@ func (m *Manager) startOutbounds(scope *adapter.Scope, outbounds []adapter.Outbo
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
})
m.access.Lock()
problemOutbound := m.outboundByTag[problemOutboundTag]
m.access.Unlock()
if problemOutbound == nil {
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]")
}
@@ -153,6 +165,33 @@ func (m *Manager) startOutbounds(scope *adapter.Scope, outbounds []adapter.Outbo
return nil
}
func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
m.access.Lock()
if !m.started {
m.access.Unlock()
return nil
}
m.started = false
outbounds := m.outbounds
m.outbounds = nil
m.access.Unlock()
var err error
for _, outbound := range outbounds {
if closer, isCloser := outbound.(io.Closer); isCloser {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
}
return nil
}
func (m *Manager) Outbounds() []adapter.Outbound {
m.access.RLock()
defer m.access.RUnlock()
@@ -176,38 +215,47 @@ func (m *Manager) Default() adapter.Outbound {
}
func (m *Manager) Remove(tag string) error {
m.startAccess.Lock()
defer m.startAccess.Unlock()
m.access.Lock()
defer m.access.Unlock()
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
index := common.Index(m.outbounds, func(it adapter.Outbound) bool {
return it == outbound
})
m.removeDependencies(outbound)
if index == -1 {
panic("invalid inbound index")
}
m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...)
started := m.started
if m.defaultOutbound == outbound {
if len(m.outbounds) > 0 {
m.defaultOutbound = m.outbounds[0]
m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag())
} else {
m.defaultOutbound = nil
}
}
scope := m.scope
m.access.Unlock()
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
if scope == nil || !isLifecycle {
return nil
dependBy := m.dependByTag[tag]
if len(dependBy) > 0 {
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
}
return scope.Remove(lifecycle)
dependencies := outbound.Dependencies()
for _, dependency := range dependencies {
if len(m.dependByTag[dependency]) == 1 {
delete(m.dependByTag, dependency)
} else {
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
return it != tag
})
}
}
if started {
return common.Close(outbound)
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error {
@@ -218,63 +266,45 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
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 {
if m.started {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
for _, startStage := range adapter.ListStartStages[:stage+1] {
err = scope.Start(name, lifecycle, startStage)
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(outbound, stage)
done()
if err != nil {
return E.Errors(err, scope.Remove(lifecycle))
return E.Cause(err, stage, " ", name)
}
}
}
m.access.Lock()
existsOutbound, loaded := m.outboundByTag[tag]
if loaded {
m.outbounds = common.Filter(m.outbounds, func(it adapter.Outbound) bool {
return it != existsOutbound
defer m.access.Unlock()
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
if m.started {
err = common.Close(existsOutbound)
if err != nil {
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
}
}
existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool {
return it == existsOutbound
})
m.removeDependencies(existsOutbound)
if existsIndex == -1 {
panic("invalid inbound index")
}
m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...)
}
m.outbounds = append(m.outbounds, outbound)
m.outboundByTag[tag] = outbound
for _, dependency := range outbound.Dependencies() {
dependencies := outbound.Dependencies()
for _, dependency := range dependencies {
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
}
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) || (loaded && m.defaultOutbound == existsOutbound) {
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
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(), "]")
if m.started {
m.logger.Info("updated default outbound to ", outbound.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
}
}
}
-8
View File
@@ -2,8 +2,6 @@ package outbound
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -46,12 +44,6 @@ func NewRegistry() *Registry {
}
}
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()
+2 -47
View File
@@ -1,7 +1,6 @@
package adapter
import (
"context"
"net/netip"
"github.com/sagernet/sing-box/option"
@@ -14,11 +13,9 @@ type PlatformInterface interface {
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
@@ -31,7 +28,8 @@ type PlatformInterface interface {
ClearDNSCache()
RequestPermissionForWIFIState() error
ReadWIFIState(ctx context.Context) WIFIState
ReadWIFIState() WIFIState
SystemCertificates() []string
UsePlatformConnectionOwnerFinder() bool
FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error)
@@ -40,51 +38,8 @@ type PlatformInterface interface {
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 {
-1
View File
@@ -10,7 +10,6 @@ import (
)
type Provider interface {
Lifecycle
Type() string
Tag() string
Outbounds() []Outbound
+24 -83
View File
@@ -3,8 +3,6 @@ package provider
import (
"context"
"reflect"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
@@ -28,21 +26,18 @@ type Adapter struct {
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
history adapter.URLTestHistoryStorage
callbackAccess sync.Mutex
callbacks list.List[adapter.ProviderUpdateCallback]
link string
enabled bool
removeEmojis bool
overrideDialerOptions *option.DialerOptions
timeout time.Duration
interval time.Duration
link string
enabled bool
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 {
@@ -73,23 +68,16 @@ func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.Out
}
}
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)
a.history = service.FromContext[adapter.URLTestHistoryStorage](a.ctx)
if a.history == nil {
a.history = urltest.NewHistoryStorage()
}
if a.enabled {
a.ticker = time.NewTicker(a.interval)
go a.loopCheck()
if clashServer := service.FromContext[adapter.ClashServer](a.ctx); clashServer != nil {
a.history = clashServer.HistoryStorage()
} else {
a.history = urltest.NewHistoryStorage()
}
}
go a.loopCheck()
return nil
}
@@ -102,23 +90,18 @@ func (a *Adapter) Tag() string {
}
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()
if a.outboundsByTag == nil {
return nil, false
}
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)
@@ -129,11 +112,6 @@ func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Ou
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)
@@ -161,13 +139,11 @@ func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Ou
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)
}
a.outbounds = outbounds
a.outboundsByTag = outboundsByTag
}
func (a *Adapter) HealthCheck(ctx context.Context) (map[string]uint16, error) {
@@ -193,21 +169,14 @@ 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 {
@@ -220,6 +189,10 @@ func (a *Adapter) Close() error {
}
func (a *Adapter) loopCheck() {
if !a.enabled {
return
}
a.ticker = time.NewTicker(a.interval)
a.healthcheck(a.ctx)
for {
select {
@@ -240,7 +213,7 @@ func (a *Adapter) healthcheck(ctx context.Context) (map[string]uint16, error) {
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
var resultAccess sync.Mutex
checked := make(map[string]bool)
for _, detour := range a.Outbounds() {
for _, detour := range a.outbounds {
tag := detour.Tag()
if checked[tag] {
continue
@@ -271,8 +244,7 @@ func (a *Adapter) healthcheck(ctx context.Context) (map[string]uint16, error) {
}
func (a *Adapter) removeUseless(newOpts []option.Outbound) {
outbounds := a.Outbounds()
if len(outbounds) == 0 {
if len(a.outbounds) == 0 {
return
}
exists := make(map[string]bool)
@@ -285,7 +257,7 @@ func (a *Adapter) removeUseless(newOpts []option.Outbound) {
}
exists[tag] = true
}
for _, opt := range outbounds {
for _, opt := range a.outbounds {
if !exists[opt.Tag()] {
if err := a.outbound.Remove(opt.Tag()); err != nil {
a.logger.Error(err, "close outbound [", opt.Tag(), "]")
@@ -293,34 +265,3 @@ func (a *Adapter) removeUseless(newOpts []option.Outbound) {
}
}
}
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,}`)
-45
View File
@@ -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)
}
}
}
+73 -41
View File
@@ -2,46 +2,80 @@ package provider
import (
"context"
"io"
"os"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
var _ adapter.ProviderManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ProviderRegistry
access sync.Mutex
startAccess sync.Mutex
scope *adapter.Scope
started bool
stage adapter.StartStage
providers []adapter.Provider
providerByTag map[string]adapter.Provider
wg sync.WaitGroup
}
func NewManager(registry adapter.ProviderRegistry) *Manager {
func NewManager(logger logger.ContextLogger, registry adapter.ProviderRegistry) *Manager {
return &Manager{
logger: logger,
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()
func (m *Manager) Initialize() {
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
m.scope = scope
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
providers := m.providers
m.access.Unlock()
for _, provider := range providers {
err := scope.Start("provider/"+provider.Type()+"["+provider.Tag()+"]", provider, stage)
err := adapter.LegacyStart(provider, stage)
if err != nil {
return err
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
}
}
return nil
}
func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
m.access.Lock()
if !m.started {
m.access.Unlock()
return nil
}
m.started = false
providers := m.providers
m.providers = nil
m.access.Unlock()
var err error
for _, provider := range providers {
if closer, isCloser := provider.(io.Closer); isCloser {
monitor.Start("close provider/", provider.Type(), "[", provider.Tag(), "]")
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close provider/", provider.Type(), "[", provider.Tag(), "]")
})
monitor.Finish()
}
}
return nil
@@ -61,8 +95,6 @@ func (m *Manager) Get(tag string) (adapter.Provider, bool) {
}
func (m *Manager) Remove(tag string) error {
m.startAccess.Lock()
defer m.startAccess.Unlock()
m.access.Lock()
provider, found := m.providerByTag[tag]
if !found {
@@ -70,56 +102,56 @@ func (m *Manager) Remove(tag string) error {
return os.ErrInvalid
}
delete(m.providerByTag, tag)
m.providers = common.Filter(m.providers, func(it adapter.Provider) bool {
return it != provider
index := common.Index(m.providers, func(it adapter.Provider) bool {
return it == provider
})
scope := m.scope
m.access.Unlock()
if scope == nil {
return nil
if index == -1 {
panic("invalid provider index")
}
return scope.Remove(provider)
m.providers = append(m.providers[:index], m.providers[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return common.Close(provider)
}
return nil
}
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)
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(provider, stage)
if err != nil {
return E.Errors(err, scope.Remove(provider))
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
}
}
}
m.access.Lock()
existsProvider, loaded := m.providerByTag[tag]
if loaded {
m.providers = common.Filter(m.providers, func(it adapter.Provider) bool {
return it != existsProvider
if existsProvider, loaded := m.providerByTag[tag]; loaded {
if m.started {
err = common.Close(existsProvider)
if err != nil {
return E.Cause(err, "close provider", provider.Type(), "[", existsProvider.Tag(), "]")
}
}
existsIndex := common.Index(m.providers, func(it adapter.Provider) bool {
return it == existsProvider
})
if existsIndex == -1 {
panic("invalid provider index")
}
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
}
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
}
+52 -96
View File
@@ -2,14 +2,17 @@ package adapter
import (
"context"
"crypto/tls"
"net"
"net/netip"
"net/http"
"sync"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/common/x/list"
"go4.org/netipx"
@@ -18,97 +21,18 @@ import (
type Router interface {
Lifecycle
ConnectionRouter
PreMatch(metadata InboundContext, firstPacket []byte) PreMatchResult
HijackDNSPacket(ctx context.Context, payload []byte, writer N.PacketWriter, metadata InboundContext)
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error)
ConnectionRouterEx
RuleSet(tag string) (RuleSet, bool)
Rules() []Rule
NeedFindProcess() bool
NeedFindNeighbor() bool
NeighborResolver() NeighborResolver
AppendTracker(tracker ConnectionTracker)
ResetNetwork()
}
type PreMatchAction uint8
const (
PreMatchContinue PreMatchAction = iota
PreMatchFlow
PreMatchReject
PreMatchDrop
PreMatchBypass
PreMatchHijackDNS
)
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.
@@ -126,6 +50,7 @@ type ConnectionRouterEx interface {
type RuleSet interface {
Name() string
StartContext(ctx context.Context, startContext *HTTPStartContext) error
PostStart() error
Metadata() RuleSetMetadata
ExtractIPSet() []*netipx.IPSet
IncRef()
@@ -139,20 +64,51 @@ type RuleSet interface {
type RuleSetUpdateCallback func(it RuleSet)
type DNSRuleSetUpdateValidator interface {
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
}
type HTTPStartContext struct {
ctx context.Context
access sync.Mutex
httpClientCache map[string]*http.Client
}
// 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
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
return &HTTPStartContext{
ctx: ctx,
httpClientCache: make(map[string]*http.Client),
}
}
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
c.access.Lock()
defer c.access.Unlock()
if httpClient, loaded := c.httpClientCache[detour]; loaded {
return httpClient
}
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: C.TCPTimeout,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
TLSClientConfig: &tls.Config{
Time: ntp.TimeFuncFromContext(c.ctx),
RootCAs: RootPoolFromContext(c.ctx),
},
},
}
c.httpClientCache[detour] = httpClient
return httpClient
}
func (c *HTTPStartContext) Close() {
c.access.Lock()
defer c.access.Unlock()
for _, client := range c.httpClientCache {
client.CloseIdleConnections()
}
}
+2 -9
View File
@@ -2,8 +2,6 @@ package adapter
import (
C "github.com/sagernet/sing-box/constant"
"github.com/miekg/dns"
)
type HeadlessRule interface {
@@ -20,13 +18,8 @@ type Rule interface {
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
MatchAddressLimit(metadata *InboundContext) bool
}
type RuleAction interface {
@@ -36,7 +29,7 @@ type RuleAction interface {
func IsFinalAction(action RuleAction) bool {
switch action.Type() {
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
return false
default:
return true
-19
View File
@@ -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)
}
+1
View File
@@ -22,5 +22,6 @@ type ServiceManager interface {
Lifecycle
Services() []Service
Get(tag string) (Service, bool)
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
}
+91 -7
View File
@@ -2,43 +2,82 @@ package service
import (
"context"
"os"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
var _ adapter.ServiceManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ServiceRegistry
access sync.Mutex
started bool
stage adapter.StartStage
services []adapter.Service
serviceByTag map[string]adapter.Service
}
func NewManager(registry adapter.ServiceRegistry) *Manager {
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
serviceByTag: make(map[string]adapter.Service),
}
}
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
services := m.services
m.access.Unlock()
for _, service := range services {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
err := scope.Start(name, service, stage)
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(service, stage)
done()
if err != nil {
return err
return E.Cause(err, stage, " ", name)
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
services := m.services
m.services = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, service := range services {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, service.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
return nil
}
func (m *Manager) Services() []adapter.Service {
m.access.Lock()
defer m.access.Unlock()
@@ -52,6 +91,29 @@ func (m *Manager) Get(tag string) (adapter.Service, bool) {
return service, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
service, found := m.serviceByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.serviceByTag, tag)
index := common.Index(m.services, func(it adapter.Service) bool {
return it == service
})
if index == -1 {
panic("invalid service index")
}
m.services = append(m.services[:index], m.services[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return service.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
if err != nil {
@@ -59,9 +121,31 @@ func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag stri
}
m.access.Lock()
defer m.access.Unlock()
_, loaded := m.serviceByTag[tag]
if loaded {
return E.New("duplicate service tag: ", tag)
if m.started {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(service, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
if existsService, loaded := m.serviceByTag[tag]; loaded {
if m.started {
err = existsService.Close()
if err != nil {
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
}
}
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
return it == existsService
})
if existsIndex == -1 {
panic("invalid service index")
}
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
}
m.services = append(m.services, service)
m.serviceByTag[tag] = service
-8
View File
@@ -2,8 +2,6 @@ package service
import (
"context"
"maps"
"slices"
"sync"
"github.com/sagernet/sing-box/adapter"
@@ -46,12 +44,6 @@ func NewRegistry() *Registry {
}
}
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()
-14
View File
@@ -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
}
-104
View File
@@ -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
}
+38 -38
View File
@@ -9,31 +9,31 @@ import (
)
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)
ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
)
func NewUpstreamHandler(
func NewUpstreamHandlerEx(
metadata InboundContext,
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
) UpstreamHandlerAdapter {
return &myUpstreamHandlerWrapper{
connectionHandler ConnectionHandlerFuncEx,
packetHandler PacketConnectionHandlerFuncEx,
) UpstreamHandlerAdapterEx {
return &myUpstreamHandlerWrapperEx{
metadata: metadata,
connectionHandler: connectionHandler,
packetHandler: packetHandler,
}
}
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil)
type myUpstreamHandlerWrapper struct {
type myUpstreamHandlerWrapperEx struct {
metadata InboundContext
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
connectionHandler ConnectionHandlerFuncEx
packetHandler PacketConnectionHandlerFuncEx
}
func (w *myUpstreamHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := w.metadata
if source.IsValid() {
myMetadata.Source = source
@@ -44,7 +44,7 @@ func (w *myUpstreamHandlerWrapper) NewConnectionEx(ctx context.Context, conn net
w.connectionHandler(ctx, conn, myMetadata, onClose)
}
func (w *myUpstreamHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := w.metadata
if source.IsValid() {
myMetadata.Source = source
@@ -55,24 +55,24 @@ func (w *myUpstreamHandlerWrapper) NewPacketConnectionEx(ctx context.Context, co
w.packetHandler(ctx, conn, myMetadata, onClose)
}
var _ UpstreamHandlerAdapter = (*myUpstreamContextHandlerWrapper)(nil)
var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil)
type myUpstreamContextHandlerWrapper struct {
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
type myUpstreamContextHandlerWrapperEx struct {
connectionHandler ConnectionHandlerFuncEx
packetHandler PacketConnectionHandlerFuncEx
}
func NewUpstreamContextHandler(
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
) UpstreamHandlerAdapter {
return &myUpstreamContextHandlerWrapper{
func NewUpstreamContextHandlerEx(
connectionHandler ConnectionHandlerFuncEx,
packetHandler PacketConnectionHandlerFuncEx,
) UpstreamHandlerAdapterEx {
return &myUpstreamContextHandlerWrapperEx{
connectionHandler: connectionHandler,
packetHandler: packetHandler,
}
}
func (w *myUpstreamContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
func (w *myUpstreamContextHandlerWrapperEx) 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
@@ -83,7 +83,7 @@ func (w *myUpstreamContextHandlerWrapper) NewConnectionEx(ctx context.Context, c
w.connectionHandler(ctx, conn, *myMetadata, onClose)
}
func (w *myUpstreamContextHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
func (w *myUpstreamContextHandlerWrapperEx) 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
@@ -94,24 +94,24 @@ func (w *myUpstreamContextHandlerWrapper) NewPacketConnectionEx(ctx context.Cont
w.packetHandler(ctx, conn, *myMetadata, onClose)
}
func NewRouteHandler(
func NewRouteHandlerEx(
metadata InboundContext,
router ConnectionRouterEx,
) UpstreamHandlerAdapter {
return &routeHandlerWrapper{
) UpstreamHandlerAdapterEx {
return &routeHandlerWrapperEx{
metadata: metadata,
router: router,
}
}
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil)
type routeHandlerWrapper struct {
type routeHandlerWrapperEx struct {
metadata InboundContext
router ConnectionRouterEx
}
func (r *routeHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
if source.IsValid() {
r.metadata.Source = source
}
@@ -121,7 +121,7 @@ func (r *routeHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn
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) {
func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
if source.IsValid() {
r.metadata.Source = source
}
@@ -131,21 +131,21 @@ func (r *routeHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.
r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose)
}
func NewRouteContextHandler(
func NewRouteContextHandlerEx(
router ConnectionRouterEx,
) UpstreamHandlerAdapter {
return &routeContextHandlerWrapper{
) UpstreamHandlerAdapterEx {
return &routeContextHandlerWrapperEx{
router: router,
}
}
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil)
type routeContextHandlerWrapper struct {
type routeContextHandlerWrapperEx struct {
router ConnectionRouterEx
}
func (r *routeContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
func (r *routeContextHandlerWrapperEx) 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
@@ -156,7 +156,7 @@ func (r *routeContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn n
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) {
func (r *routeContextHandlerWrapperEx) 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
+50 -59
View File
@@ -12,30 +12,21 @@ import (
type (
// Deprecated
LegacyConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
// Deprecated
LegacyPacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
PacketConnectionHandlerFunc = 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(
func NewUpstreamHandler(
metadata InboundContext,
connectionHandler LegacyConnectionHandlerFunc,
packetHandler LegacyPacketConnectionHandlerFunc,
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
errorHandler E.Handler,
) LegacyUpstreamHandlerAdapter {
return &legacyUpstreamHandlerWrapper{
) UpstreamHandlerAdapter {
return &myUpstreamHandlerWrapper{
metadata: metadata,
connectionHandler: connectionHandler,
packetHandler: packetHandler,
@@ -43,20 +34,20 @@ func NewLegacyUpstreamHandler(
}
}
var _ LegacyUpstreamHandlerAdapter = (*legacyUpstreamHandlerWrapper)(nil)
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
// Deprecated: use NewUpstreamHandler instead.
// Deprecated: use myUpstreamHandlerWrapperEx instead.
//
//nolint:staticcheck
type legacyUpstreamHandlerWrapper struct {
type myUpstreamHandlerWrapper struct {
metadata InboundContext
connectionHandler LegacyConnectionHandlerFunc
packetHandler LegacyPacketConnectionHandlerFunc
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler
}
// Deprecated: use NewUpstreamHandler instead.
func (w *legacyUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
// Deprecated: use myUpstreamHandlerWrapperEx instead.
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -67,8 +58,8 @@ func (w *legacyUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn n
return w.connectionHandler(ctx, conn, myMetadata)
}
// Deprecated: use NewUpstreamHandler instead.
func (w *legacyUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
// Deprecated: use myUpstreamHandlerWrapperEx instead.
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -79,8 +70,8 @@ func (w *legacyUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context,
return w.packetHandler(ctx, conn, myMetadata)
}
// Deprecated: use NewUpstreamHandler instead.
func (w *legacyUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
// Deprecated: use myUpstreamHandlerWrapperEx instead.
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
w.errorHandler.NewError(ctx, err)
}
@@ -92,28 +83,28 @@ func UpstreamMetadata(metadata InboundContext) M.Metadata {
}
}
// Deprecated: Use NewUpstreamContextHandler instead.
type legacyUpstreamContextHandlerWrapper struct {
connectionHandler LegacyConnectionHandlerFunc
packetHandler LegacyPacketConnectionHandlerFunc
// Deprecated: Use NewUpstreamContextHandlerEx instead.
type myUpstreamContextHandlerWrapper struct {
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler
}
// Deprecated: Use NewUpstreamContextHandler instead.
func NewLegacyUpstreamContextHandler(
connectionHandler LegacyConnectionHandlerFunc,
packetHandler LegacyPacketConnectionHandlerFunc,
// Deprecated: Use NewUpstreamContextHandlerEx instead.
func NewUpstreamContextHandler(
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
errorHandler E.Handler,
) LegacyUpstreamHandlerAdapter {
return &legacyUpstreamContextHandlerWrapper{
) UpstreamHandlerAdapter {
return &myUpstreamContextHandlerWrapper{
connectionHandler: connectionHandler,
packetHandler: packetHandler,
errorHandler: errorHandler,
}
}
// Deprecated: Use NewUpstreamContextHandler instead.
func (w *legacyUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
// Deprecated: Use NewUpstreamContextHandlerEx instead.
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -124,8 +115,8 @@ func (w *legacyUpstreamContextHandlerWrapper) NewConnection(ctx context.Context,
return w.connectionHandler(ctx, conn, *myMetadata)
}
// Deprecated: Use NewUpstreamContextHandler instead.
func (w *legacyUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
// Deprecated: Use NewUpstreamContextHandlerEx instead.
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -136,18 +127,18 @@ func (w *legacyUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Co
return w.packetHandler(ctx, conn, *myMetadata)
}
// Deprecated: Use NewUpstreamContextHandler instead.
func (w *legacyUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
// Deprecated: Use NewUpstreamContextHandlerEx instead.
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.errorHandler.NewError(ctx, err)
}
// Deprecated: Use ConnectionRouterEx instead.
func NewLegacyRouteHandler(
func NewRouteHandler(
metadata InboundContext,
router ConnectionRouter,
logger logger.ContextLogger,
) LegacyUpstreamHandlerAdapter {
return &legacyRouteHandlerWrapper{
) UpstreamHandlerAdapter {
return &routeHandlerWrapper{
metadata: metadata,
router: router,
logger: logger,
@@ -155,29 +146,29 @@ func NewLegacyRouteHandler(
}
// Deprecated: Use ConnectionRouterEx instead.
func NewLegacyRouteContextHandler(
func NewRouteContextHandler(
router ConnectionRouter,
logger logger.ContextLogger,
) LegacyUpstreamHandlerAdapter {
return &legacyRouteContextHandlerWrapper{
) UpstreamHandlerAdapter {
return &routeContextHandlerWrapper{
router: router,
logger: logger,
}
}
var _ LegacyUpstreamHandlerAdapter = (*legacyRouteHandlerWrapper)(nil)
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
// Deprecated: Use ConnectionRouterEx instead.
//
//nolint:staticcheck
type legacyRouteHandlerWrapper struct {
type routeHandlerWrapper 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 {
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -189,7 +180,7 @@ func (w *legacyRouteHandlerWrapper) NewConnection(ctx context.Context, conn net.
}
// Deprecated: Use ConnectionRouterEx instead.
func (w *legacyRouteHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -201,20 +192,20 @@ func (w *legacyRouteHandlerWrapper) NewPacketConnection(ctx context.Context, con
}
// Deprecated: Use ConnectionRouterEx instead.
func (w *legacyRouteHandlerWrapper) NewError(ctx context.Context, err error) {
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}
var _ LegacyUpstreamHandlerAdapter = (*legacyRouteContextHandlerWrapper)(nil)
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
// Deprecated: Use ConnectionRouterEx instead.
type legacyRouteContextHandlerWrapper struct {
type routeContextHandlerWrapper struct {
router ConnectionRouter
logger logger.ContextLogger
}
// Deprecated: Use ConnectionRouterEx instead.
func (w *legacyRouteContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -226,7 +217,7 @@ func (w *legacyRouteContextHandlerWrapper) NewConnection(ctx context.Context, co
}
// Deprecated: Use ConnectionRouterEx instead.
func (w *legacyRouteContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
@@ -238,6 +229,6 @@ func (w *legacyRouteContextHandlerWrapper) NewPacketConnection(ctx context.Conte
}
// Deprecated: Use ConnectionRouterEx instead.
func (w *legacyRouteContextHandlerWrapper) NewError(ctx context.Context, err error) {
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}
-15
View File
@@ -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))
}
-7
View File
@@ -1,7 +0,0 @@
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
package adapter
type USBIPDynamicServer interface {
usbipNotIncluded()
}
-5
View File
@@ -22,8 +22,3 @@ type V2RayClientTransport interface {
DialContext(ctx context.Context) (net.Conn, error)
Close() error
}
type V2RayMultiplexClientTransport interface {
V2RayClientTransport
MultiplexEnabled() bool
}
+136 -266
View File
@@ -2,11 +2,13 @@ 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"
@@ -14,18 +16,13 @@ import (
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/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/direct"
@@ -41,33 +38,27 @@ import (
var _ adapter.SimpleLifecycle = (*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
logFactory log.Factory
logger log.ContextLogger
network *route.NetworkManager
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
provider *provider.Manager
service *boxService.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
internalService []adapter.LifecycleService
done chan struct{}
}
type Options struct {
option.Options
Context context.Context
PlatformLogWriter log.PlatformWriter
NetworkNamespaceHolderArgs []string
Context context.Context
PlatformLogWriter log.PlatformWriter
}
func Context(
@@ -78,7 +69,6 @@ func Context(
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 {
@@ -108,10 +98,6 @@ func Context(
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
}
@@ -129,7 +115,6 @@ func New(options Options) (*Box, error) {
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")
@@ -149,14 +134,10 @@ func New(options Options) (*Box, error) {
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)
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
debugOptions := common.PtrValueOrDefault(experimentalOptions.Debug)
err := checkDebugOptions(debugOptions)
err := applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
if err != nil {
return nil, err
}
@@ -166,7 +147,7 @@ func New(options Options) (*Box, error) {
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
needCacheFile = true
}
if experimentalOptions.ClashAPI != nil {
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
needClashAPI = true
}
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
@@ -175,12 +156,6 @@ func New(options Options) (*Box, error) {
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 {
@@ -189,7 +164,7 @@ func New(options Options) (*Box, error) {
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,
@@ -200,7 +175,6 @@ func New(options Options) (*Box, error) {
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 ||
@@ -213,65 +187,36 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
internalServices = append(internalServices, certificateStore)
}
netnsManager, err := netns.NewManager(logFactory.NewLogger("netns"), options.NetworkNamespaces, options.NetworkNamespaceHolderArgs)
if err != nil {
return nil, err
}
service.MustRegister[adapter.NetworkNamespaceManager](ctx, netnsManager)
internalServices = append(internalServices, netnsManager)
routeOptions := common.PtrValueOrDefault(options.Route)
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)
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
providerManager := provider.NewManager(logFactory.NewLogger("provider"), providerRegistry)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.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")
}
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
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)
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
service.MustRegister[adapter.Router](ctx, router)
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
if err != nil {
return nil, E.Cause(err, "initialize router")
}
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 {
@@ -349,24 +294,6 @@ func New(options Options) (*Box, error) {
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, "]")
}
}
for i, outboundOptions := range options.Outbounds {
var tag string
if outboundOptions.Tag != "" {
@@ -412,22 +339,22 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize provider[", i, "]")
}
}
for i, certificateProviderOptions := range options.CertificateProviders {
for i, serviceOptions := range options.Services {
var tag string
if certificateProviderOptions.Tag != "" {
tag = certificateProviderOptions.Tag
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
} else {
tag = F.ToString(i)
}
err = certificateProviderManager.Create(
err = serviceManager.Create(
ctx,
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
tag,
certificateProviderOptions.Type,
certificateProviderOptions.Options,
serviceOptions.Type,
serviceOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize certificate provider[", i, "]")
return nil, E.Cause(err, "initialize service[", i, "]")
}
}
outboundManager.Initialize(func() (adapter.Outbound, error) {
@@ -448,12 +375,6 @@ func New(options Options) (*Box, error) {
&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 err != nil {
@@ -461,15 +382,19 @@ func New(options Options) (*Box, error) {
}
}
if needCacheFile {
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
internalServices = append(internalServices, cacheFile)
}
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, logFactory.(log.ObservableFactory), clashAPIOptions)
if err != nil {
return nil, E.Cause(err, "create clash-server")
}
router.AppendTracker(clashServer)
service.MustRegister[adapter.ClashServer](ctx, clashServer)
internalServices = append(internalServices, clashServer)
}
if needV2RayAPI {
@@ -483,19 +408,12 @@ func New(options Options) (*Box, error) {
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{
ntpService := ntp.NewService(ntp.Options{
Context: ctx,
Dialer: ntpDialer,
Logger: logFactory.NewLogger("ntp"),
@@ -504,73 +422,62 @@ func New(options Options) (*Box, error) {
WriteToSystem: ntpOptions.WriteToSystem,
})
timeService.TimeService = ntpService
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
}
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()),
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
provider: providerManager,
dnsTransport: dnsTransportManager,
service: serviceManager,
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
internalService: internalServices,
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 {
println(err.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 {
println(err.Error())
debug.PrintStack()
println("panic on early start: " + 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
}
@@ -582,46 +489,15 @@ func (s *Box) preStart() error {
if err != nil {
return E.Cause(err, "start logger")
}
s.scope.Add(s.logFactory.Close)
applyDebugOptions(s.debugOptions)
debugHTTPServer, err := startDebugHTTPServer(s.debugOptions)
err = adapter.StartNamed(s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
if err != nil {
return err
}
if debugHTTPServer != nil {
s.scope.Add(debugHTTPServer.Close)
}
err = s.startInternalServices(adapter.StartStateInitialize) // cache-file clash-api v2ray-api
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.provider, s.service)
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},
)
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.provider, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
if err != nil {
return err
}
@@ -633,65 +509,27 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = s.startInternalServices(adapter.StartStateStart)
err = adapter.StartNamed(s.logger, adapter.StartStateStart, s.internalService)
if err != nil {
return err
}
if s.ntpService != nil {
done := adapter.LogElapsed(s.logger, "start ntp service")
err = s.ntpService.Start()
done()
if err != nil {
return E.Cause(err, "start ntp service")
}
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},
)
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
if err != nil {
return err
}
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},
)
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.provider, s.service)
if err != nil {
return err
}
err = s.startInternalServices(adapter.StartStatePostStart)
err = adapter.StartNamed(s.logger, adapter.StartStatePostStart, s.internalService)
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},
)
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.provider, s.service)
if err != nil {
return err
}
err = s.startInternalServices(adapter.StartStateStarted)
err = adapter.StartNamed(s.logger, adapter.StartStateStarted, s.internalService)
if err != nil {
return err
}
@@ -699,7 +537,47 @@ func (s *Box) start() error {
}
func (s *Box) Close() error {
return s.scope.Close()
select {
case <-s.done:
return os.ErrClosed
default:
close(s.done)
}
var err error
for _, closeItem := range []struct {
name string
service adapter.Lifecycle
}{
{"service", s.service},
{"endpoint", s.endpoint},
{"inbound", s.inbound},
{"provider", s.provider},
{"outbound", s.outbound},
{"router", s.router},
{"connection", s.connection},
{"dns-router", s.dnsRouter},
{"dns-transport", s.dnsTransport},
{"network", s.network},
} {
done := adapter.LogElapsed(s.logger, "close ", closeItem.name)
err = E.Append(err, closeItem.service.Close(), func(err error) error {
return E.Cause(err, "close ", closeItem.name)
})
done()
}
for _, lifecycleService := range s.internalService {
done := adapter.LogElapsed(s.logger, "close ", lifecycleService.Name())
err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name())
})
done()
}
done := adapter.LogElapsed(s.logger, "close logger")
err = E.Append(err, s.logFactory.Close(), func(err error) error {
return E.Cause(err, "close logger")
})
done()
return err
}
func (s *Box) Network() adapter.NetworkManager {
@@ -722,14 +600,6 @@ 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
}
+1
Submodule clients/android added at eb87216961
Submodule
+1
Submodule clients/apple added at c19945f65b
Submodule clients/desktop deleted from 50f5038491
+241 -447
View File
@@ -4,14 +4,12 @@ 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"
@@ -21,11 +19,8 @@ import (
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])
case "next_macos_project_version":
err := fetchMacOSVersion(ctx)
if err != nil {
log.Fatal(err)
}
@@ -34,37 +29,18 @@ func main() {
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:])
case "prepare_app_store":
err := prepareAppStore(ctx)
if err != nil {
log.Fatal(err)
}
case "publish_app_store":
err := publishAppStore(ctx, os.Args[2:])
err := publishAppStore(ctx)
if err != nil {
log.Fatal(err)
}
@@ -74,37 +50,10 @@ func main() {
}
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
appID = "6673731168"
groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda"
)
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 {
@@ -117,84 +66,36 @@ func createClient(expireDuration time.Duration) *asc.Client {
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)}
}
func fetchMacOSVersion(ctx context.Context) error {
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,
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{"MAC_OS"},
})
if err != nil {
return err
}
for _, build := range builds.Data {
var betaGroup string
if slices.Contains(betaBuildIDs, build.ID) {
betaGroup = " testflight"
var versionID string
findVersion:
for _, version := range versions.Data {
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStateReadyForSale,
asc.AppStoreVersionStatePendingDeveloperRelease:
versionID = version.ID
break findVersion
}
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"))
}
if versionID == "" {
return E.New("no version found")
}
latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{})
if err != nil {
return err
}
versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version)
if err != nil {
return E.Cause(err, "parse version code")
}
os.Stdout.WriteString(F.ToString(versionInt+1, "\n"))
return nil
}
@@ -202,9 +103,16 @@ 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
var platform asc.Platform
switch os.Args[2] {
case "ios":
platform = asc.PlatformIOS
case "macos":
platform = asc.PlatformMACOS
case "tvos":
platform = asc.PlatformTVOS
default:
return E.New("unknown platform: ", os.Args[2])
}
tagVersion, err := build_shared.ReadTagVersion()
@@ -212,8 +120,6 @@ func publishTestflight(ctx context.Context) error {
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 {
@@ -231,37 +137,27 @@ func publishTestflight(ctx context.Context) error {
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")
waitingForProcess := false
log.Info(string(platform), " list builds")
for {
builds, _, err := client.Builds.ListBuilds(ctx, query)
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
})
if err != nil {
return err
}
if len(builds.Data) == 0 {
log.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)
log.Info(string(platform), " ", tag, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
continue
}
if *build.Attributes.ProcessingState != "VALID" {
log.Info(string(platform), " ", testFlightVersion, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(buildPollInterval)
waitingForProcess = true
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second)
continue
}
log.Info(string(platform), " ", tag, " list localizations")
@@ -286,7 +182,7 @@ func publishTestflight(ctx context.Context) error {
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)
time.Sleep(15 * time.Second)
continue
} else if err != nil {
return err
@@ -314,341 +210,227 @@ func publishTestflight(ctx context.Context) error {
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
func cancelAppStore(ctx context.Context, platform string) error {
switch platform {
case "ios":
platform = string(asc.PlatformIOS)
case "macos":
platform = string(asc.PlatformMACOS)
case "tvos":
platform = string(asc.PlatformTVOS)
}
tag, err := build_shared.ReadTag()
if err != nil {
return err
}
client := createClient(time.Minute)
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 {
for {
log.Info(platform, " list versions")
versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
if isRetryable(response) {
continue
} else if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
})
if version.ID == "" {
return nil
}
log.Info(platform, " ", tag, " get submission")
submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
if response != nil && response.StatusCode == http.StatusNotFound {
return nil
}
if isRetryable(response) {
continue
} else if err != nil {
return err
}
log.Info(platform, " ", tag, " delete submission")
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
if err != nil {
return err
}
return nil
}
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),
},
})
}
func prepareAppStore(ctx context.Context) error {
tag, err := build_shared.ReadTag()
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),
client := createClient(time.Minute)
for _, platform := range []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
} {
log.Info(string(platform), " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
if err != nil {
return err
}
}
var build asc.Build
for {
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
})
log.Info(string(platform), " ", tag, " list builds")
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
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
log.Fatal(platform, " ", tag, " no build found")
}
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
buildID := common.Ptr(builds.Data[0].ID)
if version.ID == "" {
log.Info(string(platform), " ", tag, " create version")
newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
Platform: platform,
VersionString: tag,
}, appID, buildID)
if err != nil {
return err
}
version = newVersion.Data
} else {
log.Info(string(platform), " ", tag, " check build")
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
if err != nil {
return err
}
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStatePrepareForSubmission,
asc.AppStoreVersionStateRejected,
asc.AppStoreVersionStateDeveloperRejected:
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview,
asc.AppStoreVersionStatePendingDeveloperRelease:
submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
if err != nil {
return err
}
if submission != nil {
log.Info(string(platform), " ", tag, " delete submission")
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
if err != nil {
return err
}
time.Sleep(5 * time.Second)
}
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
log.Info(string(platform), " ", tag, " update build")
response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
if err != nil {
return err
}
if response.StatusCode != http.StatusNoContent {
response.Write(os.Stderr)
log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
}
} else {
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStatePrepareForSubmission,
asc.AppStoreVersionStateRejected,
asc.AppStoreVersionStateDeveloperRejected:
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview,
asc.AppStoreVersionStatePendingDeveloperRelease:
continue
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
}
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))
log.Info(string(platform), " ", tag, " list localization")
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
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
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
return *it.Attributes.Locale == "en-US"
})
if localization.ID == "" {
log.Info(string(platform), " ", tag, " no en-US localization found")
}
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
})
if err != nil {
return err
}
}
log.Info(string(platform), " ", tag, " create submission")
fixSubmit:
for {
_, response, err := client.Submission.CreateSubmission(ctx, version.ID)
if err != nil {
switch response.StatusCode {
case http.StatusInternalServerError:
continue
default:
return err
}
}
switch response.StatusCode {
case http.StatusCreated:
break fixSubmit
default:
return err
}
}
}
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
func publishAppStore(ctx context.Context) error {
tag, err := build_shared.ReadTag()
if err != nil {
return err
}
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")
for _, platform := range []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
} {
log.Info(string(platform), " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
FilterVersionString: []string{versionString},
FilterPlatform: []string{string(platform)},
})
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 {
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
})
switch *version.Attributes.AppStoreState {
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")
log.Fatal(string(platform), " ", tag, " not submitted")
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview:
log.Warn(string(platform), " ", tag, " waiting for review")
continue
case asc.AppStoreVersionStatePendingDeveloperRelease:
default:
return E.New(string(platform), " ", versionString, " unknown state ", string(state))
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
log.Info(string(platform), " ", versionString, " release")
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
if err != nil {
return err
@@ -656,3 +438,15 @@ func publishAppStore(ctx context.Context, platformNames []string) error {
}
return nil
}
func isRetryable(response *asc.Response) bool {
if response == nil {
return false
}
switch response.StatusCode {
case http.StatusInternalServerError, http.StatusUnprocessableEntity:
return true
default:
return false
}
}
-236
View File
@@ -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 -8
View File
@@ -60,10 +60,10 @@ func init() {
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, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
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")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_masque", "with_mtproxy", "with_utls", "with_naive_outbound", "with_clash_api", "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")
@@ -163,14 +163,14 @@ func buildAndroid() {
bindTarget := getAndroidBindTarget()
// Build main variant (SDK 24)
// Build main variant (SDK 23)
mainTags := append([]string{}, sharedTags...)
// mainTags = append(mainTags, memcTags...)
if debugEnabled {
mainTags = append(mainTags, debugTags...)
}
buildAndroidVariant(AndroidBuildConfig{
AndroidAPI: 24,
AndroidAPI: 23,
OutputName: "libbox.aar",
Tags: mainTags,
}, bindTarget)
@@ -204,9 +204,6 @@ func buildApple() {
"-target", bindTarget,
"-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, ","))
-15
View File
@@ -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, " ")
}
-5
View File
@@ -3,7 +3,6 @@ package build_shared
import (
"github.com/sagernet/sing-box/common/badversion"
"github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/shell"
)
@@ -37,7 +36,3 @@ func ReadTagVersion() (badversion.Version, error) {
}
return version, nil
}
func TestFlightVersion(version badversion.Version) string {
return F.ToString(version.Major, ".", version.Minor, ".1000")
}
-47
View File
@@ -1,47 +0,0 @@
package build_shared
import (
"os"
"path/filepath"
"strings"
E "github.com/sagernet/sing/common/exceptions"
"howett.net/plist"
)
func ReadAppleMarketingVersion(applePath string, directory string) (string, error) {
projectFile, err := os.Open(filepath.Join(applePath, "sing-box.xcodeproj", "project.pbxproj"))
if err != nil {
return "", err
}
defer projectFile.Close()
var project map[string]any
err = plist.NewDecoder(projectFile).Decode(&project)
if err != nil {
return "", err
}
var marketingVersion string
for _, object := range project["objects"].(map[string]any) {
buildSettings, isMap := object.(map[string]any)["buildSettings"].(map[string]any)
if !isMap {
continue
}
infoPlistFile, isString := buildSettings["INFOPLIST_FILE"].(string)
if !isString || !strings.HasPrefix(infoPlistFile, directory+"/") {
continue
}
version, isString := buildSettings["MARKETING_VERSION"].(string)
if !isString {
continue
}
if marketingVersion != "" && marketingVersion != version {
return "", E.New("inconsistent marketing versions in ", directory, ": ", marketingVersion, ", ", version)
}
marketingVersion = version
}
if marketingVersion == "" {
return "", E.New("marketing version not found in ", directory)
}
return marketingVersion, nil
}
-165
View File
@@ -1,165 +0,0 @@
package main
import (
"archive/zip"
"crypto/sha256"
"flag"
"io"
"os"
"path/filepath"
"strings"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
)
var outputPath string
func init() {
flag.StringVar(&outputPath, "output", "", "output AAR path")
}
func main() {
flag.Parse()
err := merge()
if err != nil {
log.Fatal(err)
}
}
func merge() error {
inputPaths := flag.Args()
if outputPath == "" {
return E.New("missing output path")
}
if len(inputPaths) == 0 {
return E.New("missing input AAR paths")
}
archiveReaders := make([]*zip.ReadCloser, 0, len(inputPaths))
for _, inputPath := range inputPaths {
archiveReader, err := zip.OpenReader(inputPath)
if err != nil {
return E.Cause(err, "open input AAR: ", inputPath)
}
archiveReaders = append(archiveReaders, archiveReader)
}
defer func() {
for _, archiveReader := range archiveReaders {
archiveReader.Close()
}
}()
referenceEntries := make(map[string][sha256.Size]byte)
selectedEntries := make([]*zip.File, 0)
selectedJNIEntries := make(map[string]bool)
for inputIndex, archiveReader := range archiveReaders {
seenEntries := make(map[string]bool)
for _, archiveFile := range archiveReader.File {
if strings.HasPrefix(archiveFile.Name, "jni/") {
if archiveFile.FileInfo().IsDir() {
continue
}
if selectedJNIEntries[archiveFile.Name] {
return E.New("duplicate AAR JNI entry: ", archiveFile.Name)
}
selectedJNIEntries[archiveFile.Name] = true
selectedEntries = append(selectedEntries, archiveFile)
continue
}
entryDigest, err := digestEntry(archiveFile)
if err != nil {
return E.Cause(err, "read AAR entry: ", archiveFile.Name)
}
if inputIndex == 0 {
referenceEntries[archiveFile.Name] = entryDigest
selectedEntries = append(selectedEntries, archiveFile)
} else {
referenceDigest, loaded := referenceEntries[archiveFile.Name]
if !loaded {
return E.New("unexpected AAR entry: ", archiveFile.Name)
}
if referenceDigest != entryDigest {
return E.New("AAR entry differs between architectures: ", archiveFile.Name)
}
}
seenEntries[archiveFile.Name] = true
}
if inputIndex > 0 {
for referenceName := range referenceEntries {
if !seenEntries[referenceName] {
return E.New("missing AAR entry: ", referenceName)
}
}
}
}
absoluteOutputPath, err := filepath.Abs(outputPath)
if err != nil {
return E.Cause(err, "resolve output AAR path")
}
err = os.MkdirAll(filepath.Dir(absoluteOutputPath), 0o755)
if err != nil {
return E.Cause(err, "create output AAR directory")
}
temporaryFile, err := os.CreateTemp(filepath.Dir(absoluteOutputPath), ".merge-aar-*.aar")
if err != nil {
return E.Cause(err, "create temporary output AAR")
}
temporaryPath := temporaryFile.Name()
defer os.Remove(temporaryPath)
archiveWriter := zip.NewWriter(temporaryFile)
for _, archiveFile := range selectedEntries {
rawReader, openErr := archiveFile.OpenRaw()
if openErr != nil {
archiveWriter.Close()
temporaryFile.Close()
return E.Cause(openErr, "open raw AAR entry: ", archiveFile.Name)
}
header := archiveFile.FileHeader
rawWriter, createErr := archiveWriter.CreateRaw(&header)
if createErr != nil {
archiveWriter.Close()
temporaryFile.Close()
return E.Cause(createErr, "create output AAR entry: ", archiveFile.Name)
}
_, copyErr := io.Copy(rawWriter, rawReader)
if copyErr != nil {
archiveWriter.Close()
temporaryFile.Close()
return E.Cause(copyErr, "copy output AAR entry: ", archiveFile.Name)
}
}
err = archiveWriter.Close()
if err != nil {
temporaryFile.Close()
return E.Cause(err, "finalize output AAR")
}
err = temporaryFile.Close()
if err != nil {
return E.Cause(err, "close output AAR")
}
err = os.Rename(temporaryPath, absoluteOutputPath)
if err != nil {
return E.Cause(err, "replace output AAR")
}
return nil
}
func digestEntry(archiveFile *zip.File) ([sha256.Size]byte, error) {
entryReader, err := archiveFile.Open()
if err != nil {
return [sha256.Size]byte{}, err
}
digest := sha256.New()
_, err = io.Copy(digest, entryReader)
closeErr := entryReader.Close()
if err != nil {
return [sha256.Size]byte{}, err
}
if closeErr != nil {
return [sha256.Size]byte{}, closeErr
}
var result [sha256.Size]byte
copy(result[:], digest.Sum(nil))
return result, nil
}
@@ -1,172 +0,0 @@
package main
import (
"flag"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"howett.net/plist"
)
type xcFrameworkInfo struct {
AvailableLibraries []xcFrameworkLibrary `plist:"AvailableLibraries"`
}
type xcFrameworkLibrary struct {
BinaryPath string `plist:"BinaryPath"`
LibraryIdentifier string `plist:"LibraryIdentifier"`
LibraryPath string `plist:"LibraryPath"`
SupportedArchitectures []string `plist:"SupportedArchitectures"`
SupportedPlatform string `plist:"SupportedPlatform"`
SupportedPlatformVariant string `plist:"SupportedPlatformVariant"`
}
type frameworkSlice struct {
rootPath string
library xcFrameworkLibrary
}
var outputPath string
func init() {
flag.StringVar(&outputPath, "output", "", "output XCFramework path")
}
func main() {
flag.Parse()
err := merge()
if err != nil {
log.Fatal(err)
}
}
func merge() error {
inputPaths := flag.Args()
if outputPath == "" {
return E.New("missing output path")
}
if len(inputPaths) == 0 {
return E.New("missing input XCFramework paths")
}
frameworkGroups := make(map[string][]frameworkSlice)
for _, inputPath := range inputPaths {
infoFile, err := os.Open(filepath.Join(inputPath, "Info.plist"))
if err != nil {
return E.Cause(err, "open XCFramework metadata: ", inputPath)
}
var info xcFrameworkInfo
decoder := plist.NewDecoder(infoFile)
err = decoder.Decode(&info)
closeErr := infoFile.Close()
if err != nil {
return E.Cause(err, "decode XCFramework metadata: ", inputPath)
}
if closeErr != nil {
return E.Cause(closeErr, "close XCFramework metadata: ", inputPath)
}
for _, library := range info.AvailableLibraries {
groupName := library.SupportedPlatform + "|" + library.SupportedPlatformVariant
frameworkGroups[groupName] = append(frameworkGroups[groupName], frameworkSlice{
rootPath: inputPath,
library: library,
})
}
}
groupNames := make([]string, 0, len(frameworkGroups))
for groupName := range frameworkGroups {
groupNames = append(groupNames, groupName)
}
sort.Strings(groupNames)
absoluteOutputPath, err := filepath.Abs(outputPath)
if err != nil {
return E.Cause(err, "resolve output XCFramework path")
}
err = os.MkdirAll(filepath.Dir(absoluteOutputPath), 0o755)
if err != nil {
return E.Cause(err, "create output XCFramework directory")
}
temporaryDirectory, err := os.MkdirTemp(filepath.Dir(absoluteOutputPath), ".merge-xcframework-*")
if err != nil {
return E.Cause(err, "create XCFramework merge directory")
}
defer os.RemoveAll(temporaryDirectory)
frameworkPaths := make([]string, 0, len(groupNames))
for groupIndex, groupName := range groupNames {
frameworkSlices := frameworkGroups[groupName]
firstSlice := frameworkSlices[0]
firstFrameworkPath := filepath.Join(firstSlice.rootPath, firstSlice.library.LibraryIdentifier, firstSlice.library.LibraryPath)
if len(frameworkSlices) == 1 {
frameworkPaths = append(frameworkPaths, firstFrameworkPath)
continue
}
architectures := make(map[string]bool)
binaryPaths := make([]string, 0, len(frameworkSlices))
for _, currentSlice := range frameworkSlices {
if currentSlice.library.LibraryPath != firstSlice.library.LibraryPath || currentSlice.library.BinaryPath != firstSlice.library.BinaryPath {
return E.New("incompatible XCFramework slices for platform: ", currentSlice.library.SupportedPlatform)
}
for _, architecture := range currentSlice.library.SupportedArchitectures {
if architectures[architecture] {
return E.New("duplicate XCFramework architecture: ", architecture)
}
architectures[architecture] = true
}
binaryPaths = append(binaryPaths, filepath.Join(currentSlice.rootPath, currentSlice.library.LibraryIdentifier, currentSlice.library.BinaryPath))
}
mergedFrameworkPath := filepath.Join(temporaryDirectory, "framework-"+strconv.Itoa(groupIndex), filepath.Base(firstSlice.library.LibraryPath))
copyCommand := exec.Command("ditto", firstFrameworkPath, mergedFrameworkPath)
copyCommand.Stdout = os.Stdout
copyCommand.Stderr = os.Stderr
err = copyCommand.Run()
if err != nil {
return E.Cause(err, "copy XCFramework slice")
}
binaryRelativePath, relativeErr := filepath.Rel(firstSlice.library.LibraryPath, firstSlice.library.BinaryPath)
if relativeErr != nil {
return E.Cause(relativeErr, "resolve XCFramework binary path")
}
if binaryRelativePath == "." || strings.HasPrefix(binaryRelativePath, ".."+string(filepath.Separator)) {
return E.New("invalid XCFramework binary path: ", firstSlice.library.BinaryPath)
}
mergedBinaryPath := filepath.Join(mergedFrameworkPath, binaryRelativePath)
temporaryBinaryPath := mergedBinaryPath + ".merged"
lipoArguments := append([]string{"lipo", "-create"}, binaryPaths...)
lipoArguments = append(lipoArguments, "-output", temporaryBinaryPath)
lipoCommand := exec.Command("xcrun", lipoArguments...)
lipoCommand.Stdout = os.Stdout
lipoCommand.Stderr = os.Stderr
err = lipoCommand.Run()
if err != nil {
return E.Cause(err, "merge XCFramework binaries")
}
err = os.Rename(temporaryBinaryPath, mergedBinaryPath)
if err != nil {
return E.Cause(err, "replace merged XCFramework binary")
}
frameworkPaths = append(frameworkPaths, mergedFrameworkPath)
}
err = os.RemoveAll(absoluteOutputPath)
if err != nil {
return E.Cause(err, "remove output XCFramework")
}
xcodebuildArguments := []string{"-create-xcframework"}
for _, frameworkPath := range frameworkPaths {
xcodebuildArguments = append(xcodebuildArguments, "-framework", frameworkPath)
}
xcodebuildArguments = append(xcodebuildArguments, "-output", absoluteOutputPath)
xcodebuildCommand := exec.Command("xcodebuild", xcodebuildArguments...)
xcodebuildCommand.Stdout = os.Stdout
xcodebuildCommand.Stderr = os.Stderr
err = xcodebuildCommand.Run()
if err != nil {
return E.Cause(err, "create XCFramework")
}
return nil
}
+14 -87
View File
@@ -8,21 +8,16 @@ import (
"strings"
"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"
"howett.net/plist"
)
var (
flagRunInCI bool
flagTestFlight bool
)
var flagRunInCI bool
func init() {
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
flag.BoolVar(&flagTestFlight, "testflight", false, "Override the App Store marketing version with the version reserved for TestFlight")
}
func main() {
@@ -45,39 +40,19 @@ func main() {
common.Must(decoder.Decode(&project))
objectsMap := project["objects"].(map[string]any)
projectContent := string(common.Must1(os.ReadFile("sing-box.xcodeproj/project.pbxproj")))
newContent := projectContent
var marketingVersionUpdated bool
if flagTestFlight {
testFlightVersion := build_shared.TestFlightVersion(newVersion)
newContent, marketingVersionUpdated = findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfamt"}, testFlightVersion)
if marketingVersionUpdated {
log.Info("updated App Store version to ", testFlightVersion)
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfavt"}, newVersion.VersionString())
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfavt.standalone", "io.nekohasekai.sfavt.system"}, newVersion.String())
if updated0 || updated1 {
log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")")
}
var updated2 bool
if macProjectVersion := os.Getenv("MACOS_PROJECT_VERSION"); macProjectVersion != "" {
newContent, updated2 = findAndReplaceProjectVersion(objectsMap, newContent, []string{"SFM"}, macProjectVersion)
if updated2 {
log.Info("updated macos project version to ", macProjectVersion)
}
}
var standaloneVersionUpdated bool
newContent, standaloneVersionUpdated = findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfamt.standalone", "io.nekohasekai.sfamt.system"}, newVersion.String())
if standaloneVersionUpdated {
marketingVersionUpdated = true
log.Info("updated version to ", newVersion.String())
}
var projectVersionUpdated bool
for environmentName, directory := range map[string]string{
"IOS_PROJECT_VERSION": "SFI",
"MACOS_PROJECT_VERSION": "SFM",
"TVOS_PROJECT_VERSION": "SFT",
} {
projectVersion := os.Getenv(environmentName)
if projectVersion == "" {
continue
}
var updated bool
newContent, updated = findAndReplaceProjectVersion(objectsMap, newContent, []string{directory}, projectVersion)
if updated {
projectVersionUpdated = true
log.Info("updated ", directory, " project version to ", projectVersion)
}
}
if marketingVersionUpdated || projectVersionUpdated {
if updated0 || updated1 || updated2 {
common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj", []byte(newContent), 0o644))
}
}
@@ -101,20 +76,11 @@ func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDLi
continue
}
updated = true
projectContent = projectContent[:versionStart] + formatProjectVersion(newVersion) + projectContent[versionEnd:]
projectContent = projectContent[:versionStart] + "\"" + newVersion + "\"" + projectContent[versionEnd:]
}
return projectContent, updated
}
// Xcode serializes a version without quotes unless it contains a pre-release
// part; always quoting makes Xcode rewrite the value on the next save.
func formatProjectVersion(version string) string {
if badversion.Parse(version).PreReleaseIdentifier == "" {
return version
}
return "\"" + version + "\""
}
func findAndReplaceProjectVersion(objectsMap map[string]any, projectContent string, directoryList []string, newVersion string) (string, bool) {
objectKeyList := findObjectKeyByDirectory(objectsMap, directoryList)
var updated bool
@@ -140,7 +106,6 @@ func findAndReplaceProjectVersion(objectsMap map[string]any, projectContent stri
}
func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string {
globalSettings := collectBuildSettings(objectsMap)
var objectKeyList []string
for objectKey, object := range objectsMap {
buildSettings := object.(map[string]any)["buildSettings"]
@@ -151,51 +116,13 @@ func findObjectKey(objectsMap map[string]any, bundleIDList []string) []string {
if bundleIDObject == nil {
continue
}
bundleID := expandBuildVariables(bundleIDObject.(string), globalSettings)
if common.Contains(bundleIDList, bundleID) {
if common.Contains(bundleIDList, bundleIDObject.(string)) {
objectKeyList = append(objectKeyList, objectKey)
}
}
return objectKeyList
}
func collectBuildSettings(objectsMap map[string]any) map[string]string {
settings := make(map[string]string)
for _, object := range objectsMap {
buildSettings, loaded := object.(map[string]any)["buildSettings"].(map[string]any)
if !loaded {
continue
}
for key, value := range buildSettings {
valueString, isString := value.(string)
if !isString {
continue
}
settings[key] = valueString
}
}
return settings
}
var buildVariableRegexp = regexp.MustCompile(`\$[({]([A-Za-z0-9_]+)[)}]`)
func expandBuildVariables(value string, settings map[string]string) string {
for {
expanded := buildVariableRegexp.ReplaceAllStringFunc(value, func(match string) string {
name := buildVariableRegexp.FindStringSubmatch(match)[1]
replacement, loaded := settings[name]
if !loaded {
return match
}
return replacement
})
if expanded == value {
return expanded
}
value = expanded
}
}
func findObjectKeyByDirectory(objectsMap map[string]any, directoryList []string) []string {
var objectKeyList []string
for objectKey, object := range objectsMap {
+26 -12
View File
@@ -43,8 +43,11 @@ func updateMozillaIncludedRootCAs() error {
package certificate
func mozillaIncludedPEM() string {
return ` + "`")
import "crypto/x509"
func newMozillaIncluded() *x509.CertPool {
pool := x509.NewCertPool()
`)
for {
record, err := reader.Read()
if err == io.EOF {
@@ -55,14 +58,17 @@ func mozillaIncludedPEM() string {
if record[geoIndex] == "China" {
continue
}
cert := strings.Trim(record[certIndex], "'")
generated.WriteString("\n// ")
generated.WriteString("\n // ")
generated.WriteString(record[nameIndex])
generated.WriteString("\n")
generated.WriteString(" pool.AppendCertsFromPEM([]byte(`")
cert := record[certIndex]
// Remove single quotes
cert = cert[1 : len(cert)-1]
generated.WriteString(cert)
generated.WriteString("\n")
generated.WriteString("`))\n")
}
generated.WriteString("`\n}\n")
generated.WriteString("\treturn pool\n}\n")
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
}
@@ -121,8 +127,11 @@ func updateChromeIncludedRootCAs() error {
package certificate
func chromeIncludedPEM() string {
return ` + "`")
import "crypto/x509"
func newChromeIncluded() *x509.CertPool {
pool := x509.NewCertPool()
`)
for {
record, err := reader.Read()
if err == io.EOF {
@@ -136,13 +145,18 @@ func chromeIncludedPEM() string {
if chinaFingerprints[record[fingerprintIndex]] {
continue
}
cert := strings.Trim(record[certIndex], "'")
generated.WriteString("\n// ")
generated.WriteString("\n // ")
generated.WriteString(record[subjectIndex])
generated.WriteString("\n")
generated.WriteString(" pool.AppendCertsFromPEM([]byte(`")
cert := record[certIndex]
// Remove single quotes if present
if len(cert) > 0 && cert[0] == '\'' {
cert = cert[1 : len(cert)-1]
}
generated.WriteString(cert)
generated.WriteString("\n")
generated.WriteString("`))\n")
}
generated.WriteString("`\n}\n")
generated.WriteString("\treturn pool\n}\n")
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
}
@@ -1,70 +0,0 @@
package main
import (
"encoding/json"
"flag"
"os"
"path/filepath"
"regexp"
"runtime"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
)
var (
flagRunInCI bool
flagRunNightly bool
)
type versionMetadata struct {
Version string `json:"version"`
GoVersion string `json:"go_version"`
}
func init() {
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
}
func main() {
flag.Parse()
newVersion := common.Must1(build_shared.ReadTag())
desktopPath := "../sing-box-for-desktop"
if flagRunInCI {
desktopPath = "clients/desktop"
}
desktopPath = common.Must1(filepath.Abs(desktopPath))
versionPath := filepath.Join(desktopPath, "version.json")
versionFile := common.Must1(os.Open(versionPath))
var metadata versionMetadata
common.Must(json.NewDecoder(versionFile).Decode(&metadata))
common.Must(versionFile.Close())
newGoVersion := runtime.Version()
if match := regexp.MustCompile(`^go\d+\.\d+(?:\.\d+)?`).FindString(newGoVersion); match != "" {
newGoVersion = match
}
versionUpdated := metadata.Version != newVersion
goVersionUpdated := metadata.GoVersion != newGoVersion
if !(versionUpdated || goVersionUpdated) {
log.Info("version not changed")
return
}
if versionUpdated {
log.Info("updated version from ", metadata.Version, " to ", newVersion)
}
if goVersionUpdated {
log.Info("updated Go version from ", metadata.GoVersion, " to ", newGoVersion)
}
if flagRunInCI && !flagRunNightly {
log.Fatal("version changed, commit changes first.")
}
metadata.Version = newVersion
metadata.GoVersion = newGoVersion
outputFile := common.Must1(os.Create(versionPath))
encoder := json.NewEncoder(outputFile)
encoder.SetIndent("", " ")
common.Must(encoder.Encode(metadata))
common.Must(outputFile.Close())
}
+1 -4
View File
@@ -10,7 +10,6 @@ import (
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
@@ -53,9 +52,7 @@ func preRun(cmd *cobra.Command, args []string) {
globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
}
if disableColor {
logFactory := log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false)
common.Must(logFactory.Start())
log.SetStdLogger(logFactory.Logger())
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
}
if workingDir != "" {
_, err := os.Stat(workingDir)
-107
View File
@@ -1,107 +0,0 @@
package main
import (
"os"
"strings"
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
commandAPIFlagURL string
commandAPIFlagSecret string
commandAPIServerURL string
)
var commandAPI = &cobra.Command{
Use: "api <command>",
Short: "API service client",
DisableFlagParsing: true,
Run: func(cmd *cobra.Command, args []string) {
err := runAPI(args)
if err != nil {
log.Fatal(err)
}
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
targetCommand, remainingArgs, err := commandAPIRoot.Find(args)
if err != nil || len(remainingArgs) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return common.Map(common.Filter(targetCommand.Commands(), func(it *cobra.Command) bool {
return it.IsAvailableCommand() && strings.HasPrefix(it.Name(), toComplete)
}), func(it *cobra.Command) string {
return it.Name() + "\t" + it.Short
}), cobra.ShellCompDirectiveNoFileComp
},
}
var commandAPIRoot = &cobra.Command{
Use: "api",
Short: "API service client",
SilenceUsage: true,
SilenceErrors: true,
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
}
func init() {
commandAPIRoot.PersistentFlags().StringVar(&commandAPIFlagURL, "url", "", "API service URL (default: $BOX_API_URL)")
commandAPIRoot.PersistentFlags().StringVar(&commandAPIFlagSecret, "secret", "", "API service secret (default: $BOX_API_SECRET)")
mainCommand.AddCommand(commandAPI)
}
func runAPI(args []string) error {
commandAPIRoot.SetArgs(append([]string{}, args...))
err := commandAPIRoot.Execute()
if err == nil {
return nil
}
grpcStatus, isStatus := status.FromError(err)
if !isStatus {
return err
}
switch grpcStatus.Code() {
case codes.Unavailable:
return E.New("failed to connect to API service at ", commandAPIServerURL, ": ", grpcStatus.Message())
case codes.Unknown:
return E.New(grpcStatus.Message())
case codes.Unimplemented:
return E.New(grpcStatus.Code().String(), ": ", grpcStatus.Message(), " (client API version ", daemon.APIVersion, ")")
default:
return E.New(grpcStatus.Code().String(), ": ", grpcStatus.Message())
}
}
func createAPIClient() (*grpc.ClientConn, daemon.StartedServiceClient, error) {
serverURL := commandAPIFlagURL
if serverURL == "" {
serverURL = os.Getenv("BOX_API_URL")
}
if serverURL == "" {
return nil, nil, E.New("missing API service URL, set --url or BOX_API_URL")
}
if !strings.Contains(serverURL, "://") {
serverURL = "http://" + serverURL
}
commandAPIServerURL = serverURL
secret := commandAPIFlagSecret
if secret == "" {
secret = os.Getenv("BOX_API_SECRET")
}
clientConn, err := daemon.NewRemoteClient(daemon.RemoteClientOptions{
ServerURL: serverURL,
Secret: secret,
})
if err != nil {
return nil, nil, err
}
return clientConn, daemon.NewStartedServiceClient(clientConn), nil
}
-37
View File
@@ -1,37 +0,0 @@
package main
import (
"time"
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing/common"
"github.com/spf13/cobra"
)
var commandAPIConnection = &cobra.Command{
Use: "connection",
Short: "Manage connections",
}
func init() {
commandAPIRoot.AddCommand(commandAPIConnection)
}
func fetchConnections(client daemon.StartedServiceClient) ([]*daemon.Connection, error) {
stream, err := client.SubscribeConnections(globalCtx, &daemon.SubscribeConnectionsRequest{Interval: int64(time.Second)})
if err != nil {
return nil, err
}
events, err := stream.Recv()
if err != nil {
return nil, err
}
connections := common.FilterNotNil(common.Map(events.GetEvents(), func(it *daemon.ConnectionEvent) *daemon.Connection {
return it.GetConnection()
}))
common.SortBy(connections, func(it *daemon.Connection) int64 {
return it.GetCreatedAt()
})
return connections, nil
}
-47
View File
@@ -1,47 +0,0 @@
package main
import (
"github.com/sagernet/sing-box/daemon"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/emptypb"
)
var commandAPIConnectionCloseFlagAll bool
var commandAPIConnectionClose = &cobra.Command{
Use: "close <id>",
Short: "Close connections",
Long: "Close connections.\n\nThe id must be a full UUID; the service reports success for an unknown or already closed connection.",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runAPIConnectionClose(args)
},
}
func init() {
commandAPIConnectionClose.Flags().BoolVar(&commandAPIConnectionCloseFlagAll, "all", false, "Close all connections")
commandAPIConnection.AddCommand(commandAPIConnectionClose)
}
func runAPIConnectionClose(args []string) error {
if commandAPIConnectionCloseFlagAll {
if len(args) > 0 {
return E.New("--all takes no connection id")
}
} else if len(args) == 0 {
return E.New("missing connection id")
}
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
if commandAPIConnectionCloseFlagAll {
_, err = client.CloseAllConnections(globalCtx, &emptypb.Empty{})
} else {
_, err = client.CloseConnection(globalCtx, &daemon.CloseConnectionRequest{Id: args[0]})
}
return err
}
-201
View File
@@ -1,201 +0,0 @@
package main
import (
"slices"
"strings"
"time"
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var commandAPIConnectionListFlagColumns []string
type connectionRate struct {
uplink int64
downlink int64
}
type connectionColumn struct {
header string
value func(connection *daemon.Connection, rates map[string]connectionRate) string
}
var connectionColumnNames = []string{
"id", "network", "source", "destination", "inbound", "outbound",
"chain", "rule", "protocol", "user", "process", "created", "rate", "total",
}
var connectionColumns = map[string]connectionColumn{
"id": {"ID", func(connection *daemon.Connection, _ map[string]connectionRate) string {
return connection.GetId()
}},
"network": {"NETWORK", func(connection *daemon.Connection, _ map[string]connectionRate) string {
return connection.GetNetwork()
}},
"source": {"SOURCE", func(connection *daemon.Connection, _ map[string]connectionRate) string {
return connection.GetSource()
}},
"destination": {"DESTINATION", func(connection *daemon.Connection, _ map[string]connectionRate) string {
return connectionDestination(connection)
}},
"inbound": {"INBOUND", func(connection *daemon.Connection, _ map[string]connectionRate) string {
if connection.GetInbound() == "" {
return connection.GetInboundType()
}
return connection.GetInboundType() + "/" + connection.GetInbound()
}},
"outbound": {"OUTBOUND", func(connection *daemon.Connection, _ map[string]connectionRate) string {
return connection.GetOutbound()
}},
"chain": {"CHAIN", func(connection *daemon.Connection, _ map[string]connectionRate) string {
chain := slices.Clone(connection.GetChainList())
slices.Reverse(chain)
return strings.Join(chain, "/")
}},
"rule": {"RULE", func(connection *daemon.Connection, _ map[string]connectionRate) string {
return connection.GetRule()
}},
"protocol": {"PROTOCOL", func(connection *daemon.Connection, _ map[string]connectionRate) string {
return connection.GetProtocol()
}},
"user": {"USER", func(connection *daemon.Connection, _ map[string]connectionRate) string {
return connection.GetUser()
}},
"process": {"PROCESS", func(connection *daemon.Connection, _ map[string]connectionRate) string {
processInfo := connection.GetProcessInfo()
if processInfo.GetProcessPath() != "" {
return processInfo.GetProcessPath()
}
if len(processInfo.GetPackageNames()) > 0 {
return processInfo.GetPackageNames()[0]
}
return ""
}},
"created": {"CREATED", func(connection *daemon.Connection, _ map[string]connectionRate) string {
return formatConnectionTime(connection.GetCreatedAt())
}},
"rate": {"RATE", func(connection *daemon.Connection, rates map[string]connectionRate) string {
rate, found := rates[connection.GetId()]
if !found || (rate.uplink == 0 && rate.downlink == 0) {
return ""
}
return "↑" + byteformats.FormatBytes(uint64(rate.uplink)) + "/s ↓" + byteformats.FormatBytes(uint64(rate.downlink)) + "/s"
}},
"total": {"TOTAL", func(connection *daemon.Connection, _ map[string]connectionRate) string {
return "↑" + byteformats.FormatBytes(uint64(connection.GetUplinkTotal())) + " ↓" + byteformats.FormatBytes(uint64(connection.GetDownlinkTotal()))
}},
}
var commandAPIConnectionList = &cobra.Command{
Use: "list",
Short: "List open connections",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runAPIConnectionList()
},
}
func init() {
commandAPIConnectionList.Flags().StringSliceVar(
&commandAPIConnectionListFlagColumns,
"columns",
[]string{"id", "network", "destination", "inbound", "outbound", "total"},
"Columns to display (available: "+strings.Join(connectionColumnNames, ", ")+")",
)
commandAPIConnection.AddCommand(commandAPIConnectionList)
}
func runAPIConnectionList() error {
columns := make([]connectionColumn, 0, len(commandAPIConnectionListFlagColumns))
sampleRates := false
for _, name := range commandAPIConnectionListFlagColumns {
column, found := connectionColumns[name]
if !found {
return E.New("unknown column: ", name, ", available: ", strings.Join(connectionColumnNames, ", "))
}
if name == "rate" {
sampleRates = true
}
columns = append(columns, column)
}
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
var (
connections []*daemon.Connection
rates map[string]connectionRate
)
if sampleRates {
connections, rates, err = fetchConnectionsAndRates(client)
} else {
connections, err = fetchConnections(client)
}
if err != nil {
return err
}
table := tableWriter{
header: common.Map(columns, func(it connectionColumn) string {
return it.header
}),
emptyMessage: "no connections",
}
for _, connection := range connections {
if connection.GetClosedAt() != 0 {
continue
}
table.addRow(common.Map(columns, func(it connectionColumn) string {
return it.value(connection, rates)
})...)
}
table.flush()
return nil
}
func connectionDestination(connection *daemon.Connection) string {
destination := connection.GetDestination()
domain := connection.GetDomain()
if domain == "" {
return destination
}
portIndex := strings.LastIndex(destination, ":")
if portIndex == -1 {
return domain
}
return domain + destination[portIndex:]
}
func fetchConnectionsAndRates(client daemon.StartedServiceClient) ([]*daemon.Connection, map[string]connectionRate, error) {
stream, err := client.SubscribeConnections(globalCtx, &daemon.SubscribeConnectionsRequest{Interval: int64(time.Second)})
if err != nil {
return nil, nil, err
}
initialEvents, err := stream.Recv()
if err != nil {
return nil, nil, err
}
connections := common.FilterNotNil(common.Map(initialEvents.GetEvents(), func(it *daemon.ConnectionEvent) *daemon.Connection {
return it.GetConnection()
}))
common.SortBy(connections, func(it *daemon.Connection) int64 {
return it.GetCreatedAt()
})
updateEvents, err := stream.Recv()
if err != nil {
return nil, nil, err
}
rates := make(map[string]connectionRate, len(updateEvents.GetEvents()))
for _, event := range updateEvents.GetEvents() {
rates[event.GetId()] = connectionRate{
uplink: event.GetUplinkDelta(),
downlink: event.GetDownlinkDelta(),
}
}
return connections, rates, nil
}
-112
View File
@@ -1,112 +0,0 @@
package main
import (
"strings"
"time"
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/spf13/cobra"
)
var commandAPIConnectionShow = &cobra.Command{
Use: "show <id>",
Short: "Print connection details",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runAPIConnectionShow(args[0])
},
}
func init() {
commandAPIConnection.AddCommand(commandAPIConnectionShow)
}
func runAPIConnectionShow(connectionID string) error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
connections, err := fetchConnections(client)
if err != nil {
return err
}
connection := common.Find(connections, func(it *daemon.Connection) bool {
return it.GetId() == connectionID
})
if connection == nil {
return E.New("connection not found: ", connectionID)
}
state := "open"
if connection.GetClosedAt() != 0 {
state = "closed"
}
var ipVersion string
if connection.GetIpVersion() != 0 {
ipVersion = F.ToString(connection.GetIpVersion())
}
inbound := connection.GetInboundType()
if connection.GetInbound() != "" {
inbound = connection.GetInboundType() + "/" + connection.GetInbound()
}
outbound := connection.GetOutbound()
if outbound != "" && connection.GetOutboundType() != "" {
outbound = F.ToString(outbound, " (", connection.GetOutboundType(), ")")
}
var block blockWriter
block.addLine("ID", connection.GetId())
block.addLine("State", state)
block.addLine("Created", formatConnectionTime(connection.GetCreatedAt()))
block.addLine("Closed", formatConnectionTime(connection.GetClosedAt()))
block.addLine("Network", connection.GetNetwork())
block.addLine("IP version", ipVersion)
block.addLine("Protocol", connection.GetProtocol())
block.addLine("Inbound", inbound)
block.addLine("Source", connection.GetSource())
block.addLine("Destination", connection.GetDestination())
block.addLine("Domain", connection.GetDomain())
block.addLine("User", connection.GetUser())
block.addLine("Process", formatProcessInfo(connection.GetProcessInfo()))
block.addLine("Rule", connection.GetRule())
block.addLine("Outbound", outbound)
block.addLine("Chain", strings.Join(connection.GetChainList(), " <- "))
block.addLine("From outbound", connection.GetFromOutbound())
block.addLine("Uplink", byteformats.FormatBytes(uint64(connection.GetUplinkTotal())))
block.addLine("Downlink", byteformats.FormatBytes(uint64(connection.GetDownlinkTotal())))
block.flush()
return nil
}
func formatConnectionTime(timestamp int64) string {
if timestamp == 0 {
return ""
}
return time.UnixMilli(timestamp).Local().Format(time.RFC3339)
}
func formatProcessInfo(processInfo *daemon.ProcessInfo) string {
if processInfo == nil {
return ""
}
var process string
if processInfo.GetProcessPath() != "" {
process = processInfo.GetProcessPath()
} else if len(processInfo.GetPackageNames()) > 0 {
process = processInfo.GetPackageNames()[0]
}
if process == "" {
if processInfo.GetUserId() != -1 {
process = F.ToString(processInfo.GetUserId())
}
} else if processInfo.GetUserName() != "" {
process = F.ToString(process, " (", processInfo.GetUserName(), ")")
} else if processInfo.GetUserId() != -1 {
process = F.ToString(process, " (", processInfo.GetUserId(), ")")
}
return process
}
-29
View File
@@ -1,29 +0,0 @@
package main
import (
"github.com/sagernet/sing-box/daemon"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/emptypb"
)
var commandAPIGroup = &cobra.Command{
Use: "group",
Short: "Manage outbound groups",
}
func init() {
commandAPIRoot.AddCommand(commandAPIGroup)
}
func fetchGroups(client daemon.StartedServiceClient) ([]*daemon.Group, error) {
stream, err := client.SubscribeGroups(globalCtx, &emptypb.Empty{})
if err != nil {
return nil, err
}
groups, err := stream.Recv()
if err != nil {
return nil, err
}
return groups.GetGroup(), nil
}
-39
View File
@@ -1,39 +0,0 @@
package main
import (
"github.com/spf13/cobra"
)
var commandAPIGroupList = &cobra.Command{
Use: "list",
Short: "List outbound groups",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runAPIGroupList()
},
}
func init() {
commandAPIGroup.AddCommand(commandAPIGroupList)
}
func runAPIGroupList() error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
groups, err := fetchGroups(client)
if err != nil {
return err
}
table := tableWriter{
header: []string{"TAG", "TYPE", "SELECTED"},
emptyMessage: "no groups",
}
for _, group := range groups {
table.addRow(group.GetTag(), group.GetType(), group.GetSelected())
}
table.flush()
return nil
}
-33
View File
@@ -1,33 +0,0 @@
package main
import (
"github.com/sagernet/sing-box/daemon"
"github.com/spf13/cobra"
)
var commandAPIGroupSelect = &cobra.Command{
Use: "select <group> <outbound>",
Short: "Select an outbound in a group",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
return runAPIGroupSelect(args[0], args[1])
},
}
func init() {
commandAPIGroup.AddCommand(commandAPIGroupSelect)
}
func runAPIGroupSelect(groupTag string, outboundTag string) error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
_, err = client.SelectOutbound(globalCtx, &daemon.SelectOutboundRequest{
GroupTag: groupTag,
OutboundTag: outboundTag,
})
return err
}
-51
View File
@@ -1,51 +0,0 @@
package main
import (
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var commandAPIGroupShow = &cobra.Command{
Use: "show <group>",
Short: "Show an outbound group",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runAPIGroupShow(args[0])
},
}
func init() {
commandAPIGroup.AddCommand(commandAPIGroupShow)
}
func runAPIGroupShow(groupTag string) error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
groups, err := fetchGroups(client)
if err != nil {
return err
}
for _, group := range groups {
if group.GetTag() != groupTag {
continue
}
block := blockWriter{}
block.addLine("Tag", group.GetTag())
block.addLine("Type", group.GetType())
block.addLine("Selected", group.GetSelected())
block.flush()
table := tableWriter{
header: []string{"TAG", "TYPE", "DELAY"},
}
for _, item := range group.GetItems() {
table.addRow(item.GetTag(), item.GetType(), formatDelay(item.GetUrlTestDelay()))
}
table.flush()
return nil
}
return E.New("group not found: ", groupTag)
}
-31
View File
@@ -1,31 +0,0 @@
package main
import (
"github.com/sagernet/sing-box/daemon"
"github.com/spf13/cobra"
)
var commandAPIGroupURLTest = &cobra.Command{
Use: "urltest <group>",
Short: "Start a URL test",
Long: "Start a URL test.\n\nThe tests are only spawned: results appear in `outbounds --group <group>` a few seconds later.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runAPIGroupURLTest(args[0])
},
}
func init() {
commandAPIGroup.AddCommand(commandAPIGroupURLTest)
}
func runAPIGroupURLTest(groupTag string) error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
_, err = client.URLTest(globalCtx, &daemon.URLTestRequest{OutboundTag: groupTag})
return err
}
-96
View File
@@ -1,96 +0,0 @@
package main
import (
"os"
"os/signal"
"strings"
"syscall"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/emptypb"
)
var (
commandAPILogsFlagFollow bool
commandAPILogsFlagLevel string
commandAPILogsFlagSearch string
)
var commandAPILogs = &cobra.Command{
Use: "logs",
Short: "Print the service logs",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runAPILogs()
},
}
func init() {
commandAPILogs.Flags().BoolVarP(&commandAPILogsFlagFollow, "follow", "f", false, "Keep printing new log entries until interrupted")
commandAPILogs.Flags().StringVar(&commandAPILogsFlagLevel, "level", "", "Print entries at this level or more severe (default: the service log level)")
commandAPILogs.Flags().StringVar(&commandAPILogsFlagSearch, "search", "", "Print entries containing this text, case-insensitive")
commandAPIRoot.AddCommand(commandAPILogs)
}
func runAPILogs() error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
ctx, cancel := signal.NotifyContext(globalCtx, os.Interrupt, syscall.SIGTERM)
defer cancel()
var level log.Level
if commandAPILogsFlagLevel != "" {
level, err = log.ParseLevel(commandAPILogsFlagLevel)
if err != nil {
return err
}
} else {
defaultLevel, levelErr := client.GetDefaultLogLevel(ctx, &emptypb.Empty{})
if levelErr != nil {
return levelErr
}
level = log.Level(defaultLevel.GetLevel())
}
stream, err := client.SubscribeLog(ctx, &emptypb.Empty{})
if err != nil {
return err
}
searchQuery := strings.ToLower(strings.TrimSpace(commandAPILogsFlagSearch))
for backlog := true; ; backlog = false {
message, recvErr := stream.Recv()
if recvErr != nil {
if ctx.Err() != nil {
return nil
}
return recvErr
}
if message.GetReset_() && len(message.GetMessages()) == 0 && !backlog {
writeStderrLine("log buffer cleared")
continue
}
var output strings.Builder
for _, entry := range message.GetMessages() {
if log.Level(entry.GetLevel()) > level {
continue
}
plainMessage := stripColors(entry.GetMessage())
if searchQuery != "" && !strings.Contains(strings.ToLower(plainMessage), searchQuery) {
continue
}
if stdoutIsTerminal {
output.WriteString(entry.GetMessage())
} else {
output.WriteString(plainMessage)
}
output.WriteString("\n")
}
os.Stdout.WriteString(output.String())
if backlog && !commandAPILogsFlagFollow {
return nil
}
}
}
-39
View File
@@ -1,39 +0,0 @@
package main
import (
"os"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/emptypb"
)
var commandAPIMode = &cobra.Command{
Use: "mode",
Short: "Print the current clash mode",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runAPIMode()
},
}
func init() {
commandAPIRoot.AddCommand(commandAPIMode)
}
func runAPIMode() error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
modeStatus, err := client.GetClashModeStatus(globalCtx, &emptypb.Empty{})
if err != nil {
return err
}
currentMode := modeStatus.GetCurrentMode()
if currentMode == "" {
currentMode = "-"
}
os.Stdout.WriteString(currentMode + "\n")
return nil
}
-45
View File
@@ -1,45 +0,0 @@
package main
import (
"os"
"strings"
"github.com/spf13/cobra"
"google.golang.org/protobuf/types/known/emptypb"
)
var commandAPIModeList = &cobra.Command{
Use: "list",
Short: "List clash modes",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runAPIModeList()
},
}
func init() {
commandAPIMode.AddCommand(commandAPIModeList)
}
func runAPIModeList() error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
modeStatus, err := client.GetClashModeStatus(globalCtx, &emptypb.Empty{})
if err != nil {
return err
}
if len(modeStatus.GetModeList()) == 0 {
writeStderrLine("no clash modes")
return nil
}
var output strings.Builder
for _, mode := range modeStatus.GetModeList() {
output.WriteString(mode)
output.WriteString("\n")
}
os.Stdout.WriteString(output.String())
return nil
}
-31
View File
@@ -1,31 +0,0 @@
package main
import (
"github.com/sagernet/sing-box/daemon"
"github.com/spf13/cobra"
)
var commandAPIModeSet = &cobra.Command{
Use: "set <mode>",
Short: "Set the clash mode",
Long: "Set the clash mode.\n\nThe value is not validated against the mode list: setting an unknown mode reports success.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runAPIModeSet(args[0])
},
}
func init() {
commandAPIMode.AddCommand(commandAPIModeSet)
}
func runAPIModeSet(mode string) error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
_, err = client.SetClashMode(globalCtx, &daemon.ClashMode{Mode: mode})
return err
}
-131
View File
@@ -1,131 +0,0 @@
package main
import (
"fmt"
"os"
"strings"
"time"
"github.com/sagernet/sing-box/common/networkquality"
"github.com/sagernet/sing-box/daemon"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
)
var (
commandAPINetworkQualityFlagConfigURL string
commandAPINetworkQualityFlagSerial bool
commandAPINetworkQualityFlagMaxRuntime int
commandAPINetworkQualityFlagHTTP3 bool
commandAPINetworkQualityFlagOutbound string
)
var commandAPINetworkQuality = &cobra.Command{
Use: "networkquality",
Short: "Run a network quality test",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runAPINetworkQuality()
},
}
func init() {
commandAPINetworkQuality.Flags().StringVar(
&commandAPINetworkQualityFlagConfigURL,
"config-url", "",
"Network quality test config URL (default: Apple mensura)",
)
commandAPINetworkQuality.Flags().BoolVar(
&commandAPINetworkQualityFlagSerial,
"serial", false,
"Run download and upload tests sequentially instead of in parallel",
)
commandAPINetworkQuality.Flags().IntVar(
&commandAPINetworkQualityFlagMaxRuntime,
"max-runtime", int(networkquality.DefaultMaxRuntime/time.Second),
"Network quality maximum runtime in seconds",
)
commandAPINetworkQuality.Flags().BoolVar(
&commandAPINetworkQualityFlagHTTP3,
"http3", false,
"Use HTTP/3 (QUIC) for measurement traffic",
)
commandAPINetworkQuality.Flags().StringVarP(
&commandAPINetworkQualityFlagOutbound,
"outbound", "o", "",
"Use specified tag instead of default outbound",
)
commandAPIRoot.AddCommand(commandAPINetworkQuality)
}
func runAPINetworkQuality() error {
clientConn, client, err := createAPIClient()
if err != nil {
return err
}
defer clientConn.Close()
stream, err := client.StartNetworkQualityTest(globalCtx, &daemon.NetworkQualityTestRequest{
ConfigURL: commandAPINetworkQualityFlagConfigURL,
OutboundTag: commandAPINetworkQualityFlagOutbound,
Serial: commandAPINetworkQualityFlagSerial,
MaxRuntimeSeconds: int32(commandAPINetworkQualityFlagMaxRuntime),
Http3: commandAPINetworkQualityFlagHTTP3,
})
if err != nil {
return err
}
writeStderrLine("==== NETWORK QUALITY TEST ====")
for {
progress, recvErr := stream.Recv()
if recvErr != nil {
return recvErr
}
if !progress.GetIsFinal() {
writeNetworkQualityProgress(progress)
continue
}
writeStderrLine("")
if progress.GetError() != "" {
return E.New(progress.GetError())
}
writeStderrLine(strings.Repeat("-", 40))
fmt.Fprintf(os.Stdout, "Idle Latency: %d ms\n", progress.GetIdleLatencyMs())
fmt.Fprintf(os.Stdout, "Download Capacity: %-20s Accuracy: %s\n",
networkquality.FormatBitrate(progress.GetDownloadCapacity()),
networkquality.Accuracy(progress.GetDownloadCapacityAccuracy()))
fmt.Fprintf(os.Stdout, "Upload Capacity: %-20s Accuracy: %s\n",
networkquality.FormatBitrate(progress.GetUploadCapacity()),
networkquality.Accuracy(progress.GetUploadCapacityAccuracy()))
fmt.Fprintf(os.Stdout, "Download Responsiveness: %-20s Accuracy: %s\n",
fmt.Sprintf("%d RPM", progress.GetDownloadRPM()),
networkquality.Accuracy(progress.GetDownloadRPMAccuracy()))
fmt.Fprintf(os.Stdout, "Upload Responsiveness: %-20s Accuracy: %s\n",
fmt.Sprintf("%d RPM", progress.GetUploadRPM()),
networkquality.Accuracy(progress.GetUploadRPMAccuracy()))
return nil
}
}
func writeNetworkQualityProgress(progress *daemon.NetworkQualityTestProgress) {
if !commandAPINetworkQualityFlagSerial && networkquality.Phase(progress.GetPhase()) != networkquality.PhaseIdle {
writeProgress(fmt.Sprintf("Download: %s RPM: %d Upload: %s RPM: %d",
networkquality.FormatBitrate(progress.GetDownloadCapacity()), progress.GetDownloadRPM(),
networkquality.FormatBitrate(progress.GetUploadCapacity()), progress.GetUploadRPM()))
return
}
switch networkquality.Phase(progress.GetPhase()) {
case networkquality.PhaseIdle:
if progress.GetIdleLatencyMs() > 0 {
writeProgress(fmt.Sprintf("Idle Latency: %d ms", progress.GetIdleLatencyMs()))
} else {
writeProgress("Measuring idle latency...")
}
case networkquality.PhaseDownload:
writeProgress(fmt.Sprintf("Download: %s RPM: %d",
networkquality.FormatBitrate(progress.GetDownloadCapacity()), progress.GetDownloadRPM()))
case networkquality.PhaseUpload:
writeProgress(fmt.Sprintf("Upload: %s RPM: %d",
networkquality.FormatBitrate(progress.GetUploadCapacity()), progress.GetUploadRPM()))
}
}
-63
View File
@@ -1,63 +0,0 @@
package main
import (
"context"
"errors"
"io"
"github.com/sagernet/sing-box/daemon"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)
var commandAPIOpenConnect = &cobra.Command{
Use: "openconnect",
Short: "Manage OpenConnect authentication",
}
func init() {
commandAPIRoot.AddCommand(commandAPIOpenConnect)
}
func subscribeOpenConnectStatus(ctx context.Context, client daemon.StartedServiceClient) (grpc.ServerStreamingClient[daemon.OpenConnectStatusUpdate], []*daemon.OpenConnectEndpointStatus, error) {
stream, err := client.SubscribeOpenConnectStatus(ctx, &emptypb.Empty{})
if err != nil {
return nil, nil, err
}
endpoints, err := recvOpenConnectStatus(stream)
if err != nil {
return nil, nil, err
}
return stream, endpoints, nil
}
func recvOpenConnectStatus(stream grpc.ServerStreamingClient[daemon.OpenConnectStatusUpdate]) ([]*daemon.OpenConnectEndpointStatus, error) {
update, err := stream.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
return nil, E.New("api service closed the status stream")
}
return nil, err
}
return update.GetEndpoints(), nil
}
func openConnectChallengeSummary(challenge *daemon.OpenConnectAuthChallenge) string {
form := challenge.GetForm()
if form != nil {
return F.ToString("form (", len(form.GetFields()), " fields)")
}
browser := challenge.GetBrowser()
if browser == nil {
return "unknown"
}
mode, err := deriveOpenConnectBrowserMode(browser)
if err != nil {
return "browser (invalid)"
}
return "browser (" + mode + ")"
}

Some files were not shown because too many files have changed in this diff Show More