mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-10-26 13:06:56 +00:00
Compare commits
480
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9442d14b1 | ||
|
|
7dd51eb244 | ||
|
|
f269dee097 | ||
|
|
60094a12b4 | ||
|
|
dee34e0300 | ||
|
|
b2bb7bf347 | ||
|
|
5be93998c6 | ||
|
|
364c6a6e6a | ||
|
|
cb165a381c | ||
|
|
6b5ee55ba2 | ||
|
|
2283c95513 | ||
|
|
7054cac512 | ||
|
|
78d44d52d9 | ||
|
|
1a773a425a | ||
|
|
194ebd18b6 | ||
|
|
80c1171410 | ||
|
|
67fb235ee8 | ||
|
|
02537831e1 | ||
|
|
f9fcb8964f | ||
|
|
a691a44023 | ||
|
|
a364ff4794 | ||
|
|
bff5acf292 | ||
|
|
52f9bad034 | ||
|
|
a0d775ea66 | ||
|
|
79fc394d67 | ||
|
|
f95aa6ac11 | ||
|
|
6e3c86f518 | ||
|
|
7a3d4e4a8e | ||
|
|
351ce12f73 | ||
|
|
85c4a530b7 | ||
|
|
cdea9e87f1 | ||
|
|
21ba30b8c3 | ||
|
|
07130365d1 | ||
|
|
eb7deeae8a | ||
|
|
fd9a076d25 | ||
|
|
d2cf9747d7 | ||
|
|
d41e1745e6 | ||
|
|
0efa644942 | ||
|
|
021e0877a3 | ||
|
|
ddbf34639a | ||
|
|
5956780ba3 | ||
|
|
d75ac4ea21 | ||
|
|
4537a1ac00 | ||
|
|
af6e64c3b6 | ||
|
|
e6a1a3073e | ||
|
|
83aec4222f | ||
|
|
17c52d03b3 | ||
|
|
111ecb3483 | ||
|
|
6b6c20a248 | ||
|
|
4abede8ceb | ||
|
|
0d91c45479 | ||
|
|
ca7216a381 | ||
|
|
19f57607a9 | ||
|
|
886422d2e0 | ||
|
|
1ea3bb4563 | ||
|
|
78afbb2e62 | ||
|
|
6cfc2a86f8 | ||
|
|
55faa763f9 | ||
|
|
466eb0245a | ||
|
|
aae5f7ff02 | ||
|
|
311d9b51eb | ||
|
|
197cf99b73 | ||
|
|
aab74fea51 | ||
|
|
f5e8cdae19 | ||
|
|
60f0a0369b | ||
|
|
6a7f11fca3 | ||
|
|
afb0b30558 | ||
|
|
bea01fe80a | ||
|
|
fbb74b13e6 | ||
|
|
7b1d7e1c85 | ||
|
|
3f0958bf58 | ||
|
|
18b0317018 | ||
|
|
9be5fcf4e1 | ||
|
|
9cf7a7a28a | ||
|
|
a1f948c330 | ||
|
|
cd568a563b | ||
|
|
96b004bab1 | ||
|
|
5ae597bfba | ||
|
|
c0b8fe8b57 | ||
|
|
f5f4995c32 | ||
|
|
d7cc756eba | ||
|
|
9fa1890aea | ||
|
|
394285b78b | ||
|
|
491813c360 | ||
|
|
474aa5b5d9 | ||
|
|
6439d67043 | ||
|
|
e63f3b6be2 | ||
|
|
2c379103f3 | ||
|
|
1ac1a339cb | ||
|
|
dc3b490149 | ||
|
|
6c87a0c044 | ||
|
|
b5d5338e52 | ||
|
|
73a4196cb3 | ||
|
|
9713a546e6 | ||
|
|
54e5c497bb | ||
|
|
c827ec8303 | ||
|
|
b7eb49bb87 | ||
|
|
378f151c75 | ||
|
|
8d0500eded | ||
|
|
70d0ba7279 | ||
|
|
6d1fc214c1 | ||
|
|
046c46eacb | ||
|
|
89ca622e99 | ||
|
|
f909997082 | ||
|
|
6364d9a5f1 | ||
|
|
d6ec014ca4 | ||
|
|
b2a5ac3f60 | ||
|
|
515a73e4ee | ||
|
|
0c041aa7f6 | ||
|
|
0d7fae13bc | ||
|
|
0265f31f84 | ||
|
|
4ef3432c0d | ||
|
|
288411b0b9 | ||
|
|
6a6a1e0a31 | ||
|
|
217f77643f | ||
|
|
7afda58818 | ||
|
|
a25ad8ce52 | ||
|
|
64605fc47a | ||
|
|
08703c9035 | ||
|
|
28f6f4289e | ||
|
|
8208af72ac | ||
|
|
bcb83578d9 | ||
|
|
102330f968 | ||
|
|
de3c7ff37e | ||
|
|
7fc9e5e773 | ||
|
|
2eeb120091 | ||
|
|
301df12dee | ||
|
|
673a408dca | ||
|
|
04922b1898 | ||
|
|
3540c09b93 | ||
|
|
25618424e6 | ||
|
|
eac79f2080 | ||
|
|
b0ec582349 | ||
|
|
c0bfc731ec | ||
|
|
94ae161956 | ||
|
|
5c7467aaba | ||
|
|
e4a19a2a7c | ||
|
|
99bbd49406 | ||
|
|
4bc15be97c | ||
|
|
ad8d053985 | ||
|
|
21b8fcacc6 | ||
|
|
0944cd9d7b | ||
|
|
bdd86c1fef | ||
|
|
a556d49491 | ||
|
|
6c4589c933 | ||
|
|
b807ff30d0 | ||
|
|
3e185071eb | ||
|
|
650ef881c8 | ||
|
|
f0a2c348f6 | ||
|
|
df34f5068b | ||
|
|
0b8995879f | ||
|
|
65a50758e1 | ||
|
|
138ba19b25 | ||
|
|
336cde087e | ||
|
|
a48b4cbd89 | ||
|
|
05b6802cfa | ||
|
|
a9713335fd | ||
|
|
66beaf541e | ||
|
|
dd9cdbc712 | ||
|
|
384344ead3 | ||
|
|
6aecbe2750 | ||
|
|
2a38f30125 | ||
|
|
4658dfc6c6 | ||
|
|
fb0063221d | ||
|
|
f50ca2244b | ||
|
|
de7460ec7c | ||
|
|
c53c2375c4 | ||
|
|
3f986d584e | ||
|
|
103e266ebc | ||
|
|
55b18b2271 | ||
|
|
3439be1bbb | ||
|
|
a349a0d769 | ||
|
|
310206a889 | ||
|
|
be54f9afef | ||
|
|
673485755c | ||
|
|
33e63afb0f | ||
|
|
37da1712e0 | ||
|
|
080f1c56aa | ||
|
|
82098a8760 | ||
|
|
6d431a2c43 | ||
|
|
08986ad405 | ||
|
|
aea28ca6dc | ||
|
|
e6b7c6ca75 | ||
|
|
7d84f7d3e0 | ||
|
|
ba4146ea31 | ||
|
|
e4ec3b0345 | ||
|
|
6720a27109 | ||
|
|
cfddedb3da | ||
|
|
2e238abe01 | ||
|
|
ec497c7dda | ||
|
|
0a5078a848 | ||
|
|
f7e6a85166 | ||
|
|
680f37182f | ||
|
|
495446cf4f | ||
|
|
4b30a03adf | ||
|
|
c86164bd1b | ||
|
|
bd059fc8aa | ||
|
|
fcee5656b9 | ||
|
|
24d1f271bf | ||
|
|
b3a45e0839 | ||
|
|
4e91d92c5f | ||
|
|
96fe4f7317 | ||
|
|
20b4b8e17d | ||
|
|
87c4f84a89 | ||
|
|
0e75f5f45c | ||
|
|
28b25598ed | ||
|
|
1e2846a1a5 | ||
|
|
ce25e7791d | ||
|
|
0b850d6b56 | ||
|
|
a8091761d7 | ||
|
|
097f784e7a | ||
|
|
06a82b6601 | ||
|
|
baa8580efd | ||
|
|
059508ed7a | ||
|
|
5572b3c329 | ||
|
|
0dd17d78d8 | ||
|
|
baa99c6f8f | ||
|
|
9ea88e556a | ||
|
|
2f766be0be | ||
|
|
5c50f7a084 | ||
|
|
93a4bbacbe | ||
|
|
eb6db316cc | ||
|
|
ec3e95935f | ||
|
|
10d37384f0 | ||
|
|
e797c2d0ba | ||
|
|
9bf39f85f9 | ||
|
|
475eb7c845 | ||
|
|
b3a6dcda5f | ||
|
|
cdbfceafd1 | ||
|
|
34397d5dc8 | ||
|
|
9220eb136c | ||
|
|
525e98b05b | ||
|
|
251a508fae | ||
|
|
c90f3564e8 | ||
|
|
1698e6cf66 | ||
|
|
ec884dd508 | ||
|
|
6e09f2892a | ||
|
|
e09e3b49f9 | ||
|
|
510586c4f8 | ||
|
|
442177de59 | ||
|
|
7a20afb289 | ||
|
|
88ab140cf9 | ||
|
|
df0946c2a4 | ||
|
|
b84fb4952e | ||
|
|
80044d655f | ||
|
|
b491cac8f7 | ||
|
|
866f93fa2d | ||
|
|
bc5f34343f | ||
|
|
33e42bf423 | ||
|
|
71aedcac87 | ||
|
|
30d40d09de | ||
|
|
b9c7d52582 | ||
|
|
b2f1f630d9 | ||
|
|
086e0b87cf | ||
|
|
18398cd3f2 | ||
|
|
61f950bf61 | ||
|
|
fc57a1b8ac | ||
|
|
abf7fe1b5b | ||
|
|
71c2388bed | ||
|
|
f61d14b3ae | ||
|
|
e4c8b49eaf | ||
|
|
3afd290278 | ||
|
|
ee0ff1c4a2 | ||
|
|
5d424ea2ac | ||
|
|
90d75b9673 | ||
|
|
013dab8737 | ||
|
|
6269e1c2b3 | ||
|
|
b02ec478c5 | ||
|
|
719676519d | ||
|
|
2df7a9bac7 | ||
|
|
23ca3b415f | ||
|
|
668ced35f8 | ||
|
|
1f7d571017 | ||
|
|
a072f21978 | ||
|
|
9dfe842794 | ||
|
|
f65065fdf3 | ||
|
|
3af1325a93 | ||
|
|
5ee69759e1 | ||
|
|
b9572e5812 | ||
|
|
0a4e4c8061 | ||
|
|
b4600dd4f7 | ||
|
|
24d2626b52 | ||
|
|
5ba8abd0fd | ||
|
|
c66dd4c8ad | ||
|
|
ae2c5c84ef | ||
|
|
58d2ed88aa | ||
|
|
680f37e9a6 | ||
|
|
c7c9feef95 | ||
|
|
9a96e63643 | ||
|
|
7f1a2bd2be | ||
|
|
2c65ed1ea1 | ||
|
|
73e9ff81de | ||
|
|
4f330d73ec | ||
|
|
9eb60234fc | ||
|
|
9a543cfe42 | ||
|
|
2e0466da94 | ||
|
|
13ca2511b1 | ||
|
|
9bb5bf421d | ||
|
|
4b79c5bdf1 | ||
|
|
678b3d1703 | ||
|
|
16c8393569 | ||
|
|
9531a6a9cd | ||
|
|
7b3993db12 | ||
|
|
b0eaf05225 | ||
|
|
970d93d428 | ||
|
|
b35a747dab | ||
|
|
fa743743e1 | ||
|
|
17b12494de | ||
|
|
7004fc6fd4 | ||
|
|
edb7e1da15 | ||
|
|
9df9269421 | ||
|
|
017e2c53de | ||
|
|
ec544485b9 | ||
|
|
97bbf2a9a4 | ||
|
|
1e2f1d04da | ||
|
|
01e5196c12 | ||
|
|
fa479edc12 | ||
|
|
bcae189da3 | ||
|
|
5f2054bf90 | ||
|
|
14fd94e9d4 | ||
|
|
d1d19fcca3 | ||
|
|
154d74b7a0 | ||
|
|
2eacabec1d | ||
|
|
485761cbe8 | ||
|
|
31922cb2ba | ||
|
|
98512fc8f6 | ||
|
|
2d581b35f3 | ||
|
|
99242f4113 | ||
|
|
20846b2183 | ||
|
|
7fc5657e8f | ||
|
|
70c8549f02 | ||
|
|
12431fe93c | ||
|
|
fbc260b430 | ||
|
|
d43170b00a | ||
|
|
16f2a39298 | ||
|
|
38b447c6b4 | ||
|
|
a123c041f7 | ||
|
|
2a7240ae27 | ||
|
|
d8cd38d31b | ||
|
|
7e1187a48f | ||
|
|
9ef9a411b2 | ||
|
|
a89cac300d | ||
|
|
1f4e9e75ed | ||
|
|
c12367a013 | ||
|
|
2bd6a233dc | ||
|
|
5d7a0d92c6 | ||
|
|
3d237fed39 | ||
|
|
4b853215c6 | ||
|
|
b151c9e88d | ||
|
|
07e83f5981 | ||
|
|
3b9f5a0193 | ||
|
|
c03353ad0e | ||
|
|
0d2fb35733 | ||
|
|
fe34a79277 | ||
|
|
87dc0f797d | ||
|
|
27fe18d715 | ||
|
|
18fd5d7a7a | ||
|
|
39a0167cbb | ||
|
|
704853b3be | ||
|
|
f720383adc | ||
|
|
9895d60484 | ||
|
|
97b7c5f91f | ||
|
|
6da670ac45 | ||
|
|
ae0c62aae8 | ||
|
|
d9290bc940 | ||
|
|
3c016c978b | ||
|
|
caf940c805 | ||
|
|
14b8f1bd4d | ||
|
|
f8a623e0c6 | ||
|
|
35540b4ff3 | ||
|
|
87c2b89120 | ||
|
|
a0fcede368 | ||
|
|
c17407ec94 | ||
|
|
c9fb076dd7 | ||
|
|
9471dde765 | ||
|
|
2bbff23640 | ||
|
|
9ab4c0f5cb | ||
|
|
1322c0671b | ||
|
|
7963713314 | ||
|
|
0e7f176a16 | ||
|
|
2496c846f4 | ||
|
|
b4e670c36f | ||
|
|
63373385f5 | ||
|
|
a33858523f | ||
|
|
9bcc0f1d37 | ||
|
|
836fa8ce58 | ||
|
|
7de1c4b4cf | ||
|
|
6877fba346 | ||
|
|
c56d0f2462 | ||
|
|
2ddfecb47a | ||
|
|
3e8ea4e590 | ||
|
|
a04daaff75 | ||
|
|
8e8d1dcca8 | ||
|
|
40e2b5fd4b | ||
|
|
c82163f287 | ||
|
|
2515811356 | ||
|
|
3f242a7114 | ||
|
|
c322ad5d33 | ||
|
|
5cce5454e1 | ||
|
|
fe88004eaf | ||
|
|
435d7e0cdf | ||
|
|
42ca206037 | ||
|
|
ac7f1ca3aa | ||
|
|
2b691a94a4 | ||
|
|
3b4f7c8ab7 | ||
|
|
bc6f9703e3 | ||
|
|
cdf596da19 | ||
|
|
d72c1d9be7 | ||
|
|
ba059b0b2a | ||
|
|
22e852cf36 | ||
|
|
496043dd9c | ||
|
|
d2b10d3b09 | ||
|
|
bd9a10d349 | ||
|
|
f24691cd60 | ||
|
|
07cdca0a2b | ||
|
|
7f86dffa06 | ||
|
|
1735da6c17 | ||
|
|
37ac4d6b16 | ||
|
|
cd77e78f0a | ||
|
|
86170406af | ||
|
|
03412b12d0 | ||
|
|
da18cb70fa | ||
|
|
01b3d19d6d | ||
|
|
564cb451ac | ||
|
|
1cde511d6d | ||
|
|
39592518b0 | ||
|
|
57c62b5330 | ||
|
|
0c7b333743 | ||
|
|
6766b17131 | ||
|
|
bd602b6d11 | ||
|
|
56f91dfeab | ||
|
|
849ac908c8 | ||
|
|
0b5391447e | ||
|
|
d34fbe99e8 | ||
|
|
0aefee74c6 | ||
|
|
1c99c4ceac | ||
|
|
4f15700db3 | ||
|
|
1389e2fd8e | ||
|
|
4444f297c2 | ||
|
|
c375fb0b7f | ||
|
|
f7634d5846 | ||
|
|
e4a4e8c790 | ||
|
|
9d8d4b8796 | ||
|
|
b5ebaa1fc0 | ||
|
|
bf41bb4ced | ||
|
|
cdb88a33ac | ||
|
|
077a4a0b54 | ||
|
|
e8f6936480 | ||
|
|
6126cc2158 | ||
|
|
4926a0cb91 | ||
|
|
f74146648e | ||
|
|
b0491faeee | ||
|
|
9fa673d105 | ||
|
|
7ac8e5c0ab | ||
|
|
b900e3513c | ||
|
|
65380457b8 | ||
|
|
efbd84276c | ||
|
|
f260e771ea | ||
|
|
dec3f0713f | ||
|
|
21a23d7488 | ||
|
|
cb664d1a4b | ||
|
|
45ca32dcb9 | ||
|
|
93df3ef612 | ||
|
|
f682ceb8e2 | ||
|
|
545424b86b | ||
|
|
1d10636a5f | ||
|
|
9f34cdb2f8 | ||
|
|
da4c532efb | ||
|
|
97c981764d | ||
|
|
3310474137 | ||
|
|
6c81ddd00f | ||
|
|
da50e2969d | ||
|
|
7afc0aa2ba | ||
|
|
1e35987658 | ||
|
|
44c9c36c65 | ||
|
|
4c02326989 | ||
|
|
af40956e58 | ||
|
|
3b22615aec | ||
|
|
cc1c891400 | ||
|
|
3d85cbd7f0 |
@@ -1 +1 @@
|
||||
617d38f41f935b46a68f550d9add2e38abb3f168
|
||||
0d28acc44093df24b2526dea3d6ffefd6b0a54f0
|
||||
|
||||
@@ -31,6 +31,8 @@ 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
|
||||
|
||||
@@ -31,6 +31,8 @@ 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)
|
||||
- 其他
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
--- 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
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/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
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.25.12"
|
||||
VERSION="1.26.8"
|
||||
PATCH_COMMITS=(
|
||||
"afe69d3cec1c6dcf0f1797b20546795730850070"
|
||||
"1ed289b0cf87dc5aae9c6fe1aa5f200a83412938"
|
||||
"f080b0c6346eb690c0dc82497b35925f385b35ac"
|
||||
"2d9c12887c342fb9051d231aa5388743cb7e9cb6"
|
||||
"367663849c656612fc3c8875ef6e7f6b0c93dbd0"
|
||||
)
|
||||
CURL_ARGS=(
|
||||
-fL
|
||||
@@ -25,12 +26,13 @@ tar -xzf "go${VERSION}.darwin-arm64.tar.gz"
|
||||
mv go go_osx
|
||||
cd go_osx
|
||||
|
||||
# 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/
|
||||
# these patch URLs only work on golang1.26.x
|
||||
# that means after golang1.27 release it must be changed
|
||||
# see: https://github.com/SagerNet/go/commits/release-branch.go1.26/
|
||||
# revert:
|
||||
# 33d3f603c1: "cmd/link/internal/ld: use 12.0.0 OS/SDK versions for macOS linking"
|
||||
# 937368f84e: "crypto/x509: change how we retrieve chains on darwin"
|
||||
# d90a57ffe8: "cmd/link/internal/ld: unify OS/SDK versions for macOS linking"
|
||||
|
||||
for patch_commit in "${PATCH_COMMITS[@]}"; do
|
||||
curl "${CURL_ARGS[@]}" "https://github.com/SagerNet/go/commit/${patch_commit}.diff" | patch --verbose -p 1
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="1.25.12"
|
||||
VERSION="1.26.8"
|
||||
PATCH_COMMITS=(
|
||||
"da4094da73b3b419e3f347594d805e2831f65667"
|
||||
"824aa60e77f06dbae86c20a164c78df722eb7047"
|
||||
"a3b6ba31c8cc67b6d899b978bba7b53e95afc46b"
|
||||
"edfa8de63435a409a59f60731b66ab5940d6d3a4"
|
||||
"284f9b24d6284984966a8431e30fdc2583938f96"
|
||||
"9864798dee8dd47b55d1d5100d2f1b909a2a6e6c"
|
||||
"a4ae550aa148b04c9d4890e98bee63aede5c4b53"
|
||||
"95b851f661584711faa8115b3234a461044f4510"
|
||||
"b0bf0a863cf218b9bb0d6c013903ab160ae0c39b"
|
||||
"7c7a2a0d68920f8764d9a3aeae7ad1067a17b3fa"
|
||||
"ba34356e82f3c12a7303d2231e6ea0e42bbbb3bf"
|
||||
"472a88edbc2a42feb53fa31d29ab54d33840dca6"
|
||||
)
|
||||
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.25.x
|
||||
# that means after golang1.26 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
||||
# these patch URLs only work on golang1.26.x
|
||||
# that means after golang1.27 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.26/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
|
||||
@@ -4,7 +4,7 @@ PROJECTS=$(dirname "$0")/../..
|
||||
|
||||
function updateClient() {
|
||||
pushd clients/$1
|
||||
git fetch
|
||||
git fetch origin main
|
||||
git reset FETCH_HEAD --hard
|
||||
popd
|
||||
git add clients/$1
|
||||
@@ -12,3 +12,4 @@ function updateClient() {
|
||||
|
||||
updateClient "apple"
|
||||
updateClient "android"
|
||||
updateClient "desktop"
|
||||
|
||||
@@ -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/dev > "$SCRIPT_DIR/CRONET_GO_VERSION"
|
||||
git -C $PROJECTS/cronet-go rev-parse origin/go_dev > "$SCRIPT_DIR/CRONET_GO_VERSION"
|
||||
|
||||
+405
-71
@@ -16,12 +16,15 @@ on:
|
||||
- All
|
||||
- Binary
|
||||
- Android
|
||||
- Windows
|
||||
- Linux
|
||||
- Apple
|
||||
- Apple-release
|
||||
- iOS
|
||||
- macOS
|
||||
- tvOS
|
||||
- macOS-standalone
|
||||
- iOS-jailbreak
|
||||
- publish-android
|
||||
test_message:
|
||||
description: "TestFlight test message"
|
||||
@@ -51,7 +54,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.12
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -142,7 +145,7 @@ jobs:
|
||||
if: ${{ ! matrix.legacy_win7 }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.12
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -157,7 +160,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/go/go_win7
|
||||
key: go_win7_12512
|
||||
key: go_win7_1268
|
||||
- name: Setup Go for Windows 7
|
||||
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
||||
env:
|
||||
@@ -175,24 +178,25 @@ jobs:
|
||||
with:
|
||||
ndk-version: r28
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
if: matrix.naive && matrix.variant != 'purego'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
|
||||
git init ~/cronet-go
|
||||
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
|
||||
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go sparse-checkout set --no-cone '/*' '!/lib'
|
||||
git -C ~/cronet-go fetch --depth=1 --filter=blob:none origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go checkout FETCH_HEAD
|
||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||
- name: Regenerate Debian keyring
|
||||
if: matrix.naive
|
||||
if: matrix.naive && matrix.variant != 'purego'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
|
||||
cd ~/cronet-go
|
||||
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
|
||||
- name: Download Chromium toolchain
|
||||
if: matrix.naive
|
||||
if: matrix.naive && matrix.variant != 'purego'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
cd ~/cronet-go
|
||||
@@ -202,7 +206,7 @@ jobs:
|
||||
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} download-toolchain
|
||||
fi
|
||||
- name: Set Chromium toolchain environment
|
||||
if: matrix.naive
|
||||
if: matrix.naive && matrix.variant != 'purego'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
cd ~/cronet-go
|
||||
@@ -252,9 +256,15 @@ jobs:
|
||||
- name: Extract libcronet.so
|
||||
if: matrix.variant == 'purego' && matrix.naive
|
||||
run: |
|
||||
cd ~/cronet-go
|
||||
CGO_ENABLED=0 go build -v -o "$RUNNER_TEMP/build-naive" ./cmd/build-naive
|
||||
GOPROXY=direct GOSUMDB=off "$RUNNER_TEMP/build-naive" extract-lib --target ${{ matrix.os }}/${{ matrix.arch }} -o $GITHUB_WORKSPACE/dist
|
||||
set -xeuo pipefail
|
||||
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
|
||||
LIBRARY_PATH="lib/${{ matrix.os }}_${{ matrix.arch }}/libcronet.so"
|
||||
git init "$RUNNER_TEMP/cronet-go-lib"
|
||||
git -C "$RUNNER_TEMP/cronet-go-lib" remote add origin https://github.com/sagernet/cronet-go.git
|
||||
git -C "$RUNNER_TEMP/cronet-go-lib" sparse-checkout set --no-cone "/$LIBRARY_PATH"
|
||||
git -C "$RUNNER_TEMP/cronet-go-lib" fetch --depth=1 --filter=blob:none origin "$CRONET_GO_VERSION"
|
||||
git -C "$RUNNER_TEMP/cronet-go-lib" checkout FETCH_HEAD
|
||||
cp "$RUNNER_TEMP/cronet-go-lib/$LIBRARY_PATH" dist/
|
||||
- name: Build (glibc)
|
||||
if: matrix.variant == 'glibc'
|
||||
run: |
|
||||
@@ -478,7 +488,7 @@ jobs:
|
||||
if: ${{ ! matrix.legacy_osx }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.3
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -493,7 +503,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/go/go_osx
|
||||
key: go_osx_1258
|
||||
key: go_osx_1268
|
||||
- name: Setup Go for macOS 10.13
|
||||
if: matrix.legacy_osx && steps.cache-go-for-macos1013.outputs.cache-hit != 'true'
|
||||
env:
|
||||
@@ -515,7 +525,7 @@ jobs:
|
||||
if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then
|
||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
|
||||
else
|
||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
||||
TAGS=$(sed 's/,with_usbip//' release/DEFAULT_BUILD_TAGS_OTHERS)
|
||||
fi
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Set shared ldflags
|
||||
@@ -577,7 +587,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.4
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
@@ -621,15 +631,13 @@ jobs:
|
||||
if: matrix.naive
|
||||
run: |
|
||||
$CRONET_GO_VERSION = Get-Content .github/CRONET_GO_VERSION
|
||||
git init "$env:RUNNER_TEMP/cronet-go"
|
||||
git -C "$env:RUNNER_TEMP/cronet-go" remote add origin https://github.com/sagernet/cronet-go.git
|
||||
git -C "$env:RUNNER_TEMP/cronet-go" fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||||
git -C "$env:RUNNER_TEMP/cronet-go" checkout FETCH_HEAD
|
||||
$env:CGO_ENABLED = "0"
|
||||
go -C "$env:RUNNER_TEMP/cronet-go" build -v -o "$env:RUNNER_TEMP/build-naive.exe" ./cmd/build-naive
|
||||
$env:GOPROXY = "direct"
|
||||
$env:GOSUMDB = "off"
|
||||
& "$env:RUNNER_TEMP/build-naive.exe" extract-lib --target windows/${{ matrix.arch }} -o dist
|
||||
$LIBRARY_PATH = "lib/windows_${{ matrix.arch }}/libcronet.dll"
|
||||
git init "$env:RUNNER_TEMP/cronet-go-lib"
|
||||
git -C "$env:RUNNER_TEMP/cronet-go-lib" remote add origin https://github.com/sagernet/cronet-go.git
|
||||
git -C "$env:RUNNER_TEMP/cronet-go-lib" sparse-checkout set --no-cone "/$LIBRARY_PATH"
|
||||
git -C "$env:RUNNER_TEMP/cronet-go-lib" fetch --depth=1 --filter=blob:none origin "$CRONET_GO_VERSION"
|
||||
git -C "$env:RUNNER_TEMP/cronet-go-lib" checkout FETCH_HEAD
|
||||
Copy-Item "$env:RUNNER_TEMP/cronet-go-lib/$LIBRARY_PATH" dist
|
||||
- name: Archive
|
||||
if: matrix.naive
|
||||
run: |
|
||||
@@ -682,7 +690,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.12
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
@@ -746,7 +754,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.12
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -854,7 +862,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.12
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache/restore@v4
|
||||
@@ -936,10 +944,278 @@ jobs:
|
||||
env:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||
build_windows_client:
|
||||
name: Build Windows client (${{ matrix.desktop_arch }})
|
||||
if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Windows') && github.ref != 'refs/heads/oldstable'
|
||||
runs-on: windows-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { desktop_arch: x64, rust_target: x86_64-pc-windows-msvc }
|
||||
- { desktop_arch: x86, rust_target: i686-pc-windows-msvc }
|
||||
- { desktop_arch: arm64, rust_target: aarch64-pc-windows-msvc }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Checkout desktop client
|
||||
run: git submodule update --init clients/desktop
|
||||
- name: Checkout main branch
|
||||
if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
|
||||
run: git -C clients/desktop checkout main
|
||||
- name: Checkout dev branch
|
||||
if: github.ref == 'refs/heads/testing'
|
||||
run: git -C clients/desktop checkout dev
|
||||
- name: Checkout desktop submodules
|
||||
run: git -C clients/desktop submodule update --init --recursive
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: go-mod-desktop-windows-${{ hashFiles('go.sum') }}
|
||||
restore-keys: go-mod-desktop-windows-
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@1.88.0
|
||||
with:
|
||||
targets: ${{ matrix.rust_target }}
|
||||
- name: Cache Windows sharing module
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/git
|
||||
~/.cargo/registry
|
||||
clients/desktop/bin/windows-share-toolchain/cargo-target
|
||||
key: windows-share-${{ matrix.rust_target }}-${{ hashFiles('clients/desktop/native/windows-share/Cargo.lock') }}
|
||||
restore-keys: windows-share-${{ matrix.rust_target }}-
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: clients/desktop/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: clients/desktop/package.json
|
||||
cache: pnpm
|
||||
cache-dependency-path: |
|
||||
clients/desktop/pnpm-lock.yaml
|
||||
clients/desktop/dashboard/pnpm-lock.yaml
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }}
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
"PUBLISHED=false" >> $env:GITHUB_ENV
|
||||
}
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Update version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_desktop_version --ci
|
||||
- name: Update nightly version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_desktop_version --ci --nightly
|
||||
- name: Setup signing
|
||||
run: |-
|
||||
$certificatePath = Join-Path $env:RUNNER_TEMP "windows-signing.p12"
|
||||
[IO.File]::WriteAllBytes($certificatePath, [Convert]::FromBase64String($env:WINDOWS_CERTIFICATES_P12))
|
||||
@{
|
||||
windows = @{
|
||||
certificateFile = $certificatePath
|
||||
certificatePassword = $env:WINDOWS_P12_PASSWORD
|
||||
}
|
||||
} | ConvertTo-Json -Depth 3 | Set-Content -Encoding utf8NoBOM clients/desktop/signing.local.json
|
||||
env:
|
||||
WINDOWS_CERTIFICATES_P12: ${{ secrets.WINDOWS_CERTIFICATES_P12 }}
|
||||
WINDOWS_P12_PASSWORD: ${{ secrets.WINDOWS_P12_PASSWORD }}
|
||||
- name: Install dependencies
|
||||
run: |-
|
||||
pnpm -C clients/desktop install
|
||||
- name: Build
|
||||
run: |-
|
||||
pnpm -C clients/desktop package:win -- ${{ matrix.desktop_arch }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Prepare upload
|
||||
run: |-
|
||||
$artifacts = @(Get-ChildItem clients/desktop/release/SFW-*.exe -File)
|
||||
if ($artifacts.Count -ne 1) {
|
||||
throw "expected 1 Windows installer for ${{ matrix.desktop_arch }}, found $($artifacts.Count)"
|
||||
}
|
||||
New-Item -ItemType Directory -Force dist | Out-Null
|
||||
Copy-Item $artifacts.FullName dist
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-windows-client-${{ matrix.desktop_arch }}
|
||||
path: 'dist'
|
||||
build_linux_client:
|
||||
name: Build Linux client (${{ matrix.desktop_arch }})
|
||||
if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Linux') && github.ref != 'refs/heads/oldstable'
|
||||
runs-on: ubuntu-26.04
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- { desktop_arch: x64, go_arch: amd64 }
|
||||
- { desktop_arch: arm64, go_arch: arm64 }
|
||||
- { desktop_arch: armv7l, go_arch: arm, go_arm: "7" }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Checkout desktop client
|
||||
run: git submodule update --init clients/desktop
|
||||
- name: Checkout main branch
|
||||
if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
|
||||
run: git -C clients/desktop checkout main
|
||||
- name: Checkout dev branch
|
||||
if: github.ref == 'refs/heads/testing'
|
||||
run: git -C clients/desktop checkout dev
|
||||
- name: Checkout desktop submodules
|
||||
run: git -C clients/desktop submodule update --init --recursive
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: go-mod-desktop-linux-${{ hashFiles('go.sum') }}
|
||||
restore-keys: go-mod-desktop-linux-
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: clients/desktop/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: clients/desktop/package.json
|
||||
cache: pnpm
|
||||
cache-dependency-path: |
|
||||
clients/desktop/pnpm-lock.yaml
|
||||
clients/desktop/dashboard/pnpm-lock.yaml
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Update version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_desktop_version --ci
|
||||
- name: Update nightly version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_desktop_version --ci --nightly
|
||||
- name: Clone cronet-go
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
CRONET_GO_VERSION=$(cat .github/CRONET_GO_VERSION)
|
||||
git init ~/cronet-go
|
||||
git -C ~/cronet-go remote add origin https://github.com/sagernet/cronet-go.git
|
||||
git -C ~/cronet-go 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
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
|
||||
cd ~/cronet-go
|
||||
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
|
||||
- name: Download Chromium toolchain
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
cd ~/cronet-go
|
||||
go run ./cmd/build-naive --target=linux/${{ matrix.go_arch }} download-toolchain
|
||||
- name: Set Chromium toolchain environment
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
cd ~/cronet-go
|
||||
go run ./cmd/build-naive --target=linux/${{ matrix.go_arch }} env >> "$GITHUB_ENV"
|
||||
- name: Install package tools
|
||||
run: |-
|
||||
set -euo pipefail
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y debsigs libarchive-tools rpm zstd
|
||||
echo '%_rpmformat 4' > "$HOME/.rpmmacros"
|
||||
- name: Install dependencies
|
||||
run: |-
|
||||
pnpm -C clients/desktop install
|
||||
- name: Build
|
||||
run: |-
|
||||
pnpm -C clients/desktop package:linux -- deb rpm pacman ${{ matrix.desktop_arch }}
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARM: ${{ matrix.go_arm }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup package signing
|
||||
run: |-
|
||||
set -euo pipefail
|
||||
SIGNING_GNUPG_HOME="$RUNNER_TEMP/package-signing-gnupg"
|
||||
SIGNING_PASSPHRASE_FILE="$RUNNER_TEMP/package-signing-passphrase"
|
||||
mkdir -m 700 "$SIGNING_GNUPG_HOME"
|
||||
printf '%s' "$GPG_PASSPHRASE" > "$SIGNING_PASSPHRASE_FILE"
|
||||
chmod 600 "$SIGNING_PASSPHRASE_FILE"
|
||||
{
|
||||
echo "GNUPGHOME=$SIGNING_GNUPG_HOME"
|
||||
echo "SIGNING_PASSPHRASE_FILE=$SIGNING_PASSPHRASE_FILE"
|
||||
} >> "$GITHUB_ENV"
|
||||
printf '%s' "$GPG_KEY" | gpg \
|
||||
--homedir "$SIGNING_GNUPG_HOME" \
|
||||
--batch \
|
||||
--pinentry-mode loopback \
|
||||
--passphrase-file "$SIGNING_PASSPHRASE_FILE" \
|
||||
--import
|
||||
cat > "$HOME/.rpmmacros" <<EOF
|
||||
%_gpg_name $GPG_KEY_ID
|
||||
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase-file $SIGNING_PASSPHRASE_FILE
|
||||
EOF
|
||||
env:
|
||||
GPG_KEY: ${{ secrets.GPG_KEY }}
|
||||
GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
- name: Sign Linux packages
|
||||
run: |-
|
||||
set -euo pipefail
|
||||
debsigs \
|
||||
--sign=origin \
|
||||
-k "$GPG_KEY_ID" \
|
||||
--gpgopts "--batch --pinentry-mode loopback --passphrase-file $SIGNING_PASSPHRASE_FILE" \
|
||||
clients/desktop/release/SFL-*.deb
|
||||
rpmsign --addsign clients/desktop/release/SFL-*.rpm
|
||||
env:
|
||||
GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
|
||||
- name: Prepare upload
|
||||
run: |-
|
||||
shopt -s nullglob
|
||||
artifacts=(clients/desktop/release/SFL-*.deb clients/desktop/release/SFL-*.rpm clients/desktop/release/SFL-*.pkg.tar.zst)
|
||||
if (( ${#artifacts[@]} != 3 )); then
|
||||
echo "expected 3 Linux packages, found ${#artifacts[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p dist
|
||||
cp "${artifacts[@]}" dist
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-linux-client-${{ matrix.desktop_arch }}
|
||||
path: 'dist'
|
||||
build_apple_library:
|
||||
name: Build Apple library (${{ matrix.artifact }})
|
||||
runs-on: ${{ matrix.build && 'macos-26' || 'ubuntu-latest' }}
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone'
|
||||
runs-on: ${{ matrix.build && 'xcode-27' || 'ubuntu-latest' }}
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone' || inputs.build == 'iOS-jailbreak'
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
@@ -948,7 +1224,7 @@ jobs:
|
||||
include:
|
||||
- artifact: ios-arm64
|
||||
platform: ios/arm64
|
||||
build: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'iOS' }}
|
||||
build: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'iOS' || inputs.build == 'iOS-jailbreak' }}
|
||||
- artifact: tvos-arm64
|
||||
platform: tvos/arm64
|
||||
build: ${{ github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'tvOS') }}
|
||||
@@ -968,8 +1244,11 @@ jobs:
|
||||
if: matrix.build
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.12
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Patch Go for iOS
|
||||
if: matrix.build && (matrix.platform == 'ios/arm64' || matrix.platform == 'tvos/arm64')
|
||||
run: .github/patch_go_for_ios.sh
|
||||
- name: Cache Go modules
|
||||
if: matrix.build
|
||||
uses: actions/cache@v4
|
||||
@@ -982,13 +1261,15 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/Library/Caches/go-build
|
||||
key: libbox-go-build-${{ matrix.artifact }}-1.25.12-${{ hashFiles('go.sum') }}
|
||||
restore-keys: libbox-go-build-${{ matrix.artifact }}-1.25.12-
|
||||
key: libbox-go-build-${{ matrix.artifact }}-1.26.8-${{ hashFiles('go.sum') }}
|
||||
restore-keys: libbox-go-build-${{ matrix.artifact }}-1.26.8-
|
||||
- name: Set tag
|
||||
if: matrix.build
|
||||
run: git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Build library
|
||||
if: matrix.build
|
||||
env:
|
||||
GOTOOLCHAIN: local
|
||||
run: |-
|
||||
make lib_install
|
||||
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||
@@ -1004,8 +1285,8 @@ jobs:
|
||||
compression-level: 1
|
||||
build_apple:
|
||||
name: Build Apple release clients
|
||||
runs-on: ${{ matrix.if && 'macos-26' || 'ubuntu-latest' }}
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone'
|
||||
runs-on: ${{ matrix.if && 'xcode-27' || 'ubuntu-latest' }}
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone' || inputs.build == 'iOS-jailbreak'
|
||||
needs:
|
||||
- calculate_version
|
||||
- build_apple_library
|
||||
@@ -1023,6 +1304,20 @@ jobs:
|
||||
destination: ''
|
||||
archive: ''
|
||||
upload: ''
|
||||
upload_extension: ''
|
||||
altool_platform: ''
|
||||
- name: iOS-jailbreak
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple-release' || inputs.build == 'iOS-jailbreak' }}
|
||||
type: jailbreak
|
||||
platform: ios
|
||||
variant: ''
|
||||
library_arch: ''
|
||||
scheme: ''
|
||||
destination: ''
|
||||
archive: ''
|
||||
upload: ''
|
||||
upload_extension: ''
|
||||
altool_platform: ''
|
||||
steps: &build_apple_steps
|
||||
- name: Checkout
|
||||
if: matrix.if
|
||||
@@ -1036,7 +1331,7 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.12
|
||||
go-version: 1.26.8
|
||||
cache: false
|
||||
- name: Cache Go modules
|
||||
if: matrix.if
|
||||
@@ -1080,36 +1375,32 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ matrix.type == 'standalone' && 'clients/apple/build/SFM.System-*.dd' || matrix.type == 'jailbreak' && 'clients/apple/build/jailbreak/DerivedData' || format('{0}/DerivedData', runner.temp) }}
|
||||
key: apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ github.sha }}
|
||||
key: apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ github.ref_name }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-
|
||||
apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-
|
||||
apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ github.ref_name }}-${{ hashFiles('clients/apple/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}-
|
||||
apple-derived-data-${{ matrix.type }}-${{ matrix.platform }}-${{ matrix.variant || 'default' }}-${{ github.ref_name }}-
|
||||
- name: Setup App Store Connect key
|
||||
if: matrix.if && matrix.type != 'jailbreak' && github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
ASC_KEY_PATH=$RUNNER_TEMP/AuthKey.p8
|
||||
# altool selects individual key authentication by the ApiKey_ file name prefix;
|
||||
# an AuthKey_ prefixed file is signed as a team key and rejected.
|
||||
ASC_PRIVATE_KEYS_DIR=$RUNNER_TEMP/private_keys
|
||||
mkdir -p $ASC_PRIVATE_KEYS_DIR
|
||||
ASC_KEY_PATH=$ASC_PRIVATE_KEYS_DIR/ApiKey_$ASC_KEY_ID.p8
|
||||
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
|
||||
echo "API_PRIVATE_KEYS_DIR=$ASC_PRIVATE_KEYS_DIR" >> "$GITHUB_ENV"
|
||||
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
|
||||
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
|
||||
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
|
||||
env:
|
||||
ASC_KEY: ${{ secrets.ASC_KEY }}
|
||||
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
|
||||
- name: Install Apple provisioning profiles
|
||||
if: matrix.if && matrix.type != 'jailbreak'
|
||||
run: |-
|
||||
PROFILES_ZIP_PATH=$RUNNER_TEMP/AppleProvisioningProfiles.zip
|
||||
printf '%s%s%s%s%s%s%s%s' \
|
||||
"$PROVISIONING_PROFILES_1" \
|
||||
"$PROVISIONING_PROFILES_2" \
|
||||
"$PROVISIONING_PROFILES_3" \
|
||||
"$PROVISIONING_PROFILES_4" \
|
||||
"$PROVISIONING_PROFILES_5" \
|
||||
"$PROVISIONING_PROFILES_6" \
|
||||
"$PROVISIONING_PROFILES_7" \
|
||||
"$PROVISIONING_PROFILES_8" \
|
||||
| base64 --decode -o $PROFILES_ZIP_PATH
|
||||
for variable in $(compgen -v | grep -E '^PROVISIONING_PROFILES_[0-9]+$' | sort -t_ -k3 -n); do
|
||||
printf '%s' "${!variable}"
|
||||
done | base64 --decode -o $PROFILES_ZIP_PATH
|
||||
PROFILES_PATH="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
|
||||
mkdir -p "$PROFILES_PATH"
|
||||
unzip -q $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
|
||||
@@ -1122,6 +1413,14 @@ jobs:
|
||||
PROVISIONING_PROFILES_6: ${{ secrets.APPLE_PROVISIONING_PROFILES_6 }}
|
||||
PROVISIONING_PROFILES_7: ${{ secrets.APPLE_PROVISIONING_PROFILES_7 }}
|
||||
PROVISIONING_PROFILES_8: ${{ secrets.APPLE_PROVISIONING_PROFILES_8 }}
|
||||
PROVISIONING_PROFILES_9: ${{ secrets.APPLE_PROVISIONING_PROFILES_9 }}
|
||||
PROVISIONING_PROFILES_10: ${{ secrets.APPLE_PROVISIONING_PROFILES_10 }}
|
||||
PROVISIONING_PROFILES_11: ${{ secrets.APPLE_PROVISIONING_PROFILES_11 }}
|
||||
PROVISIONING_PROFILES_12: ${{ secrets.APPLE_PROVISIONING_PROFILES_12 }}
|
||||
PROVISIONING_PROFILES_13: ${{ secrets.APPLE_PROVISIONING_PROFILES_13 }}
|
||||
PROVISIONING_PROFILES_14: ${{ secrets.APPLE_PROVISIONING_PROFILES_14 }}
|
||||
PROVISIONING_PROFILES_15: ${{ secrets.APPLE_PROVISIONING_PROFILES_15 }}
|
||||
PROVISIONING_PROFILES_16: ${{ secrets.APPLE_PROVISIONING_PROFILES_16 }}
|
||||
- name: Setup Apple Development certificate
|
||||
if: matrix.if && matrix.type != 'jailbreak'
|
||||
run: |-
|
||||
@@ -1160,14 +1459,33 @@ jobs:
|
||||
DEVELOPER_ID_CERTIFICATES_P12: ${{ secrets.DEVELOPER_ID_CERTIFICATES_P12 }}
|
||||
P12_PASSWORD: ${{ secrets.DEVELOPER_ID_P12_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
|
||||
- name: Setup Apple Distribution certificate
|
||||
if: matrix.if && matrix.type == 'app-store'
|
||||
run: |-
|
||||
CERTIFICATE_PATH=$RUNNER_TEMP/AppleDistributionCertificates.p12
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/apple-distribution.keychain-db
|
||||
echo -n "$APPLE_DISTRIBUTION_CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security list-keychains -d user -s $KEYCHAIN_PATH $APPLE_DEVELOPMENT_KEYCHAIN_PATH
|
||||
security default-keychain -d user -s $KEYCHAIN_PATH
|
||||
security find-identity -v -p codesigning $KEYCHAIN_PATH
|
||||
env:
|
||||
APPLE_DISTRIBUTION_CERTIFICATES_P12: ${{ secrets.APPLE_DISTRIBUTION_CERTIFICATES_P12 }}
|
||||
P12_PASSWORD: ${{ secrets.APPLE_DISTRIBUTION_P12_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
|
||||
- name: Setup notarization credentials
|
||||
if: matrix.if && matrix.type == 'standalone' && github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $SIGNING_KEYCHAIN_PATH
|
||||
xcrun notarytool store-credentials "notarytool-password" \
|
||||
# store-credentials prompts for the issuer ID even when --issuer is absent;
|
||||
# an empty answer selects individual key authentication.
|
||||
printf '\n' | xcrun notarytool store-credentials "notarytool-password" \
|
||||
--key $ASC_KEY_PATH \
|
||||
--key-id $ASC_KEY_ID \
|
||||
--issuer $ASC_KEY_ISSUER_ID \
|
||||
--keychain $SIGNING_KEYCHAIN_PATH
|
||||
env:
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
|
||||
@@ -1201,16 +1519,17 @@ jobs:
|
||||
go run ./cmd/internal/merge_apple_xcframework \
|
||||
-output clients/apple/Libbox.xcframework \
|
||||
"${library_inputs[@]}"
|
||||
- name: Update macOS version
|
||||
if: matrix.if && matrix.name == 'macOS' && github.event_name == 'workflow_dispatch'
|
||||
- name: Update project version
|
||||
if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
|
||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
|
||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
|
||||
PROJECT_VERSION_VARIABLE=$(echo "${{ matrix.platform }}" | tr '[:lower:]' '[:upper:]')_PROJECT_VERSION
|
||||
PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_project_version ${{ matrix.platform }})
|
||||
echo "$PROJECT_VERSION_VARIABLE=$PROJECT_VERSION"
|
||||
echo "$PROJECT_VERSION_VARIABLE=$PROJECT_VERSION" >> "$GITHUB_ENV"
|
||||
- name: Update version
|
||||
if: matrix.if && matrix.name != 'iOS'
|
||||
if: matrix.if
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_apple_version --ci
|
||||
go run -v ./cmd/internal/update_apple_version --ci ${{ matrix.type == 'app-store' && '--testflight' || '' }}
|
||||
- name: Archive App Store build
|
||||
if: matrix.if && matrix.type == 'app-store'
|
||||
run: |-
|
||||
@@ -1231,12 +1550,16 @@ jobs:
|
||||
cd clients/apple
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "${{ matrix.archive }}" \
|
||||
-exportOptionsPlist ${{ matrix.upload }} \
|
||||
-authenticationKeyPath $ASC_KEY_PATH \
|
||||
-authenticationKeyID $ASC_KEY_ID \
|
||||
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||
-exportPath "$RUNNER_TEMP/export" \
|
||||
-exportOptionsPlist ${{ matrix.upload }}
|
||||
# altool requires --api-issuer even for an individual key, and ignores its value.
|
||||
xcrun altool --upload-app \
|
||||
-f "$RUNNER_TEMP"/export/*.${{ matrix.upload_extension }} \
|
||||
-t ${{ matrix.altool_platform }} \
|
||||
--api-key $ASC_KEY_ID \
|
||||
--api-issuer 00000000-0000-0000-0000-000000000000
|
||||
- name: Publish to TestFlight
|
||||
if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/testing'
|
||||
if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
arguments=(publish_testflight "${{ matrix.platform }}")
|
||||
if [[ -n "$TEST_MESSAGE" ]]; then
|
||||
@@ -1273,6 +1596,7 @@ jobs:
|
||||
make -C clients/apple build_ios_deb
|
||||
mkdir -p dist
|
||||
cp "clients/apple/build/jailbreak/SFI-${VERSION}-iphoneos-arm64.deb" dist
|
||||
cp "clients/apple/build/jailbreak/SFI-${VERSION}-iphoneos-arm64e.deb" dist
|
||||
env:
|
||||
XCODEBUILD_CLONED_SOURCE_PACKAGES_DIR_PATH: ${{ runner.temp }}/SourcePackages
|
||||
- name: Report cache directory sizes
|
||||
@@ -1293,7 +1617,7 @@ jobs:
|
||||
path: 'dist'
|
||||
build_apple_app_store:
|
||||
name: Build Apple App Store clients
|
||||
runs-on: ${{ matrix.if && 'macos-26' || 'ubuntu-latest' }}
|
||||
runs-on: ${{ matrix.if && 'xcode-27' || 'ubuntu-latest' }}
|
||||
if: github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS')
|
||||
needs:
|
||||
- calculate_version
|
||||
@@ -1309,7 +1633,9 @@ jobs:
|
||||
scheme: SFI
|
||||
destination: 'generic/platform=iOS'
|
||||
archive: build/SFI.xcarchive
|
||||
upload: SFI/Upload.plist
|
||||
upload: SFI/AppStoreExport.plist
|
||||
upload_extension: ipa
|
||||
altool_platform: ios
|
||||
variant: ''
|
||||
library_arch: ''
|
||||
- name: macOS
|
||||
@@ -1319,7 +1645,9 @@ jobs:
|
||||
scheme: SFM
|
||||
destination: 'generic/platform=macOS'
|
||||
archive: build/SFM.xcarchive
|
||||
upload: SFI/Upload.plist
|
||||
upload: SFI/AppStoreExport.plist
|
||||
upload_extension: pkg
|
||||
altool_platform: macos
|
||||
variant: ''
|
||||
library_arch: ''
|
||||
- name: tvOS
|
||||
@@ -1329,13 +1657,15 @@ jobs:
|
||||
scheme: SFT
|
||||
destination: 'generic/platform=tvOS'
|
||||
archive: build/SFT.xcarchive
|
||||
upload: SFI/Upload.plist
|
||||
upload: SFI/AppStoreExport.plist
|
||||
upload_extension: ipa
|
||||
altool_platform: appletvos
|
||||
variant: ''
|
||||
library_arch: ''
|
||||
steps: *build_apple_steps
|
||||
upload:
|
||||
name: Upload builds
|
||||
if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone')"
|
||||
if: "!failure() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Windows' || inputs.build == 'Linux' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone' || inputs.build == 'iOS-jailbreak')"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -1349,6 +1679,8 @@ jobs:
|
||||
- build_windows
|
||||
- build_android_library
|
||||
- build_android
|
||||
- build_windows_client
|
||||
- build_linux_client
|
||||
- build_apple_library
|
||||
- build_apple
|
||||
steps:
|
||||
@@ -1411,6 +1743,8 @@ jobs:
|
||||
- build_windows
|
||||
- build_android_library
|
||||
- build_android
|
||||
- build_windows_client
|
||||
- build_linux_client
|
||||
- build_apple_library
|
||||
- build_apple
|
||||
- build_apple_app_store
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.12
|
||||
go-version: 1.26.8
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
@@ -63,7 +63,8 @@ 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 fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go sparse-checkout set --no-cone '/*' '!/lib'
|
||||
git -C ~/cronet-go fetch --depth=1 --filter=blob:none origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go checkout FETCH_HEAD
|
||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||
- name: Regenerate Debian keyring
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25
|
||||
go-version: ^1.26
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.12
|
||||
go-version: 1.26.8
|
||||
- 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-latest
|
||||
runs-on: ubuntu-26.04
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.25.12
|
||||
go-version: 1.26.8
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
@@ -80,7 +80,8 @@ 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 fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go sparse-checkout set --no-cone '/*' '!/lib'
|
||||
git -C ~/cronet-go fetch --depth=1 --filter=blob:none origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go checkout FETCH_HEAD
|
||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||
- name: Regenerate Debian keyring
|
||||
@@ -182,8 +183,6 @@ 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 }}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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
-1
@@ -18,7 +18,6 @@ linters:
|
||||
default: none
|
||||
enable:
|
||||
- ineffassign
|
||||
- paralleltest
|
||||
- staticcheck
|
||||
- unused
|
||||
- modernize
|
||||
@@ -32,6 +31,7 @@ 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:
|
||||
|
||||
+58
-20
@@ -18,16 +18,20 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_snell
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
@@ -58,16 +62,20 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_snell
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
@@ -119,16 +127,20 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_snell
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
@@ -154,16 +166,20 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_snell
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
@@ -189,16 +205,20 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_snell
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
@@ -224,16 +244,20 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_snell
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
@@ -259,16 +283,20 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_snell
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
@@ -310,16 +338,20 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_snell
|
||||
- with_manager
|
||||
- with_admin_panel
|
||||
- with_profiler
|
||||
@@ -369,16 +401,20 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_openvpn
|
||||
- with_openconnect
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_masque
|
||||
- with_mtproxy
|
||||
- with_openvpn
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- with_cloudflared
|
||||
- with_usbip
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_snell
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
@@ -441,18 +477,20 @@ builds:
|
||||
- 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_openvpn
|
||||
- with_trusttunnel
|
||||
- with_call
|
||||
- with_sudoku
|
||||
- with_snell
|
||||
- with_profiler
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
|
||||
@@ -24,7 +24,7 @@ DOCKER_PLATFORMS ?= linux/amd64,linux/arm64
|
||||
|
||||
CRONET_GO_PATH ?= $(shell pwd)/cronet-go
|
||||
|
||||
.PHONY: test release docs build
|
||||
.PHONY: test release docs build schema
|
||||
|
||||
build:
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
@@ -57,6 +57,9 @@ 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)
|
||||
|
||||
@@ -78,8 +81,7 @@ lint_install:
|
||||
|
||||
proto:
|
||||
@go run ./cmd/internal/protogen
|
||||
@gofumpt -l -w .
|
||||
@gofumpt -l -w .
|
||||
@golangci-lint fmt
|
||||
|
||||
proto_install:
|
||||
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
@@ -118,6 +120,9 @@ 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
|
||||
|
||||
@@ -136,34 +141,49 @@ release_android: lib_android update_android_version build_android upload_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 && \
|
||||
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 "❌"
|
||||
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 "❌"
|
||||
|
||||
upload_ios_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
xcodebuild -exportArchive -archivePath build/SFI.xcarchive -exportOptionsPlist SFI/Upload.plist -allowProvisioningUpdates
|
||||
|
||||
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
|
||||
build_ios_deb:
|
||||
$(MAKE) -C ../sing-box-for-apple build_ios_deb
|
||||
|
||||
upload_ios_ipa:
|
||||
cd dist && \
|
||||
cp SFI.ipa "SFI-${VERSION}.ipa" && \
|
||||
ghr --replace --draft --prerelease "v${VERSION}" "SFI-${VERSION}.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"
|
||||
|
||||
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 -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
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 "❌"
|
||||
|
||||
upload_macos_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
@@ -232,7 +252,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 -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
||||
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 "❌"
|
||||
|
||||
upload_tvos_app_store:
|
||||
cd ../sing-box-for-apple && \
|
||||
@@ -254,7 +274,7 @@ update_apple_version:
|
||||
go run ./cmd/internal/update_apple_version
|
||||
|
||||
update_macos_version:
|
||||
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
|
||||
MACOS_PROJECT_VERSION=$(shell go run ./cmd/internal/app_store_connect next_project_version macos) go run ./cmd/internal/update_apple_version
|
||||
|
||||
release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone
|
||||
|
||||
@@ -263,11 +283,13 @@ 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))
|
||||
|
||||
prepare_app_store:
|
||||
go run -v ./cmd/internal/app_store_connect prepare_app_store
|
||||
submit_app_store:
|
||||
go run -v ./cmd/internal/app_store_connect submit_app_store $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
release_app_store: release_ios release_tvos submit_app_store
|
||||
|
||||
publish_app_store:
|
||||
go run -v ./cmd/internal/app_store_connect publish_app_store
|
||||
go run -v ./cmd/internal/app_store_connect publish_app_store $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
test:
|
||||
@go test -v ./... && \
|
||||
@@ -297,8 +319,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.12
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.13
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.13
|
||||
|
||||
docs:
|
||||
venv/bin/mkdocs serve
|
||||
|
||||
@@ -14,10 +14,8 @@ Sing-box with extended features.
|
||||
- **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
|
||||
- **OpenVPN** — OpenVPN client with tls-auth, tls-crypt and tls-crypt-v2 support
|
||||
- **TrustTunnel** — AdGuard's obfuscated VPN protocol, indistinguishable from HTTPS traffic
|
||||
- **Sudoku** — Traffic obfuscation protocol based on 4×4 Sudoku puzzles with low-entropy fingerprints
|
||||
- **Snell** — Lightweight encrypted proxy (v1–v5) with TLS / HTTP obfuscation
|
||||
- **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
|
||||
@@ -36,13 +34,13 @@ Sing-box with extended features.
|
||||
- **Rate Limiter** — Request rate limiting
|
||||
|
||||
### Encryption & Obfuscation
|
||||
- **Amnezia 3.0** — WireGuard traffic obfuscation
|
||||
- **Amnezia 3.1** — WireGuard traffic obfuscation
|
||||
- **VLESS encryption** — XRAY encryption for VLESS protocol
|
||||
|
||||
### Transports
|
||||
- **mKCP** — Reliable UDP-based transport
|
||||
- **XHTTP** — Modern XRAY transport
|
||||
- **rmux** — Improved smux multiplex protocol
|
||||
- **Rmux** — Improved smux multiplexer
|
||||
|
||||
### Services
|
||||
- **Admin Panel** — Web-based management interface
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
type CertificateStore interface {
|
||||
LifecycleService
|
||||
Pool() *x509.CertPool
|
||||
ExclusiveAnchors() bool
|
||||
}
|
||||
|
||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//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
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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
|
||||
}
|
||||
+30
-32
@@ -3,13 +3,12 @@ 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"
|
||||
)
|
||||
@@ -17,6 +16,7 @@ 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,36 +25,22 @@ type DNSRouter interface {
|
||||
|
||||
type DNSClient interface {
|
||||
Start()
|
||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error)
|
||||
ExchangeAsync(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool, callback func(response *dns.Msg, err error))
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
|
||||
ClearCache()
|
||||
}
|
||||
|
||||
type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
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
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
DisableOptimisticCache bool
|
||||
RewriteTTL *uint32
|
||||
Timeout time.Duration
|
||||
ClientSubnet netip.Prefix
|
||||
RemoveClientSubnet bool
|
||||
}
|
||||
|
||||
type RDRCStore interface {
|
||||
@@ -63,6 +49,13 @@ 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
|
||||
@@ -72,11 +65,17 @@ 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 LegacyDNSTransport interface {
|
||||
LegacyStrategy() C.DomainStrategy
|
||||
LegacyClientSubnet() netip.Prefix
|
||||
type DNSTransportWithPreferredDomain interface {
|
||||
DNSTransport
|
||||
PreferredDomain(domain string) bool
|
||||
}
|
||||
|
||||
type DNSTransportWithEnvironment interface {
|
||||
DNSTransport
|
||||
Environment() []string
|
||||
}
|
||||
|
||||
type DNSTransportRegistry interface {
|
||||
@@ -90,6 +89,5 @@ 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
@@ -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
|
||||
}
|
||||
|
||||
+12
-89
@@ -2,46 +2,36 @@ package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.EndpointManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.EndpointRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
scope *adapter.Scope
|
||||
endpoints []adapter.Endpoint
|
||||
endpointByTag map[string]adapter.Endpoint
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager {
|
||||
func NewManager(registry adapter.EndpointRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpointByTag: make(map[string]adapter.Endpoint),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
if stage == adapter.StartStateInitialize {
|
||||
m.scope = scope
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
if stage == adapter.StartStateStart {
|
||||
// started with outbound manager
|
||||
m.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
@@ -49,38 +39,16 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Unlock()
|
||||
for _, endpoint := range endpoints {
|
||||
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err := adapter.LegacyStart(endpoint, stage)
|
||||
done()
|
||||
err := scope.Start(name, endpoint, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
endpoints := m.endpoints
|
||||
m.endpoints = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, endpoint := range endpoints {
|
||||
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) StartEndpoint(endpoint adapter.Endpoint) error {
|
||||
return m.scope.Start("endpoint/"+endpoint.Type()+"["+endpoint.Tag()+"]", endpoint, adapter.StartStateStart)
|
||||
}
|
||||
|
||||
func (m *Manager) Endpoints() []adapter.Endpoint {
|
||||
@@ -96,29 +64,6 @@ 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 {
|
||||
@@ -126,31 +71,9 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
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:]...)
|
||||
_, loaded := m.endpointByTag[tag]
|
||||
if loaded {
|
||||
return E.New("duplicate endpoint tag: ", tag)
|
||||
}
|
||||
m.endpoints = append(m.endpoints, endpoint)
|
||||
m.endpointByTag[tag] = endpoint
|
||||
|
||||
@@ -2,6 +2,8 @@ package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -44,6 +46,12 @@ 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()
|
||||
|
||||
+42
-26
@@ -7,32 +7,15 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"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
|
||||
@@ -41,6 +24,8 @@ type V2RayServer interface {
|
||||
type CacheFile interface {
|
||||
LifecycleService
|
||||
|
||||
CacheID() string
|
||||
|
||||
StoreFakeIP() bool
|
||||
FakeIPStorage
|
||||
|
||||
@@ -49,6 +34,13 @@ 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
|
||||
@@ -70,11 +62,12 @@ 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(1))
|
||||
err := binary.Write(&buffer, binary.BigEndian, uint8(2))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,6 +91,14 @@ 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
|
||||
}
|
||||
|
||||
@@ -112,6 +113,9 @@ 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 {
|
||||
@@ -127,12 +131,30 @@ 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
|
||||
}
|
||||
|
||||
@@ -145,11 +167,5 @@ type OutboundGroup interface {
|
||||
type URLTestGroup interface {
|
||||
OutboundGroup
|
||||
URLTest(ctx context.Context) (map[string]uint16, error)
|
||||
}
|
||||
|
||||
func OutboundTag(detour Outbound) string {
|
||||
if group, isGroup := detour.(OutboundGroup); isGroup {
|
||||
return group.Now()
|
||||
}
|
||||
return detour.Tag()
|
||||
PerformUpdateCheck()
|
||||
}
|
||||
|
||||
+6
-32
@@ -5,57 +5,31 @@ 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) error
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
type ConnectionHandlerEx interface {
|
||||
NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
// Deprecated: use PacketHandlerEx instead
|
||||
type PacketHandler interface {
|
||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
|
||||
NewPacket(buffer *buf.Buffer, source M.Socksaddr)
|
||||
}
|
||||
|
||||
type PacketHandlerEx interface {
|
||||
NewPacketEx(buffer *buf.Buffer, source M.Socksaddr)
|
||||
type PacketBatchHandler interface {
|
||||
NewPacketBatch(buffers []*buf.Buffer, sources []M.Socksaddr)
|
||||
}
|
||||
|
||||
// Deprecated: use OOBPacketHandlerEx instead
|
||||
type OOBPacketHandler interface {
|
||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
|
||||
NewPacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
|
||||
}
|
||||
|
||||
type OOBPacketHandlerEx interface {
|
||||
NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
type PacketConnectionHandler interface {
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
+95
-11
@@ -2,13 +2,17 @@ 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 {
|
||||
@@ -19,12 +23,12 @@ type Inbound interface {
|
||||
|
||||
type TCPInjectableInbound interface {
|
||||
Inbound
|
||||
ConnectionHandlerEx
|
||||
ConnectionHandler
|
||||
}
|
||||
|
||||
type UDPInjectableInbound interface {
|
||||
Inbound
|
||||
PacketConnectionHandlerEx
|
||||
PacketConnectionHandler
|
||||
}
|
||||
|
||||
type InboundRegistry interface {
|
||||
@@ -37,7 +41,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -52,6 +55,11 @@ type InboundContext struct {
|
||||
User string
|
||||
Outbound string
|
||||
|
||||
// power report
|
||||
|
||||
RouteRule string
|
||||
RouteOutbound string
|
||||
|
||||
// sniffer
|
||||
|
||||
Protocol string
|
||||
@@ -74,18 +82,28 @@ 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
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
DestinationAddresses []netip.Addr
|
||||
DNSResponse *dns.Msg
|
||||
NamedDNSResponses map[string]*dns.Msg
|
||||
DestinationAddressMatchFromResponse bool
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
SourceMACAddress net.HardwareAddr
|
||||
SourceHostname string
|
||||
QueryType uint16
|
||||
QueryClientSubnet netip.Prefix
|
||||
QueryDNSSEC bool
|
||||
FakeIP bool
|
||||
PreMatch bool
|
||||
|
||||
// rule cache
|
||||
|
||||
@@ -96,7 +114,7 @@ type InboundContext struct {
|
||||
SourcePortMatch bool
|
||||
DestinationAddressMatch bool
|
||||
DestinationPortMatch bool
|
||||
DidMatch bool
|
||||
DeferredIPCIDRMatchGroups uint8
|
||||
IgnoreDestinationIPCIDRMatch bool
|
||||
}
|
||||
|
||||
@@ -111,11 +129,77 @@ func (c *InboundContext) ResetRuleMatchCache() {
|
||||
c.SourcePortMatch = false
|
||||
c.DestinationAddressMatch = false
|
||||
c.DestinationPortMatch = false
|
||||
c.DidMatch = 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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -2,84 +2,45 @@ package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.InboundManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.InboundRegistry
|
||||
endpoint adapter.EndpointManager
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
inbounds []adapter.Inbound
|
||||
inboundByTag map[string]adapter.Inbound
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
|
||||
func NewManager(registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpoint: endpoint,
|
||||
inboundByTag: make(map[string]adapter.Inbound),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) 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() + "]"
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err := adapter.LegacyStart(inbound, stage)
|
||||
done()
|
||||
err := scope.Start(name, inbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
return err
|
||||
}
|
||||
}
|
||||
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()
|
||||
@@ -96,29 +57,6 @@ 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 {
|
||||
@@ -126,31 +64,9 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
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:]...)
|
||||
_, loaded := m.inboundByTag[tag]
|
||||
if loaded {
|
||||
return E.New("duplicate inbound tag: ", tag)
|
||||
}
|
||||
m.inbounds = append(m.inbounds, inbound)
|
||||
m.inboundByTag[tag] = inbound
|
||||
|
||||
@@ -2,6 +2,8 @@ package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -44,6 +46,12 @@ 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()
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
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())
|
||||
}
|
||||
+99
-38
@@ -1,13 +1,16 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
type SimpleLifecycle interface {
|
||||
@@ -47,8 +50,7 @@ func (s StartStage) String() string {
|
||||
}
|
||||
|
||||
type Lifecycle interface {
|
||||
Start(stage StartStage) error
|
||||
Close() error
|
||||
Start(stage StartStage, scope *Scope) error
|
||||
}
|
||||
|
||||
type LifecycleService interface {
|
||||
@@ -56,47 +58,106 @@ type LifecycleService interface {
|
||||
Lifecycle
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
t := reflect.TypeOf(service)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
return strings.ToLower(t.Name())
|
||||
type Scope struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
logger log.ContextLogger
|
||||
access sync.Mutex
|
||||
cleanups list.List[func() error]
|
||||
children map[Lifecycle]*scopeChild
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
type scopeChild struct {
|
||||
scope *Scope
|
||||
cleanup *list.Element[func() error]
|
||||
}
|
||||
|
||||
func NewScope(ctx context.Context, logger log.ContextLogger) *Scope {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &Scope{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logger: logger,
|
||||
children: make(map[Lifecycle]*scopeChild),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scope) Context() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
|
||||
func (s *Scope) Add(cleanup func() error) {
|
||||
s.access.Lock()
|
||||
s.cleanups.PushBack(cleanup)
|
||||
s.access.Unlock()
|
||||
}
|
||||
|
||||
func (s *Scope) Start(name string, component Lifecycle, stage StartStage) error {
|
||||
s.access.Lock()
|
||||
err := s.ctx.Err()
|
||||
if err != nil {
|
||||
s.access.Unlock()
|
||||
return err
|
||||
}
|
||||
child, loaded := s.children[component]
|
||||
if !loaded {
|
||||
childScope := NewScope(s.ctx, s.logger)
|
||||
child = &scopeChild{scope: childScope}
|
||||
child.cleanup = s.cleanups.PushBack(func() error {
|
||||
done := LogElapsed(s.logger, "close ", name)
|
||||
monitor := taskmonitor.New(s.logger, C.StopTimeout)
|
||||
monitor.Start("close ", name)
|
||||
closeErr := childScope.Close()
|
||||
monitor.Finish()
|
||||
done()
|
||||
if closeErr != nil {
|
||||
return E.Cause(closeErr, "close ", name)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
s.children[component] = child
|
||||
}
|
||||
s.access.Unlock()
|
||||
done := LogElapsed(s.logger, stage, " ", name)
|
||||
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||
monitor.Start(stage, " ", name)
|
||||
err = component.Start(stage, child.scope)
|
||||
monitor.Finish()
|
||||
done()
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func 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())
|
||||
}
|
||||
func (s *Scope) Remove(component Lifecycle) error {
|
||||
s.access.Lock()
|
||||
child, loaded := s.children[component]
|
||||
if !loaded {
|
||||
s.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
delete(s.children, component)
|
||||
cleanup := s.cleanups.Remove(child.cleanup)
|
||||
s.access.Unlock()
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
func (s *Scope) Close() error {
|
||||
s.access.Lock()
|
||||
s.cancel()
|
||||
var cleanups []func() error
|
||||
for element := s.cleanups.Back(); element != nil; element = element.Prev() {
|
||||
cleanups = append(cleanups, element.Value)
|
||||
}
|
||||
s.cleanups.Init()
|
||||
s.children = nil
|
||||
s.access.Unlock()
|
||||
var err error
|
||||
for _, cleanup := range cleanups {
|
||||
err = E.Errors(err, cleanup())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func LogElapsed(logger log.ContextLogger, description ...any) func() {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package adapter
|
||||
|
||||
func LegacyStart(starter any, stage StartStage) error {
|
||||
if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle {
|
||||
return lifecycle.Start(stage)
|
||||
}
|
||||
switch stage {
|
||||
case StartStateInitialize:
|
||||
if preStarter, isPreStarter := starter.(interface {
|
||||
PreStart() error
|
||||
}); isPreStarter {
|
||||
return preStarter.PreStart()
|
||||
}
|
||||
case StartStateStart:
|
||||
if starter, isStarter := starter.(interface {
|
||||
Start() error
|
||||
}); isStarter {
|
||||
return starter.Start()
|
||||
}
|
||||
case StartStateStarted:
|
||||
if postStarter, isPostStarter := starter.(interface {
|
||||
PostStart() error
|
||||
}); isPostStarter {
|
||||
return postStarter.PostStart()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type lifecycleServiceWrapper struct {
|
||||
SimpleLifecycle
|
||||
name string
|
||||
}
|
||||
|
||||
func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
|
||||
return &lifecycleServiceWrapper{
|
||||
SimpleLifecycle: service,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Name() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
|
||||
return LegacyStart(l.SimpleLifecycle, stage)
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Close() error {
|
||||
return l.SimpleLifecycle.Close()
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package adapter
|
||||
|
||||
type NetworkNamespaceManager interface {
|
||||
ResolvePath(nameOrPath string) string
|
||||
}
|
||||
+7
-3
@@ -1,8 +1,10 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,6 +20,7 @@ type NetworkManager interface {
|
||||
UpdateInterfaces() error
|
||||
DefaultNetworkInterface() *NetworkInterface
|
||||
NetworkInterfaces() []NetworkInterface
|
||||
NetworkEnvironment() uint64
|
||||
AutoDetectInterface() bool
|
||||
AutoDetectInterfaceFunc() control.Func
|
||||
ProtectFunc() control.Func
|
||||
@@ -30,8 +33,8 @@ type NetworkManager interface {
|
||||
PackageManager() tun.PackageManager
|
||||
NeedWIFIState() bool
|
||||
WIFIState() WIFIState
|
||||
UpdateWIFIState()
|
||||
ResetNetwork()
|
||||
UpdateWIFIState(ctx context.Context)
|
||||
ResetNetwork(ctx context.Context)
|
||||
}
|
||||
|
||||
type NetworkOptions struct {
|
||||
@@ -46,7 +49,7 @@ type NetworkOptions struct {
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
InterfaceUpdated()
|
||||
InterfaceUpdated(ctx context.Context)
|
||||
}
|
||||
|
||||
type WIFIState struct {
|
||||
@@ -76,6 +79,7 @@ type NetworkInterface struct {
|
||||
control.Interface
|
||||
Type C.InterfaceType
|
||||
DNSServers []string
|
||||
Gateways []netip.Addr
|
||||
Expensive bool
|
||||
Constrained bool
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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
|
||||
}
|
||||
+14
-5
@@ -3,7 +3,6 @@ package adapter
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -23,8 +22,8 @@ type Outbound interface {
|
||||
|
||||
type OutboundWithPreferredRoutes interface {
|
||||
Outbound
|
||||
PreferredDomain(domain string) bool
|
||||
PreferredAddress(address netip.Addr) bool
|
||||
PreferredDomain(metadata *InboundContext, domain string) bool
|
||||
PreferredAddress(metadata *InboundContext, address netip.Addr) bool
|
||||
}
|
||||
|
||||
type OutboundWithMultiplex interface {
|
||||
@@ -32,9 +31,10 @@ type OutboundWithMultiplex interface {
|
||||
MultiplexEnabled() bool
|
||||
}
|
||||
|
||||
type DirectRouteOutbound interface {
|
||||
type FlowOutbound interface {
|
||||
Outbound
|
||||
NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
|
||||
tun.Port
|
||||
PreMatchFlow(network string, destination netip.Addr) PreMatchAction
|
||||
}
|
||||
|
||||
type OutboundRegistry interface {
|
||||
@@ -51,3 +51,12 @@ 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
|
||||
}
|
||||
|
||||
+103
-133
@@ -2,29 +2,25 @@ package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
var _ adapter.OutboundManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.OutboundRegistry
|
||||
endpoint adapter.EndpointManager
|
||||
defaultTag string
|
||||
access sync.RWMutex
|
||||
started bool
|
||||
startAccess sync.Mutex
|
||||
scope *adapter.Scope
|
||||
stage adapter.StartStage
|
||||
outbounds []adapter.Outbound
|
||||
outboundByTag map[string]adapter.Outbound
|
||||
@@ -33,9 +29,8 @@ type Manager struct {
|
||||
defaultOutboundFallback func() (adapter.Outbound, error)
|
||||
}
|
||||
|
||||
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
|
||||
func NewManager(registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpoint: endpoint,
|
||||
defaultTag: defaultTag,
|
||||
@@ -48,14 +43,13 @@ func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, e
|
||||
m.defaultOutboundFallback = defaultOutboundFallback
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.scope = scope
|
||||
m.stage = stage
|
||||
if stage == adapter.StartStateStart {
|
||||
if stage == adapter.StartStateInitialize {
|
||||
if m.defaultTag != "" && m.defaultOutbound == nil {
|
||||
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
|
||||
if !loaded {
|
||||
@@ -74,27 +68,27 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.outboundByTag[directOutbound.Tag()] = directOutbound
|
||||
m.defaultOutbound = directOutbound
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
outbounds := m.outbounds
|
||||
m.access.Unlock()
|
||||
if stage == adapter.StartStateStart {
|
||||
return m.startOutbounds(scope, append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
|
||||
}
|
||||
for _, outbound := range outbounds {
|
||||
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
|
||||
if !isLifecycle {
|
||||
continue
|
||||
}
|
||||
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
||||
err := scope.Start(name, lifecycle, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
||||
monitor := taskmonitor.New(m.logger, C.StartTimeout)
|
||||
func (m *Manager) startOutbounds(scope *adapter.Scope, outbounds []adapter.Outbound) error {
|
||||
started := make(map[string]bool)
|
||||
for {
|
||||
canContinue := false
|
||||
@@ -112,27 +106,21 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
||||
}
|
||||
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 + "]"
|
||||
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)
|
||||
}
|
||||
err := scope.Start(name, lifecycle, adapter.StartStateStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(started) == len(outbounds) {
|
||||
@@ -152,9 +140,9 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
||||
if common.Contains(oTree, problemOutboundTag) {
|
||||
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
|
||||
}
|
||||
m.access.Lock()
|
||||
problemOutbound := m.outboundByTag[problemOutboundTag]
|
||||
m.access.Unlock()
|
||||
problemOutbound := common.Find(outbounds, func(it adapter.Outbound) bool {
|
||||
return it.Tag() == problemOutboundTag
|
||||
})
|
||||
if problemOutbound == nil {
|
||||
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]")
|
||||
}
|
||||
@@ -165,33 +153,6 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
||||
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()
|
||||
@@ -215,47 +176,38 @@ 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
|
||||
}
|
||||
delete(m.outboundByTag, tag)
|
||||
index := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it == outbound
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid inbound index")
|
||||
dependBy := m.dependByTag[tag]
|
||||
if len(dependBy) > 0 {
|
||||
m.access.Unlock()
|
||||
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
|
||||
}
|
||||
m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...)
|
||||
started := m.started
|
||||
delete(m.outboundByTag, tag)
|
||||
m.outbounds = common.Filter(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it != outbound
|
||||
})
|
||||
m.removeDependencies(outbound)
|
||||
if m.defaultOutbound == outbound {
|
||||
if len(m.outbounds) > 0 {
|
||||
m.defaultOutbound = m.outbounds[0]
|
||||
m.logger.Notice("updated default outbound to ", m.defaultOutbound.Tag())
|
||||
} else {
|
||||
m.defaultOutbound = nil
|
||||
}
|
||||
}
|
||||
dependBy := m.dependByTag[tag]
|
||||
if len(dependBy) > 0 {
|
||||
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
|
||||
scope := m.scope
|
||||
m.access.Unlock()
|
||||
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
|
||||
if scope == nil || !isLifecycle {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
return scope.Remove(lifecycle)
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error {
|
||||
@@ -266,45 +218,63 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.started {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.RLock()
|
||||
scope := m.scope
|
||||
stage := m.stage
|
||||
m.access.RUnlock()
|
||||
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
|
||||
if scope != nil && isLifecycle {
|
||||
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err = adapter.LegacyStart(outbound, stage)
|
||||
done()
|
||||
for _, startStage := range adapter.ListStartStages[:stage+1] {
|
||||
err = scope.Start(name, lifecycle, startStage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
return E.Errors(err, scope.Remove(lifecycle))
|
||||
}
|
||||
}
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = common.Close(existsOutbound)
|
||||
if err != nil {
|
||||
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it == existsOutbound
|
||||
existsOutbound, loaded := m.outboundByTag[tag]
|
||||
if loaded {
|
||||
m.outbounds = common.Filter(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it != existsOutbound
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...)
|
||||
m.removeDependencies(existsOutbound)
|
||||
}
|
||||
m.outbounds = append(m.outbounds, outbound)
|
||||
m.outboundByTag[tag] = outbound
|
||||
dependencies := outbound.Dependencies()
|
||||
for _, dependency := range dependencies {
|
||||
for _, dependency := range outbound.Dependencies() {
|
||||
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
|
||||
}
|
||||
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
|
||||
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) || (loaded && m.defaultOutbound == existsOutbound) {
|
||||
m.defaultOutbound = outbound
|
||||
if m.started {
|
||||
m.logger.Notice("updated default outbound to ", outbound.Tag())
|
||||
}
|
||||
}
|
||||
m.access.Unlock()
|
||||
if !loaded || scope == nil {
|
||||
return nil
|
||||
}
|
||||
existsLifecycle, isExistsLifecycle := existsOutbound.(adapter.Lifecycle)
|
||||
if !isExistsLifecycle {
|
||||
return nil
|
||||
}
|
||||
err = scope.Remove(existsLifecycle)
|
||||
if err != nil {
|
||||
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) removeDependencies(outbound adapter.Outbound) {
|
||||
tag := outbound.Tag()
|
||||
for _, dependency := range outbound.Dependencies() {
|
||||
dependBy := common.Filter(m.dependByTag[dependency], func(it string) bool {
|
||||
return it != tag
|
||||
})
|
||||
if len(dependBy) == 0 {
|
||||
delete(m.dependByTag, dependency)
|
||||
} else {
|
||||
m.dependByTag[dependency] = dependBy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -44,6 +46,12 @@ 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()
|
||||
|
||||
+46
-2
@@ -1,6 +1,7 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -17,6 +18,7 @@ type PlatformInterface interface {
|
||||
|
||||
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
|
||||
@@ -29,8 +31,7 @@ type PlatformInterface interface {
|
||||
|
||||
ClearDNSCache()
|
||||
RequestPermissionForWIFIState() error
|
||||
ReadWIFIState() WIFIState
|
||||
SystemCertificates() []string
|
||||
ReadWIFIState(ctx context.Context) WIFIState
|
||||
|
||||
UsePlatformConnectionOwnerFinder() bool
|
||||
FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error)
|
||||
@@ -39,8 +40,51 @@ 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 {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
Outbounds() []Outbound
|
||||
|
||||
+42
-25
@@ -28,19 +28,21 @@ 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 adapter.URLTestHistoryStorage
|
||||
history *urltest.HistoryStorage
|
||||
callbackAccess sync.Mutex
|
||||
callbacks list.List[adapter.ProviderUpdateCallback]
|
||||
|
||||
link string
|
||||
enabled bool
|
||||
removeEmojis bool
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
link string
|
||||
enabled bool
|
||||
removeEmojis bool
|
||||
overrideDialerOptions *option.DialerOptions
|
||||
timeout time.Duration
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.OutboundManager, logFactory log.Factory, logger log.ContextLogger, providerTag string, providerType string, options option.ProviderHealthCheckOptions) Adapter {
|
||||
@@ -75,16 +77,19 @@ 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.FromContext[adapter.URLTestHistoryStorage](a.ctx)
|
||||
a.history = service.PtrFromContext[urltest.HistoryStorage](a.ctx)
|
||||
if a.history == nil {
|
||||
if clashServer := service.FromContext[adapter.ClashServer](a.ctx); clashServer != nil {
|
||||
a.history = clashServer.HistoryStorage()
|
||||
} else {
|
||||
a.history = urltest.NewHistoryStorage()
|
||||
}
|
||||
a.history = urltest.NewHistoryStorage()
|
||||
}
|
||||
if a.enabled {
|
||||
a.ticker = time.NewTicker(a.interval)
|
||||
go a.loopCheck()
|
||||
}
|
||||
go a.loopCheck()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -97,13 +102,14 @@ 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) {
|
||||
if a.outboundsByTag == nil {
|
||||
return nil, false
|
||||
}
|
||||
a.outboundAccess.RLock()
|
||||
defer a.outboundAccess.RUnlock()
|
||||
detour, ok := a.outboundsByTag[tag]
|
||||
return detour, ok
|
||||
}
|
||||
@@ -123,6 +129,11 @@ 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)
|
||||
@@ -150,11 +161,13 @@ 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) {
|
||||
@@ -180,14 +193,21 @@ 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 {
|
||||
@@ -200,10 +220,6 @@ 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 {
|
||||
@@ -224,7 +240,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
|
||||
@@ -255,7 +271,8 @@ func (a *Adapter) healthcheck(ctx context.Context) (map[string]uint16, error) {
|
||||
}
|
||||
|
||||
func (a *Adapter) removeUseless(newOpts []option.Outbound) {
|
||||
if len(a.outbounds) == 0 {
|
||||
outbounds := a.Outbounds()
|
||||
if len(outbounds) == 0 {
|
||||
return
|
||||
}
|
||||
exists := make(map[string]bool)
|
||||
@@ -268,7 +285,7 @@ func (a *Adapter) removeUseless(newOpts []option.Outbound) {
|
||||
}
|
||||
exists[tag] = true
|
||||
}
|
||||
for _, opt := range a.outbounds {
|
||||
for _, opt := range outbounds {
|
||||
if !exists[opt.Tag()] {
|
||||
if err := a.outbound.Remove(opt.Tag()); err != nil {
|
||||
a.logger.Error(err, "close outbound [", opt.Tag(), "]")
|
||||
|
||||
+40
-72
@@ -2,80 +2,46 @@ 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
|
||||
started bool
|
||||
startAccess sync.Mutex
|
||||
scope *adapter.Scope
|
||||
stage adapter.StartStage
|
||||
providers []adapter.Provider
|
||||
providerByTag map[string]adapter.Provider
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewManager(logger logger.ContextLogger, registry adapter.ProviderRegistry) *Manager {
|
||||
func NewManager(registry adapter.ProviderRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
providerByTag: make(map[string]adapter.Provider),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Initialize() {
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.scope = scope
|
||||
m.stage = stage
|
||||
providers := m.providers
|
||||
m.access.Unlock()
|
||||
for _, provider := range providers {
|
||||
err := adapter.LegacyStart(provider, stage)
|
||||
err := scope.Start("provider/"+provider.Type()+"["+provider.Tag()+"]", provider, stage)
|
||||
if err != nil {
|
||||
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 err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -95,6 +61,8 @@ 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 {
|
||||
@@ -102,56 +70,56 @@ func (m *Manager) Remove(tag string) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.providerByTag, tag)
|
||||
index := common.Index(m.providers, func(it adapter.Provider) bool {
|
||||
return it == provider
|
||||
m.providers = common.Filter(m.providers, func(it adapter.Provider) bool {
|
||||
return it != provider
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid provider index")
|
||||
}
|
||||
m.providers = append(m.providers[:index], m.providers[index+1:]...)
|
||||
started := m.started
|
||||
scope := m.scope
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return common.Close(provider)
|
||||
if scope == nil {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return scope.Remove(provider)
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) error {
|
||||
if tag == "" {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
provider, err := m.registry.CreateProvider(ctx, router, logFactory, tag, providerType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.startAccess.Lock()
|
||||
defer m.startAccess.Unlock()
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(provider, stage)
|
||||
scope := m.scope
|
||||
stage := m.stage
|
||||
m.access.Unlock()
|
||||
if scope != nil {
|
||||
name := "provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
for _, startStage := range adapter.ListStartStages[:stage+1] {
|
||||
err = scope.Start(name, provider, startStage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
|
||||
return E.Errors(err, scope.Remove(provider))
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
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 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
|
||||
}
|
||||
|
||||
+97
-53
@@ -2,17 +2,14 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"net/netip"
|
||||
"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"
|
||||
@@ -21,18 +18,97 @@ import (
|
||||
type Router interface {
|
||||
Lifecycle
|
||||
ConnectionRouter
|
||||
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error)
|
||||
PreMatch(metadata InboundContext, firstPacket []byte) PreMatchResult
|
||||
HijackDNSPacket(ctx context.Context, payload []byte, writer N.PacketWriter, metadata InboundContext)
|
||||
ConnectionRouterEx
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
Rules() []Rule
|
||||
NeedFindProcess() bool
|
||||
NeedFindNeighbor() bool
|
||||
NeighborResolver() NeighborResolver
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -50,7 +126,6 @@ type ConnectionRouterEx interface {
|
||||
type RuleSet interface {
|
||||
Name() string
|
||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||
PostStart() error
|
||||
Metadata() RuleSetMetadata
|
||||
ExtractIPSet() []*netipx.IPSet
|
||||
IncRef()
|
||||
@@ -64,51 +139,20 @@ type RuleSet interface {
|
||||
|
||||
type RuleSetUpdateCallback func(it RuleSet)
|
||||
|
||||
type DNSRuleSetUpdateValidator interface {
|
||||
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
|
||||
}
|
||||
|
||||
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
|
||||
type RuleSetMetadata struct {
|
||||
ContainsProcessRule bool
|
||||
ContainsWIFIRule bool
|
||||
ContainsIPCIDRRule bool
|
||||
}
|
||||
type HTTPStartContext struct {
|
||||
ctx context.Context
|
||||
access sync.Mutex
|
||||
httpClientCache map[string]*http.Client
|
||||
}
|
||||
|
||||
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||
return &HTTPStartContext{
|
||||
ctx: ctx,
|
||||
httpClientCache: make(map[string]*http.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
if httpClient, loaded := c.httpClientCache[detour]; loaded {
|
||||
return httpClient
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
TLSHandshakeTimeout: C.TCPTimeout,
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||
RootCAs: RootPoolFromContext(c.ctx),
|
||||
},
|
||||
},
|
||||
}
|
||||
c.httpClientCache[detour] = httpClient
|
||||
return httpClient
|
||||
}
|
||||
|
||||
func (c *HTTPStartContext) Close() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
for _, client := range c.httpClientCache {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
+9
-2
@@ -2,6 +2,8 @@ package adapter
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type HeadlessRule interface {
|
||||
@@ -18,8 +20,13 @@ type Rule interface {
|
||||
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
LegacyPreMatch(metadata *InboundContext) bool
|
||||
WithAddressLimit() bool
|
||||
MatchAddressLimit(metadata *InboundContext) bool
|
||||
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
|
||||
MatchResponseTag() string
|
||||
MatchResponseTags() []string
|
||||
MatchResponseAnonymous() bool
|
||||
Race() bool
|
||||
}
|
||||
|
||||
type RuleAction interface {
|
||||
@@ -29,7 +36,7 @@ type RuleAction interface {
|
||||
|
||||
func IsFinalAction(action RuleAction) bool {
|
||||
switch action.Type() {
|
||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
|
||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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)
|
||||
}
|
||||
@@ -22,6 +22,5 @@ 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
|
||||
}
|
||||
|
||||
@@ -2,82 +2,43 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.ServiceManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.ServiceRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
services []adapter.Service
|
||||
serviceByTag map[string]adapter.Service
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
|
||||
func NewManager(registry adapter.ServiceRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
serviceByTag: make(map[string]adapter.Service),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) 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() + "]"
|
||||
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||
err := adapter.LegacyStart(service, stage)
|
||||
done()
|
||||
err := scope.Start(name, service, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
return err
|
||||
}
|
||||
}
|
||||
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()
|
||||
@@ -91,29 +52,6 @@ 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 {
|
||||
@@ -121,31 +59,9 @@ func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag stri
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
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:]...)
|
||||
_, loaded := m.serviceByTag[tag]
|
||||
if loaded {
|
||||
return E.New("duplicate service tag: ", tag)
|
||||
}
|
||||
m.services = append(m.services, service)
|
||||
m.serviceByTag[tag] = service
|
||||
|
||||
@@ -2,6 +2,8 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -44,6 +46,12 @@ 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()
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
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
@@ -9,31 +9,31 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
)
|
||||
|
||||
func NewUpstreamHandlerEx(
|
||||
func NewUpstreamHandler(
|
||||
metadata InboundContext,
|
||||
connectionHandler ConnectionHandlerFuncEx,
|
||||
packetHandler PacketConnectionHandlerFuncEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &myUpstreamHandlerWrapperEx{
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamHandlerWrapper{
|
||||
metadata: metadata,
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil)
|
||||
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
|
||||
|
||||
type myUpstreamHandlerWrapperEx struct {
|
||||
type myUpstreamHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
connectionHandler ConnectionHandlerFuncEx
|
||||
packetHandler PacketConnectionHandlerFuncEx
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamHandlerWrapper) 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 *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn n
|
||||
w.connectionHandler(ctx, conn, myMetadata, onClose)
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamHandlerWrapper) 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 *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context,
|
||||
w.packetHandler(ctx, conn, myMetadata, onClose)
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil)
|
||||
var _ UpstreamHandlerAdapter = (*myUpstreamContextHandlerWrapper)(nil)
|
||||
|
||||
type myUpstreamContextHandlerWrapperEx struct {
|
||||
connectionHandler ConnectionHandlerFuncEx
|
||||
packetHandler PacketConnectionHandlerFuncEx
|
||||
type myUpstreamContextHandlerWrapper struct {
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
}
|
||||
|
||||
func NewUpstreamContextHandlerEx(
|
||||
connectionHandler ConnectionHandlerFuncEx,
|
||||
packetHandler PacketConnectionHandlerFuncEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &myUpstreamContextHandlerWrapperEx{
|
||||
func NewUpstreamContextHandler(
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamContextHandlerWrapper{
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, myMetadata := ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
@@ -83,7 +83,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context,
|
||||
w.connectionHandler(ctx, conn, *myMetadata, onClose)
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, myMetadata := ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
@@ -94,24 +94,24 @@ func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Co
|
||||
w.packetHandler(ctx, conn, *myMetadata, onClose)
|
||||
}
|
||||
|
||||
func NewRouteHandlerEx(
|
||||
func NewRouteHandler(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouterEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &routeHandlerWrapperEx{
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeHandlerWrapper{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil)
|
||||
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
|
||||
|
||||
type routeHandlerWrapperEx struct {
|
||||
type routeHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouterEx
|
||||
}
|
||||
|
||||
func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (r *routeHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
if source.IsValid() {
|
||||
r.metadata.Source = source
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Co
|
||||
r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose)
|
||||
}
|
||||
|
||||
func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (r *routeHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
if source.IsValid() {
|
||||
r.metadata.Source = source
|
||||
}
|
||||
@@ -131,21 +131,21 @@ func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn
|
||||
r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose)
|
||||
}
|
||||
|
||||
func NewRouteContextHandlerEx(
|
||||
func NewRouteContextHandler(
|
||||
router ConnectionRouterEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &routeContextHandlerWrapperEx{
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeContextHandlerWrapper{
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil)
|
||||
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
|
||||
|
||||
type routeContextHandlerWrapperEx struct {
|
||||
type routeContextHandlerWrapper struct {
|
||||
router ConnectionRouterEx
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (r *routeContextHandlerWrapper) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, metadata := ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
@@ -156,7 +156,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn
|
||||
r.router.RouteConnectionEx(ctx, conn, *metadata, onClose)
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
func (r *routeContextHandlerWrapper) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, metadata := ExtendContext(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
|
||||
+59
-50
@@ -12,21 +12,30 @@ import (
|
||||
|
||||
type (
|
||||
// Deprecated
|
||||
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
LegacyConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
// Deprecated
|
||||
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
LegacyPacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
)
|
||||
|
||||
// Deprecated
|
||||
//
|
||||
//nolint:staticcheck
|
||||
func NewUpstreamHandler(
|
||||
type LegacyUpstreamHandlerAdapter interface {
|
||||
N.TCPConnectionHandler
|
||||
N.UDPConnectionHandler
|
||||
E.Handler
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
//
|
||||
//nolint:staticcheck
|
||||
func NewLegacyUpstreamHandler(
|
||||
metadata InboundContext,
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
connectionHandler LegacyConnectionHandlerFunc,
|
||||
packetHandler LegacyPacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamHandlerWrapper{
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyUpstreamHandlerWrapper{
|
||||
metadata: metadata,
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
@@ -34,20 +43,20 @@ func NewUpstreamHandler(
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
|
||||
var _ LegacyUpstreamHandlerAdapter = (*legacyUpstreamHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
// Deprecated: use NewUpstreamHandler instead.
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type myUpstreamHandlerWrapper struct {
|
||||
type legacyUpstreamHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
connectionHandler LegacyConnectionHandlerFunc
|
||||
packetHandler LegacyPacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
// Deprecated: use NewUpstreamHandler instead.
|
||||
func (w *legacyUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -58,8 +67,8 @@ func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.C
|
||||
return w.connectionHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
// Deprecated: use NewUpstreamHandler instead.
|
||||
func (w *legacyUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -70,8 +79,8 @@ func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn
|
||||
return w.packetHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
// Deprecated: use NewUpstreamHandler instead.
|
||||
func (w *legacyUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
||||
@@ -83,28 +92,28 @@ func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
type myUpstreamContextHandlerWrapper struct {
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
type legacyUpstreamContextHandlerWrapper struct {
|
||||
connectionHandler LegacyConnectionHandlerFunc
|
||||
packetHandler LegacyPacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func NewUpstreamContextHandler(
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
func NewLegacyUpstreamContextHandler(
|
||||
connectionHandler LegacyConnectionHandlerFunc,
|
||||
packetHandler LegacyPacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamContextHandlerWrapper{
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyUpstreamContextHandlerWrapper{
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
func (w *legacyUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -115,8 +124,8 @@ func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, con
|
||||
return w.connectionHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
func (w *legacyUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -127,18 +136,18 @@ func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Contex
|
||||
return w.packetHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
// Deprecated: Use NewUpstreamContextHandler instead.
|
||||
func (w *legacyUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func NewRouteHandler(
|
||||
func NewLegacyRouteHandler(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeHandlerWrapper{
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyRouteHandlerWrapper{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
logger: logger,
|
||||
@@ -146,29 +155,29 @@ func NewRouteHandler(
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func NewRouteContextHandler(
|
||||
func NewLegacyRouteContextHandler(
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeContextHandlerWrapper{
|
||||
) LegacyUpstreamHandlerAdapter {
|
||||
return &legacyRouteContextHandlerWrapper{
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
|
||||
var _ LegacyUpstreamHandlerAdapter = (*legacyRouteHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type routeHandlerWrapper struct {
|
||||
type legacyRouteHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
func (w *legacyRouteHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -180,7 +189,7 @@ func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn,
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
func (w *legacyRouteHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -192,20 +201,20 @@ func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.Pa
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
func (w *legacyRouteHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
|
||||
var _ LegacyUpstreamHandlerAdapter = (*legacyRouteContextHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
type routeContextHandlerWrapper struct {
|
||||
type legacyRouteContextHandlerWrapper struct {
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
func (w *legacyRouteContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -217,7 +226,7 @@ func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
func (w *legacyRouteContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
@@ -229,6 +238,6 @@ func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, co
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
func (w *legacyRouteContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//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))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !with_usbip || !(linux || (darwin && cgo) || windows)
|
||||
|
||||
package adapter
|
||||
|
||||
type USBIPDynamicServer interface {
|
||||
usbipNotIncluded()
|
||||
}
|
||||
@@ -22,3 +22,8 @@ type V2RayClientTransport interface {
|
||||
DialContext(ctx context.Context) (net.Conn, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type V2RayMultiplexClientTransport interface {
|
||||
V2RayClientTransport
|
||||
MultiplexEnabled() bool
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@ 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"
|
||||
@@ -16,13 +14,18 @@ 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"
|
||||
@@ -38,27 +41,33 @@ import (
|
||||
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
provider *provider.Manager
|
||||
service *boxService.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
createdAt time.Time
|
||||
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
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
option.Options
|
||||
Context context.Context
|
||||
PlatformLogWriter log.PlatformWriter
|
||||
Context context.Context
|
||||
PlatformLogWriter log.PlatformWriter
|
||||
NetworkNamespaceHolderArgs []string
|
||||
}
|
||||
|
||||
func Context(
|
||||
@@ -69,6 +78,7 @@ 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 {
|
||||
@@ -98,6 +108,10 @@ 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
|
||||
}
|
||||
|
||||
@@ -115,6 +129,7 @@ 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")
|
||||
@@ -134,10 +149,14 @@ 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)
|
||||
err := applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||
debugOptions := common.PtrValueOrDefault(experimentalOptions.Debug)
|
||||
err := checkDebugOptions(debugOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -147,7 +166,7 @@ func New(options Options) (*Box, error) {
|
||||
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
|
||||
needCacheFile = true
|
||||
}
|
||||
if experimentalOptions.ClashAPI != nil || options.PlatformLogWriter != nil {
|
||||
if experimentalOptions.ClashAPI != nil {
|
||||
needClashAPI = true
|
||||
}
|
||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||
@@ -156,6 +175,12 @@ 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 {
|
||||
@@ -164,7 +189,7 @@ func New(options Options) (*Box, error) {
|
||||
logFactory, err := log.New(log.Options{
|
||||
Context: ctx,
|
||||
Options: common.PtrValueOrDefault(options.Log),
|
||||
Observable: needClashAPI,
|
||||
Observable: needClashAPI && experimentalOptions.ClashAPI.ExternalController != "",
|
||||
DefaultWriter: defaultLogWriter,
|
||||
BaseTime: createdAt,
|
||||
PlatformWriter: options.PlatformLogWriter,
|
||||
@@ -175,6 +200,7 @@ 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 ||
|
||||
@@ -187,36 +213,65 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
internalServices = append(internalServices, certificateStore)
|
||||
}
|
||||
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
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)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||
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)
|
||||
endpointManager := endpoint.NewManager(endpointRegistry)
|
||||
inboundManager := inbound.NewManager(inboundRegistry, endpointManager)
|
||||
outboundManager := outbound.NewManager(outboundRegistry, endpointManager, routeOptions.Final)
|
||||
providerManager := provider.NewManager(providerRegistry)
|
||||
dnsTransportManager := dns.NewTransportManager(dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||
serviceManager := boxService.NewManager(serviceRegistry)
|
||||
certificateProviderManager := boxCertificate.NewManager(certificateProviderRegistry)
|
||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||
service.MustRegister[adapter.ProviderManager](ctx, providerManager)
|
||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
||||
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize DNS router")
|
||||
}
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
}
|
||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||
// Must register after ConnectionManager: the Apple HTTP engine's proxy bridge reads it from the context when Manager.Start resolves the default client.
|
||||
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
||||
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
||||
httpClientService := adapter.LifecycleService(httpClientManager)
|
||||
router := route.NewRouter(ctx, logFactory, "router", routeOptions, dnsOptions)
|
||||
service.MustRegister[adapter.Router](ctx, router)
|
||||
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize router")
|
||||
}
|
||||
if needClashAPI || needAPIService || options.PlatformLogWriter != nil {
|
||||
trafficManager := trafficcontrol.NewManager(outboundManager)
|
||||
service.MustRegisterPtr(ctx, trafficManager)
|
||||
router.AppendTracker(trafficManager)
|
||||
internalServices = append(internalServices, trafficManager)
|
||||
var clashDefaultMode string
|
||||
if experimentalOptions.ClashAPI != nil {
|
||||
clashDefaultMode = experimentalOptions.ClashAPI.DefaultMode
|
||||
}
|
||||
clashMode := clashmode.NewManager(ctx, logFactory.NewLogger("clash-mode"), clashDefaultMode, clashmode.CalculateModeList(options.Options))
|
||||
service.MustRegisterPtr(ctx, clashMode)
|
||||
internalServices = append(internalServices, clashMode)
|
||||
}
|
||||
referenceManager := route.NewReferenceManager(ctx, logFactory.NewLogger("reference"), options.Options)
|
||||
internalServices = append(internalServices, referenceManager)
|
||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
if ntpOptions.Enabled {
|
||||
@@ -294,6 +349,24 @@ 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 != "" {
|
||||
@@ -339,22 +412,22 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "initialize provider[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, serviceOptions := range options.Services {
|
||||
for i, certificateProviderOptions := range options.CertificateProviders {
|
||||
var tag string
|
||||
if serviceOptions.Tag != "" {
|
||||
tag = serviceOptions.Tag
|
||||
if certificateProviderOptions.Tag != "" {
|
||||
tag = certificateProviderOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = serviceManager.Create(
|
||||
err = certificateProviderManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
serviceOptions.Type,
|
||||
serviceOptions.Options,
|
||||
certificateProviderOptions.Type,
|
||||
certificateProviderOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||
return nil, E.Cause(err, "initialize certificate provider[", i, "]")
|
||||
}
|
||||
}
|
||||
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||
@@ -375,6 +448,12 @@ 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 {
|
||||
@@ -382,19 +461,15 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
}
|
||||
if needCacheFile {
|
||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
}
|
||||
if needClashAPI {
|
||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions)
|
||||
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(experimentalOptions.ClashAPI))
|
||||
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 {
|
||||
@@ -408,12 +483,19 @@ 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"),
|
||||
@@ -422,39 +504,34 @@ 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,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
done: make(chan struct{}),
|
||||
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()),
|
||||
}, 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
|
||||
}
|
||||
@@ -465,15 +542,6 @@ func (s *Box) PreStart() error {
|
||||
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
|
||||
}
|
||||
@@ -481,6 +549,31 @@ func (s *Box) Start() error {
|
||||
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
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) preStart() error {
|
||||
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||
monitor.Start("start logger")
|
||||
@@ -489,15 +582,46 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return E.Cause(err, "start logger")
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
||||
s.scope.Add(s.logFactory.Close)
|
||||
applyDebugOptions(s.debugOptions)
|
||||
debugHTTPServer, err := startDebugHTTPServer(s.debugOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 debugHTTPServer != nil {
|
||||
s.scope.Add(debugHTTPServer.Close)
|
||||
}
|
||||
err = s.startInternalServices(adapter.StartStateInitialize) // cache-file clash-api v2ray-api
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.provider, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||
err = s.startComponents(adapter.StartStateInitialize,
|
||||
boxComponent{s.httpClientService.Name(), s.httpClientService},
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"endpoint", s.endpoint},
|
||||
boxComponent{"certificate-provider", s.certificateProvider},
|
||||
boxComponent{"inbound", s.inbound},
|
||||
boxComponent{"service", s.service},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.startComponents(adapter.StartStateStart,
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{s.httpClientService.Name(), s.httpClientService},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -509,27 +633,65 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateStart, s.internalService)
|
||||
err = s.startInternalServices(adapter.StartStateStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||
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},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
err = s.startComponents(adapter.StartStatePostStart,
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"endpoint", s.endpoint},
|
||||
boxComponent{"certificate-provider", s.certificateProvider},
|
||||
boxComponent{"inbound", s.inbound},
|
||||
boxComponent{"service", s.service},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStatePostStart, s.internalService)
|
||||
err = s.startInternalServices(adapter.StartStatePostStart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
err = s.startComponents(adapter.StartStateStarted,
|
||||
boxComponent{"network", s.network},
|
||||
boxComponent{"dns-transport", s.dnsTransport},
|
||||
boxComponent{"dns-router", s.dnsRouter},
|
||||
boxComponent{"connection", s.connection},
|
||||
boxComponent{"router", s.router},
|
||||
boxComponent{"outbound", s.outbound},
|
||||
boxComponent{"provider", s.provider},
|
||||
boxComponent{"endpoint", s.endpoint},
|
||||
boxComponent{"certificate-provider", s.certificateProvider},
|
||||
boxComponent{"inbound", s.inbound},
|
||||
boxComponent{"service", s.service},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(s.logger, adapter.StartStateStarted, s.internalService)
|
||||
err = s.startInternalServices(adapter.StartStateStarted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -537,47 +699,7 @@ func (s *Box) start() error {
|
||||
}
|
||||
|
||||
func (s *Box) Close() error {
|
||||
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
|
||||
return s.scope.Close()
|
||||
}
|
||||
|
||||
func (s *Box) Network() adapter.NetworkManager {
|
||||
@@ -600,6 +722,14 @@ 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
|
||||
}
|
||||
|
||||
Submodule clients/android deleted from eb87216961
Submodule clients/apple deleted from c19945f65b
Submodule
+1
Submodule clients/desktop added at 50f5038491
@@ -4,12 +4,14 @@ 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"
|
||||
@@ -19,8 +21,11 @@ import (
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
switch os.Args[1] {
|
||||
case "next_macos_project_version":
|
||||
err := fetchMacOSVersion(ctx)
|
||||
case "next_project_version":
|
||||
if len(os.Args) < 3 {
|
||||
log.Fatal("platform required: ios, macos, or tvos")
|
||||
}
|
||||
err := fetchNextProjectVersion(ctx, os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -29,18 +34,37 @@ 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 "prepare_app_store":
|
||||
err := prepareAppStore(ctx)
|
||||
case "submit_app_store":
|
||||
err := submitAppStore(ctx, os.Args[2:])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case "publish_app_store":
|
||||
err := publishAppStore(ctx)
|
||||
err := publishAppStore(ctx, os.Args[2:])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -50,10 +74,37 @@ func main() {
|
||||
}
|
||||
|
||||
const (
|
||||
appID = "6673731168"
|
||||
groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda"
|
||||
appID = "6785326793"
|
||||
groupID = "39f9ebdc-05d4-421f-9595-dae71df227c4"
|
||||
|
||||
applePath = "../sing-box-for-apple"
|
||||
appStorePromotionalText = "The universal proxy platform."
|
||||
appStoreWhatsNew = "Fixes and improvements"
|
||||
buildPollInterval = 15 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
appStorePlatforms = []string{"ios", "tvos"}
|
||||
platformDirectories = map[asc.Platform]string{
|
||||
asc.PlatformIOS: "SFI",
|
||||
asc.PlatformMACOS: "SFM",
|
||||
asc.PlatformTVOS: "SFT",
|
||||
}
|
||||
)
|
||||
|
||||
func parsePlatform(name string) (asc.Platform, error) {
|
||||
switch name {
|
||||
case "ios":
|
||||
return asc.PlatformIOS, nil
|
||||
case "macos":
|
||||
return asc.PlatformMACOS, nil
|
||||
case "tvos":
|
||||
return asc.PlatformTVOS, nil
|
||||
default:
|
||||
return "", E.New("unknown platform: ", name)
|
||||
}
|
||||
}
|
||||
|
||||
func createClient(expireDuration time.Duration) *asc.Client {
|
||||
privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
|
||||
if err != nil {
|
||||
@@ -66,36 +117,84 @@ func createClient(expireDuration time.Duration) *asc.Client {
|
||||
return asc.NewClient(tokenConfig.Client())
|
||||
}
|
||||
|
||||
func fetchMacOSVersion(ctx context.Context) error {
|
||||
func fetchNextProjectVersion(ctx context.Context, platformName string) error {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
Limit: 200,
|
||||
}
|
||||
if platform != asc.PlatformMACOS {
|
||||
tagVersion, err := build_shared.ReadTagVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query.FilterPreReleaseVersionVersion = []string{build_shared.TestFlightVersion(tagVersion)}
|
||||
}
|
||||
|
||||
client := createClient(time.Minute)
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{"MAC_OS"},
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextProjectVersion := 1
|
||||
var projectVersion int
|
||||
for _, build := range builds.Data {
|
||||
projectVersion, err = strconv.Atoi(*build.Attributes.Version)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse version code")
|
||||
}
|
||||
if projectVersion >= nextProjectVersion {
|
||||
nextProjectVersion = projectVersion + 1
|
||||
}
|
||||
}
|
||||
os.Stdout.WriteString(F.ToString(nextProjectVersion, "\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func listBuilds(ctx context.Context, platformName string) error {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
betaBuildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
|
||||
return it.ID
|
||||
})
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
Include: []string{"preReleaseVersion"},
|
||||
Sort: []string{"-uploadedDate"},
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var versionID string
|
||||
findVersion:
|
||||
for _, version := range versions.Data {
|
||||
switch *version.Attributes.AppStoreState {
|
||||
case asc.AppStoreVersionStateReadyForSale,
|
||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
versionID = version.ID
|
||||
break findVersion
|
||||
for _, build := range builds.Data {
|
||||
var betaGroup string
|
||||
if slices.Contains(betaBuildIDs, build.ID) {
|
||||
betaGroup = " testflight"
|
||||
}
|
||||
var versionString string
|
||||
if build.Relationships != nil && build.Relationships.PreReleaseVersion != nil && build.Relationships.PreReleaseVersion.Data != nil {
|
||||
for _, included := range builds.Included {
|
||||
preReleaseVersion := included.PrereleaseVersion()
|
||||
if preReleaseVersion != nil && preReleaseVersion.ID == build.Relationships.PreReleaseVersion.Data.ID {
|
||||
versionString = *preReleaseVersion.Attributes.Version
|
||||
}
|
||||
}
|
||||
}
|
||||
os.Stdout.WriteString(F.ToString(versionString, " (", *build.Attributes.Version, ") ", *build.Attributes.ProcessingState, " ", build.Attributes.UploadedDate.Time.Format(time.RFC3339), betaGroup, "\n"))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -103,16 +202,9 @@ func publishTestflight(ctx context.Context) error {
|
||||
if len(os.Args) < 3 {
|
||||
return E.New("platform required: ios, macos, or tvos")
|
||||
}
|
||||
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])
|
||||
platform, err := parsePlatform(os.Args[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tagVersion, err := build_shared.ReadTagVersion()
|
||||
@@ -120,6 +212,8 @@ 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 {
|
||||
@@ -137,27 +231,37 @@ func publishTestflight(ctx context.Context) error {
|
||||
return it.ID
|
||||
})
|
||||
|
||||
waitingForProcess := false
|
||||
log.Info(string(platform), " list builds")
|
||||
query := &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
FilterPreReleaseVersionVersion: []string{testFlightVersion},
|
||||
Sort: []string{"-uploadedDate"},
|
||||
Limit: 1,
|
||||
}
|
||||
if projectVersion != "" {
|
||||
query.FilterVersion = []string{projectVersion}
|
||||
}
|
||||
log.Info(string(platform), " ", testFlightVersion, " (", projectVersion, ") list builds")
|
||||
for {
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
})
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(builds.Data) == 0 {
|
||||
log.Info(string(platform), " ", testFlightVersion, " waiting for build upload")
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
build := builds.Data[0]
|
||||
log.Info(string(platform), " ", 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)
|
||||
log.Info(string(platform), " ", testFlightVersion, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
|
||||
if projectVersion == "" && common.Contains(buildIDs, build.ID) {
|
||||
log.Info(string(platform), " ", testFlightVersion, " build ", *build.Attributes.Version, " already published, waiting for new upload")
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
if *build.Attributes.ProcessingState != "VALID" {
|
||||
waitingForProcess = true
|
||||
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
||||
time.Sleep(15 * time.Second)
|
||||
log.Info(string(platform), " ", testFlightVersion, " waiting for process: ", *build.Attributes.ProcessingState)
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " list localizations")
|
||||
@@ -182,7 +286,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(15 * time.Second)
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
@@ -210,227 +314,341 @@ func publishTestflight(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func cancelAppStore(ctx context.Context, platform string) error {
|
||||
switch platform {
|
||||
case "ios":
|
||||
platform = string(asc.PlatformIOS)
|
||||
case "macos":
|
||||
platform = string(asc.PlatformMACOS)
|
||||
case "tvos":
|
||||
platform = string(asc.PlatformTVOS)
|
||||
}
|
||||
tag, err := build_shared.ReadTag()
|
||||
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)
|
||||
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
|
||||
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 prepareAppStore(ctx context.Context) error {
|
||||
func cancelAppStore(ctx context.Context, platformName string) error {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
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)},
|
||||
})
|
||||
log.Info(string(platform), " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterVersionString: []string{tag},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(versions.Data) == 0 {
|
||||
return nil
|
||||
}
|
||||
versionID := versions.Data[0].ID
|
||||
log.Info(string(platform), " ", tag, " list review submissions")
|
||||
submissions, _, err := client.Submission.ListReviewSubmissions(ctx, &asc.ListReviewSubmissionsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterState: []string{
|
||||
string(asc.ReviewSubmissionStateReadyForReview),
|
||||
string(asc.ReviewSubmissionStateWaitingForReview),
|
||||
string(asc.ReviewSubmissionStateInReview),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, submission := range submissions.Data {
|
||||
containsVersion, err := reviewSubmissionContainsVersion(ctx, client, submission.ID, versionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
||||
return *it.Attributes.VersionString == tag
|
||||
})
|
||||
log.Info(string(platform), " ", tag, " list builds")
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
})
|
||||
if !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
|
||||
}
|
||||
if len(builds.Data) == 0 {
|
||||
log.Fatal(string(platform), " ", tag, " no build found")
|
||||
}
|
||||
buildID := common.Ptr(builds.Data[0].ID)
|
||||
if version.ID == "" {
|
||||
log.Info(string(platform), " ", tag, " create version")
|
||||
newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
|
||||
Platform: platform,
|
||||
VersionString: tag,
|
||||
}, appID, buildID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version = newVersion.Data
|
||||
|
||||
} else {
|
||||
log.Info(string(platform), " ", tag, " check build")
|
||||
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
|
||||
switch *version.Attributes.AppStoreState {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
||||
asc.AppStoreVersionStateRejected,
|
||||
asc.AppStoreVersionStateDeveloperRejected:
|
||||
case asc.AppStoreVersionStateWaitingForReview,
|
||||
asc.AppStoreVersionStateInReview,
|
||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if submission != nil {
|
||||
log.Info(string(platform), " ", tag, " delete submission")
|
||||
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
default:
|
||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " update build")
|
||||
response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != http.StatusNoContent {
|
||||
response.Write(os.Stderr)
|
||||
log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
|
||||
}
|
||||
} else {
|
||||
switch *version.Attributes.AppStoreState {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
||||
asc.AppStoreVersionStateRejected,
|
||||
asc.AppStoreVersionStateDeveloperRejected:
|
||||
case asc.AppStoreVersionStateWaitingForReview,
|
||||
asc.AppStoreVersionStateInReview,
|
||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
continue
|
||||
default:
|
||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " list localization")
|
||||
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
|
||||
return *it.Attributes.Locale == "en-US"
|
||||
})
|
||||
if localization.ID == "" {
|
||||
log.Info(string(platform), " ", tag, " no en-US localization found")
|
||||
}
|
||||
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
||||
log.Info(string(platform), " ", tag, " update localization")
|
||||
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
|
||||
PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
|
||||
WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Info(string(platform), " ", tag, " create submission")
|
||||
fixSubmit:
|
||||
for {
|
||||
_, response, err := client.Submission.CreateSubmission(ctx, version.ID)
|
||||
if err != nil {
|
||||
switch response.StatusCode {
|
||||
case http.StatusInternalServerError:
|
||||
continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch response.StatusCode {
|
||||
case http.StatusCreated:
|
||||
break fixSubmit
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishAppStore(ctx context.Context) error {
|
||||
tag, err := build_shared.ReadTag()
|
||||
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
|
||||
}
|
||||
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)},
|
||||
localVersion := badversion.Parse(versionString)
|
||||
var version asc.AppStoreVersion
|
||||
for _, remoteVersion := range versions.Data {
|
||||
remoteVersionString := *remoteVersion.Attributes.VersionString
|
||||
if remoteVersionString == versionString {
|
||||
version = remoteVersion
|
||||
continue
|
||||
}
|
||||
if !badversion.Parse(remoteVersionString).LessThan(localVersion) {
|
||||
return E.New(string(platform), " ", versionString, " is not newer than App Store version ", remoteVersionString)
|
||||
}
|
||||
}
|
||||
if version.ID != "" {
|
||||
state := *version.Attributes.AppStoreState
|
||||
switch state {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
||||
asc.AppStoreVersionStateRejected,
|
||||
asc.AppStoreVersionStateDeveloperRejected,
|
||||
asc.AppStoreVersionStateMetadataRejected,
|
||||
asc.AppStoreVersionStateInvalidBinary:
|
||||
log.Info(string(platform), " ", versionString, " found version in state ", string(state))
|
||||
case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview:
|
||||
log.Info(string(platform), " ", versionString, " already submitted: ", string(state))
|
||||
return nil
|
||||
case asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
log.Info(string(platform), " ", versionString, " approved, run publish_app_store to release")
|
||||
return nil
|
||||
default:
|
||||
return E.New(string(platform), " ", versionString, " already released: ", string(state))
|
||||
}
|
||||
} else {
|
||||
log.Info(string(platform), " ", versionString, " create version")
|
||||
created, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
|
||||
Platform: platform,
|
||||
ReleaseType: common.Ptr("MANUAL"),
|
||||
VersionString: versionString,
|
||||
}, appID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version = created.Data
|
||||
}
|
||||
|
||||
log.Info(string(platform), " ", versionString, " list localizations")
|
||||
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
|
||||
return *it.Attributes.Locale == "en-US"
|
||||
})
|
||||
if localization.ID == "" {
|
||||
log.Info(string(platform), " ", versionString, " create en-US localization")
|
||||
_, _, err = client.Apps.CreateAppStoreVersionLocalization(ctx, asc.AppStoreVersionLocalizationCreateRequestAttributes{
|
||||
Locale: "en-US",
|
||||
PromotionalText: common.Ptr(appStorePromotionalText),
|
||||
WhatsNew: common.Ptr(appStoreWhatsNew),
|
||||
}, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if localization.Attributes.PromotionalText == nil || *localization.Attributes.PromotionalText != appStorePromotionalText ||
|
||||
localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew != appStoreWhatsNew {
|
||||
log.Info(string(platform), " ", versionString, " update en-US localization")
|
||||
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
|
||||
PromotionalText: common.Ptr(appStorePromotionalText),
|
||||
WhatsNew: common.Ptr(appStoreWhatsNew),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
||||
return *it.Attributes.VersionString == tag
|
||||
}
|
||||
|
||||
var build asc.Build
|
||||
for {
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
FilterPreReleaseVersionVersion: []string{versionString},
|
||||
Sort: []string{"-uploadedDate"},
|
||||
Limit: 1,
|
||||
})
|
||||
switch *version.Attributes.AppStoreState {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(builds.Data) == 0 {
|
||||
log.Info(string(platform), " ", versionString, " waiting for build upload")
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
build = builds.Data[0]
|
||||
processingState := *build.Attributes.ProcessingState
|
||||
if processingState == "VALID" {
|
||||
break
|
||||
}
|
||||
if processingState == "PROCESSING" {
|
||||
log.Info(string(platform), " ", versionString, " waiting for build ", *build.Attributes.Version, ": ", processingState)
|
||||
time.Sleep(buildPollInterval)
|
||||
continue
|
||||
}
|
||||
return E.New(string(platform), " ", versionString, " build ", *build.Attributes.Version, " ", processingState)
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " found build ", *build.Attributes.Version)
|
||||
|
||||
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != build.ID {
|
||||
log.Info(string(platform), " ", versionString, " attach build ", *build.Attributes.Version)
|
||||
_, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, common.Ptr(build.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info(string(platform), " ", versionString, " list review submissions")
|
||||
submissions, _, err := client.Submission.ListReviewSubmissions(ctx, &asc.ListReviewSubmissionsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterState: []string{
|
||||
string(asc.ReviewSubmissionStateReadyForReview),
|
||||
string(asc.ReviewSubmissionStateWaitingForReview),
|
||||
string(asc.ReviewSubmissionStateInReview),
|
||||
string(asc.ReviewSubmissionStateUnresolvedIssues),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
submission asc.ReviewSubmission
|
||||
submissionHasItem bool
|
||||
)
|
||||
for _, remoteSubmission := range submissions.Data {
|
||||
containsVersion, err := reviewSubmissionContainsVersion(ctx, client, remoteSubmission.ID, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state := *remoteSubmission.Attributes.State
|
||||
switch state {
|
||||
case asc.ReviewSubmissionStateReadyForReview:
|
||||
submission = remoteSubmission
|
||||
submissionHasItem = containsVersion
|
||||
case asc.ReviewSubmissionStateWaitingForReview, asc.ReviewSubmissionStateInReview:
|
||||
if containsVersion {
|
||||
log.Info(string(platform), " ", versionString, " already submitted: ", string(state))
|
||||
return nil
|
||||
}
|
||||
return E.New(string(platform), " review submission ", remoteSubmission.ID, " with another version is ", string(state))
|
||||
default:
|
||||
return E.New(string(platform), " review submission ", remoteSubmission.ID, " is ", string(state))
|
||||
}
|
||||
}
|
||||
if submission.ID == "" {
|
||||
log.Info(string(platform), " ", versionString, " create review submission")
|
||||
created, _, err := client.Submission.CreateReviewSubmission(ctx, appID, common.Ptr(platform))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
submission = created.Data
|
||||
}
|
||||
if !submissionHasItem {
|
||||
log.Info(string(platform), " ", versionString, " add version to review submission ", submission.ID)
|
||||
_, _, err = client.Submission.CreateReviewSubmissionItemForAppStoreVersion(ctx, submission.ID, version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " submit review submission ", submission.ID)
|
||||
_, _, err = client.Submission.UpdateReviewSubmission(ctx, submission.ID, common.Ptr(true), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func publishAppStore(ctx context.Context, platformNames []string) error {
|
||||
if len(platformNames) == 0 {
|
||||
platformNames = appStorePlatforms
|
||||
}
|
||||
client := createClient(time.Minute)
|
||||
for _, platformName := range platformNames {
|
||||
platform, err := parsePlatform(platformName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
versionString, err := build_shared.ReadAppleMarketingVersion(applePath, platformDirectories[platform])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " list versions")
|
||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
||||
FilterPlatform: []string{string(platform)},
|
||||
FilterVersionString: []string{versionString},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(versions.Data) == 0 {
|
||||
return E.New(string(platform), " ", versionString, " not found")
|
||||
}
|
||||
version := versions.Data[0]
|
||||
state := *version.Attributes.AppStoreState
|
||||
switch state {
|
||||
case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
|
||||
log.Fatal(string(platform), " ", tag, " not submitted")
|
||||
case asc.AppStoreVersionStateWaitingForReview,
|
||||
asc.AppStoreVersionStateInReview:
|
||||
log.Warn(string(platform), " ", tag, " waiting for review")
|
||||
return E.New(string(platform), " ", versionString, " not submitted")
|
||||
case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview:
|
||||
log.Warn(string(platform), " ", versionString, " waiting for review")
|
||||
continue
|
||||
case asc.AppStoreVersionStatePendingDeveloperRelease:
|
||||
default:
|
||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
||||
return E.New(string(platform), " ", versionString, " unknown state ", string(state))
|
||||
}
|
||||
log.Info(string(platform), " ", versionString, " release")
|
||||
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -438,15 +656,3 @@ func publishAppStore(ctx context.Context) 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
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
|
||||
}
|
||||
@@ -60,10 +60,10 @@ func init() {
|
||||
if err != nil {
|
||||
currentTag = "unknown"
|
||||
}
|
||||
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")
|
||||
sharedFlags = append(sharedFlags, "-ldflags", build_shared.LinkerFlags(currentTag, false))
|
||||
debugFlags = append(debugFlags, "-ldflags", build_shared.LinkerFlags(currentTag, true))
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_masque", "with_mtproxy", "with_trusttunnel", "with_call", "with_openvpn", "with_sudoku", "with_snell", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_masque", "with_mtproxy", "with_trusttunnel", "with_call", "with_sudoku", "with_utls", "with_naive_outbound", "with_clash_api", "with_usbip", "with_openvpn", "with_openconnect", "badlinkname", "tfogo_checklinkname0")
|
||||
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
|
||||
// memcTags = append(memcTags, "with_tailscale")
|
||||
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
|
||||
@@ -163,14 +163,14 @@ func buildAndroid() {
|
||||
|
||||
bindTarget := getAndroidBindTarget()
|
||||
|
||||
// Build main variant (SDK 23)
|
||||
// Build main variant (SDK 24)
|
||||
mainTags := append([]string{}, sharedTags...)
|
||||
// mainTags = append(mainTags, memcTags...)
|
||||
if debugEnabled {
|
||||
mainTags = append(mainTags, debugTags...)
|
||||
}
|
||||
buildAndroidVariant(AndroidBuildConfig{
|
||||
AndroidAPI: 23,
|
||||
AndroidAPI: 24,
|
||||
OutputName: "libbox.aar",
|
||||
Tags: mainTags,
|
||||
}, bindTarget)
|
||||
@@ -204,6 +204,9 @@ 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, ","))
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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, " ")
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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"
|
||||
)
|
||||
|
||||
@@ -36,3 +37,7 @@ func ReadTagVersion() (badversion.Version, error) {
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func TestFlightVersion(version badversion.Version) string {
|
||||
return F.ToString(version.Major, ".", version.Minor, ".1000")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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
|
||||
}
|
||||
@@ -8,16 +8,21 @@ 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
|
||||
var (
|
||||
flagRunInCI bool
|
||||
flagTestFlight 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() {
|
||||
@@ -40,19 +45,39 @@ 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, 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
if updated0 || updated1 || updated2 {
|
||||
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 {
|
||||
common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj", []byte(newContent), 0o644))
|
||||
}
|
||||
}
|
||||
@@ -76,11 +101,20 @@ func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDLi
|
||||
continue
|
||||
}
|
||||
updated = true
|
||||
projectContent = projectContent[:versionStart] + "\"" + newVersion + "\"" + projectContent[versionEnd:]
|
||||
projectContent = projectContent[:versionStart] + formatProjectVersion(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
|
||||
|
||||
@@ -43,11 +43,8 @@ func updateMozillaIncludedRootCAs() error {
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
func newMozillaIncluded() *x509.CertPool {
|
||||
pool := x509.NewCertPool()
|
||||
`)
|
||||
func mozillaIncludedPEM() string {
|
||||
return ` + "`")
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -58,17 +55,14 @@ func newMozillaIncluded() *x509.CertPool {
|
||||
if record[geoIndex] == "China" {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
cert := strings.Trim(record[certIndex], "'")
|
||||
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("\treturn pool\n}\n")
|
||||
generated.WriteString("`\n}\n")
|
||||
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||
}
|
||||
|
||||
@@ -127,11 +121,8 @@ func updateChromeIncludedRootCAs() error {
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
func newChromeIncluded() *x509.CertPool {
|
||||
pool := x509.NewCertPool()
|
||||
`)
|
||||
func chromeIncludedPEM() string {
|
||||
return ` + "`")
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
@@ -145,18 +136,13 @@ func newChromeIncluded() *x509.CertPool {
|
||||
if chinaFingerprints[record[fingerprintIndex]] {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
cert := strings.Trim(record[certIndex], "'")
|
||||
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("\treturn pool\n}\n")
|
||||
generated.WriteString("`\n}\n")
|
||||
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
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())
|
||||
}
|
||||
+4
-1
@@ -10,6 +10,7 @@ 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"
|
||||
|
||||
@@ -52,7 +53,9 @@ func preRun(cmd *cobra.Command, args []string) {
|
||||
globalCtx = filemanager.WithDefault(globalCtx, "", "", sudoUID, sudoGID)
|
||||
}
|
||||
if disableColor {
|
||||
log.SetStdLogger(log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false).Logger())
|
||||
logFactory := log.NewDefaultFactory(context.Background(), log.Formatter{BaseTime: time.Now(), DisableColors: true}, os.Stderr, "", nil, false)
|
||||
common.Must(logFactory.Start())
|
||||
log.SetStdLogger(logFactory.Logger())
|
||||
}
|
||||
if workingDir != "" {
|
||||
_, err := os.Stat(workingDir)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
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()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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 + ")"
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
commandAPIOpenConnectAuthFlagEndpoint string
|
||||
commandAPIOpenConnectAuthFlagCallbackPort uint16
|
||||
)
|
||||
|
||||
var commandAPIOpenConnectAuth = &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Answer OpenConnect authentication challenges",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := runAPIOpenConnectAuth()
|
||||
if errors.Is(err, errAuthInterrupted) {
|
||||
writeAuthLine(`interrupted; the challenge is still pending — run "sing-box api openconnect auth" again, or "sing-box api openconnect cancel" to restart authentication`)
|
||||
os.Exit(130)
|
||||
}
|
||||
return wrapAuthError("openconnect", err)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIOpenConnectAuth.Flags().StringVar(&commandAPIOpenConnectAuthFlagEndpoint, "endpoint", "", "OpenConnect endpoint tag (default: the only configured endpoint)")
|
||||
commandAPIOpenConnectAuth.Flags().Uint16Var(&commandAPIOpenConnectAuthFlagCallbackPort, "callback-port", 8020, "Local port for the browser single sign-on callback listener")
|
||||
commandAPIOpenConnect.AddCommand(commandAPIOpenConnectAuth)
|
||||
}
|
||||
|
||||
func runAPIOpenConnectAuth() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
ctx, cancel := signal.NotifyContext(globalCtx, os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
stream, endpoints, err := subscribeOpenConnectStatus(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointStatus, err := resolveVPNEndpoint(endpoints, commandAPIOpenConnectAuthFlagEndpoint, "openconnect")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointTag := endpointStatus.GetEndpointTag()
|
||||
if endpointStatus.GetAuthChallenge() == nil {
|
||||
switch endpointStatus.GetState() {
|
||||
case adapter.OpenConnectStateConnected:
|
||||
return E.New("endpoint ", endpointTag, " is already connected")
|
||||
case adapter.OpenConnectStateError:
|
||||
return E.New("endpoint ", endpointTag, " failed: ", endpointStatus.GetError())
|
||||
}
|
||||
}
|
||||
watcher := newVPNStatusWatcher(endpoints, func() ([]*daemon.OpenConnectEndpointStatus, error) {
|
||||
return recvOpenConnectStatus(stream)
|
||||
})
|
||||
err = openConnectAuthLoop(ctx, client, watcher, newInteractiveInput(), endpointTag)
|
||||
if err != nil && ctx.Err() != nil {
|
||||
return errAuthInterrupted
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func openConnectAuthLoop(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
watcher *vpnStatusWatcher[*daemon.OpenConnectEndpointStatus],
|
||||
input *interactiveInput,
|
||||
endpointTag string,
|
||||
) error {
|
||||
var (
|
||||
renderedID string
|
||||
waitingPrinted bool
|
||||
)
|
||||
for {
|
||||
endpoints, updated, streamErr := watcher.current()
|
||||
if streamErr != nil {
|
||||
return streamErr
|
||||
}
|
||||
index := slices.IndexFunc(endpoints, func(it *daemon.OpenConnectEndpointStatus) bool {
|
||||
return it.GetEndpointTag() == endpointTag
|
||||
})
|
||||
if index == -1 {
|
||||
return E.New("endpoint not found: ", endpointTag)
|
||||
}
|
||||
endpointStatus := endpoints[index]
|
||||
challenge := endpointStatus.GetAuthChallenge()
|
||||
switch {
|
||||
case challenge == nil && endpointStatus.GetState() == adapter.OpenConnectStateConnected:
|
||||
os.Stdout.WriteString(endpointTag + ": connected\n")
|
||||
return nil
|
||||
case challenge == nil && endpointStatus.GetState() == adapter.OpenConnectStateError:
|
||||
return E.New("endpoint ", endpointTag, " failed: ", endpointStatus.GetError())
|
||||
case challenge != nil && challenge.GetId() != renderedID:
|
||||
renderedID = challenge.GetId()
|
||||
waitingPrinted = false
|
||||
handleErr := handleOpenConnectChallenge(ctx, client, watcher, input, endpointTag, challenge)
|
||||
switch {
|
||||
case handleErr == nil:
|
||||
case errors.Is(handleErr, errAuthChallengeWithdrawn):
|
||||
writeAuthLine(errAuthChallengeWithdrawn.Error())
|
||||
default:
|
||||
return handleErr
|
||||
}
|
||||
continue
|
||||
case challenge == nil && !waitingPrinted:
|
||||
waitingPrinted = true
|
||||
writeAuthLine("waiting for an authentication challenge on " + endpointTag + "...")
|
||||
}
|
||||
select {
|
||||
case <-updated:
|
||||
case <-ctx.Done():
|
||||
return errAuthInterrupted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleOpenConnectChallenge(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
watcher *vpnStatusWatcher[*daemon.OpenConnectEndpointStatus],
|
||||
input *interactiveInput,
|
||||
endpointTag string,
|
||||
challenge *daemon.OpenConnectAuthChallenge,
|
||||
) error {
|
||||
prompter := &authPrompter{ctx: ctx, input: input, aborted: make(chan struct{})}
|
||||
watchCtx, cancelWatch := context.WithCancel(ctx)
|
||||
defer cancelWatch()
|
||||
go watchOpenConnectChallenge(watchCtx, watcher, endpointTag, challenge.GetId(), prompter)
|
||||
form := challenge.GetForm()
|
||||
browser := challenge.GetBrowser()
|
||||
switch {
|
||||
case form != nil:
|
||||
return submitOpenConnectForm(ctx, client, prompter, endpointTag, challenge, form)
|
||||
case browser != nil:
|
||||
return submitOpenConnectBrowser(ctx, client, prompter, endpointTag, challenge, browser)
|
||||
default:
|
||||
return E.New("unsupported authentication challenge")
|
||||
}
|
||||
}
|
||||
|
||||
func watchOpenConnectChallenge(
|
||||
ctx context.Context,
|
||||
watcher *vpnStatusWatcher[*daemon.OpenConnectEndpointStatus],
|
||||
endpointTag string,
|
||||
challengeID string,
|
||||
prompter *authPrompter,
|
||||
) {
|
||||
for {
|
||||
endpoints, updated, streamErr := watcher.current()
|
||||
if streamErr != nil {
|
||||
prompter.abort(streamErr)
|
||||
return
|
||||
}
|
||||
index := slices.IndexFunc(endpoints, func(it *daemon.OpenConnectEndpointStatus) bool {
|
||||
return it.GetEndpointTag() == endpointTag
|
||||
})
|
||||
if index == -1 || endpoints[index].GetAuthChallenge().GetId() != challengeID {
|
||||
prompter.abort(errAuthChallengeWithdrawn)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-updated:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func submitOpenConnectForm(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
prompter *authPrompter,
|
||||
endpointTag string,
|
||||
challenge *daemon.OpenConnectAuthChallenge,
|
||||
form *daemon.OpenConnectAuthForm,
|
||||
) error {
|
||||
if !authInputIsTerminal {
|
||||
return errAuthNotInteractive
|
||||
}
|
||||
writeAuthHeader(endpointTag, "authentication")
|
||||
preambleWritten := false
|
||||
if challenge.GetBanner() != "" {
|
||||
writeAuthBanner(challenge.GetBanner())
|
||||
preambleWritten = true
|
||||
}
|
||||
if challenge.GetError() != "" {
|
||||
writeAuthLine("previous attempt failed: " + challenge.GetError())
|
||||
preambleWritten = true
|
||||
}
|
||||
if challenge.GetMessage() != "" {
|
||||
writeAuthLine(challenge.GetMessage())
|
||||
preambleWritten = true
|
||||
}
|
||||
if preambleWritten {
|
||||
writeAuthLine("")
|
||||
}
|
||||
for {
|
||||
values := make(map[string]string, len(form.GetFields()))
|
||||
for _, field := range form.GetFields() {
|
||||
value, err := promptOpenConnectField(prompter, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values[field.GetSubmissionKey()] = value
|
||||
}
|
||||
_, err := client.SubmitOpenConnectAuthResponse(ctx, &daemon.OpenConnectAuthResponseSubmission{
|
||||
EndpointTag: endpointTag,
|
||||
ChallengeID: challenge.GetId(),
|
||||
Response: &daemon.OpenConnectAuthResponseSubmission_Form{
|
||||
Form: &daemon.OpenConnectAuthFormResponse{Values: values},
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
outcome, message := classifySubmitError(err)
|
||||
switch outcome {
|
||||
case submitStale:
|
||||
return errAuthChallengeWithdrawn
|
||||
case submitFatal:
|
||||
return err
|
||||
}
|
||||
writeAuthError("openconnect", "submit rejected: "+message)
|
||||
}
|
||||
}
|
||||
|
||||
func promptOpenConnectField(prompter *authPrompter, field *daemon.OpenConnectAuthFormField) (string, error) {
|
||||
label := field.GetLabel()
|
||||
if label == "" {
|
||||
label = field.GetName()
|
||||
}
|
||||
switch field.GetKind() {
|
||||
case "text":
|
||||
return prompter.promptText(label, field.GetValue())
|
||||
case "password":
|
||||
return prompter.promptPassword(label, field.GetValue())
|
||||
case "select":
|
||||
return promptOpenConnectSelect(prompter, label, field.GetOptions(), field.GetValue())
|
||||
default:
|
||||
return "", E.New("unsupported authentication field kind: ", field.GetKind())
|
||||
}
|
||||
}
|
||||
|
||||
func promptOpenConnectSelect(prompter *authPrompter, label string, options []*daemon.OpenConnectAuthFormChoice, defaultValue string) (string, error) {
|
||||
prompt := strings.TrimSuffix(label, ":")
|
||||
var menu strings.Builder
|
||||
menu.WriteString(prompt + ":\n")
|
||||
for index, option := range options {
|
||||
optionLabel := option.GetLabel()
|
||||
if optionLabel == "" {
|
||||
optionLabel = option.GetValue()
|
||||
}
|
||||
menu.WriteString(" " + strconv.Itoa(index+1) + ") " + optionLabel)
|
||||
if option.GetValue() == defaultValue {
|
||||
menu.WriteString(" [default]")
|
||||
}
|
||||
menu.WriteString("\n")
|
||||
}
|
||||
os.Stderr.WriteString(menu.String())
|
||||
for {
|
||||
line, err := prompter.read(prompt+": ", false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" && defaultValue != "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
selected, parseErr := strconv.Atoi(line)
|
||||
if parseErr == nil && selected >= 1 && selected <= len(options) {
|
||||
return options[selected-1].GetValue(), nil
|
||||
}
|
||||
if slices.ContainsFunc(options, func(it *daemon.OpenConnectAuthFormChoice) bool {
|
||||
return it.GetValue() == line
|
||||
}) {
|
||||
return line, nil
|
||||
}
|
||||
writeAuthLine("select a number between 1 and " + strconv.Itoa(len(options)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
const (
|
||||
openConnectBrowserModeCallback = "callback"
|
||||
openConnectBrowserModeCookies = "cookies"
|
||||
openConnectBrowserModeHeaders = "headers"
|
||||
)
|
||||
|
||||
const openConnectCallbackPage = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="utf-8"><title>single sign-on</title></head>
|
||||
<body style="font-family:system-ui,sans-serif;text-align:center;margin-top:4em">
|
||||
<h3>Single sign-on completed</h3>
|
||||
<p>You may close this tab and return to the terminal.</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
func deriveOpenConnectBrowserMode(request *daemon.OpenConnectBrowserRequest) (string, error) {
|
||||
callbackMode := len(request.GetCallbackURLPrefixes()) > 0
|
||||
cookieMode := request.GetFinalURL() != "" || len(request.GetCookieNames()) > 0 || len(request.GetEarlyCookieNames()) > 0
|
||||
headerMode := len(request.GetHeaderNames()) > 0
|
||||
selectedModes := common.Filter([]bool{callbackMode, cookieMode, headerMode}, func(it bool) bool {
|
||||
return it
|
||||
})
|
||||
invalidRequest := E.New("openconnect browser request must select exactly one completion mode")
|
||||
if len(selectedModes) != 1 {
|
||||
return "", invalidRequest
|
||||
}
|
||||
switch {
|
||||
case callbackMode:
|
||||
if len(common.Uniq(request.GetCallbackURLPrefixes())) != len(request.GetCallbackURLPrefixes()) {
|
||||
return "", invalidRequest
|
||||
}
|
||||
return openConnectBrowserModeCallback, nil
|
||||
case cookieMode:
|
||||
cookieNames := append(slices.Clone(request.GetCookieNames()), request.GetEarlyCookieNames()...)
|
||||
if len(common.Uniq(cookieNames)) != len(cookieNames) {
|
||||
return "", invalidRequest
|
||||
}
|
||||
return openConnectBrowserModeCookies, nil
|
||||
default:
|
||||
headerNames := common.Map(request.GetHeaderNames(), strings.ToLower)
|
||||
if len(common.Uniq(headerNames)) != len(headerNames) {
|
||||
return "", invalidRequest
|
||||
}
|
||||
return openConnectBrowserModeHeaders, nil
|
||||
}
|
||||
}
|
||||
|
||||
func submitOpenConnectBrowser(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
prompter *authPrompter,
|
||||
endpointTag string,
|
||||
challenge *daemon.OpenConnectAuthChallenge,
|
||||
request *daemon.OpenConnectBrowserRequest,
|
||||
) error {
|
||||
mode, err := deriveOpenConnectBrowserMode(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeAuthHeader(endpointTag, "browser authentication")
|
||||
if challenge.GetError() != "" {
|
||||
writeAuthLine("previous attempt failed: " + challenge.GetError())
|
||||
}
|
||||
if challenge.GetMessage() != "" {
|
||||
writeAuthLine(challenge.GetMessage())
|
||||
}
|
||||
for {
|
||||
result, earlyFailure, collectErr := collectOpenConnectBrowserResult(ctx, prompter, mode, request)
|
||||
if collectErr != nil {
|
||||
return collectErr
|
||||
}
|
||||
warnPlaintextAPIConnection()
|
||||
_, submitErr := client.SubmitOpenConnectAuthResponse(ctx, &daemon.OpenConnectAuthResponseSubmission{
|
||||
EndpointTag: endpointTag,
|
||||
ChallengeID: challenge.GetId(),
|
||||
Response: &daemon.OpenConnectAuthResponseSubmission_Browser{Browser: result},
|
||||
})
|
||||
if submitErr == nil {
|
||||
if earlyFailure {
|
||||
writeAuthLine("single sign-on failed; the client will retry authentication")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
outcome, message := classifySubmitError(submitErr)
|
||||
switch outcome {
|
||||
case submitStale:
|
||||
return errAuthChallengeWithdrawn
|
||||
case submitFatal:
|
||||
return submitErr
|
||||
}
|
||||
writeAuthError("openconnect", "browser authentication rejected: "+message)
|
||||
}
|
||||
}
|
||||
|
||||
func collectOpenConnectBrowserResult(
|
||||
ctx context.Context,
|
||||
prompter *authPrompter,
|
||||
mode string,
|
||||
request *daemon.OpenConnectBrowserRequest,
|
||||
) (*daemon.OpenConnectBrowserResult, bool, error) {
|
||||
switch {
|
||||
case mode == openConnectBrowserModeCallback:
|
||||
target, err := parseOpenConnectCallbackTarget(request.GetCallbackURLPrefixes())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !authInputIsTerminal {
|
||||
return nil, false, errAuthNotInteractive
|
||||
}
|
||||
finalURL, err := runOpenConnectCallbackListener(ctx, prompter, target, request.GetUrl())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &daemon.OpenConnectBrowserResult{FinalURL: finalURL}, false, nil
|
||||
case mode == openConnectBrowserModeCookies && len(request.GetCookieNames()) > 0:
|
||||
if !authInputIsTerminal {
|
||||
return nil, false, errAuthNotInteractive
|
||||
}
|
||||
return promptOpenConnectBrowserCookies(prompter, request)
|
||||
case mode == openConnectBrowserModeHeaders:
|
||||
return nil, false, E.New("this single sign-on requires reading HTTP response headers, which cannot be done manually; use the sing-box desktop application")
|
||||
default:
|
||||
return nil, false, E.New("this single sign-on cannot be completed manually; use the sing-box desktop application")
|
||||
}
|
||||
}
|
||||
|
||||
type openConnectCallbackTarget struct {
|
||||
scheme string
|
||||
host string
|
||||
port string
|
||||
}
|
||||
|
||||
func (t openConnectCallbackTarget) resolve(requestURI string) string {
|
||||
return t.scheme + "://" + net.JoinHostPort(t.host, t.port) + requestURI
|
||||
}
|
||||
|
||||
func parseOpenConnectCallbackTarget(prefixes []string) (openConnectCallbackTarget, error) {
|
||||
var target openConnectCallbackTarget
|
||||
for _, prefix := range prefixes {
|
||||
parsed, err := url.Parse(prefix)
|
||||
if err != nil || !isLoopbackHost(parsed.Hostname()) {
|
||||
return target, E.New("callback URL prefix is not on loopback: ", prefix)
|
||||
}
|
||||
if target.scheme == "" {
|
||||
target.scheme = parsed.Scheme
|
||||
target.host = parsed.Hostname()
|
||||
}
|
||||
if target.port == "" {
|
||||
target.port = parsed.Port()
|
||||
}
|
||||
}
|
||||
if target.port == "" {
|
||||
target.port = strconv.Itoa(int(commandAPIOpenConnectAuthFlagCallbackPort))
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func runOpenConnectCallbackListener(ctx context.Context, prompter *authPrompter, target openConnectCallbackTarget, loginURL string) (string, error) {
|
||||
listener, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", target.port))
|
||||
if err != nil {
|
||||
return "", E.New("cannot listen on 127.0.0.1:", target.port, ": ", err.Error(), "; pass --callback-port")
|
||||
}
|
||||
defer listener.Close()
|
||||
writeAuthLine("Complete single sign-on in your browser; this command finishes automatically.")
|
||||
writeAuthLine("")
|
||||
writeAuthLine(" listening on " + target.resolve("/"))
|
||||
writeAuthLine(" url " + loginURL)
|
||||
writeAuthLine("")
|
||||
confirmed, err := prompter.promptConfirm("Open it now? [Y/n] ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !confirmed {
|
||||
writeAuthLine("waiting for the callback...")
|
||||
} else {
|
||||
openErr := openURLInBrowser(loginURL)
|
||||
if openErr != nil {
|
||||
writeAuthLine("failed to open the default browser: " + openErr.Error())
|
||||
writeAuthLine("waiting for the callback...")
|
||||
} else {
|
||||
writeAuthLine("opened in the default browser; waiting for the callback...")
|
||||
}
|
||||
}
|
||||
requestURIs := make(chan string, 1)
|
||||
server := &http.Server{Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
select {
|
||||
case requestURIs <- request.RequestURI:
|
||||
default:
|
||||
}
|
||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
writer.Header().Set("Connection", "close")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
writer.Write([]byte(openConnectCallbackPage))
|
||||
})}
|
||||
go func() {
|
||||
_ = server.Serve(listener)
|
||||
}()
|
||||
defer server.Close()
|
||||
select {
|
||||
case requestURI := <-requestURIs:
|
||||
writeAuthLine("received callback")
|
||||
return target.resolve(requestURI), nil
|
||||
case <-prompter.aborted:
|
||||
return "", prompter.abortErr
|
||||
case <-ctx.Done():
|
||||
return "", errAuthInterrupted
|
||||
}
|
||||
}
|
||||
|
||||
func promptOpenConnectBrowserCookies(prompter *authPrompter, request *daemon.OpenConnectBrowserRequest) (*daemon.OpenConnectBrowserResult, bool, error) {
|
||||
writeAuthLine("This single sign-on must be completed manually.")
|
||||
writeAuthLine("")
|
||||
step := 1
|
||||
writeAuthLine(" " + strconv.Itoa(step) + ". Open this URL in any browser:")
|
||||
writeAuthLine(" " + request.GetUrl())
|
||||
if request.GetFinalURL() != "" {
|
||||
step++
|
||||
writeAuthLine(" " + strconv.Itoa(step) + ". Sign in until the browser lands on:")
|
||||
writeAuthLine(" " + request.GetFinalURL())
|
||||
}
|
||||
step++
|
||||
writeAuthLine(" " + strconv.Itoa(step) + ". Open the developer tools (F12) > Application > Cookies, and read the")
|
||||
writeAuthLine(" value of the cookie listed below for that page.")
|
||||
writeAuthLine("")
|
||||
earlyCookieNames := request.GetEarlyCookieNames()
|
||||
var cookies []*daemon.OpenConnectBrowserCookie
|
||||
for index, name := range request.GetCookieNames() {
|
||||
prompt := `Cookie "` + name + `": `
|
||||
if index == 0 && len(earlyCookieNames) > 0 {
|
||||
prompt = `Cookie "` + name + `" (or "!" if the page reported an error): `
|
||||
}
|
||||
for {
|
||||
value, err := prompter.read(prompt, true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if index == 0 && len(earlyCookieNames) > 0 && value == "!" {
|
||||
earlyCookie, earlyErr := promptOpenConnectEarlyCookie(prompter, earlyCookieNames[0])
|
||||
if earlyErr != nil {
|
||||
return nil, false, earlyErr
|
||||
}
|
||||
return &daemon.OpenConnectBrowserResult{Cookies: []*daemon.OpenConnectBrowserCookie{earlyCookie}}, true, nil
|
||||
}
|
||||
if value == "" {
|
||||
writeAuthLine("cookie value must not be empty")
|
||||
continue
|
||||
}
|
||||
cookies = append(cookies, &daemon.OpenConnectBrowserCookie{Name: name, Value: value})
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(cookies) == 1 {
|
||||
writeAuthLine("submitting 1 cookie")
|
||||
} else {
|
||||
writeAuthLine(F.ToString("submitting ", len(cookies), " cookies"))
|
||||
}
|
||||
return &daemon.OpenConnectBrowserResult{FinalURL: request.GetFinalURL(), Cookies: cookies}, false, nil
|
||||
}
|
||||
|
||||
func promptOpenConnectEarlyCookie(prompter *authPrompter, name string) (*daemon.OpenConnectBrowserCookie, error) {
|
||||
for {
|
||||
value, err := prompter.read(`Error cookie "`+name+`": `, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == "" {
|
||||
writeAuthLine("cookie value must not be empty")
|
||||
continue
|
||||
}
|
||||
return &daemon.OpenConnectBrowserCookie{Name: name, Value: value}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIOpenConnectCancelFlagEndpoint string
|
||||
|
||||
var commandAPIOpenConnectCancel = &cobra.Command{
|
||||
Use: "cancel",
|
||||
Short: "Cancel the pending OpenConnect authentication challenge",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIOpenConnectCancel()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIOpenConnectCancel.Flags().StringVar(&commandAPIOpenConnectCancelFlagEndpoint, "endpoint", "", "OpenConnect endpoint tag (default: the only configured endpoint)")
|
||||
commandAPIOpenConnect.AddCommand(commandAPIOpenConnectCancel)
|
||||
}
|
||||
|
||||
func runAPIOpenConnectCancel() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
defer cancel()
|
||||
_, endpoints, err := subscribeOpenConnectStatus(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointStatus, err := resolveVPNEndpoint(endpoints, commandAPIOpenConnectCancelFlagEndpoint, "openconnect")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointTag := endpointStatus.GetEndpointTag()
|
||||
challenge := endpointStatus.GetAuthChallenge()
|
||||
if challenge == nil {
|
||||
return E.New("no pending authentication challenge on ", endpointTag)
|
||||
}
|
||||
_, err = client.CancelOpenConnectAuthChallenge(ctx, &daemon.OpenConnectAuthChallengeCancel{
|
||||
EndpointTag: endpointTag,
|
||||
ChallengeID: challenge.GetId(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.WriteString(endpointTag + ": authentication challenge canceled; the client will restart authentication\n")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/term"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var (
|
||||
errAuthInterrupted = E.New("interrupted")
|
||||
errAuthChallengeWithdrawn = E.New("challenge no longer pending, waiting for the next one")
|
||||
errAuthDeadlineExpired = E.New("challenge deadline expired; the server will retry the connection")
|
||||
errAuthNotInteractive = E.New("authentication requires an interactive terminal")
|
||||
)
|
||||
|
||||
var authInputIsTerminal = term.IsTerminal(int(os.Stdin.Fd())) && stderrIsTerminal
|
||||
|
||||
type vpnEndpointStatus interface {
|
||||
GetEndpointTag() string
|
||||
}
|
||||
|
||||
func resolveVPNEndpoint[T vpnEndpointStatus](endpoints []T, endpointTag string, domain string) (T, error) {
|
||||
var zero T
|
||||
if endpointTag != "" {
|
||||
index := slices.IndexFunc(endpoints, func(it T) bool {
|
||||
return it.GetEndpointTag() == endpointTag
|
||||
})
|
||||
if index == -1 {
|
||||
return zero, E.New("endpoint not found: ", endpointTag)
|
||||
}
|
||||
return endpoints[index], nil
|
||||
}
|
||||
switch len(endpoints) {
|
||||
case 0:
|
||||
return zero, E.New("no ", domain, " endpoint is configured")
|
||||
case 1:
|
||||
return endpoints[0], nil
|
||||
default:
|
||||
return zero, E.New("multiple ", domain, " endpoints; select one with -e: ", strings.Join(common.Map(endpoints, func(it T) string {
|
||||
return it.GetEndpointTag()
|
||||
}), ", "))
|
||||
}
|
||||
}
|
||||
|
||||
type vpnStatusWatcher[T any] struct {
|
||||
access sync.Mutex
|
||||
updated chan struct{}
|
||||
endpoints []T
|
||||
err error
|
||||
}
|
||||
|
||||
func newVPNStatusWatcher[T any](endpoints []T, recv func() ([]T, error)) *vpnStatusWatcher[T] {
|
||||
watcher := &vpnStatusWatcher[T]{
|
||||
updated: make(chan struct{}),
|
||||
endpoints: endpoints,
|
||||
}
|
||||
go watcher.run(recv)
|
||||
return watcher
|
||||
}
|
||||
|
||||
func (w *vpnStatusWatcher[T]) run(recv func() ([]T, error)) {
|
||||
for {
|
||||
endpoints, err := recv()
|
||||
w.access.Lock()
|
||||
if err != nil {
|
||||
w.err = err
|
||||
} else {
|
||||
w.endpoints = endpoints
|
||||
}
|
||||
close(w.updated)
|
||||
w.updated = make(chan struct{})
|
||||
w.access.Unlock()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *vpnStatusWatcher[T]) current() ([]T, <-chan struct{}, error) {
|
||||
w.access.Lock()
|
||||
defer w.access.Unlock()
|
||||
return w.endpoints, w.updated, w.err
|
||||
}
|
||||
|
||||
type interactiveReadResult struct {
|
||||
line string
|
||||
err error
|
||||
}
|
||||
|
||||
type interactiveReadRequest struct {
|
||||
prompt string
|
||||
hidden bool
|
||||
result chan interactiveReadResult
|
||||
}
|
||||
|
||||
type interactiveInput struct {
|
||||
requests chan interactiveReadRequest
|
||||
}
|
||||
|
||||
func newInteractiveInput() *interactiveInput {
|
||||
input := &interactiveInput{requests: make(chan interactiveReadRequest)}
|
||||
go input.run()
|
||||
return input
|
||||
}
|
||||
|
||||
func (i *interactiveInput) run() {
|
||||
for request := range i.requests {
|
||||
os.Stderr.WriteString(request.prompt)
|
||||
line, err := readTerminalLine(request.hidden)
|
||||
request.result <- interactiveReadResult{line: line, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func readTerminalLine(hidden bool) (string, error) {
|
||||
if hidden {
|
||||
line, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
os.Stderr.WriteString("\n")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(line), nil
|
||||
}
|
||||
var builder strings.Builder
|
||||
buffer := make([]byte, 1)
|
||||
for {
|
||||
n, err := os.Stdin.Read(buffer)
|
||||
if n > 0 {
|
||||
if buffer[0] == '\n' {
|
||||
break
|
||||
}
|
||||
builder.WriteByte(buffer[0])
|
||||
}
|
||||
if err != nil {
|
||||
if builder.Len() == 0 {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(builder.String(), "\r"), nil
|
||||
}
|
||||
|
||||
type authPrompter struct {
|
||||
ctx context.Context
|
||||
input *interactiveInput
|
||||
once sync.Once
|
||||
aborted chan struct{}
|
||||
abortErr error
|
||||
}
|
||||
|
||||
func (p *authPrompter) abort(cause error) {
|
||||
p.once.Do(func() {
|
||||
p.abortErr = cause
|
||||
close(p.aborted)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *authPrompter) read(prompt string, hidden bool) (string, error) {
|
||||
result := make(chan interactiveReadResult, 1)
|
||||
select {
|
||||
case p.input.requests <- interactiveReadRequest{prompt: prompt, hidden: hidden, result: result}:
|
||||
case <-p.aborted:
|
||||
return "", p.abortErr
|
||||
case <-p.ctx.Done():
|
||||
return "", errAuthInterrupted
|
||||
}
|
||||
select {
|
||||
case value := <-result:
|
||||
return value.line, value.err
|
||||
case <-p.aborted:
|
||||
return "", p.abortErr
|
||||
case <-p.ctx.Done():
|
||||
return "", errAuthInterrupted
|
||||
}
|
||||
}
|
||||
|
||||
func (p *authPrompter) promptText(label string, value string) (string, error) {
|
||||
prompt := strings.TrimSuffix(label, ":")
|
||||
if value != "" {
|
||||
prompt += " [" + value + "]"
|
||||
}
|
||||
line, err := p.read(prompt+": ", false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if line == "" {
|
||||
return value, nil
|
||||
}
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func (p *authPrompter) promptPassword(label string, value string) (string, error) {
|
||||
prompt := strings.TrimSuffix(label, ":")
|
||||
if value != "" {
|
||||
prompt += " (unchanged)"
|
||||
}
|
||||
line, err := p.read(prompt+": ", true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if line == "" {
|
||||
return value, nil
|
||||
}
|
||||
return line, nil
|
||||
}
|
||||
|
||||
func (p *authPrompter) promptConfirm(prompt string) (bool, error) {
|
||||
line, err := p.read(prompt, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(line)) {
|
||||
case "", "y", "yes":
|
||||
return true, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func writeAuthLine(message string) {
|
||||
os.Stderr.WriteString(message + "\n")
|
||||
}
|
||||
|
||||
func writeAuthError(domain string, message string) {
|
||||
os.Stderr.WriteString(domain + " auth: " + message + "\n")
|
||||
}
|
||||
|
||||
func writeAuthHeader(endpointTag string, title string) {
|
||||
os.Stderr.WriteString("\n" + endpointTag + ": " + title + "\n")
|
||||
}
|
||||
|
||||
func writeAuthBanner(banner string) {
|
||||
var output strings.Builder
|
||||
for line := range strings.SplitSeq(strings.ReplaceAll(banner, "\r\n", "\n"), "\n") {
|
||||
output.WriteString(" " + line + "\n")
|
||||
}
|
||||
os.Stderr.WriteString(output.String())
|
||||
}
|
||||
|
||||
type submitOutcome int
|
||||
|
||||
const (
|
||||
submitRejected submitOutcome = iota
|
||||
submitStale
|
||||
submitFatal
|
||||
)
|
||||
|
||||
func classifySubmitError(err error) (submitOutcome, string) {
|
||||
grpcStatus, isStatus := status.FromError(err)
|
||||
if !isStatus {
|
||||
return submitFatal, err.Error()
|
||||
}
|
||||
switch grpcStatus.Code() {
|
||||
case codes.Unavailable, codes.Canceled, codes.DeadlineExceeded, codes.Unauthenticated, codes.Unimplemented:
|
||||
return submitFatal, grpcStatus.Message()
|
||||
}
|
||||
if strings.Contains(grpcStatus.Message(), "no pending") {
|
||||
return submitStale, grpcStatus.Message()
|
||||
}
|
||||
return submitRejected, grpcStatus.Message()
|
||||
}
|
||||
|
||||
func wrapAuthError(domain string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
_, isStatus := status.FromError(err)
|
||||
if isStatus {
|
||||
return err
|
||||
}
|
||||
return E.Cause(err, domain+" auth")
|
||||
}
|
||||
|
||||
func formatVPNConnectedSince(connectedSince int64) string {
|
||||
if connectedSince == 0 {
|
||||
return ""
|
||||
}
|
||||
since := time.Unix(connectedSince, 0).Local()
|
||||
return since.Format(time.RFC3339) + " (" + time.Since(since).Truncate(time.Second).String() + ")"
|
||||
}
|
||||
|
||||
func formatAuthDeadline(deadline int64) string {
|
||||
if deadline == 0 {
|
||||
return ""
|
||||
}
|
||||
return max(time.Until(time.Unix(deadline, 0)).Truncate(time.Second), 0).String()
|
||||
}
|
||||
|
||||
func isLoopbackHost(host string) bool {
|
||||
if host == "localhost" {
|
||||
return true
|
||||
}
|
||||
address, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return address.IsLoopback()
|
||||
}
|
||||
|
||||
func warnPlaintextAPIConnection() {
|
||||
parsed, err := url.Parse(commandAPIServerURL)
|
||||
if err != nil || parsed.Scheme == "https" || isLoopbackHost(parsed.Hostname()) {
|
||||
return
|
||||
}
|
||||
writeAuthLine("warning: submitting single sign-on credentials over a plaintext API connection")
|
||||
}
|
||||
|
||||
func openURLInBrowser(target string) error {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return exec.Command("open", target).Start()
|
||||
case "windows":
|
||||
return exec.Command("rundll32", "url.dll,FileProtocolHandler", target).Start()
|
||||
default:
|
||||
return exec.Command("xdg-open", target).Start()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIOpenConnectStatus = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Print OpenConnect endpoint status",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIOpenConnectStatus()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIOpenConnect.AddCommand(commandAPIOpenConnectStatus)
|
||||
}
|
||||
|
||||
func runAPIOpenConnectStatus() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
defer cancel()
|
||||
_, endpoints, err := subscribeOpenConnectStatus(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
writeStderrLine("no openconnect endpoint is configured")
|
||||
return nil
|
||||
}
|
||||
for index, endpointStatus := range endpoints {
|
||||
if index > 0 {
|
||||
os.Stdout.WriteString("\n")
|
||||
}
|
||||
writeOpenConnectStatusBlock(endpointStatus)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeOpenConnectStatusBlock(endpointStatus *daemon.OpenConnectEndpointStatus) {
|
||||
var block blockWriter
|
||||
block.addLine("Endpoint", endpointStatus.GetEndpointTag())
|
||||
block.addLine("State", endpointStatus.GetState())
|
||||
challenge := endpointStatus.GetAuthChallenge()
|
||||
tunnelInfo := endpointStatus.GetTunnelInfo()
|
||||
switch {
|
||||
case challenge != nil:
|
||||
block.addLine("Challenge", openConnectChallengeSummary(challenge))
|
||||
if challenge.GetMessage() != "" {
|
||||
block.addLine("Message", challenge.GetMessage())
|
||||
}
|
||||
if challenge.GetError() != "" {
|
||||
block.addLine("Error", challenge.GetError())
|
||||
}
|
||||
case tunnelInfo != nil:
|
||||
block.addLine("Server", tunnelInfo.GetServer())
|
||||
block.addLine("Flavor", tunnelInfo.GetFlavor())
|
||||
block.addLine("Transport", tunnelInfo.GetTransport())
|
||||
if len(tunnelInfo.GetIpv4()) > 0 {
|
||||
block.addLine("IPv4", strings.Join(tunnelInfo.GetIpv4(), ", "))
|
||||
}
|
||||
if len(tunnelInfo.GetIpv6()) > 0 {
|
||||
block.addLine("IPv6", strings.Join(tunnelInfo.GetIpv6(), ", "))
|
||||
}
|
||||
if len(tunnelInfo.GetDns()) > 0 {
|
||||
block.addLine("DNS", strings.Join(tunnelInfo.GetDns(), ", "))
|
||||
}
|
||||
if tunnelInfo.GetMtu() > 0 {
|
||||
block.addLine("MTU", F.ToString(tunnelInfo.GetMtu()))
|
||||
}
|
||||
block.addLine("Connected since", formatVPNConnectedSince(tunnelInfo.GetConnectedSince()))
|
||||
case endpointStatus.GetState() == adapter.OpenConnectStateError:
|
||||
block.addLine("Error", endpointStatus.GetError())
|
||||
}
|
||||
block.flush()
|
||||
if challenge != nil {
|
||||
writeStderrLine("")
|
||||
writeStderrLine(`run "sing-box api openconnect auth" to answer`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
const (
|
||||
openVPNChallengeCredentials = "credentials"
|
||||
openVPNChallengeSecret = "secret"
|
||||
openVPNChallengeMessage = "message"
|
||||
openVPNChallengeOpenURL = "open-url"
|
||||
)
|
||||
|
||||
var commandAPIOpenVPN = &cobra.Command{
|
||||
Use: "openvpn",
|
||||
Short: "Manage OpenVPN authentication",
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIRoot.AddCommand(commandAPIOpenVPN)
|
||||
}
|
||||
|
||||
func subscribeOpenVPNStatus(ctx context.Context, client daemon.StartedServiceClient) (grpc.ServerStreamingClient[daemon.OpenVPNStatusUpdate], []*daemon.OpenVPNEndpointStatus, error) {
|
||||
stream, err := client.SubscribeOpenVPNStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
endpoints, err := recvOpenVPNStatus(stream)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return stream, endpoints, nil
|
||||
}
|
||||
|
||||
func recvOpenVPNStatus(stream grpc.ServerStreamingClient[daemon.OpenVPNStatusUpdate]) ([]*daemon.OpenVPNEndpointStatus, 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 openVPNChallengeSummary(challenge *daemon.OpenVPNChallenge) string {
|
||||
switch challenge.GetKind() {
|
||||
case openVPNChallengeMessage, openVPNChallengeOpenURL:
|
||||
return challenge.GetKind() + " (not answerable)"
|
||||
default:
|
||||
return challenge.GetKind()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIOpenVPNAuthFlagEndpoint string
|
||||
|
||||
var commandAPIOpenVPNAuth = &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Answer OpenVPN authentication challenges",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := runAPIOpenVPNAuth()
|
||||
if errors.Is(err, errAuthInterrupted) {
|
||||
writeAuthLine(`interrupted; the challenge is still pending — run "sing-box api openvpn auth" again, or "sing-box api openvpn cancel" to stop the client`)
|
||||
os.Exit(130)
|
||||
}
|
||||
return wrapAuthError("openvpn", err)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIOpenVPNAuth.Flags().StringVar(&commandAPIOpenVPNAuthFlagEndpoint, "endpoint", "", "OpenVPN endpoint tag (default: the only configured endpoint)")
|
||||
commandAPIOpenVPN.AddCommand(commandAPIOpenVPNAuth)
|
||||
}
|
||||
|
||||
func runAPIOpenVPNAuth() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
ctx, cancel := signal.NotifyContext(globalCtx, os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
stream, endpoints, err := subscribeOpenVPNStatus(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointStatus, err := resolveVPNEndpoint(endpoints, commandAPIOpenVPNAuthFlagEndpoint, "openvpn")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointTag := endpointStatus.GetEndpointTag()
|
||||
if endpointStatus.GetChallenge() == nil {
|
||||
switch endpointStatus.GetState() {
|
||||
case adapter.OpenVPNStateConnected:
|
||||
return E.New("endpoint ", endpointTag, " is already connected")
|
||||
case adapter.OpenVPNStateError:
|
||||
return E.New("endpoint ", endpointTag, " failed: ", endpointStatus.GetError())
|
||||
}
|
||||
}
|
||||
watcher := newVPNStatusWatcher(endpoints, func() ([]*daemon.OpenVPNEndpointStatus, error) {
|
||||
return recvOpenVPNStatus(stream)
|
||||
})
|
||||
err = openVPNAuthLoop(ctx, client, watcher, newInteractiveInput(), endpointTag)
|
||||
if err != nil && ctx.Err() != nil {
|
||||
return errAuthInterrupted
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func openVPNAuthLoop(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
watcher *vpnStatusWatcher[*daemon.OpenVPNEndpointStatus],
|
||||
input *interactiveInput,
|
||||
endpointTag string,
|
||||
) error {
|
||||
var (
|
||||
renderedID string
|
||||
waitingPrinted bool
|
||||
)
|
||||
for {
|
||||
endpoints, updated, streamErr := watcher.current()
|
||||
if streamErr != nil {
|
||||
return streamErr
|
||||
}
|
||||
index := slices.IndexFunc(endpoints, func(it *daemon.OpenVPNEndpointStatus) bool {
|
||||
return it.GetEndpointTag() == endpointTag
|
||||
})
|
||||
if index == -1 {
|
||||
return E.New("endpoint not found: ", endpointTag)
|
||||
}
|
||||
endpointStatus := endpoints[index]
|
||||
challenge := endpointStatus.GetChallenge()
|
||||
switch {
|
||||
case challenge == nil && endpointStatus.GetState() == adapter.OpenVPNStateConnected:
|
||||
os.Stdout.WriteString(endpointTag + ": connected\n")
|
||||
return nil
|
||||
case challenge == nil && endpointStatus.GetState() == adapter.OpenVPNStateError:
|
||||
return E.New("endpoint ", endpointTag, " failed: ", endpointStatus.GetError())
|
||||
case challenge != nil && challenge.GetId() != renderedID:
|
||||
renderedID = challenge.GetId()
|
||||
waitingPrinted = false
|
||||
handleErr := handleOpenVPNChallenge(ctx, client, watcher, input, endpointTag, challenge)
|
||||
switch {
|
||||
case handleErr == nil:
|
||||
case errors.Is(handleErr, errAuthChallengeWithdrawn):
|
||||
writeAuthLine(errAuthChallengeWithdrawn.Error())
|
||||
case errors.Is(handleErr, errAuthDeadlineExpired):
|
||||
writeAuthError("openvpn", errAuthDeadlineExpired.Error())
|
||||
default:
|
||||
return handleErr
|
||||
}
|
||||
continue
|
||||
case challenge == nil && !waitingPrinted:
|
||||
waitingPrinted = true
|
||||
writeAuthLine("waiting for an authentication challenge on " + endpointTag + "...")
|
||||
}
|
||||
select {
|
||||
case <-updated:
|
||||
case <-ctx.Done():
|
||||
return errAuthInterrupted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleOpenVPNChallenge(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
watcher *vpnStatusWatcher[*daemon.OpenVPNEndpointStatus],
|
||||
input *interactiveInput,
|
||||
endpointTag string,
|
||||
challenge *daemon.OpenVPNChallenge,
|
||||
) error {
|
||||
prompter := &authPrompter{ctx: ctx, input: input, aborted: make(chan struct{})}
|
||||
watchCtx, cancelWatch := context.WithCancel(ctx)
|
||||
defer cancelWatch()
|
||||
go watchOpenVPNChallenge(watchCtx, watcher, endpointTag, challenge.GetId(), prompter)
|
||||
switch challenge.GetKind() {
|
||||
case openVPNChallengeCredentials:
|
||||
return submitOpenVPNCredentials(ctx, client, prompter, endpointTag, challenge)
|
||||
case openVPNChallengeSecret:
|
||||
return submitOpenVPNSecret(ctx, client, prompter, endpointTag, challenge)
|
||||
case openVPNChallengeMessage:
|
||||
writeAuthHeader(endpointTag, "notice")
|
||||
writeAuthLine(challenge.GetMessage() + openVPNRemainingSuffix(challenge))
|
||||
return nil
|
||||
case openVPNChallengeOpenURL:
|
||||
return openOpenVPNChallengeURL(prompter, endpointTag, challenge)
|
||||
default:
|
||||
return E.New("unsupported authentication challenge kind: ", challenge.GetKind())
|
||||
}
|
||||
}
|
||||
|
||||
func watchOpenVPNChallenge(
|
||||
ctx context.Context,
|
||||
watcher *vpnStatusWatcher[*daemon.OpenVPNEndpointStatus],
|
||||
endpointTag string,
|
||||
challengeID string,
|
||||
prompter *authPrompter,
|
||||
) {
|
||||
timer := time.NewTimer(time.Hour)
|
||||
timer.Stop()
|
||||
defer timer.Stop()
|
||||
for {
|
||||
endpoints, updated, streamErr := watcher.current()
|
||||
if streamErr != nil {
|
||||
prompter.abort(streamErr)
|
||||
return
|
||||
}
|
||||
index := slices.IndexFunc(endpoints, func(it *daemon.OpenVPNEndpointStatus) bool {
|
||||
return it.GetEndpointTag() == endpointTag
|
||||
})
|
||||
if index == -1 || endpoints[index].GetChallenge().GetId() != challengeID {
|
||||
prompter.abort(errAuthChallengeWithdrawn)
|
||||
return
|
||||
}
|
||||
var expired <-chan time.Time
|
||||
deadline := endpoints[index].GetChallenge().GetDeadline()
|
||||
if deadline != 0 {
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
timer.Reset(time.Until(time.Unix(deadline, 0)))
|
||||
expired = timer.C
|
||||
}
|
||||
select {
|
||||
case <-updated:
|
||||
case <-expired:
|
||||
prompter.abort(errAuthDeadlineExpired)
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func submitOpenVPNCredentials(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
prompter *authPrompter,
|
||||
endpointTag string,
|
||||
challenge *daemon.OpenVPNChallenge,
|
||||
) error {
|
||||
if !authInputIsTerminal {
|
||||
return errAuthNotInteractive
|
||||
}
|
||||
writeAuthHeader(endpointTag, "authentication")
|
||||
if challenge.GetPreviousError() != "" {
|
||||
writeAuthLine("previous attempt failed: " + challenge.GetPreviousError())
|
||||
writeAuthLine("")
|
||||
}
|
||||
secretLabel := challenge.GetSecretMessage()
|
||||
if secretLabel == "" {
|
||||
secretLabel = "Secret"
|
||||
}
|
||||
for {
|
||||
username, err := prompter.promptText("Username", challenge.GetUsername())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
password, err := prompter.promptPassword("Password", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret, err := prompter.read(strings.TrimSuffix(secretLabel, ":")+": ", !challenge.GetEcho())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
answered, err := submitOpenVPNChallengeResponse(ctx, client, &daemon.OpenVPNChallengeSubmission{
|
||||
EndpointTag: endpointTag,
|
||||
ChallengeID: challenge.GetId(),
|
||||
Username: username,
|
||||
Password: password,
|
||||
Secret: secret,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if answered {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func submitOpenVPNSecret(
|
||||
ctx context.Context,
|
||||
client daemon.StartedServiceClient,
|
||||
prompter *authPrompter,
|
||||
endpointTag string,
|
||||
challenge *daemon.OpenVPNChallenge,
|
||||
) error {
|
||||
if !authInputIsTerminal {
|
||||
return errAuthNotInteractive
|
||||
}
|
||||
writeAuthHeader(endpointTag, "authentication")
|
||||
contextWritten := false
|
||||
if challenge.GetPreviousError() != "" {
|
||||
writeAuthLine("previous attempt failed: " + challenge.GetPreviousError())
|
||||
contextWritten = true
|
||||
}
|
||||
if challenge.GetUsername() != "" {
|
||||
writeAuthLine("user: " + challenge.GetUsername())
|
||||
contextWritten = true
|
||||
}
|
||||
if contextWritten {
|
||||
writeAuthLine("")
|
||||
}
|
||||
label := challenge.GetMessage()
|
||||
if challenge.GetDeadline() != 0 {
|
||||
if label != "" {
|
||||
writeAuthLine(label + openVPNRemainingSuffix(challenge))
|
||||
}
|
||||
label = "Code"
|
||||
}
|
||||
if label == "" {
|
||||
label = "Secret"
|
||||
}
|
||||
for {
|
||||
secret, err := prompter.read(strings.TrimSuffix(label, ":")+": ", !challenge.GetEcho())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
answered, err := submitOpenVPNChallengeResponse(ctx, client, &daemon.OpenVPNChallengeSubmission{
|
||||
EndpointTag: endpointTag,
|
||||
ChallengeID: challenge.GetId(),
|
||||
Secret: secret,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if answered {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func submitOpenVPNChallengeResponse(ctx context.Context, client daemon.StartedServiceClient, submission *daemon.OpenVPNChallengeSubmission) (bool, error) {
|
||||
_, err := client.SubmitOpenVPNChallengeResponse(ctx, submission)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
outcome, message := classifySubmitError(err)
|
||||
switch outcome {
|
||||
case submitStale:
|
||||
return false, errAuthChallengeWithdrawn
|
||||
case submitFatal:
|
||||
return false, err
|
||||
}
|
||||
writeAuthError("openvpn", "submit rejected: "+message)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func openOpenVPNChallengeURL(prompter *authPrompter, endpointTag string, challenge *daemon.OpenVPNChallenge) error {
|
||||
writeAuthHeader(endpointTag, "authentication")
|
||||
if challenge.GetPreviousError() != "" {
|
||||
writeAuthLine("previous attempt failed: " + challenge.GetPreviousError())
|
||||
}
|
||||
writeAuthLine("Complete authentication in your browser:")
|
||||
writeAuthLine("")
|
||||
writeAuthLine(" " + challenge.GetUrl())
|
||||
writeAuthLine("")
|
||||
if authInputIsTerminal {
|
||||
confirmed, err := prompter.promptConfirm("Open it now? [Y/n] ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if confirmed {
|
||||
openErr := openURLInBrowser(challenge.GetUrl())
|
||||
if openErr != nil {
|
||||
writeAuthLine("failed to open the default browser: " + openErr.Error())
|
||||
} else {
|
||||
writeAuthLine("opened in the default browser; waiting for the server" + openVPNRemainingSuffix(challenge))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
writeAuthLine("waiting for the server" + openVPNRemainingSuffix(challenge))
|
||||
return nil
|
||||
}
|
||||
|
||||
func openVPNRemainingSuffix(challenge *daemon.OpenVPNChallenge) string {
|
||||
if challenge.GetDeadline() == 0 {
|
||||
return ""
|
||||
}
|
||||
return " (" + formatAuthDeadline(challenge.GetDeadline()) + " remaining)"
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandAPIOpenVPNCancelFlagEndpoint string
|
||||
|
||||
var commandAPIOpenVPNCancel = &cobra.Command{
|
||||
Use: "cancel",
|
||||
Short: "Cancel the pending OpenVPN challenge and stop the client",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runAPIOpenVPNCancel()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandAPIOpenVPNCancel.Flags().StringVar(&commandAPIOpenVPNCancelFlagEndpoint, "endpoint", "", "OpenVPN endpoint tag (default: the only configured endpoint)")
|
||||
commandAPIOpenVPN.AddCommand(commandAPIOpenVPNCancel)
|
||||
}
|
||||
|
||||
func runAPIOpenVPNCancel() error {
|
||||
clientConn, client, err := createAPIClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clientConn.Close()
|
||||
ctx, cancel := context.WithCancel(globalCtx)
|
||||
defer cancel()
|
||||
_, endpoints, err := subscribeOpenVPNStatus(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointStatus, err := resolveVPNEndpoint(endpoints, commandAPIOpenVPNCancelFlagEndpoint, "openvpn")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointTag := endpointStatus.GetEndpointTag()
|
||||
challenge := endpointStatus.GetChallenge()
|
||||
if challenge == nil {
|
||||
return E.New("no pending authentication challenge on ", endpointTag)
|
||||
}
|
||||
_, err = client.CancelOpenVPNChallenge(ctx, &daemon.OpenVPNChallengeCancel{
|
||||
EndpointTag: endpointTag,
|
||||
ChallengeID: challenge.GetId(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// sing-openvpn treats a canceled challenge as terminal: unlike OpenConnect, the client does not
|
||||
// reconnect afterwards.
|
||||
os.Stdout.WriteString(endpointTag + ": authentication challenge canceled; the client has stopped\n")
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user