mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
Compare commits
292 Commits
v1.11.0-be
...
dev-next
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0e5f1814cc | ||
![]() |
d8e1cd0d51 | ||
![]() |
0a5f09f147 | ||
![]() |
e7ae3ddf31 | ||
![]() |
8b15576289 | ||
![]() |
c86f47bdf8 | ||
![]() |
ce013ceabb | ||
![]() |
d0824ea7f6 | ||
![]() |
7291d7e802 | ||
![]() |
3f38520f5e | ||
![]() |
de7e0364e8 | ||
![]() |
867c267bef | ||
![]() |
2dfe93c99c | ||
![]() |
656301bb9e | ||
![]() |
301549f453 | ||
![]() |
cd7229f39b | ||
![]() |
f0e42f8b8f | ||
![]() |
65a629cbb5 | ||
![]() |
7774f16552 | ||
![]() |
2ded81f1f7 | ||
![]() |
e68036ea62 | ||
![]() |
f5dd883fb5 | ||
![]() |
aa2a4ea2bb | ||
![]() |
aeb340170d | ||
![]() |
009b224de6 | ||
![]() |
f6898a7806 | ||
![]() |
44ada21eb2 | ||
![]() |
d276db1ccc | ||
![]() |
1016ba6cef | ||
![]() |
ecfd010ac9 | ||
![]() |
3c07a9dbf5 | ||
![]() |
115eb9e872 | ||
![]() |
26f0c5dd93 | ||
![]() |
0d9883b2c8 | ||
![]() |
b026e78f79 | ||
![]() |
2e20110f02 | ||
![]() |
9fc424498d | ||
![]() |
886ea0c893 | ||
![]() |
fbd79337e5 | ||
![]() |
de31c316a0 | ||
![]() |
ebaaee7b75 | ||
![]() |
1f35f25ada | ||
![]() |
5a1d726242 | ||
![]() |
2f3ebe6f68 | ||
![]() |
c2e962f9da | ||
![]() |
d0052e3803 | ||
![]() |
f2cb235305 | ||
![]() |
d6bd251e2d | ||
![]() |
1f07157ab5 | ||
![]() |
10afe58231 | ||
![]() |
23a99c2af1 | ||
![]() |
8337505d0d | ||
![]() |
9bd951264f | ||
![]() |
7ef938ed05 | ||
![]() |
45d20963c4 | ||
![]() |
e53428481f | ||
![]() |
9d3c47b612 | ||
![]() |
029783522c | ||
![]() |
b7f55e931e | ||
![]() |
e3405c656e | ||
![]() |
264c67f755 | ||
![]() |
b5f5e94fdb | ||
![]() |
3b53ca6e94 | ||
![]() |
6cb3030457 | ||
![]() |
47a717c6c1 | ||
![]() |
2167079ac6 | ||
![]() |
cf77712dc1 | ||
![]() |
883e9dac43 | ||
![]() |
d1ebc72b9d | ||
![]() |
27efb76ac0 | ||
![]() |
ada4633f49 | ||
![]() |
f4b6f828d6 | ||
![]() |
0ddef18bce | ||
![]() |
2e196ebba6 | ||
![]() |
15e41980f3 | ||
![]() |
6e8eff3296 | ||
![]() |
243a40ae1a | ||
![]() |
0d300cd5f8 | ||
![]() |
4879dd6992 | ||
![]() |
ad2ff84166 | ||
![]() |
753f036748 | ||
![]() |
27fa1a730d | ||
![]() |
afdf750da4 | ||
![]() |
75e0ce993f | ||
![]() |
88b4a8ae8e | ||
![]() |
92c639040c | ||
![]() |
f329e859dd | ||
![]() |
4918580820 | ||
![]() |
9dbd482971 | ||
![]() |
7cc6e1c0ae | ||
![]() |
2a03668d15 | ||
![]() |
095721e0e5 | ||
![]() |
7d0d59c9ee | ||
![]() |
18e9eaa244 | ||
![]() |
3439b75e41 | ||
![]() |
7b095b7719 | ||
![]() |
5dd1a09663 | ||
![]() |
750a9be4a9 | ||
![]() |
d21d724345 | ||
![]() |
74f1c3aa09 | ||
![]() |
aef0d477d2 | ||
![]() |
24cf4e9062 | ||
![]() |
dd89730ee7 | ||
![]() |
912bd00f1c | ||
![]() |
3ab6e6c0b0 | ||
![]() |
81ff3eef20 | ||
![]() |
d511698f3f | ||
![]() |
cb435ea232 | ||
![]() |
43a9016c83 | ||
![]() |
255068fd40 | ||
![]() |
098a00b025 | ||
![]() |
dba0b5276b | ||
![]() |
78ae935468 | ||
![]() |
3ea5f76470 | ||
![]() |
b4d294c05e | ||
![]() |
83cf5f5c6a | ||
![]() |
e7b3a8eebe | ||
![]() |
ee3a42a67e | ||
![]() |
50227c0f5f | ||
![]() |
bc5eb1e1a5 | ||
![]() |
995267a042 | ||
![]() |
41226a6075 | ||
![]() |
81d32181ce | ||
![]() |
c5ecca3938 | ||
![]() |
900888731c | ||
![]() |
13e648e4b1 | ||
![]() |
aff12ff671 | ||
![]() |
101fb88255 | ||
![]() |
8b489354e4 | ||
![]() |
7dea6eb7a6 | ||
![]() |
af1bfe4e3e | ||
![]() |
d574e9eb52 | ||
![]() |
2d7df1e1f2 | ||
![]() |
1c0ffcf5b1 | ||
![]() |
348cc39975 | ||
![]() |
987899f94a | ||
![]() |
d8b2d5142f | ||
![]() |
134802d1ee | ||
![]() |
e5e81b4de1 | ||
![]() |
300c961efa | ||
![]() |
7c7f512405 | ||
![]() |
03e8d029c2 | ||
![]() |
787b5f1931 | ||
![]() |
56a7624618 | ||
![]() |
3a84acf122 | ||
![]() |
f600e02e47 | ||
![]() |
e6d19de58a | ||
![]() |
f2bbf6b2aa | ||
![]() |
c54d50fd36 | ||
![]() |
6a051054db | ||
![]() |
49498f6439 | ||
![]() |
144a890c71 | ||
![]() |
afb4993445 | ||
![]() |
4c9455b944 | ||
![]() |
5fdc051a08 | ||
![]() |
cb68a40c43 | ||
![]() |
023218e6e7 | ||
![]() |
2a24b94b8d | ||
![]() |
c6531cf184 | ||
![]() |
d4fa0ed349 | ||
![]() |
10874d2dc4 | ||
![]() |
5adaf1ac75 | ||
![]() |
9668ea69b8 | ||
![]() |
ae9bc7acf1 | ||
![]() |
594ee480a2 | ||
![]() |
a15b5a2463 | ||
![]() |
991e755789 | ||
![]() |
97d41ffde8 | ||
![]() |
24af0766ac | ||
![]() |
af17eaa537 | ||
![]() |
3adc10a797 | ||
![]() |
5eeef6b28e | ||
![]() |
f4c29840c3 | ||
![]() |
47fc3ebda4 | ||
![]() |
9774a659b0 | ||
![]() |
2e4a6de4e7 | ||
![]() |
a530e424e9 | ||
![]() |
0bfd487ee9 | ||
![]() |
6aae834493 | ||
![]() |
f56131f38e | ||
![]() |
273a11d550 | ||
![]() |
ae8ce75e41 | ||
![]() |
d6d94b689f | ||
![]() |
30d785f1ee | ||
![]() |
db5ec3cdfc | ||
![]() |
9aca54d039 | ||
![]() |
d55d5009c2 | ||
![]() |
4f3ee61104 | ||
![]() |
96eb98c00a | ||
![]() |
68ce9577c6 | ||
![]() |
3ae036e997 | ||
![]() |
5da2d1d470 | ||
![]() |
8e2baf40f1 | ||
![]() |
c24c40dfee | ||
![]() |
32e52ce1ed | ||
![]() |
ed46438359 | ||
![]() |
0b5490d5a3 | ||
![]() |
2d73ef511d | ||
![]() |
63e6c85f6f | ||
![]() |
8946a6d2d0 | ||
![]() |
d3132645fb | ||
![]() |
373f158fe0 | ||
![]() |
ce36835fab | ||
![]() |
619fa671d7 | ||
![]() |
eb07c7a79e | ||
![]() |
7eb3535094 | ||
![]() |
93b68312cf | ||
![]() |
97ce666e43 | ||
![]() |
4000e1e66d | ||
![]() |
270740e859 | ||
![]() |
6cad142cfe | ||
![]() |
093013687c | ||
![]() |
ff31c469a0 | ||
![]() |
fbe390268c | ||
![]() |
07ac01dcb7 | ||
![]() |
badfdb62cd | ||
![]() |
986a410b30 | ||
![]() |
9db2d58545 | ||
![]() |
4eed46ac59 | ||
![]() |
abc38d1dab | ||
![]() |
8d6c4f1289 | ||
![]() |
a2d40eb8b8 | ||
![]() |
17b502bb4b | ||
![]() |
a0d4421085 | ||
![]() |
0d443072d1 | ||
![]() |
c9fb99b799 | ||
![]() |
92d245ad04 | ||
![]() |
0908627297 | ||
![]() |
7f79458b4f | ||
![]() |
9b4c11ba95 | ||
![]() |
27c31eac5d | ||
![]() |
bab8dc0b82 | ||
![]() |
d09d2fb665 | ||
![]() |
e64cf3b7df | ||
![]() |
9b73222314 | ||
![]() |
3923b57abf | ||
![]() |
4807e64609 | ||
![]() |
eeb37d89f1 | ||
![]() |
08c1ec4b7e | ||
![]() |
6b4cf67add | ||
![]() |
e65926fd08 | ||
![]() |
f2ec319fe1 | ||
![]() |
32377a61b7 | ||
![]() |
7aac801ccd | ||
![]() |
96fdf59ee4 | ||
![]() |
50b8f3ab94 | ||
![]() |
ff7aaf977b | ||
![]() |
9a1efbe54d | ||
![]() |
906c21f458 | ||
![]() |
d5e7af7a7e | ||
![]() |
4d41f03bd5 | ||
![]() |
30704a15a7 | ||
![]() |
83889178ed | ||
![]() |
1d2720bf5e | ||
![]() |
c4b6d0eadb | ||
![]() |
0c66888691 | ||
![]() |
68781387fe | ||
![]() |
fd299a0961 | ||
![]() |
285a82050c | ||
![]() |
2dbb8c55c9 | ||
![]() |
effcf39469 | ||
![]() |
9db9484863 | ||
![]() |
ca813f461b | ||
![]() |
bb46cdb2b3 | ||
![]() |
dcb10c21a1 | ||
![]() |
05ea0ca00e | ||
![]() |
c098f282b1 | ||
![]() |
ecf82d197c | ||
![]() |
9afe75586a | ||
![]() |
a1be455202 | ||
![]() |
19fb214226 | ||
![]() |
28ec898a8c | ||
![]() |
467b1bbeeb | ||
![]() |
02ab8ce806 | ||
![]() |
ce69e620e9 | ||
![]() |
1133cf3ef5 | ||
![]() |
59a607e303 | ||
![]() |
313be3d7a4 | ||
![]() |
4fe40fcee0 | ||
![]() |
e233fd4fe5 | ||
![]() |
9f7683818f | ||
![]() |
179e3cb2f5 | ||
![]() |
41b960552d | ||
![]() |
8304295c48 | ||
![]() |
253b41936e | ||
![]() |
ce5b4b06b5 | ||
![]() |
50f5006c43 | ||
![]() |
e42ff22c2e | ||
![]() |
578571b972 | ||
![]() |
935beca45d | ||
![]() |
3e246f1173 | ||
![]() |
1bc27a32c2 |
30
.fpm_openwrt
Normal file
30
.fpm_openwrt
Normal file
@ -0,0 +1,30 @@
|
||||
-s dir
|
||||
--name sing-box
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--no-deb-generate-changes
|
||||
|
||||
--config-files /etc/config/sing-box
|
||||
--config-files /etc/sing-box/config.json
|
||||
|
||||
--depends ca-bundle
|
||||
--depends kmod-inet-diag
|
||||
--depends kmod-tun
|
||||
--depends firewall4
|
||||
|
||||
--before-remove release/config/openwrt.prerm
|
||||
|
||||
release/config/config.json=/etc/sing-box/config.json
|
||||
|
||||
release/config/openwrt.conf=/etc/config/sing-box
|
||||
release/config/openwrt.init=/etc/init.d/sing-box
|
||||
release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box
|
||||
|
||||
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
||||
|
||||
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
25
.fpm_systemd
Normal file
25
.fpm_systemd
Normal file
@ -0,0 +1,25 @@
|
||||
-s dir
|
||||
--name sing-box
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||
--no-deb-generate-changes
|
||||
--config-files /etc/sing-box/config.json
|
||||
--after-install release/config/sing-box.postinst
|
||||
|
||||
release/config/config.json=/etc/sing-box/config.json
|
||||
|
||||
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
|
||||
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
|
||||
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
|
||||
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
|
||||
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||
|
||||
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
||||
|
||||
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
28
.github/deb2ipk.sh
vendored
Executable file
28
.github/deb2ipk.sh
vendored
Executable file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
PROJECT=$(dirname "$0")/../..
|
||||
TMP_PATH=`mktemp -d`
|
||||
cp $2 $TMP_PATH
|
||||
pushd $TMP_PATH
|
||||
|
||||
DEB_NAME=`ls *.deb`
|
||||
ar x $DEB_NAME
|
||||
|
||||
mkdir control
|
||||
pushd control
|
||||
tar xf ../control.tar.gz
|
||||
rm md5sums
|
||||
sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i
|
||||
cat control
|
||||
tar czf ../control.tar.gz ./*
|
||||
popd
|
||||
|
||||
DEB_NAME=${DEB_NAME%.deb}
|
||||
tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary
|
||||
popd
|
||||
|
||||
cp $TMP_PATH/$DEB_NAME.ipk $3
|
||||
rm -r $TMP_PATH
|
25
.github/setup_legacy_go.sh
vendored
Executable file
25
.github/setup_legacy_go.sh
vendored
Executable file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION="1.23.6"
|
||||
|
||||
mkdir -p $HOME/go
|
||||
cd $HOME/go
|
||||
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
|
||||
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
|
||||
mv go go_legacy
|
||||
cd go_legacy
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# this patch file only works on golang1.23.x
|
||||
# that means after golang1.24 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
|
||||
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
|
340
.github/workflows/build.yml
vendored
340
.github/workflows/build.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
go-version: ^1.24.4
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@ -55,7 +55,7 @@ jobs:
|
||||
- name: Calculate version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/read_tag --nightly
|
||||
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||
- name: Set outputs
|
||||
id: outputs
|
||||
run: |-
|
||||
@ -69,141 +69,216 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: linux_386
|
||||
goos: linux
|
||||
goarch: 386
|
||||
- name: linux_amd64
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- name: linux_arm64
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
- name: linux_arm
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
- name: linux_arm_v7
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
- name: linux_s390x
|
||||
goos: linux
|
||||
goarch: s390x
|
||||
- name: linux_riscv64
|
||||
goos: linux
|
||||
goarch: riscv64
|
||||
- name: linux_mips64le
|
||||
goos: linux
|
||||
goarch: mips64le
|
||||
- name: windows_amd64
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
require_legacy_go: true
|
||||
- name: windows_386
|
||||
goos: windows
|
||||
goarch: 386
|
||||
require_legacy_go: true
|
||||
- name: windows_arm64
|
||||
goos: windows
|
||||
goarch: arm64
|
||||
- name: darwin_arm64
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
- name: darwin_amd64
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
require_legacy_go: true
|
||||
- name: android_arm64
|
||||
goos: android
|
||||
goarch: arm64
|
||||
- name: android_arm
|
||||
goos: android
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
- name: android_amd64
|
||||
goos: android
|
||||
goarch: amd64
|
||||
- name: android_386
|
||||
goos: android
|
||||
goarch: 386
|
||||
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
|
||||
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
|
||||
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
|
||||
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
|
||||
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
|
||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
|
||||
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
|
||||
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
|
||||
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
|
||||
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
|
||||
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
|
||||
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
|
||||
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
|
||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
|
||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
||||
|
||||
- { os: windows, arch: amd64 }
|
||||
- { os: windows, arch: amd64, legacy_go: true }
|
||||
- { os: windows, arch: "386" }
|
||||
- { os: windows, arch: "386", legacy_go: true }
|
||||
- { os: windows, arch: arm64 }
|
||||
|
||||
- { os: darwin, arch: amd64 }
|
||||
- { os: darwin, arch: arm64 }
|
||||
|
||||
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
|
||||
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
|
||||
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
|
||||
- { os: android, arch: "386", ndk: "i686-linux-android21" }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
if: ${{ ! matrix.legacy_go }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
- name: Cache legacy Go
|
||||
go-version: ^1.24.4
|
||||
- name: Cache Legacy Go
|
||||
if: matrix.require_legacy_go
|
||||
id: cache-legacy-go
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/go/go1.20.14
|
||||
key: go120
|
||||
- name: Setup legacy Go
|
||||
if: matrix.require_legacy_go == 'true' && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
||||
~/go/go_legacy
|
||||
key: go_legacy_1236
|
||||
- name: Setup Legacy Go
|
||||
if: matrix.legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
||||
run: |-
|
||||
wget https://dl.google.com/go/go1.20.14.linux-amd64.tar.gz
|
||||
tar -xzf go1.20.14.linux-amd64.tar.gz
|
||||
mv go $HOME/go/go1.20.14
|
||||
.github/setup_legacy_go.sh
|
||||
- name: Setup Legacy Go 2
|
||||
if: matrix.legacy_go
|
||||
run: |-
|
||||
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
|
||||
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
|
||||
- name: Setup Android NDK
|
||||
if: matrix.goos == 'android'
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28-beta2
|
||||
ndk-version: r28
|
||||
local-cache: true
|
||||
- name: Setup Goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
install-only: true
|
||||
- name: Extract signing key
|
||||
run: |-
|
||||
mkdir -p $HOME/.gnupg
|
||||
cat > $HOME/.gnupg/sagernet.key <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
if: matrix.goos != 'android'
|
||||
run: |-
|
||||
goreleaser release --clean --split
|
||||
if: matrix.os != 'android'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOPATH: ${{ env.HOME }}/go
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GO386: ${{ matrix.go386 }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
GOMIPS64: ${{ matrix.gomips }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
- name: Build Android
|
||||
if: matrix.goos == 'android'
|
||||
run: |-
|
||||
if: matrix.os == 'android'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
go install -v ./cmd/internal/build
|
||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build goreleaser release --clean --split
|
||||
export CC='${{ matrix.ndk }}-clang'
|
||||
export CXX="${CC}++"
|
||||
mkdir -p dist
|
||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
BUILD_GOOS: ${{ matrix.goos }}
|
||||
BUILD_GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
CGO_ENABLED: "1"
|
||||
BUILD_GOOS: ${{ matrix.os }}
|
||||
BUILD_GOARCH: ${{ matrix.arch }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
- name: Set name
|
||||
run: |-
|
||||
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
if [[ -n "${{ matrix.goarm }}" ]]; then
|
||||
DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}"
|
||||
elif [[ -n "${{ matrix.go386 }}" && "${{ matrix.go386 }}" != 'sse2' ]]; then
|
||||
DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}"
|
||||
elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then
|
||||
DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}"
|
||||
elif [[ "${{ matrix.legacy_go }}" == 'true' ]]; then
|
||||
DIR_NAME="${DIR_NAME}-legacy"
|
||||
fi
|
||||
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||
- name: Package DEB
|
||||
if: matrix.debian != ''
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y debsigs
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t deb \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \
|
||||
--architecture ${{ matrix.debian }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||
rm -rf $HOME/.gnupg
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||
- name: Package RPM
|
||||
if: matrix.rpm != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t rpm \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.rpm }}.rpm" \
|
||||
--architecture ${{ matrix.rpm }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
cat > $HOME/.rpmmacros <<EOF
|
||||
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||
EOF
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
rpmsign --addsign dist/*.rpm
|
||||
- name: Package Pacman
|
||||
if: matrix.pacman != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libarchive-tools
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t pacman \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
||||
--architecture ${{ matrix.pacman }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
- name: Package OpenWrt
|
||||
if: matrix.openwrt != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .fpm_openwrt .fpm
|
||||
fpm -t deb \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/openwrt.deb" \
|
||||
--architecture all \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
for architecture in ${{ matrix.openwrt }}; do
|
||||
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
|
||||
done
|
||||
rm "dist/openwrt.deb"
|
||||
- name: Archive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
cd dist
|
||||
mkdir -p "${DIR_NAME}"
|
||||
cp ../LICENSE "${DIR_NAME}"
|
||||
if [ '${{ matrix.os }}' = 'windows' ]; then
|
||||
cp sing-box "${DIR_NAME}/sing-box.exe"
|
||||
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
|
||||
else
|
||||
cp sing-box "${DIR_NAME}"
|
||||
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
|
||||
fi
|
||||
rm -r "${DIR_NAME}"
|
||||
- name: Cleanup
|
||||
run: rm dist/sing-box
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-${{ matrix.name }}
|
||||
path: 'dist'
|
||||
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||
path: "dist"
|
||||
build_android:
|
||||
name: Build Android
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
||||
@ -219,12 +294,12 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
go-version: ^1.24.4
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28-beta2
|
||||
ndk-version: r28
|
||||
- name: Setup OpenJDK
|
||||
run: |-
|
||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||
@ -256,9 +331,16 @@ jobs:
|
||||
with:
|
||||
path: ~/.gradle
|
||||
key: gradle-${{ hashFiles('**/*.gradle') }}
|
||||
- name: Build
|
||||
- name: Update version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_android_version --ci
|
||||
- name: Update nightly version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_android_version --ci --nightly
|
||||
- name: Build
|
||||
run: |-
|
||||
mkdir clients/android/app/libs
|
||||
cp libbox.aar clients/android/app/libs
|
||||
cd clients/android
|
||||
@ -268,13 +350,11 @@ jobs:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||
- name: Prepare upload
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
mkdir -p dist/release
|
||||
cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release
|
||||
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release
|
||||
mkdir -p dist
|
||||
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
|
||||
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-android-apks
|
||||
@ -294,12 +374,12 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
go-version: ^1.24.4
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28-beta2
|
||||
ndk-version: r28
|
||||
- name: Setup OpenJDK
|
||||
run: |-
|
||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||
@ -392,7 +472,7 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
go-version: ^1.24.4
|
||||
- name: Setup Xcode stable
|
||||
if: matrix.if && github.ref == 'refs/heads/main-next'
|
||||
run: |-
|
||||
@ -469,10 +549,13 @@ jobs:
|
||||
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
|
||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
|
||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
|
||||
- name: Update version
|
||||
if: matrix.if && matrix.name != 'iOS'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_apple_version --ci
|
||||
- name: Build
|
||||
if: matrix.if
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_apple_version --ci
|
||||
cd clients/apple
|
||||
xcodebuild archive \
|
||||
-scheme "${{ matrix.scheme }}" \
|
||||
@ -521,9 +604,9 @@ jobs:
|
||||
zip -r SFM.dSYMs.zip dSYMs
|
||||
popd
|
||||
|
||||
mkdir -p dist/release
|
||||
cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg"
|
||||
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip"
|
||||
mkdir -p dist
|
||||
cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg"
|
||||
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip"
|
||||
- name: Upload image
|
||||
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v4
|
||||
@ -544,12 +627,6 @@ jobs:
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
install-only: true
|
||||
- name: Cache ghr
|
||||
uses: actions/cache@v4
|
||||
id: cache-ghr
|
||||
@ -574,26 +651,17 @@ jobs:
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Merge builds
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
||||
run: |-
|
||||
goreleaser continue --merge --skip publish
|
||||
mkdir -p dist/release
|
||||
mv dist/*/sing-box*{tar.gz,zip,deb,rpm,_amd64.pkg.tar.zst,_arm64.pkg.tar.zst} dist/release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
- name: Upload builds
|
||||
if: ${{ env.PUBLISHED == 'false' }}
|
||||
run: |-
|
||||
export PATH="$PATH:$HOME/go/bin"
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Replace builds
|
||||
if: ${{ env.PUBLISHED != 'false' }}
|
||||
run: |-
|
||||
export PATH="$PATH:$HOME/go/bin"
|
||||
ghr --replace -p 5 "v${VERSION}" dist/release
|
||||
ghr --replace -p 5 "v${VERSION}" dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
3
.github/workflows/lint.yml
vendored
3
.github/workflows/lint.yml
vendored
@ -28,10 +28,11 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
go-version: ^1.24.4
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=30m
|
||||
install-mode: binary
|
||||
verify: false
|
||||
|
182
.github/workflows/linux.yml
vendored
182
.github/workflows/linux.yml
vendored
@ -1,13 +1,22 @@
|
||||
name: Release to Linux repository
|
||||
name: Build Linux Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version name"
|
||||
required: true
|
||||
type: string
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
build:
|
||||
calculate_version:
|
||||
name: Calculate version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.outputs.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
@ -16,23 +25,160 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
- name: Extract signing key
|
||||
go-version: ^1.24.4
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
mkdir -p $HOME/.gnupg
|
||||
cat > $HOME/.gnupg/sagernet.key <<EOF
|
||||
echo "version=${{ inputs.version }}"
|
||||
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Calculate version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||
- name: Set outputs
|
||||
id: outputs
|
||||
run: |-
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
build:
|
||||
name: Build binary
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
|
||||
- { os: linux, arch: "386", debian: i386, rpm: i386 }
|
||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
||||
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
||||
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
|
||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
|
||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24.4
|
||||
- name: Setup Android NDK
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28
|
||||
local-cache: true
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set mtime
|
||||
run: |-
|
||||
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||
- name: Set name
|
||||
if: ${{ ! contains(needs.calculate_version.outputs.version, '-') }}
|
||||
run: |-
|
||||
echo "NAME=sing-box" >> "$GITHUB_ENV"
|
||||
- name: Set beta name
|
||||
if: contains(needs.calculate_version.outputs.version, '-')
|
||||
run: |-
|
||||
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||
- name: Set version
|
||||
run: |-
|
||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||
- name: Package DEB
|
||||
if: matrix.debian != ''
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get install -y debsigs
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t deb \
|
||||
--name "${NAME}" \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
||||
--architecture ${{ matrix.debian }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||
rm -rf $HOME/.gnupg
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
||||
- name: Publish release
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||
- name: Package RPM
|
||||
if: matrix.rpm != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t rpm \
|
||||
--name "${NAME}" \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
||||
--architecture ${{ matrix.rpm }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
cat > $HOME/.rpmmacros <<EOF
|
||||
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||
EOF
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
rpmsign --addsign dist/*.rpm
|
||||
- name: Cleanup
|
||||
run: rm dist/sing-box
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release -f .goreleaser.fury.yaml --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
|
||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||
path: "dist"
|
||||
upload:
|
||||
name: Upload builds
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
- build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Download builds
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Publish packages
|
||||
run: |-
|
||||
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/
|
||||
|
@ -22,6 +22,14 @@ linters-settings:
|
||||
|
||||
run:
|
||||
go: "1.23"
|
||||
build-tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
|
@ -6,17 +6,18 @@ builds:
|
||||
- -v
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||
- -s
|
||||
- -buildid=
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_ech
|
||||
- with_utls
|
||||
- with_reality_server
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
targets:
|
||||
@ -48,12 +49,18 @@ nfpms:
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: config
|
||||
type: "config|noreplace"
|
||||
|
||||
- src: release/config/sing-box.service
|
||||
dst: /usr/lib/systemd/system/sing-box.service
|
||||
- src: release/config/sing-box@.service
|
||||
dst: /usr/lib/systemd/system/sing-box@.service
|
||||
- src: release/config/sing-box.sysusers
|
||||
dst: /usr/lib/sysusers.d/sing-box.conf
|
||||
- src: release/config/sing-box.rules
|
||||
dst: /usr/share/polkit-1/rules.d/sing-box.rules
|
||||
- src: release/config/sing-box-split-dns.xml
|
||||
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||
|
||||
- src: release/completions/sing-box.bash
|
||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||
|
@ -16,13 +16,13 @@ builds:
|
||||
- with_quic
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_ech
|
||||
- with_utls
|
||||
- with_reality_server
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
targets:
|
||||
- linux_386
|
||||
- linux_amd64_v1
|
||||
@ -46,21 +46,21 @@ builds:
|
||||
- with_dhcp
|
||||
- with_wireguard
|
||||
- with_utls
|
||||
- with_reality_server
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOROOT={{ .Env.GOPATH }}/go1.20.14
|
||||
gobinary: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
|
||||
- GOROOT={{ .Env.GOPATH }}/go_legacy
|
||||
tool: "{{ .Env.GOPATH }}/go_legacy/bin/go"
|
||||
targets:
|
||||
- windows_amd64_v1
|
||||
- windows_386
|
||||
- darwin_amd64_v1
|
||||
- id: android
|
||||
<<: *template
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- GOTOOLCHAIN=local
|
||||
overrides:
|
||||
- goos: android
|
||||
goarch: arm
|
||||
@ -95,10 +95,12 @@ archives:
|
||||
builds:
|
||||
- main
|
||||
- android
|
||||
format: tar.gz
|
||||
formats:
|
||||
- tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
formats:
|
||||
- zip
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
@ -128,12 +130,18 @@ nfpms:
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: config
|
||||
type: "config|noreplace"
|
||||
|
||||
- src: release/config/sing-box.service
|
||||
dst: /usr/lib/systemd/system/sing-box.service
|
||||
- src: release/config/sing-box@.service
|
||||
dst: /usr/lib/systemd/system/sing-box@.service
|
||||
- src: release/config/sing-box.sysusers
|
||||
dst: /usr/lib/sysusers.d/sing-box.conf
|
||||
- src: release/config/sing-box.rules
|
||||
dst: /usr/share/polkit-1/rules.d/sing-box.rules
|
||||
- src: release/config/sing-box-split-dns.xml
|
||||
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||
|
||||
- src: release/completions/sing-box.bash
|
||||
dst: /usr/share/bash-completion/completions/sing-box.bash
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
COPY . /go/src/github.com/sagernet/sing-box
|
||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||
@ -13,7 +13,7 @@ RUN set -ex \
|
||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||
&& go build -v -trimpath -tags \
|
||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
|
||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale" \
|
||||
-o /go/bin/sing-box \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
|
26
Makefile
26
Makefile
@ -1,34 +1,29 @@
|
||||
NAME = sing-box
|
||||
COMMIT = $(shell git rev-parse --short HEAD)
|
||||
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
|
||||
TAGS_GO121 = with_ech
|
||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120),$(TAGS_GO121)
|
||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server
|
||||
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
|
||||
|
||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
|
||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
|
||||
|
||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
|
||||
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
|
||||
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
||||
MAIN = ./cmd/sing-box
|
||||
PREFIX ?= $(shell go env GOPATH)
|
||||
|
||||
.PHONY: test release docs build
|
||||
|
||||
build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
ci_build_go120:
|
||||
go build $(PARAMS) $(MAIN)
|
||||
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
|
||||
|
||||
ci_build:
|
||||
go build $(PARAMS) $(MAIN)
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(PARAMS) $(MAIN) && \
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
generate_completions:
|
||||
go run -v --tags generate,generate_completions $(MAIN)
|
||||
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
|
||||
|
||||
install:
|
||||
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
||||
@ -61,6 +56,9 @@ proto_install:
|
||||
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
|
||||
update_certificates:
|
||||
go run ./cmd/internal/update_certificates
|
||||
|
||||
release:
|
||||
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
||||
mkdir dist/release
|
||||
@ -227,8 +225,8 @@ lib:
|
||||
go run ./cmd/internal/build_libbox -target ios
|
||||
|
||||
lib_install:
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.4
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.4
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.6
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.6
|
||||
|
||||
docs:
|
||||
venv/bin/mkdocs serve
|
||||
|
21
adapter/certificate.go
Normal file
21
adapter/certificate.go
Normal file
@ -0,0 +1,21 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type CertificateStore interface {
|
||||
LifecycleService
|
||||
Pool() *x509.CertPool
|
||||
}
|
||||
|
||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||
store := service.FromContext[CertificateStore](ctx)
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return store.Pool()
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ConnectionRouter interface {
|
||||
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
func NewRouteHandler(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeHandlerWrapper{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func NewRouteContextHandler(
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeContextHandlerWrapper{
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
|
||||
|
||||
type routeHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RouteConnection(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RoutePacketConnection(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
|
||||
|
||||
type routeContextHandlerWrapper struct {
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RouteConnection(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
14
adapter/connections.go
Normal file
14
adapter/connections.go
Normal file
@ -0,0 +1,14 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ConnectionManager interface {
|
||||
Lifecycle
|
||||
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
94
adapter/dns.go
Normal file
94
adapter/dns.go
Normal file
@ -0,0 +1,94 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type DNSRouter interface {
|
||||
Lifecycle
|
||||
Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, domain string, options DNSQueryOptions) ([]netip.Addr, error)
|
||||
ClearCache()
|
||||
LookupReverseMapping(ip netip.Addr) (string, bool)
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
type DNSClient interface {
|
||||
Start()
|
||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
|
||||
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
|
||||
ClearCache()
|
||||
}
|
||||
|
||||
type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
LookupStrategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
}
|
||||
|
||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||
if options == nil {
|
||||
return &DNSQueryOptions{}, nil
|
||||
}
|
||||
transportManager := service.FromContext[DNSTransportManager](ctx)
|
||||
transport, loaded := transportManager.Transport(options.Server)
|
||||
if !loaded {
|
||||
return nil, E.New("domain resolver not found: " + options.Server)
|
||||
}
|
||||
return &DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: C.DomainStrategy(options.Strategy),
|
||||
DisableCache: options.DisableCache,
|
||||
RewriteTTL: options.RewriteTTL,
|
||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type RDRCStore interface {
|
||||
LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
|
||||
SaveRDRC(transportName string, qName string, qType uint16) error
|
||||
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
||||
}
|
||||
|
||||
type DNSTransport interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
Dependencies() []string
|
||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||
}
|
||||
|
||||
type LegacyDNSTransport interface {
|
||||
LegacyStrategy() C.DomainStrategy
|
||||
LegacyClientSubnet() netip.Prefix
|
||||
}
|
||||
|
||||
type DNSTransportRegistry interface {
|
||||
option.DNSTransportOptionsRegistry
|
||||
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
||||
}
|
||||
|
||||
type DNSTransportManager interface {
|
||||
Lifecycle
|
||||
Transports() []DNSTransport
|
||||
Transport(tag string) (DNSTransport, bool)
|
||||
Default() DNSTransport
|
||||
FakeIP() FakeIPTransport
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
|
||||
}
|
28
adapter/endpoint.go
Normal file
28
adapter/endpoint.go
Normal file
@ -0,0 +1,28 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type Endpoint interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
Outbound
|
||||
}
|
||||
|
||||
type EndpointRegistry interface {
|
||||
option.EndpointOptionsRegistry
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) (Endpoint, error)
|
||||
}
|
||||
|
||||
type EndpointManager interface {
|
||||
Lifecycle
|
||||
Endpoints() []Endpoint
|
||||
Get(tag string) (Endpoint, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) error
|
||||
}
|
43
adapter/endpoint/adapter.go
Normal file
43
adapter/endpoint/adapter.go
Normal file
@ -0,0 +1,43 @@
|
||||
package endpoint
|
||||
|
||||
import "github.com/sagernet/sing-box/option"
|
||||
|
||||
type Adapter struct {
|
||||
endpointType string
|
||||
endpointTag string
|
||||
network []string
|
||||
dependencies []string
|
||||
}
|
||||
|
||||
func NewAdapter(endpointType string, endpointTag string, network []string, dependencies []string) Adapter {
|
||||
return Adapter{
|
||||
endpointType: endpointType,
|
||||
endpointTag: endpointTag,
|
||||
network: network,
|
||||
dependencies: dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAdapterWithDialerOptions(endpointType string, endpointTag string, network []string, dialOptions option.DialerOptions) Adapter {
|
||||
var dependencies []string
|
||||
if dialOptions.Detour != "" {
|
||||
dependencies = []string{dialOptions.Detour}
|
||||
}
|
||||
return NewAdapter(endpointType, endpointTag, network, dependencies)
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.endpointType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.endpointTag
|
||||
}
|
||||
|
||||
func (a *Adapter) Network() []string {
|
||||
return a.network
|
||||
}
|
||||
|
||||
func (a *Adapter) Dependencies() []string {
|
||||
return a.dependencies
|
||||
}
|
147
adapter/endpoint/manager.go
Normal file
147
adapter/endpoint/manager.go
Normal file
@ -0,0 +1,147 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.EndpointManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.EndpointRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
endpoints []adapter.Endpoint
|
||||
endpointByTag map[string]adapter.Endpoint
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpointByTag: make(map[string]adapter.Endpoint),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
if stage == adapter.StartStateStart {
|
||||
// started with outbound manager
|
||||
return nil
|
||||
}
|
||||
for _, endpoint := range m.endpoints {
|
||||
err := adapter.LegacyStart(endpoint, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
endpoints := m.endpoints
|
||||
m.endpoints = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, endpoint := range endpoints {
|
||||
monitor.Start("close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||
err = E.Append(err, endpoint.Close(), func(err error) error {
|
||||
return E.Cause(err, "close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Endpoints() []adapter.Endpoint {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.endpoints
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.Endpoint, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
endpoint, found := m.endpointByTag[tag]
|
||||
return endpoint, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
endpoint, found := m.endpointByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.endpointByTag, tag)
|
||||
index := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
|
||||
return it == endpoint
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid endpoint index")
|
||||
}
|
||||
m.endpoints = append(m.endpoints[:index], m.endpoints[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return endpoint.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
|
||||
endpoint, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(endpoint, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsEndpoint.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close endpoint/", existsEndpoint.Type(), "[", existsEndpoint.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
|
||||
return it == existsEndpoint
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid endpoint index")
|
||||
}
|
||||
m.endpoints = append(m.endpoints[:existsIndex], m.endpoints[existsIndex+1:]...)
|
||||
}
|
||||
m.endpoints = append(m.endpoints, endpoint)
|
||||
m.endpointByTag[tag] = endpoint
|
||||
return nil
|
||||
}
|
72
adapter/endpoint/registry.go
Normal file
72
adapter/endpoint/registry.go
Normal file
@ -0,0 +1,72 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Endpoint, error)
|
||||
|
||||
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(outboundType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Endpoint, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.EndpointRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Endpoint, error)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
access sync.Mutex
|
||||
optionsType map[string]optionsConstructorFunc
|
||||
constructor map[string]constructorFunc
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
optionsType: make(map[string]optionsConstructorFunc),
|
||||
constructor: make(map[string]constructorFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
optionsConstructor, loaded := m.optionsType[outboundType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Endpoint, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
constructor, loaded := m.constructor[outboundType]
|
||||
if !loaded {
|
||||
return nil, E.New("outbound type not found: " + outboundType)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, options)
|
||||
}
|
||||
|
||||
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.optionsType[outboundType] = optionsConstructor
|
||||
m.constructor[outboundType] = constructor
|
||||
}
|
@ -4,34 +4,45 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
"github.com/sagernet/sing-dns"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
)
|
||||
|
||||
type ClashServer interface {
|
||||
Service
|
||||
PreStarter
|
||||
LifecycleService
|
||||
ConnectionTracker
|
||||
Mode() string
|
||||
ModeList() []string
|
||||
HistoryStorage() *urltest.HistoryStorage
|
||||
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
|
||||
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
|
||||
HistoryStorage() URLTestHistoryStorage
|
||||
}
|
||||
|
||||
type URLTestHistory struct {
|
||||
Time time.Time `json:"time"`
|
||||
Delay uint16 `json:"delay"`
|
||||
}
|
||||
|
||||
type URLTestHistoryStorage interface {
|
||||
SetHook(hook chan<- struct{})
|
||||
LoadURLTestHistory(tag string) *URLTestHistory
|
||||
DeleteURLTestHistory(tag string)
|
||||
StoreURLTestHistory(tag string, history *URLTestHistory)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type V2RayServer interface {
|
||||
LifecycleService
|
||||
StatsService() ConnectionTracker
|
||||
}
|
||||
|
||||
type CacheFile interface {
|
||||
Service
|
||||
PreStarter
|
||||
LifecycleService
|
||||
|
||||
StoreFakeIP() bool
|
||||
FakeIPStorage
|
||||
|
||||
StoreRDRC() bool
|
||||
dns.RDRCStore
|
||||
RDRCStore
|
||||
|
||||
LoadMode() string
|
||||
StoreMode(mode string) error
|
||||
@ -39,17 +50,17 @@ type CacheFile interface {
|
||||
StoreSelected(group string, selected string) error
|
||||
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
||||
StoreGroupExpand(group string, expand bool) error
|
||||
LoadRuleSet(tag string) *SavedRuleSet
|
||||
SaveRuleSet(tag string, set *SavedRuleSet) error
|
||||
LoadRuleSet(tag string) *SavedBinary
|
||||
SaveRuleSet(tag string, set *SavedBinary) error
|
||||
}
|
||||
|
||||
type SavedRuleSet struct {
|
||||
type SavedBinary struct {
|
||||
Content []byte
|
||||
LastUpdated time.Time
|
||||
LastEtag string
|
||||
}
|
||||
|
||||
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
||||
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
||||
if err != nil {
|
||||
@ -70,7 +81,7 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
|
||||
func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
||||
reader := bytes.NewReader(data)
|
||||
var version uint8
|
||||
err := binary.Read(reader, binary.BigEndian, &version)
|
||||
@ -94,10 +105,6 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type Tracker interface {
|
||||
Leave()
|
||||
}
|
||||
|
||||
type OutboundGroup interface {
|
||||
Outbound
|
||||
Now() string
|
||||
@ -115,13 +122,3 @@ func OutboundTag(detour Outbound) string {
|
||||
}
|
||||
return detour.Tag()
|
||||
}
|
||||
|
||||
type V2RayServer interface {
|
||||
Service
|
||||
StatsService() V2RayStatsService
|
||||
}
|
||||
|
||||
type V2RayStatsService interface {
|
||||
RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn
|
||||
RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn
|
||||
}
|
||||
|
@ -3,12 +3,11 @@ package adapter
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
type FakeIPStore interface {
|
||||
Service
|
||||
SimpleLifecycle
|
||||
Contains(address netip.Addr) bool
|
||||
Create(domain string, isIPv6 bool) (netip.Addr, error)
|
||||
Lookup(address netip.Addr) (string, bool)
|
||||
@ -27,6 +26,6 @@ type FakeIPStorage interface {
|
||||
}
|
||||
|
||||
type FakeIPTransport interface {
|
||||
dns.Transport
|
||||
DNSTransport
|
||||
Store() FakeIPStore
|
||||
}
|
||||
|
@ -6,27 +6,56 @@ import (
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
// Deprecated
|
||||
type ConnectionHandler interface {
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type ConnectionHandlerEx interface {
|
||||
NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
// Deprecated: use PacketHandlerEx instead
|
||||
type PacketHandler interface {
|
||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type PacketHandlerEx interface {
|
||||
NewPacketEx(buffer *buf.Buffer, source M.Socksaddr)
|
||||
}
|
||||
|
||||
// Deprecated: use OOBPacketHandlerEx instead
|
||||
type OOBPacketHandler interface {
|
||||
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type OOBPacketHandlerEx interface {
|
||||
NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
type PacketConnectionHandler interface {
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type PacketConnectionHandlerEx interface {
|
||||
NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
// Deprecated: use TCPConnectionHandlerEx instead
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type UpstreamHandlerAdapter interface {
|
||||
N.TCPConnectionHandler
|
||||
N.UDPConnectionHandler
|
||||
E.Handler
|
||||
}
|
||||
|
||||
type UpstreamHandlerAdapterEx interface {
|
||||
N.TCPConnectionHandlerEx
|
||||
N.UDPConnectionHandlerEx
|
||||
}
|
||||
|
@ -2,26 +2,43 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/process"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type Inbound interface {
|
||||
Service
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
}
|
||||
|
||||
type InjectableInbound interface {
|
||||
type TCPInjectableInbound interface {
|
||||
Inbound
|
||||
Network() []string
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
ConnectionHandlerEx
|
||||
}
|
||||
|
||||
type UDPInjectableInbound interface {
|
||||
Inbound
|
||||
PacketConnectionHandlerEx
|
||||
}
|
||||
|
||||
type InboundRegistry interface {
|
||||
option.InboundOptionsRegistry
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
|
||||
}
|
||||
|
||||
type InboundManager interface {
|
||||
Lifecycle
|
||||
Inbounds() []Inbound
|
||||
Get(tag string) (Inbound, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error
|
||||
}
|
||||
|
||||
type InboundContext struct {
|
||||
@ -40,13 +57,30 @@ type InboundContext struct {
|
||||
Domain string
|
||||
Client string
|
||||
SniffContext any
|
||||
PacketSniffError error
|
||||
|
||||
// cache
|
||||
|
||||
// Deprecated: implement in rule action
|
||||
InboundDetour string
|
||||
LastInbound string
|
||||
OriginDestination M.Socksaddr
|
||||
RouteOriginalDestination M.Socksaddr
|
||||
// Deprecated: to be removed
|
||||
//nolint:staticcheck
|
||||
InboundOptions option.InboundOptions
|
||||
UDPDisableDomainUnmapping bool
|
||||
UDPConnect bool
|
||||
UDPTimeout time.Duration
|
||||
TLSFragment bool
|
||||
TLSFragmentFallbackDelay time.Duration
|
||||
TLSRecordFragment bool
|
||||
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
FallbackDelay time.Duration
|
||||
|
||||
DestinationAddresses []netip.Addr
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
|
21
adapter/inbound/adapter.go
Normal file
21
adapter/inbound/adapter.go
Normal file
@ -0,0 +1,21 @@
|
||||
package inbound
|
||||
|
||||
type Adapter struct {
|
||||
inboundType string
|
||||
inboundTag string
|
||||
}
|
||||
|
||||
func NewAdapter(inboundType string, inboundTag string) Adapter {
|
||||
return Adapter{
|
||||
inboundType: inboundType,
|
||||
inboundTag: inboundTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.inboundType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.inboundTag
|
||||
}
|
149
adapter/inbound/manager.go
Normal file
149
adapter/inbound/manager.go
Normal file
@ -0,0 +1,149 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.InboundManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.InboundRegistry
|
||||
endpoint adapter.EndpointManager
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
inbounds []adapter.Inbound
|
||||
inboundByTag map[string]adapter.Inbound
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpoint: endpoint,
|
||||
inboundByTag: make(map[string]adapter.Inbound),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
inbounds := m.inbounds
|
||||
m.access.Unlock()
|
||||
for _, inbound := range inbounds {
|
||||
err := adapter.LegacyStart(inbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
inbounds := m.inbounds
|
||||
m.inbounds = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, inbound := range inbounds {
|
||||
monitor.Start("close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||
err = E.Append(err, inbound.Close(), func(err error) error {
|
||||
return E.Cause(err, "close inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Inbounds() []adapter.Inbound {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.inbounds
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.Inbound, bool) {
|
||||
m.access.Lock()
|
||||
inbound, found := m.inboundByTag[tag]
|
||||
m.access.Unlock()
|
||||
if found {
|
||||
return inbound, true
|
||||
}
|
||||
return m.endpoint.Get(tag)
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
inbound, found := m.inboundByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.inboundByTag, tag)
|
||||
index := common.Index(m.inbounds, func(it adapter.Inbound) bool {
|
||||
return it == inbound
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return inbound.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
|
||||
inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(inbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsInbound, loaded := m.inboundByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsInbound.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool {
|
||||
return it == existsInbound
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...)
|
||||
}
|
||||
m.inbounds = append(m.inbounds, inbound)
|
||||
m.inboundByTag[tag] = inbound
|
||||
return nil
|
||||
}
|
72
adapter/inbound/registry.go
Normal file
72
adapter/inbound/registry.go
Normal file
@ -0,0 +1,72 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Inbound, error)
|
||||
|
||||
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(outboundType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Inbound, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.InboundRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
access sync.Mutex
|
||||
optionsType map[string]optionsConstructorFunc
|
||||
constructor map[string]constructorFunc
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
optionsType: make(map[string]optionsConstructorFunc),
|
||||
constructor: make(map[string]constructorFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
optionsConstructor, loaded := m.optionsType[outboundType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
constructor, loaded := m.constructor[outboundType]
|
||||
if !loaded {
|
||||
return nil, E.New("outbound type not found: " + outboundType)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, options)
|
||||
}
|
||||
|
||||
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.optionsType[outboundType] = optionsConstructor
|
||||
m.constructor[outboundType] = constructor
|
||||
}
|
69
adapter/lifecycle.go
Normal file
69
adapter/lifecycle.go
Normal file
@ -0,0 +1,69 @@
|
||||
package adapter
|
||||
|
||||
import E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
type SimpleLifecycle interface {
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type StartStage uint8
|
||||
|
||||
const (
|
||||
StartStateInitialize StartStage = iota
|
||||
StartStateStart
|
||||
StartStatePostStart
|
||||
StartStateStarted
|
||||
)
|
||||
|
||||
var ListStartStages = []StartStage{
|
||||
StartStateInitialize,
|
||||
StartStateStart,
|
||||
StartStatePostStart,
|
||||
StartStateStarted,
|
||||
}
|
||||
|
||||
func (s StartStage) String() string {
|
||||
switch s {
|
||||
case StartStateInitialize:
|
||||
return "initialize"
|
||||
case StartStateStart:
|
||||
return "start"
|
||||
case StartStatePostStart:
|
||||
return "post-start"
|
||||
case StartStateStarted:
|
||||
return "finish-start"
|
||||
default:
|
||||
panic("unknown stage")
|
||||
}
|
||||
}
|
||||
|
||||
type Lifecycle interface {
|
||||
Start(stage StartStage) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type LifecycleService interface {
|
||||
Name() string
|
||||
Lifecycle
|
||||
}
|
||||
|
||||
func Start(stage StartStage, services ...Lifecycle) error {
|
||||
for _, service := range services {
|
||||
err := service.Start(stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func StartNamed(stage StartStage, services []LifecycleService) error {
|
||||
for _, service := range services {
|
||||
err := service.Start(stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage.String(), " ", service.Name())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
52
adapter/lifecycle_legacy.go
Normal file
52
adapter/lifecycle_legacy.go
Normal file
@ -0,0 +1,52 @@
|
||||
package adapter
|
||||
|
||||
func LegacyStart(starter any, stage StartStage) error {
|
||||
if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle {
|
||||
return lifecycle.Start(stage)
|
||||
}
|
||||
switch stage {
|
||||
case StartStateInitialize:
|
||||
if preStarter, isPreStarter := starter.(interface {
|
||||
PreStart() error
|
||||
}); isPreStarter {
|
||||
return preStarter.PreStart()
|
||||
}
|
||||
case StartStateStart:
|
||||
if starter, isStarter := starter.(interface {
|
||||
Start() error
|
||||
}); isStarter {
|
||||
return starter.Start()
|
||||
}
|
||||
case StartStateStarted:
|
||||
if postStarter, isPostStarter := starter.(interface {
|
||||
PostStart() error
|
||||
}); isPostStarter {
|
||||
return postStarter.PostStart()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type lifecycleServiceWrapper struct {
|
||||
SimpleLifecycle
|
||||
name string
|
||||
}
|
||||
|
||||
func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
|
||||
return &lifecycleServiceWrapper{
|
||||
SimpleLifecycle: service,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Name() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
|
||||
return LegacyStart(l.SimpleLifecycle, stage)
|
||||
}
|
||||
|
||||
func (l *lifecycleServiceWrapper) Close() error {
|
||||
return l.SimpleLifecycle.Close()
|
||||
}
|
57
adapter/network.go
Normal file
57
adapter/network.go
Normal file
@ -0,0 +1,57 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
)
|
||||
|
||||
type NetworkManager interface {
|
||||
Lifecycle
|
||||
InterfaceFinder() control.InterfaceFinder
|
||||
UpdateInterfaces() error
|
||||
DefaultNetworkInterface() *NetworkInterface
|
||||
NetworkInterfaces() []NetworkInterface
|
||||
AutoDetectInterface() bool
|
||||
AutoDetectInterfaceFunc() control.Func
|
||||
ProtectFunc() control.Func
|
||||
DefaultOptions() NetworkOptions
|
||||
RegisterAutoRedirectOutputMark(mark uint32) error
|
||||
AutoRedirectOutputMark() uint32
|
||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||
PackageManager() tun.PackageManager
|
||||
WIFIState() WIFIState
|
||||
ResetNetwork()
|
||||
UpdateWIFIState()
|
||||
}
|
||||
|
||||
type NetworkOptions struct {
|
||||
BindInterface string
|
||||
RoutingMark uint32
|
||||
DomainResolver string
|
||||
DomainResolveOptions DNSQueryOptions
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
FallbackDelay time.Duration
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
InterfaceUpdated()
|
||||
}
|
||||
|
||||
type WIFIState struct {
|
||||
SSID string
|
||||
BSSID string
|
||||
}
|
||||
|
||||
type NetworkInterface struct {
|
||||
control.Interface
|
||||
Type C.InterfaceType
|
||||
DNSServers []string
|
||||
Expensive bool
|
||||
Constrained bool
|
||||
}
|
@ -2,8 +2,9 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
@ -15,6 +16,18 @@ type Outbound interface {
|
||||
Network() []string
|
||||
Dependencies() []string
|
||||
N.Dialer
|
||||
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type OutboundRegistry interface {
|
||||
option.OutboundOptionsRegistry
|
||||
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||
}
|
||||
|
||||
type OutboundManager interface {
|
||||
Lifecycle
|
||||
Outbounds() []Outbound
|
||||
Outbound(tag string) (Outbound, bool)
|
||||
Default() Outbound
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error
|
||||
}
|
||||
|
45
adapter/outbound/adapter.go
Normal file
45
adapter/outbound/adapter.go
Normal file
@ -0,0 +1,45 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type Adapter struct {
|
||||
outboundType string
|
||||
outboundTag string
|
||||
network []string
|
||||
dependencies []string
|
||||
}
|
||||
|
||||
func NewAdapter(outboundType string, outboundTag string, network []string, dependencies []string) Adapter {
|
||||
return Adapter{
|
||||
outboundType: outboundType,
|
||||
outboundTag: outboundTag,
|
||||
network: network,
|
||||
dependencies: dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAdapterWithDialerOptions(outboundType string, outboundTag string, network []string, dialOptions option.DialerOptions) Adapter {
|
||||
var dependencies []string
|
||||
if dialOptions.Detour != "" {
|
||||
dependencies = []string{dialOptions.Detour}
|
||||
}
|
||||
return NewAdapter(outboundType, outboundTag, network, dependencies)
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.outboundType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.outboundTag
|
||||
}
|
||||
|
||||
func (a *Adapter) Network() []string {
|
||||
return a.network
|
||||
}
|
||||
|
||||
func (a *Adapter) Dependencies() []string {
|
||||
return a.dependencies
|
||||
}
|
287
adapter/outbound/manager.go
Normal file
287
adapter/outbound/manager.go
Normal file
@ -0,0 +1,287 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
var _ adapter.OutboundManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.OutboundRegistry
|
||||
endpoint adapter.EndpointManager
|
||||
defaultTag string
|
||||
access sync.RWMutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
outbounds []adapter.Outbound
|
||||
outboundByTag map[string]adapter.Outbound
|
||||
dependByTag map[string][]string
|
||||
defaultOutbound adapter.Outbound
|
||||
defaultOutboundFallback adapter.Outbound
|
||||
}
|
||||
|
||||
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
endpoint: endpoint,
|
||||
defaultTag: defaultTag,
|
||||
outboundByTag: make(map[string]adapter.Outbound),
|
||||
dependByTag: make(map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) {
|
||||
m.defaultOutboundFallback = defaultOutboundFallback
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
outbounds := m.outbounds
|
||||
m.access.Unlock()
|
||||
if stage == adapter.StartStateStart {
|
||||
if m.defaultTag != "" && m.defaultOutbound == nil {
|
||||
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
|
||||
if !loaded {
|
||||
return E.New("default outbound not found: ", m.defaultTag)
|
||||
}
|
||||
m.defaultOutbound = defaultEndpoint
|
||||
}
|
||||
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
|
||||
} else {
|
||||
for _, outbound := range outbounds {
|
||||
err := adapter.LegacyStart(outbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
||||
monitor := taskmonitor.New(m.logger, C.StartTimeout)
|
||||
started := make(map[string]bool)
|
||||
for {
|
||||
canContinue := false
|
||||
startOne:
|
||||
for _, outboundToStart := range outbounds {
|
||||
outboundTag := outboundToStart.Tag()
|
||||
if started[outboundTag] {
|
||||
continue
|
||||
}
|
||||
dependencies := outboundToStart.Dependencies()
|
||||
for _, dependency := range dependencies {
|
||||
if !started[dependency] {
|
||||
continue startOne
|
||||
}
|
||||
}
|
||||
started[outboundTag] = true
|
||||
canContinue = true
|
||||
if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
|
||||
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
err := starter.Start(adapter.StartStateStart)
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
}
|
||||
} else if starter, isStarter := outboundToStart.(interface {
|
||||
Start() error
|
||||
}); isStarter {
|
||||
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
err := starter.Start()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(started) == len(outbounds) {
|
||||
break
|
||||
}
|
||||
if canContinue {
|
||||
continue
|
||||
}
|
||||
currentOutbound := common.Find(outbounds, func(it adapter.Outbound) bool {
|
||||
return !started[it.Tag()]
|
||||
})
|
||||
var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error
|
||||
lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error {
|
||||
problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
|
||||
return !started[it]
|
||||
})
|
||||
if common.Contains(oTree, problemOutboundTag) {
|
||||
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
|
||||
}
|
||||
m.access.Lock()
|
||||
problemOutbound := m.outboundByTag[problemOutboundTag]
|
||||
m.access.Unlock()
|
||||
if problemOutbound == nil {
|
||||
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]")
|
||||
}
|
||||
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
|
||||
}
|
||||
return lintOutbound([]string{currentOutbound.Tag()}, currentOutbound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
m.access.Lock()
|
||||
if !m.started {
|
||||
m.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
outbounds := m.outbounds
|
||||
m.outbounds = nil
|
||||
m.access.Unlock()
|
||||
var err error
|
||||
for _, outbound := range outbounds {
|
||||
if closer, isCloser := outbound.(io.Closer); isCloser {
|
||||
monitor.Start("close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
err = E.Append(err, closer.Close(), func(err error) error {
|
||||
return E.Cause(err, "close outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Outbounds() []adapter.Outbound {
|
||||
m.access.RLock()
|
||||
defer m.access.RUnlock()
|
||||
return m.outbounds
|
||||
}
|
||||
|
||||
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
||||
m.access.RLock()
|
||||
outbound, found := m.outboundByTag[tag]
|
||||
m.access.RUnlock()
|
||||
if found {
|
||||
return outbound, true
|
||||
}
|
||||
return m.endpoint.Get(tag)
|
||||
}
|
||||
|
||||
func (m *Manager) Default() adapter.Outbound {
|
||||
m.access.RLock()
|
||||
defer m.access.RUnlock()
|
||||
if m.defaultOutbound != nil {
|
||||
return m.defaultOutbound
|
||||
} else {
|
||||
return m.defaultOutboundFallback
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
outbound, found := m.outboundByTag[tag]
|
||||
if !found {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.outboundByTag, tag)
|
||||
index := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it == outbound
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...)
|
||||
started := m.started
|
||||
if m.defaultOutbound == outbound {
|
||||
if len(m.outbounds) > 0 {
|
||||
m.defaultOutbound = m.outbounds[0]
|
||||
m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag())
|
||||
} else {
|
||||
m.defaultOutbound = nil
|
||||
}
|
||||
}
|
||||
dependBy := m.dependByTag[tag]
|
||||
if len(dependBy) > 0 {
|
||||
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
|
||||
}
|
||||
dependencies := outbound.Dependencies()
|
||||
for _, dependency := range dependencies {
|
||||
if len(m.dependByTag[dependency]) == 1 {
|
||||
delete(m.dependByTag, dependency)
|
||||
} else {
|
||||
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
|
||||
return it != tag
|
||||
})
|
||||
}
|
||||
}
|
||||
if started {
|
||||
return common.Close(outbound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error {
|
||||
if tag == "" {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
outbound, err := m.registry.CreateOutbound(ctx, router, logger, tag, inboundType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(outbound, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = common.Close(existsOutbound)
|
||||
if err != nil {
|
||||
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool {
|
||||
return it == existsOutbound
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid inbound index")
|
||||
}
|
||||
m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...)
|
||||
}
|
||||
m.outbounds = append(m.outbounds, outbound)
|
||||
m.outboundByTag[tag] = outbound
|
||||
dependencies := outbound.Dependencies()
|
||||
for _, dependency := range dependencies {
|
||||
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
|
||||
}
|
||||
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
|
||||
m.defaultOutbound = outbound
|
||||
if m.started {
|
||||
m.logger.Info("updated default outbound to ", outbound.Tag())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
72
adapter/outbound/registry.go
Normal file
72
adapter/outbound/registry.go
Normal file
@ -0,0 +1,72 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Outbound, error)
|
||||
|
||||
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(outboundType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Outbound, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.OutboundRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
access sync.Mutex
|
||||
optionsType map[string]optionsConstructorFunc
|
||||
constructors map[string]constructorFunc
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
optionsType: make(map[string]optionsConstructorFunc),
|
||||
constructors: make(map[string]constructorFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
optionsConstructor, loaded := r.optionsType[outboundType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
constructor, loaded := r.constructors[outboundType]
|
||||
if !loaded {
|
||||
return nil, E.New("outbound type not found: " + outboundType)
|
||||
}
|
||||
return constructor(ctx, router, logger, tag, options)
|
||||
}
|
||||
|
||||
func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
r.access.Lock()
|
||||
defer r.access.Unlock()
|
||||
r.optionsType[outboundType] = optionsConstructor
|
||||
r.constructors[outboundType] = constructor
|
||||
}
|
@ -1,9 +1 @@
|
||||
package adapter
|
||||
|
||||
type PreStarter interface {
|
||||
PreStart() error
|
||||
}
|
||||
|
||||
type PostStarter interface {
|
||||
PostStart() error
|
||||
}
|
||||
|
@ -2,102 +2,47 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/common/geoip"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
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"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
mdns "github.com/miekg/dns"
|
||||
"go4.org/netipx"
|
||||
)
|
||||
|
||||
type Router interface {
|
||||
Service
|
||||
PreStarter
|
||||
PostStarter
|
||||
Cleanup() error
|
||||
|
||||
Outbounds() []Outbound
|
||||
Outbound(tag string) (Outbound, bool)
|
||||
DefaultOutbound(network string) (Outbound, error)
|
||||
|
||||
FakeIPStore() FakeIPStore
|
||||
|
||||
Lifecycle
|
||||
ConnectionRouter
|
||||
|
||||
GeoIPReader() *geoip.Reader
|
||||
LoadGeosite(code string) (Rule, error)
|
||||
|
||||
PreMatch(metadata InboundContext) error
|
||||
ConnectionRouterEx
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
|
||||
NeedWIFIState() bool
|
||||
|
||||
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
||||
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
||||
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
||||
ClearDNSCache()
|
||||
|
||||
InterfaceFinder() control.InterfaceFinder
|
||||
UpdateInterfaces() error
|
||||
DefaultInterface() string
|
||||
AutoDetectInterface() bool
|
||||
AutoDetectInterfaceFunc() control.Func
|
||||
DefaultMark() uint32
|
||||
RegisterAutoRedirectOutputMark(mark uint32) error
|
||||
AutoRedirectOutputMark() uint32
|
||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||
PackageManager() tun.PackageManager
|
||||
WIFIState() WIFIState
|
||||
Rules() []Rule
|
||||
|
||||
ClashServer() ClashServer
|
||||
SetClashServer(server ClashServer)
|
||||
|
||||
V2RayServer() V2RayServer
|
||||
SetV2RayServer(server V2RayServer)
|
||||
|
||||
ResetNetwork() error
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
func ContextWithRouter(ctx context.Context, router Router) context.Context {
|
||||
return service.ContextWith(ctx, router)
|
||||
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
|
||||
}
|
||||
|
||||
func RouterFromContext(ctx context.Context) Router {
|
||||
return service.FromContext[Router](ctx)
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
type ConnectionRouter interface {
|
||||
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
}
|
||||
|
||||
type HeadlessRule interface {
|
||||
Match(metadata *InboundContext) bool
|
||||
String() string
|
||||
}
|
||||
|
||||
type Rule interface {
|
||||
HeadlessRule
|
||||
Service
|
||||
Type() string
|
||||
UpdateGeosite() error
|
||||
Outbound() string
|
||||
}
|
||||
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
DisableCache() bool
|
||||
RewriteTTL() *uint32
|
||||
ClientSubnet() *netip.Prefix
|
||||
WithAddressLimit() bool
|
||||
MatchAddressLimit(metadata *InboundContext) bool
|
||||
type ConnectionRouterEx interface {
|
||||
ConnectionRouter
|
||||
RouteConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
type RuleSet interface {
|
||||
@ -123,12 +68,14 @@ type RuleSetMetadata struct {
|
||||
ContainsIPCIDRRule bool
|
||||
}
|
||||
type HTTPStartContext struct {
|
||||
ctx context.Context
|
||||
access sync.Mutex
|
||||
httpClientCache map[string]*http.Client
|
||||
}
|
||||
|
||||
func NewHTTPStartContext() *HTTPStartContext {
|
||||
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||
return &HTTPStartContext{
|
||||
ctx: ctx,
|
||||
httpClientCache: make(map[string]*http.Client),
|
||||
}
|
||||
}
|
||||
@ -146,6 +93,10 @@ func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Clie
|
||||
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
|
||||
@ -159,12 +110,3 @@ func (c *HTTPStartContext) Close() {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
InterfaceUpdated()
|
||||
}
|
||||
|
||||
type WIFIState struct {
|
||||
SSID string
|
||||
BSSID string
|
||||
}
|
||||
|
37
adapter/rule.go
Normal file
37
adapter/rule.go
Normal file
@ -0,0 +1,37 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
)
|
||||
|
||||
type HeadlessRule interface {
|
||||
Match(metadata *InboundContext) bool
|
||||
String() string
|
||||
}
|
||||
|
||||
type Rule interface {
|
||||
HeadlessRule
|
||||
SimpleLifecycle
|
||||
Type() string
|
||||
Action() RuleAction
|
||||
}
|
||||
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
WithAddressLimit() bool
|
||||
MatchAddressLimit(metadata *InboundContext) bool
|
||||
}
|
||||
|
||||
type RuleAction interface {
|
||||
Type() string
|
||||
String() string
|
||||
}
|
||||
|
||||
func IsFinalAction(action RuleAction) bool {
|
||||
switch action.Type() {
|
||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,6 +1,27 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Start() error
|
||||
Close() error
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
}
|
||||
|
||||
type ServiceRegistry interface {
|
||||
option.ServiceOptionsRegistry
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
|
||||
}
|
||||
|
||||
type ServiceManager interface {
|
||||
Lifecycle
|
||||
Services() []Service
|
||||
Get(tag string) (Service, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
|
||||
}
|
||||
|
21
adapter/service/adapter.go
Normal file
21
adapter/service/adapter.go
Normal file
@ -0,0 +1,21 @@
|
||||
package service
|
||||
|
||||
type Adapter struct {
|
||||
serviceType string
|
||||
serviceTag string
|
||||
}
|
||||
|
||||
func NewAdapter(serviceType string, serviceTag string) Adapter {
|
||||
return Adapter{
|
||||
serviceType: serviceType,
|
||||
serviceTag: serviceTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.serviceType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.serviceTag
|
||||
}
|
144
adapter/service/manager.go
Normal file
144
adapter/service/manager.go
Normal file
@ -0,0 +1,144 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.ServiceManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.ServiceRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
services []adapter.Service
|
||||
serviceByTag map[string]adapter.Service
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
serviceByTag: make(map[string]adapter.Service),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
services := m.services
|
||||
m.access.Unlock()
|
||||
for _, service := range services {
|
||||
err := adapter.LegacyStart(service, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
services := m.services
|
||||
m.services = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, service := range services {
|
||||
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
|
||||
err = E.Append(err, service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Services() []adapter.Service {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.services
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.Service, bool) {
|
||||
m.access.Lock()
|
||||
service, found := m.serviceByTag[tag]
|
||||
m.access.Unlock()
|
||||
return service, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
service, found := m.serviceByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.serviceByTag, tag)
|
||||
index := common.Index(m.services, func(it adapter.Service) bool {
|
||||
return it == service
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid service index")
|
||||
}
|
||||
m.services = append(m.services[:index], m.services[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return service.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
|
||||
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err = adapter.LegacyStart(service, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsService, loaded := m.serviceByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsService.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
|
||||
return it == existsService
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid service index")
|
||||
}
|
||||
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
|
||||
}
|
||||
m.services = append(m.services, service)
|
||||
m.serviceByTag[tag] = service
|
||||
return nil
|
||||
}
|
72
adapter/service/registry.go
Normal file
72
adapter/service/registry.go
Normal file
@ -0,0 +1,72 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.Service, error)
|
||||
|
||||
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(outboundType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.ServiceRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, error)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
access sync.Mutex
|
||||
optionsType map[string]optionsConstructorFunc
|
||||
constructor map[string]constructorFunc
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
optionsType: make(map[string]optionsConstructorFunc),
|
||||
constructor: make(map[string]constructorFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
optionsConstructor, loaded := m.optionsType[outboundType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
constructor, loaded := m.constructor[outboundType]
|
||||
if !loaded {
|
||||
return nil, E.New("outbound type not found: " + outboundType)
|
||||
}
|
||||
return constructor(ctx, logger, tag, options)
|
||||
}
|
||||
|
||||
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.optionsType[outboundType] = optionsConstructor
|
||||
m.constructor[outboundType] = constructor
|
||||
}
|
18
adapter/ssm.go
Normal file
18
adapter/ssm.go
Normal file
@ -0,0 +1,18 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type ManagedSSMServer interface {
|
||||
Inbound
|
||||
SetTracker(tracker SSMTracker)
|
||||
UpdateUsers(users []string, uPSKs []string) error
|
||||
}
|
||||
|
||||
type SSMTracker interface {
|
||||
TrackConnection(conn net.Conn, metadata InboundContext) net.Conn
|
||||
TrackPacketConnection(conn N.PacketConn, metadata InboundContext) N.PacketConn
|
||||
}
|
@ -3,6 +3,6 @@ package adapter
|
||||
import "time"
|
||||
|
||||
type TimeService interface {
|
||||
Service
|
||||
SimpleLifecycle
|
||||
TimeFunc() func() time.Time
|
||||
}
|
||||
|
@ -4,112 +4,165 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type (
|
||||
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
)
|
||||
|
||||
func NewUpstreamHandler(
|
||||
func NewUpstreamHandlerEx(
|
||||
metadata InboundContext,
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamHandlerWrapper{
|
||||
connectionHandler ConnectionHandlerFuncEx,
|
||||
packetHandler PacketConnectionHandlerFuncEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &myUpstreamHandlerWrapperEx{
|
||||
metadata: metadata,
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
|
||||
var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil)
|
||||
|
||||
type myUpstreamHandlerWrapper struct {
|
||||
type myUpstreamHandlerWrapperEx struct {
|
||||
metadata InboundContext
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
connectionHandler ConnectionHandlerFuncEx
|
||||
packetHandler PacketConnectionHandlerFuncEx
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
if destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
}
|
||||
return w.connectionHandler(ctx, conn, myMetadata)
|
||||
w.connectionHandler(ctx, conn, myMetadata, onClose)
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
if destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
}
|
||||
return w.packetHandler(ctx, conn, myMetadata)
|
||||
w.packetHandler(ctx, conn, myMetadata, onClose)
|
||||
}
|
||||
|
||||
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil)
|
||||
|
||||
type myUpstreamContextHandlerWrapperEx struct {
|
||||
connectionHandler ConnectionHandlerFuncEx
|
||||
packetHandler PacketConnectionHandlerFuncEx
|
||||
}
|
||||
|
||||
func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
||||
return M.Metadata{
|
||||
Source: metadata.Source,
|
||||
Destination: metadata.Destination,
|
||||
}
|
||||
}
|
||||
|
||||
type myUpstreamContextHandlerWrapper struct {
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
func NewUpstreamContextHandler(
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamContextHandlerWrapper{
|
||||
func NewUpstreamContextHandlerEx(
|
||||
connectionHandler ConnectionHandlerFuncEx,
|
||||
packetHandler PacketConnectionHandlerFuncEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &myUpstreamContextHandlerWrapperEx{
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
if destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
}
|
||||
return w.connectionHandler(ctx, conn, *myMetadata)
|
||||
w.connectionHandler(ctx, conn, *myMetadata, onClose)
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
if destination.IsValid() {
|
||||
myMetadata.Destination = destination
|
||||
}
|
||||
return w.packetHandler(ctx, conn, *myMetadata)
|
||||
w.packetHandler(ctx, conn, *myMetadata, onClose)
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
func NewRouteHandlerEx(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouterEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &routeHandlerWrapperEx{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil)
|
||||
|
||||
type routeHandlerWrapperEx struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouterEx
|
||||
}
|
||||
|
||||
func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
if source.IsValid() {
|
||||
r.metadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
r.metadata.Destination = destination
|
||||
}
|
||||
r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose)
|
||||
}
|
||||
|
||||
func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
if source.IsValid() {
|
||||
r.metadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
r.metadata.Destination = destination
|
||||
}
|
||||
r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose)
|
||||
}
|
||||
|
||||
func NewRouteContextHandlerEx(
|
||||
router ConnectionRouterEx,
|
||||
) UpstreamHandlerAdapterEx {
|
||||
return &routeContextHandlerWrapperEx{
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil)
|
||||
|
||||
type routeContextHandlerWrapperEx struct {
|
||||
router ConnectionRouterEx
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
metadata := ContextFrom(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
metadata.Destination = destination
|
||||
}
|
||||
r.router.RouteConnectionEx(ctx, conn, *metadata, onClose)
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
metadata := ContextFrom(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
}
|
||||
if destination.IsValid() {
|
||||
metadata.Destination = destination
|
||||
}
|
||||
r.router.RoutePacketConnectionEx(ctx, conn, *metadata, onClose)
|
||||
}
|
||||
|
234
adapter/upstream_legacy.go
Normal file
234
adapter/upstream_legacy.go
Normal file
@ -0,0 +1,234 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type (
|
||||
// Deprecated
|
||||
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
|
||||
// Deprecated
|
||||
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
|
||||
)
|
||||
|
||||
// Deprecated
|
||||
//
|
||||
//nolint:staticcheck
|
||||
func NewUpstreamHandler(
|
||||
metadata InboundContext,
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamHandlerWrapper{
|
||||
metadata: metadata,
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type myUpstreamHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.connectionHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.packetHandler(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: use myUpstreamHandlerWrapperEx instead.
|
||||
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
||||
// Deprecated: removed
|
||||
func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
||||
return M.Metadata{
|
||||
Source: metadata.Source,
|
||||
Destination: metadata.Destination,
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
type myUpstreamContextHandlerWrapper struct {
|
||||
connectionHandler ConnectionHandlerFunc
|
||||
packetHandler PacketConnectionHandlerFunc
|
||||
errorHandler E.Handler
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func NewUpstreamContextHandler(
|
||||
connectionHandler ConnectionHandlerFunc,
|
||||
packetHandler PacketConnectionHandlerFunc,
|
||||
errorHandler E.Handler,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &myUpstreamContextHandlerWrapper{
|
||||
connectionHandler: connectionHandler,
|
||||
packetHandler: packetHandler,
|
||||
errorHandler: errorHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.connectionHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.packetHandler(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use NewUpstreamContextHandlerEx instead.
|
||||
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.errorHandler.NewError(ctx, err)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func NewRouteHandler(
|
||||
metadata InboundContext,
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeHandlerWrapper{
|
||||
metadata: metadata,
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func NewRouteContextHandler(
|
||||
router ConnectionRouter,
|
||||
logger logger.ContextLogger,
|
||||
) UpstreamHandlerAdapter {
|
||||
return &routeContextHandlerWrapper{
|
||||
router: router,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
//
|
||||
//nolint:staticcheck
|
||||
type routeHandlerWrapper struct {
|
||||
metadata InboundContext
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RouteConnection(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := w.metadata
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RoutePacketConnection(ctx, conn, myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
||||
|
||||
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
type routeContextHandlerWrapper struct {
|
||||
router ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RouteConnection(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if metadata.Source.IsValid() {
|
||||
myMetadata.Source = metadata.Source
|
||||
}
|
||||
if metadata.Destination.IsValid() {
|
||||
myMetadata.Destination = metadata.Destination
|
||||
}
|
||||
return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionRouterEx instead.
|
||||
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
|
||||
w.logger.ErrorContext(ctx, err)
|
||||
}
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
@ -16,8 +15,7 @@ type V2RayServerTransport interface {
|
||||
}
|
||||
|
||||
type V2RayServerTransportHandler interface {
|
||||
N.TCPConnectionHandler
|
||||
E.Handler
|
||||
N.TCPConnectionHandlerEx
|
||||
}
|
||||
|
||||
type V2RayClientTransport interface {
|
||||
|
489
box.go
489
box.go
@ -9,45 +9,91 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport/local"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/inbound"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/outbound"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
)
|
||||
|
||||
var _ adapter.Service = (*Box)(nil)
|
||||
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
createdAt time.Time
|
||||
router adapter.Router
|
||||
inbounds []adapter.Inbound
|
||||
outbounds []adapter.Outbound
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
preServices1 map[string]adapter.Service
|
||||
preServices2 map[string]adapter.Service
|
||||
postServices map[string]adapter.Service
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
service *boxService.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
option.Options
|
||||
Context context.Context
|
||||
PlatformInterface platform.Interface
|
||||
PlatformLogWriter log.PlatformWriter
|
||||
}
|
||||
|
||||
func Context(
|
||||
ctx context.Context,
|
||||
inboundRegistry adapter.InboundRegistry,
|
||||
outboundRegistry adapter.OutboundRegistry,
|
||||
endpointRegistry adapter.EndpointRegistry,
|
||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||
serviceRegistry adapter.ServiceRegistry,
|
||||
) context.Context {
|
||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
|
||||
ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)
|
||||
}
|
||||
if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.OutboundRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry)
|
||||
ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
|
||||
}
|
||||
if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.EndpointRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
||||
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
||||
}
|
||||
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
||||
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
||||
}
|
||||
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func New(options Options) (*Box, error) {
|
||||
createdAt := time.Now()
|
||||
ctx := options.Context
|
||||
@ -55,6 +101,29 @@ func New(options Options) (*Box, error) {
|
||||
ctx = context.Background()
|
||||
}
|
||||
ctx = service.ContextWithDefaultRegistry(ctx)
|
||||
|
||||
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||
|
||||
if endpointRegistry == nil {
|
||||
return nil, E.New("missing endpoint registry in context")
|
||||
}
|
||||
if inboundRegistry == nil {
|
||||
return nil, E.New("missing inbound registry in context")
|
||||
}
|
||||
if outboundRegistry == nil {
|
||||
return nil, E.New("missing outbound registry in context")
|
||||
}
|
||||
if dnsTransportRegistry == nil {
|
||||
return nil, E.New("missing DNS transport registry in context")
|
||||
}
|
||||
if serviceRegistry == nil {
|
||||
return nil, E.New("missing service registry in context")
|
||||
}
|
||||
|
||||
ctx = pause.WithDefaultManager(ctx)
|
||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||
@ -70,8 +139,9 @@ func New(options Options) (*Box, error) {
|
||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||
needV2RayAPI = true
|
||||
}
|
||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||
var defaultLogWriter io.Writer
|
||||
if options.PlatformInterface != nil {
|
||||
if platformInterface != nil {
|
||||
defaultLogWriter = io.Discard
|
||||
}
|
||||
logFactory, err := log.New(log.Options{
|
||||
@ -85,114 +155,244 @@ func New(options Options) (*Box, error) {
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create log factory")
|
||||
}
|
||||
router, err := route.NewRouter(
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
len(certificateOptions.CertificatePath) > 0 ||
|
||||
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
||||
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
internalServices = append(internalServices, certificateStore)
|
||||
}
|
||||
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
}
|
||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||
service.MustRegister[adapter.Router](ctx, router)
|
||||
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize router")
|
||||
}
|
||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
if ntpOptions.Enabled {
|
||||
timeService = new(tls.TimeServiceWrapper)
|
||||
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||
}
|
||||
for i, transportOptions := range dnsOptions.Servers {
|
||||
var tag string
|
||||
if transportOptions.Tag != "" {
|
||||
tag = transportOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = dnsTransportManager.Create(
|
||||
ctx,
|
||||
logFactory,
|
||||
common.PtrValueOrDefault(options.Route),
|
||||
common.PtrValueOrDefault(options.DNS),
|
||||
common.PtrValueOrDefault(options.NTP),
|
||||
options.Inbounds,
|
||||
options.PlatformInterface,
|
||||
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
transportOptions.Type,
|
||||
transportOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse route options")
|
||||
return nil, E.Cause(err, "initialize DNS server[", i, "]")
|
||||
}
|
||||
}
|
||||
err = dnsRouter.Initialize(dnsOptions.Rules)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize dns router")
|
||||
}
|
||||
for i, endpointOptions := range options.Endpoints {
|
||||
var tag string
|
||||
if endpointOptions.Tag != "" {
|
||||
tag = endpointOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
endpointCtx := ctx
|
||||
if tag != "" {
|
||||
// TODO: remove this
|
||||
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
|
||||
Outbound: tag,
|
||||
})
|
||||
}
|
||||
err = endpointManager.Create(
|
||||
endpointCtx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
endpointOptions.Type,
|
||||
endpointOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize endpoint[", i, "]")
|
||||
}
|
||||
}
|
||||
inbounds := make([]adapter.Inbound, 0, len(options.Inbounds))
|
||||
outbounds := make([]adapter.Outbound, 0, len(options.Outbounds))
|
||||
for i, inboundOptions := range options.Inbounds {
|
||||
var in adapter.Inbound
|
||||
var tag string
|
||||
if inboundOptions.Tag != "" {
|
||||
tag = inboundOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
in, err = inbound.New(
|
||||
err = inboundManager.Create(
|
||||
ctx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
inboundOptions,
|
||||
options.PlatformInterface,
|
||||
inboundOptions.Type,
|
||||
inboundOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse inbound[", i, "]")
|
||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
||||
}
|
||||
inbounds = append(inbounds, in)
|
||||
}
|
||||
for i, outboundOptions := range options.Outbounds {
|
||||
var out adapter.Outbound
|
||||
var tag string
|
||||
if outboundOptions.Tag != "" {
|
||||
tag = outboundOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
out, err = outbound.New(
|
||||
ctx,
|
||||
outboundCtx := ctx
|
||||
if tag != "" {
|
||||
// TODO: remove this
|
||||
outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{
|
||||
Outbound: tag,
|
||||
})
|
||||
}
|
||||
err = outboundManager.Create(
|
||||
outboundCtx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
outboundOptions)
|
||||
outboundOptions.Type,
|
||||
outboundOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse outbound[", i, "]")
|
||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||
}
|
||||
outbounds = append(outbounds, out)
|
||||
}
|
||||
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
|
||||
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"})
|
||||
common.Must(oErr)
|
||||
outbounds = append(outbounds, out)
|
||||
return out
|
||||
})
|
||||
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, err
|
||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||
}
|
||||
if options.PlatformInterface != nil {
|
||||
err = options.PlatformInterface.Initialize(ctx, router)
|
||||
}
|
||||
outboundManager.Initialize(common.Must1(
|
||||
direct.NewOutbound(
|
||||
ctx,
|
||||
router,
|
||||
logFactory.NewLogger("outbound/direct"),
|
||||
"direct",
|
||||
option.DirectOutboundOptions{},
|
||||
),
|
||||
))
|
||||
dnsTransportManager.Initialize(common.Must1(
|
||||
local.NewTransport(
|
||||
ctx,
|
||||
logFactory.NewLogger("dns/local"),
|
||||
"local",
|
||||
option.LocalDNSServerOptions{},
|
||||
)))
|
||||
if platformInterface != nil {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize platform interface")
|
||||
}
|
||||
}
|
||||
preServices1 := make(map[string]adapter.Service)
|
||||
preServices2 := make(map[string]adapter.Service)
|
||||
postServices := make(map[string]adapter.Service)
|
||||
if needCacheFile {
|
||||
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
||||
if cacheFile == nil {
|
||||
cacheFile = cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
}
|
||||
preServices1["cache file"] = cacheFile
|
||||
internalServices = append(internalServices, cacheFile)
|
||||
}
|
||||
if needClashAPI {
|
||||
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), clashAPIOptions)
|
||||
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create clash api server")
|
||||
return nil, E.Cause(err, "create clash-server")
|
||||
}
|
||||
router.SetClashServer(clashServer)
|
||||
preServices2["clash api"] = clashServer
|
||||
router.AppendTracker(clashServer)
|
||||
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||
internalServices = append(internalServices, clashServer)
|
||||
}
|
||||
if needV2RayAPI {
|
||||
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create v2ray api server")
|
||||
return nil, E.Cause(err, "create v2ray-server")
|
||||
}
|
||||
router.SetV2RayServer(v2rayServer)
|
||||
preServices2["v2ray api"] = v2rayServer
|
||||
if v2rayServer.StatsService() != nil {
|
||||
router.AppendTracker(v2rayServer.StatsService())
|
||||
internalServices = append(internalServices, v2rayServer)
|
||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||
}
|
||||
}
|
||||
if ntpOptions.Enabled {
|
||||
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create NTP service")
|
||||
}
|
||||
ntpService := ntp.NewService(ntp.Options{
|
||||
Context: ctx,
|
||||
Dialer: ntpDialer,
|
||||
Logger: logFactory.NewLogger("ntp"),
|
||||
Server: ntpOptions.ServerOptions.Build(),
|
||||
Interval: time.Duration(ntpOptions.Interval),
|
||||
WriteToSystem: ntpOptions.WriteToSystem,
|
||||
})
|
||||
timeService.TimeService = ntpService
|
||||
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||
}
|
||||
return &Box{
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
inbounds: inbounds,
|
||||
outbounds: outbounds,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
preServices1: preServices1,
|
||||
preServices2: preServices2,
|
||||
postServices: postServices,
|
||||
internalService: internalServices,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
@ -243,35 +443,19 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return E.Cause(err, "start logger")
|
||||
}
|
||||
for serviceName, service := range s.preServices1 {
|
||||
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
||||
monitor.Start("pre-start ", serviceName)
|
||||
err := preService.PreStart()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "pre-start ", serviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
for serviceName, service := range s.preServices2 {
|
||||
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
||||
monitor.Start("pre-start ", serviceName)
|
||||
err := preService.PreStart()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "pre-start ", serviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
err = s.router.PreStart()
|
||||
if err != nil {
|
||||
return E.Cause(err, "pre-start router")
|
||||
}
|
||||
err = s.startOutbounds()
|
||||
err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.router.Start()
|
||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) start() error {
|
||||
@ -279,64 +463,29 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for serviceName, service := range s.preServices1 {
|
||||
err = service.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start ", serviceName)
|
||||
}
|
||||
}
|
||||
for serviceName, service := range s.preServices2 {
|
||||
err = service.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start ", serviceName)
|
||||
}
|
||||
}
|
||||
for i, in := range s.inbounds {
|
||||
var tag string
|
||||
if in.Tag() == "" {
|
||||
tag = F.ToString(i)
|
||||
} else {
|
||||
tag = in.Tag()
|
||||
}
|
||||
err = in.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
||||
}
|
||||
}
|
||||
err = s.postStart()
|
||||
err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.router.Cleanup()
|
||||
}
|
||||
|
||||
func (s *Box) postStart() error {
|
||||
for serviceName, service := range s.postServices {
|
||||
err := service.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start ", serviceName)
|
||||
}
|
||||
}
|
||||
// TODO: reorganize ALL start order
|
||||
for _, out := range s.outbounds {
|
||||
if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound {
|
||||
err := lateOutbound.PostStart()
|
||||
if err != nil {
|
||||
return E.Cause(err, "post-start outbound/", out.Tag())
|
||||
}
|
||||
}
|
||||
}
|
||||
err := s.router.PostStart()
|
||||
err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, in := range s.inbounds {
|
||||
if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound {
|
||||
err = lateInbound.PostStart()
|
||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return E.Cause(err, "post-start inbound/", in.Tag())
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -348,58 +497,32 @@ func (s *Box) Close() error {
|
||||
default:
|
||||
close(s.done)
|
||||
}
|
||||
monitor := taskmonitor.New(s.logger, C.StopTimeout)
|
||||
var errors error
|
||||
for serviceName, service := range s.postServices {
|
||||
monitor.Start("close ", serviceName)
|
||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", serviceName)
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
for i, in := range s.inbounds {
|
||||
monitor.Start("close inbound/", in.Type(), "[", i, "]")
|
||||
errors = E.Append(errors, in.Close(), func(err error) error {
|
||||
return E.Cause(err, "close inbound/", in.Type(), "[", i, "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
for i, out := range s.outbounds {
|
||||
monitor.Start("close outbound/", out.Type(), "[", i, "]")
|
||||
errors = E.Append(errors, common.Close(out), func(err error) error {
|
||||
return E.Cause(err, "close outbound/", out.Type(), "[", i, "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
monitor.Start("close router")
|
||||
if err := common.Close(s.router); err != nil {
|
||||
errors = E.Append(errors, err, func(err error) error {
|
||||
return E.Cause(err, "close router")
|
||||
err := common.Close(
|
||||
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||
)
|
||||
for _, lifecycleService := range s.internalService {
|
||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", lifecycleService.Name())
|
||||
})
|
||||
}
|
||||
monitor.Finish()
|
||||
for serviceName, service := range s.preServices1 {
|
||||
monitor.Start("close ", serviceName)
|
||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", serviceName)
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
for serviceName, service := range s.preServices2 {
|
||||
monitor.Start("close ", serviceName)
|
||||
errors = E.Append(errors, service.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", serviceName)
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
if err := common.Close(s.logFactory); err != nil {
|
||||
errors = E.Append(errors, err, func(err error) error {
|
||||
err = E.Append(err, s.logFactory.Close(), func(err error) error {
|
||||
return E.Cause(err, "close logger")
|
||||
})
|
||||
}
|
||||
return errors
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Box) Network() adapter.NetworkManager {
|
||||
return s.network
|
||||
}
|
||||
|
||||
func (s *Box) Router() adapter.Router {
|
||||
return s.router
|
||||
}
|
||||
|
||||
func (s *Box) Inbound() adapter.InboundManager {
|
||||
return s.inbound
|
||||
}
|
||||
|
||||
func (s *Box) Outbound() adapter.OutboundManager {
|
||||
return s.outbound
|
||||
}
|
||||
|
@ -1,85 +0,0 @@
|
||||
package box
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
func (s *Box) startOutbounds() error {
|
||||
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||
outboundTags := make(map[adapter.Outbound]string)
|
||||
outbounds := make(map[string]adapter.Outbound)
|
||||
for i, outboundToStart := range s.outbounds {
|
||||
var outboundTag string
|
||||
if outboundToStart.Tag() == "" {
|
||||
outboundTag = F.ToString(i)
|
||||
} else {
|
||||
outboundTag = outboundToStart.Tag()
|
||||
}
|
||||
if _, exists := outbounds[outboundTag]; exists {
|
||||
return E.New("outbound tag ", outboundTag, " duplicated")
|
||||
}
|
||||
outboundTags[outboundToStart] = outboundTag
|
||||
outbounds[outboundTag] = outboundToStart
|
||||
}
|
||||
started := make(map[string]bool)
|
||||
for {
|
||||
canContinue := false
|
||||
startOne:
|
||||
for _, outboundToStart := range s.outbounds {
|
||||
outboundTag := outboundTags[outboundToStart]
|
||||
if started[outboundTag] {
|
||||
continue
|
||||
}
|
||||
dependencies := outboundToStart.Dependencies()
|
||||
for _, dependency := range dependencies {
|
||||
if !started[dependency] {
|
||||
continue startOne
|
||||
}
|
||||
}
|
||||
started[outboundTag] = true
|
||||
canContinue = true
|
||||
if starter, isStarter := outboundToStart.(interface {
|
||||
Start() error
|
||||
}); isStarter {
|
||||
monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
err := starter.Start()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(started) == len(s.outbounds) {
|
||||
break
|
||||
}
|
||||
if canContinue {
|
||||
continue
|
||||
}
|
||||
currentOutbound := common.Find(s.outbounds, func(it adapter.Outbound) bool {
|
||||
return !started[outboundTags[it]]
|
||||
})
|
||||
var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error
|
||||
lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error {
|
||||
problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
|
||||
return !started[it]
|
||||
})
|
||||
if common.Contains(oTree, problemOutboundTag) {
|
||||
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
|
||||
}
|
||||
problemOutbound := outbounds[problemOutboundTag]
|
||||
if problemOutbound == nil {
|
||||
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", outboundTags[oCurrent], "]")
|
||||
}
|
||||
return lintOutbound(append(oTree, problemOutboundTag), problemOutbound)
|
||||
}
|
||||
return lintOutbound([]string{outboundTags[currentOutbound]}, currentOutbound)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1 +1 @@
|
||||
Subproject commit 6533b62fa3af7aa9cf13a94af3e66ad6703f2be8
|
||||
Subproject commit 320170a1077ea5c93872b3e055b96b8836615ef0
|
@ -1 +1 @@
|
||||
Subproject commit 1ecaff4c902fb1dec2a93db1edf6d48e4fe148e1
|
||||
Subproject commit ae5818ee5a24af965dc91f80bffa16e1e6c109c1
|
@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/asc-go/asc"
|
||||
@ -104,7 +105,7 @@ func publishTestflight(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
tag := tagVersion.VersionString()
|
||||
client := createClient(10 * time.Minute)
|
||||
client := createClient(20 * time.Minute)
|
||||
|
||||
log.Info(tag, " list build IDs")
|
||||
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
|
||||
@ -144,7 +145,7 @@ func publishTestflight(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
build := builds.Data[0]
|
||||
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 5*time.Minute {
|
||||
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
|
||||
log.Info(string(platform), " ", tag, " waiting for process")
|
||||
time.Sleep(15 * time.Second)
|
||||
continue
|
||||
@ -194,6 +195,10 @@ func publishTestflight(ctx context.Context) error {
|
||||
log.Info(string(platform), " ", tag, " create submission")
|
||||
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
|
||||
log.Error(err)
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ var (
|
||||
debugFlags []string
|
||||
sharedTags []string
|
||||
iosTags []string
|
||||
memcTags []string
|
||||
debugTags []string
|
||||
)
|
||||
|
||||
@ -58,8 +59,9 @@ func init() {
|
||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
|
||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
|
||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory")
|
||||
memcTags = append(memcTags, "with_tailscale")
|
||||
debugTags = append(debugTags, "debug")
|
||||
}
|
||||
|
||||
@ -99,18 +101,19 @@ func buildAndroid() {
|
||||
"-javapkg=io.nekohasekai",
|
||||
"-libname=box",
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
args = append(args, sharedFlags...)
|
||||
} else {
|
||||
args = append(args, debugFlags...)
|
||||
}
|
||||
|
||||
args = append(args, "-tags")
|
||||
if !debugEnabled {
|
||||
args = append(args, strings.Join(sharedTags, ","))
|
||||
} else {
|
||||
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
|
||||
tags := append(sharedTags, memcTags...)
|
||||
if debugEnabled {
|
||||
tags = append(tags, debugTags...)
|
||||
}
|
||||
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
args = append(args, "./experimental/libbox")
|
||||
|
||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||
@ -148,7 +151,9 @@ func buildApple() {
|
||||
"-v",
|
||||
"-target", bindTarget,
|
||||
"-libname=box",
|
||||
"-tags-macos=" + strings.Join(memcTags, ","),
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
args = append(args, sharedFlags...)
|
||||
} else {
|
||||
@ -156,12 +161,11 @@ func buildApple() {
|
||||
}
|
||||
|
||||
tags := append(sharedTags, iosTags...)
|
||||
args = append(args, "-tags")
|
||||
if !debugEnabled {
|
||||
args = append(args, strings.Join(tags, ","))
|
||||
} else {
|
||||
args = append(args, strings.Join(append(tags, debugTags...), ","))
|
||||
if debugEnabled {
|
||||
tags = append(tags, debugTags...)
|
||||
}
|
||||
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
args = append(args, "./experimental/libbox")
|
||||
|
||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||
|
@ -48,7 +48,7 @@ func FindSDK() {
|
||||
}
|
||||
|
||||
func findNDK() bool {
|
||||
const fixedVersion = "28.0.12674087"
|
||||
const fixedVersion = "28.0.13004108"
|
||||
const versionFile = "source.properties"
|
||||
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
|
||||
androidNDKPath = fixedPath
|
||||
|
@ -5,40 +5,49 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/common/badversion"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
var nightly bool
|
||||
var (
|
||||
flagRunInCI bool
|
||||
flagRunNightly bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&nightly, "nightly", false, "Print nightly tag")
|
||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if nightly {
|
||||
version, err := build_shared.ReadTagVersionRev()
|
||||
var (
|
||||
versionStr string
|
||||
err error
|
||||
)
|
||||
if flagRunNightly {
|
||||
var version badversion.Version
|
||||
version, err = build_shared.ReadTagVersion()
|
||||
if err == nil {
|
||||
versionStr = version.String()
|
||||
}
|
||||
} else {
|
||||
versionStr, err = build_shared.ReadTag()
|
||||
}
|
||||
if flagRunInCI {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var versionStr string
|
||||
if version.PreReleaseIdentifier != "" {
|
||||
versionStr = version.VersionString() + "-nightly"
|
||||
} else {
|
||||
version.Patch++
|
||||
versionStr = version.VersionString() + "-nightly"
|
||||
}
|
||||
err = setGitHubEnv("version", versionStr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
tag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
os.Stdout.WriteString("unknown\n")
|
||||
} else {
|
||||
os.Stdout.WriteString(tag + "\n")
|
||||
os.Stdout.WriteString(versionStr + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,10 +13,14 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
var flagRunInCI bool
|
||||
var (
|
||||
flagRunInCI bool
|
||||
flagRunNightly bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||
}
|
||||
|
||||
func main() {
|
||||
@ -46,21 +50,23 @@ func main() {
|
||||
switch propPair[0] {
|
||||
case "VERSION_NAME":
|
||||
if propPair[1] != newVersion {
|
||||
log.Info("updated version from ", propPair[1], " to ", newVersion)
|
||||
versionUpdated = true
|
||||
propPair[1] = newVersion
|
||||
log.Info("updated version to ", newVersion)
|
||||
}
|
||||
case "GO_VERSION":
|
||||
if propPair[1] != runtime.Version() {
|
||||
log.Info("updated Go version from ", propPair[1], " to ", runtime.Version())
|
||||
goVersionUpdated = true
|
||||
propPair[1] = runtime.Version()
|
||||
log.Info("updated Go version to ", runtime.Version())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !(versionUpdated || goVersionUpdated) {
|
||||
log.Info("version not changed")
|
||||
return
|
||||
} else if flagRunInCI && !flagRunNightly {
|
||||
log.Fatal("version changed, commit changes first.")
|
||||
}
|
||||
for _, propPair := range propsList {
|
||||
switch propPair[0] {
|
||||
|
71
cmd/internal/update_certificates/main.go
Normal file
71
cmd/internal/update_certificates/main.go
Normal file
@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := updateMozillaIncludedRootCAs()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func updateMozillaIncludedRootCAs() error {
|
||||
response, err := http.Get("https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
reader := csv.NewReader(response.Body)
|
||||
header, err := reader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
geoIndex := slices.Index(header, "Geographic Focus")
|
||||
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||
certIndex := slices.Index(header, "PEM Info")
|
||||
|
||||
generated := strings.Builder{}
|
||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var mozillaIncluded *x509.CertPool
|
||||
|
||||
func init() {
|
||||
mozillaIncluded = x509.NewCertPool()
|
||||
`)
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if record[geoIndex] == "China" {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
generated.WriteString(record[nameIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||
cert := record[certIndex]
|
||||
// Remove single quotes
|
||||
cert = cert[1 : len(cert)-1]
|
||||
generated.WriteString(cert)
|
||||
generated.WriteString("`))\n")
|
||||
}
|
||||
generated.WriteString("}\n")
|
||||
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||
}
|
@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
_ "github.com/sagernet/sing-box/include"
|
||||
"github.com/sagernet/sing-box/include"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
@ -67,5 +67,5 @@ func preRun(cmd *cobra.Command, args []string) {
|
||||
if len(configPaths) == 0 && len(configDirectories) == 0 {
|
||||
configPaths = append(configPaths, "config.json")
|
||||
}
|
||||
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
|
||||
globalCtx = include.Context(service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())))
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ func format() error {
|
||||
return err
|
||||
}
|
||||
for _, optionsEntry := range optionsList {
|
||||
optionsEntry.options, err = badjson.Omitempty(optionsEntry.options)
|
||||
optionsEntry.options, err = badjson.Omitempty(globalCtx, optionsEntry.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -9,8 +9,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var pqSignatureSchemesEnabled bool
|
||||
|
||||
var commandGenerateECHKeyPair = &cobra.Command{
|
||||
Use: "ech-keypair <plain_server_name>",
|
||||
Short: "Generate TLS ECH key pair",
|
||||
@ -24,12 +22,11 @@ var commandGenerateECHKeyPair = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerateECHKeyPair.Flags().BoolVar(&pqSignatureSchemesEnabled, "pq-signature-schemes-enabled", false, "Enable PQ signature schemes")
|
||||
commandGenerate.AddCommand(commandGenerateECHKeyPair)
|
||||
}
|
||||
|
||||
func generateECHKeyPair(serverName string) error {
|
||||
configPem, keyPem, err := tls.ECHKeygenDefault(serverName, pqSignatureSchemesEnabled)
|
||||
configPem, keyPem, err := tls.ECHKeygenDefault(serverName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ func init() {
|
||||
}
|
||||
|
||||
func generateTLSKeyPair(serverName string) error {
|
||||
privateKeyPem, publicKeyPem, err := tls.GenerateKeyPair(time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
||||
privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
var commandMerge = &cobra.Command{
|
||||
Use: "merge <output>",
|
||||
Use: "merge <output-path>",
|
||||
Short: "Merge configurations",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := merge(args[0])
|
||||
@ -68,29 +68,19 @@ func merge(outputPath string) error {
|
||||
}
|
||||
|
||||
func mergePathResources(options *option.Options) error {
|
||||
for index, inbound := range options.Inbounds {
|
||||
rawOptions, err := inbound.RawOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tlsOptions, containsTLSOptions := rawOptions.(option.InboundTLSOptionsWrapper); containsTLSOptions {
|
||||
for _, inbound := range options.Inbounds {
|
||||
if tlsOptions, containsTLSOptions := inbound.Options.(option.InboundTLSOptionsWrapper); containsTLSOptions {
|
||||
tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions()))
|
||||
}
|
||||
options.Inbounds[index] = inbound
|
||||
}
|
||||
for index, outbound := range options.Outbounds {
|
||||
rawOptions, err := outbound.RawOptions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, outbound := range options.Outbounds {
|
||||
switch outbound.Type {
|
||||
case C.TypeSSH:
|
||||
outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions)
|
||||
mergeSSHOutboundOptions(outbound.Options.(*option.SSHOutboundOptions))
|
||||
}
|
||||
if tlsOptions, containsTLSOptions := rawOptions.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
|
||||
if tlsOptions, containsTLSOptions := outbound.Options.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
|
||||
tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions()))
|
||||
}
|
||||
options.Outbounds[index] = outbound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -138,13 +128,12 @@ func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.Outboun
|
||||
return options
|
||||
}
|
||||
|
||||
func mergeSSHOutboundOptions(options option.SSHOutboundOptions) option.SSHOutboundOptions {
|
||||
func mergeSSHOutboundOptions(options *option.SSHOutboundOptions) {
|
||||
if options.PrivateKeyPath != "" {
|
||||
if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil {
|
||||
options.PrivateKey = trimStringArray(strings.Split(string(content), "\n"))
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func trimStringArray(array []string) []string {
|
||||
|
@ -2,15 +2,17 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
"github.com/sagernet/sing-box/route/rule"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
@ -55,6 +57,14 @@ func ruleSetMatch(sourcePath string, domain string) error {
|
||||
if err != nil {
|
||||
return E.Cause(err, "read rule-set")
|
||||
}
|
||||
if flagRuleSetMatchFormat == "" {
|
||||
switch filepath.Ext(sourcePath) {
|
||||
case ".json":
|
||||
flagRuleSetMatchFormat = C.RuleSetFormatSource
|
||||
case ".srs":
|
||||
flagRuleSetMatchFormat = C.RuleSetFormatBinary
|
||||
}
|
||||
}
|
||||
var ruleSet option.PlainRuleSetCompat
|
||||
switch flagRuleSetMatchFormat {
|
||||
case C.RuleSetFormatSource:
|
||||
@ -83,7 +93,7 @@ func ruleSetMatch(sourcePath string, domain string) error {
|
||||
}
|
||||
for i, ruleOptions := range plainRuleSet.Rules {
|
||||
var currentRule adapter.HeadlessRule
|
||||
currentRule, err = route.NewHeadlessRule(nil, ruleOptions)
|
||||
currentRule, err = rule.NewHeadlessRule(context.Background(), ruleOptions)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||
}
|
||||
|
162
cmd/sing-box/cmd_rule_set_merge.go
Normal file
162
cmd/sing-box/cmd_rule_set_merge.go
Normal file
@ -0,0 +1,162 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
ruleSetPaths []string
|
||||
ruleSetDirectories []string
|
||||
)
|
||||
|
||||
var commandRuleSetMerge = &cobra.Command{
|
||||
Use: "merge <output-path>",
|
||||
Short: "Merge rule-set source files",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := mergeRuleSet(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetPaths, "config", "c", nil, "set input rule-set file path")
|
||||
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetDirectories, "config-directory", "C", nil, "set input rule-set directory path")
|
||||
commandRuleSet.AddCommand(commandRuleSetMerge)
|
||||
}
|
||||
|
||||
type RuleSetEntry struct {
|
||||
content []byte
|
||||
path string
|
||||
options option.PlainRuleSetCompat
|
||||
}
|
||||
|
||||
func readRuleSetAt(path string) (*RuleSetEntry, error) {
|
||||
var (
|
||||
configContent []byte
|
||||
err error
|
||||
)
|
||||
if path == "stdin" {
|
||||
configContent, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
configContent, err = os.ReadFile(path)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read config at ", path)
|
||||
}
|
||||
options, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, configContent)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode config at ", path)
|
||||
}
|
||||
return &RuleSetEntry{
|
||||
content: configContent,
|
||||
path: path,
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readRuleSet() ([]*RuleSetEntry, error) {
|
||||
var optionsList []*RuleSetEntry
|
||||
for _, path := range ruleSetPaths {
|
||||
optionsEntry, err := readRuleSetAt(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
optionsList = append(optionsList, optionsEntry)
|
||||
}
|
||||
for _, directory := range ruleSetDirectories {
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read rule-set directory at ", directory)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
optionsEntry, err := readRuleSetAt(filepath.Join(directory, entry.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
optionsList = append(optionsList, optionsEntry)
|
||||
}
|
||||
}
|
||||
sort.Slice(optionsList, func(i, j int) bool {
|
||||
return optionsList[i].path < optionsList[j].path
|
||||
})
|
||||
return optionsList, nil
|
||||
}
|
||||
|
||||
func readRuleSetAndMerge() (option.PlainRuleSetCompat, error) {
|
||||
optionsList, err := readRuleSet()
|
||||
if err != nil {
|
||||
return option.PlainRuleSetCompat{}, err
|
||||
}
|
||||
if len(optionsList) == 1 {
|
||||
return optionsList[0].options, nil
|
||||
}
|
||||
var optionVersion uint8
|
||||
for _, options := range optionsList {
|
||||
if optionVersion < options.options.Version {
|
||||
optionVersion = options.options.Version
|
||||
}
|
||||
}
|
||||
var mergedMessage json.RawMessage
|
||||
for _, options := range optionsList {
|
||||
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
|
||||
if err != nil {
|
||||
return option.PlainRuleSetCompat{}, E.Cause(err, "merge config at ", options.path)
|
||||
}
|
||||
}
|
||||
mergedOptions, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, mergedMessage)
|
||||
if err != nil {
|
||||
return option.PlainRuleSetCompat{}, E.Cause(err, "unmarshal merged config")
|
||||
}
|
||||
mergedOptions.Version = optionVersion
|
||||
return mergedOptions, nil
|
||||
}
|
||||
|
||||
func mergeRuleSet(outputPath string) error {
|
||||
mergedOptions, err := readRuleSetAndMerge()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(mergedOptions)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
if existsContent, err := os.ReadFile(outputPath); err != nil {
|
||||
if string(existsContent) == buffer.String() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
err = rw.MkdirParent(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outputPath, _ = filepath.Abs(outputPath)
|
||||
os.Stderr.WriteString(outputPath + "\n")
|
||||
return nil
|
||||
}
|
@ -61,14 +61,15 @@ func upgradeRuleSet(sourcePath string) error {
|
||||
log.Info("already up-to-date")
|
||||
return nil
|
||||
}
|
||||
plainRuleSet, err := plainRuleSetCompat.Upgrade()
|
||||
plainRuleSetCompat.Options, err = plainRuleSetCompat.Upgrade()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plainRuleSetCompat.Version = C.RuleSetVersionCurrent
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(plainRuleSet)
|
||||
err = encoder.Encode(plainRuleSetCompat)
|
||||
if err != nil {
|
||||
return E.Cause(err, "encode config")
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ func readConfigAt(path string) (*OptionsEntry, error) {
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read config at ", path)
|
||||
}
|
||||
options, err := json.UnmarshalExtended[option.Options](configContent)
|
||||
options, err := json.UnmarshalExtendedContext[option.Options](globalCtx, configContent)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode config at ", path)
|
||||
}
|
||||
@ -109,13 +109,13 @@ func readConfigAndMerge() (option.Options, error) {
|
||||
}
|
||||
var mergedMessage json.RawMessage
|
||||
for _, options := range optionsList {
|
||||
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage, false)
|
||||
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "merge config at ", options.path)
|
||||
}
|
||||
}
|
||||
var mergedOptions option.Options
|
||||
err = mergedOptions.UnmarshalJSON(mergedMessage)
|
||||
err = mergedOptions.UnmarshalJSONContext(globalCtx, mergedMessage)
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "unmarshal merged config")
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@ -23,9 +26,11 @@ func init() {
|
||||
func createPreStartedClient() (*box.Box, error) {
|
||||
options, err := readConfigAndMerge()
|
||||
if err != nil {
|
||||
if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" {
|
||||
return nil, err
|
||||
}
|
||||
instance, err := box.New(box.Options{Options: options})
|
||||
}
|
||||
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create service")
|
||||
}
|
||||
@ -36,11 +41,11 @@ func createPreStartedClient() (*box.Box, error) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) {
|
||||
func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) {
|
||||
if outboundTag == "" {
|
||||
return instance.Router().DefaultOutbound(N.NetworkName(network))
|
||||
return instance.Outbound().Default(), nil
|
||||
} else {
|
||||
outbound, loaded := instance.Router().Outbound(outboundTag)
|
||||
outbound, loaded := instance.Outbound().Outbound(outboundTag)
|
||||
if !loaded {
|
||||
return nil, E.New("outbound not found: ", outboundTag)
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ func connect(address string) error {
|
||||
return err
|
||||
}
|
||||
defer instance.Close()
|
||||
dialer, err := createDialer(instance, commandConnectFlagNetwork, commandToolsFlagOutbound)
|
||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ func fetch(args []string) error {
|
||||
httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer, err := createDialer(instance, network, commandToolsFlagOutbound)
|
||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -16,12 +16,12 @@ import (
|
||||
)
|
||||
|
||||
func initializeHTTP3Client(instance *box.Box) error {
|
||||
dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
|
||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
http3Client = &http.Client{
|
||||
Transport: &http3.RoundTripper{
|
||||
Transport: &http3.Transport{
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
destination := M.ParseSocksaddr(addr)
|
||||
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||
|
@ -4,12 +4,10 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/common/settings"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@ -45,7 +43,7 @@ func syncTime() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound)
|
||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -59,7 +57,7 @@ func syncTime() error {
|
||||
return err
|
||||
}
|
||||
if commandSyncTimeWrite {
|
||||
err = settings.SetSystemTime(response.Time)
|
||||
err = ntp.SetSystemTime(response.Time)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write time to system")
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
package adguard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
"github.com/sagernet/sing-box/route/rule"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -26,7 +27,7 @@ example.arpa
|
||||
`))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rules, 1)
|
||||
rule, err := route.NewHeadlessRule(nil, rules[0])
|
||||
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
||||
require.NoError(t, err)
|
||||
matchDomain := []string{
|
||||
"example.org",
|
||||
@ -85,7 +86,7 @@ func TestHosts(t *testing.T) {
|
||||
`))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rules, 1)
|
||||
rule, err := route.NewHeadlessRule(nil, rules[0])
|
||||
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
||||
require.NoError(t, err)
|
||||
matchDomain := []string{
|
||||
"google.com",
|
||||
@ -115,7 +116,7 @@ www.example.org
|
||||
`))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rules, 1)
|
||||
rule, err := route.NewHeadlessRule(nil, rules[0])
|
||||
rule, err := rule.NewHeadlessRule(context.Background(), rules[0])
|
||||
require.NoError(t, err)
|
||||
matchDomain := []string{
|
||||
"example.com",
|
||||
|
@ -1,31 +0,0 @@
|
||||
//go:build go1.21 && !without_badtls && with_ech
|
||||
|
||||
package badtls
|
||||
|
||||
import (
|
||||
"net"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/sagernet/cloudflare-tls"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
||||
tlsConn, loaded := common.Cast[*tls.Conn](conn)
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
return true, func() error {
|
||||
return echReadRecord(tlsConn)
|
||||
}, func() error {
|
||||
return echHandlePostHandshakeMessage(tlsConn)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
|
||||
func echReadRecord(c *tls.Conn) error
|
||||
|
||||
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
|
||||
func echHandlePostHandshakeMessage(c *tls.Conn) error
|
@ -7,7 +7,8 @@ import (
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/utls"
|
||||
|
||||
"github.com/metacubex/utls"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -24,8 +25,8 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
//go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord
|
||||
//go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord
|
||||
func utlsReadRecord(c *tls.Conn) error
|
||||
|
||||
//go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage
|
||||
//go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage
|
||||
func utlsHandlePostHandshakeMessage(c *tls.Conn) error
|
||||
|
4394
common/certificate/mozilla.go
Normal file
4394
common/certificate/mozilla.go
Normal file
File diff suppressed because it is too large
Load Diff
185
common/certificate/store.go
Normal file
185
common/certificate/store.go
Normal file
@ -0,0 +1,185 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/fswatch"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ adapter.CertificateStore = (*Store)(nil)
|
||||
|
||||
type Store struct {
|
||||
systemPool *x509.CertPool
|
||||
currentPool *x509.CertPool
|
||||
certificate string
|
||||
certificatePaths []string
|
||||
certificateDirectoryPaths []string
|
||||
watcher *fswatch.Watcher
|
||||
}
|
||||
|
||||
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
||||
var systemPool *x509.CertPool
|
||||
switch options.Store {
|
||||
case C.CertificateStoreSystem, "":
|
||||
systemPool = x509.NewCertPool()
|
||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||
var systemValid bool
|
||||
if platformInterface != nil {
|
||||
for _, cert := range platformInterface.SystemCertificates() {
|
||||
if systemPool.AppendCertsFromPEM([]byte(cert)) {
|
||||
systemValid = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !systemValid {
|
||||
certPool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
systemPool = certPool
|
||||
}
|
||||
case C.CertificateStoreMozilla:
|
||||
systemPool = mozillaIncluded
|
||||
case C.CertificateStoreNone:
|
||||
systemPool = nil
|
||||
default:
|
||||
return nil, E.New("unknown certificate store: ", options.Store)
|
||||
}
|
||||
store := &Store{
|
||||
systemPool: systemPool,
|
||||
certificate: strings.Join(options.Certificate, "\n"),
|
||||
certificatePaths: options.CertificatePath,
|
||||
certificateDirectoryPaths: options.CertificateDirectoryPath,
|
||||
}
|
||||
var watchPaths []string
|
||||
for _, target := range options.CertificatePath {
|
||||
watchPaths = append(watchPaths, target)
|
||||
}
|
||||
for _, target := range options.CertificateDirectoryPath {
|
||||
watchPaths = append(watchPaths, target)
|
||||
}
|
||||
if len(watchPaths) > 0 {
|
||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||
Path: watchPaths,
|
||||
Logger: logger,
|
||||
Callback: func(_ string) {
|
||||
err := store.update()
|
||||
if err != nil {
|
||||
logger.Error(E.Cause(err, "reload certificates"))
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "fswatch: create fsnotify watcher")
|
||||
}
|
||||
store.watcher = watcher
|
||||
}
|
||||
err := store.update()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initializing certificate store")
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (s *Store) Name() string {
|
||||
return "certificate"
|
||||
}
|
||||
|
||||
func (s *Store) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
if s.watcher != nil {
|
||||
return s.watcher.Start()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) Close() error {
|
||||
if s.watcher != nil {
|
||||
return s.watcher.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) Pool() *x509.CertPool {
|
||||
return s.currentPool
|
||||
}
|
||||
|
||||
func (s *Store) update() error {
|
||||
var currentPool *x509.CertPool
|
||||
if s.systemPool == nil {
|
||||
currentPool = x509.NewCertPool()
|
||||
} else {
|
||||
currentPool = s.systemPool.Clone()
|
||||
}
|
||||
if s.certificate != "" {
|
||||
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||
return E.New("invalid certificate PEM strings")
|
||||
}
|
||||
}
|
||||
for _, path := range s.certificatePaths {
|
||||
pemContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||
return E.New("invalid certificate PEM file: ", path)
|
||||
}
|
||||
}
|
||||
var firstErr error
|
||||
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||
directoryEntries, err := readUniqueDirectoryEntries(directoryPath)
|
||||
if err != nil {
|
||||
if firstErr == nil && !os.IsNotExist(err) {
|
||||
firstErr = E.Cause(err, "invalid certificate directory: ", directoryPath)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, directoryEntry := range directoryEntries {
|
||||
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||
if err == nil {
|
||||
currentPool.AppendCertsFromPEM(pemContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
if firstErr != nil {
|
||||
return firstErr
|
||||
}
|
||||
s.currentPool = currentPool
|
||||
return nil
|
||||
}
|
||||
|
||||
func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) {
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uniq := files[:0]
|
||||
for _, f := range files {
|
||||
if !isSameDirSymlink(f, dir) {
|
||||
uniq = append(uniq, f)
|
||||
}
|
||||
}
|
||||
return uniq, nil
|
||||
}
|
||||
|
||||
func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
||||
if f.Type()&fs.ModeSymlink == 0 {
|
||||
return false
|
||||
}
|
||||
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||
return err == nil && !strings.Contains(target, "/")
|
||||
}
|
@ -2,20 +2,31 @@ package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/conntrack"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ WireGuardListener = (*DefaultDialer)(nil)
|
||||
var (
|
||||
_ ParallelInterfaceDialer = (*DefaultDialer)(nil)
|
||||
_ WireGuardListener = (*DefaultDialer)(nil)
|
||||
)
|
||||
|
||||
type DefaultDialer struct {
|
||||
dialer4 tcpDialer
|
||||
@ -25,50 +36,95 @@ type DefaultDialer struct {
|
||||
udpListener net.ListenConfig
|
||||
udpAddr4 string
|
||||
udpAddr6 string
|
||||
isWireGuardListener bool
|
||||
netns string
|
||||
networkManager adapter.NetworkManager
|
||||
networkStrategy *C.NetworkStrategy
|
||||
defaultNetworkStrategy bool
|
||||
networkType []C.InterfaceType
|
||||
fallbackNetworkType []C.InterfaceType
|
||||
networkFallbackDelay time.Duration
|
||||
networkLastFallback atomic.TypedValue[time.Time]
|
||||
}
|
||||
|
||||
func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) {
|
||||
var dialer net.Dialer
|
||||
var listener net.ListenConfig
|
||||
if options.BindInterface != "" {
|
||||
var interfaceFinder control.InterfaceFinder
|
||||
if router != nil {
|
||||
interfaceFinder = router.InterfaceFinder()
|
||||
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||
|
||||
var (
|
||||
dialer net.Dialer
|
||||
listener net.ListenConfig
|
||||
interfaceFinder control.InterfaceFinder
|
||||
networkStrategy *C.NetworkStrategy
|
||||
defaultNetworkStrategy bool
|
||||
networkType []C.InterfaceType
|
||||
fallbackNetworkType []C.InterfaceType
|
||||
networkFallbackDelay time.Duration
|
||||
)
|
||||
if networkManager != nil {
|
||||
interfaceFinder = networkManager.InterfaceFinder()
|
||||
} else {
|
||||
interfaceFinder = control.NewDefaultInterfaceFinder()
|
||||
}
|
||||
if options.BindInterface != "" {
|
||||
if !(C.IsLinux || C.IsDarwin || C.IsWindows) {
|
||||
return nil, E.New("`bind_interface` is only supported on Linux, macOS and Windows")
|
||||
}
|
||||
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
} else if router != nil && router.AutoDetectInterface() {
|
||||
bindFunc := router.AutoDetectInterfaceFunc()
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
} else if router != nil && router.DefaultInterface() != "" {
|
||||
bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1)
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
}
|
||||
var autoRedirectOutputMark uint32
|
||||
if router != nil {
|
||||
autoRedirectOutputMark = router.AutoRedirectOutputMark()
|
||||
}
|
||||
if autoRedirectOutputMark > 0 {
|
||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
|
||||
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
|
||||
}
|
||||
if options.RoutingMark > 0 {
|
||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
|
||||
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
|
||||
if autoRedirectOutputMark > 0 {
|
||||
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`")
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("`routing_mark` is only supported on Linux")
|
||||
}
|
||||
} else if router != nil && router.DefaultMark() > 0 {
|
||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark()))
|
||||
listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
|
||||
if autoRedirectOutputMark > 0 {
|
||||
return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`")
|
||||
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
|
||||
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
|
||||
}
|
||||
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
|
||||
if disableDefaultBind || options.TCPFastOpen {
|
||||
if options.NetworkStrategy != nil || len(options.NetworkType) > 0 && options.FallbackNetworkType == nil && options.FallbackDelay == 0 {
|
||||
return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address`, `inet6_bind_address` and `tcp_fast_open`")
|
||||
}
|
||||
}
|
||||
|
||||
if networkManager != nil {
|
||||
defaultOptions := networkManager.DefaultOptions()
|
||||
if !disableDefaultBind {
|
||||
if defaultOptions.BindInterface != "" {
|
||||
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
} else if networkManager.AutoDetectInterface() {
|
||||
if platformInterface != nil {
|
||||
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
|
||||
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
|
||||
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
|
||||
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
|
||||
networkStrategy = defaultOptions.NetworkStrategy
|
||||
networkType = defaultOptions.NetworkType
|
||||
fallbackNetworkType = defaultOptions.FallbackNetworkType
|
||||
}
|
||||
networkFallbackDelay = time.Duration(options.FallbackDelay)
|
||||
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
|
||||
networkFallbackDelay = defaultOptions.FallbackDelay
|
||||
}
|
||||
if networkStrategy == nil {
|
||||
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
|
||||
defaultNetworkStrategy = true
|
||||
}
|
||||
bindFunc := networkManager.ProtectFunc()
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
} else {
|
||||
bindFunc := networkManager.AutoDetectInterfaceFunc()
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
}
|
||||
}
|
||||
}
|
||||
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
|
||||
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
||||
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
||||
}
|
||||
}
|
||||
if options.ReuseAddr {
|
||||
@ -102,7 +158,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
||||
udpAddr4 string
|
||||
)
|
||||
if options.Inet4BindAddress != nil {
|
||||
bindAddr := options.Inet4BindAddress.Build()
|
||||
bindAddr := options.Inet4BindAddress.Build(netip.IPv4Unspecified())
|
||||
dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
|
||||
udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
||||
udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String()
|
||||
@ -113,7 +169,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
||||
udpAddr6 string
|
||||
)
|
||||
if options.Inet6BindAddress != nil {
|
||||
bindAddr := options.Inet6BindAddress.Build()
|
||||
bindAddr := options.Inet6BindAddress.Build(netip.IPv6Unspecified())
|
||||
dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
|
||||
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
|
||||
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
|
||||
@ -124,11 +180,6 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
||||
}
|
||||
setMultiPathTCP(&dialer4)
|
||||
}
|
||||
if options.IsWireGuardListener {
|
||||
for _, controlFn := range wgControlFns {
|
||||
listener.Control = control.Append(listener.Control, controlFn)
|
||||
}
|
||||
}
|
||||
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -138,48 +189,175 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
|
||||
return nil, err
|
||||
}
|
||||
return &DefaultDialer{
|
||||
tcpDialer4,
|
||||
tcpDialer6,
|
||||
udpDialer4,
|
||||
udpDialer6,
|
||||
listener,
|
||||
udpAddr4,
|
||||
udpAddr6,
|
||||
options.IsWireGuardListener,
|
||||
dialer4: tcpDialer4,
|
||||
dialer6: tcpDialer6,
|
||||
udpDialer4: udpDialer4,
|
||||
udpDialer6: udpDialer6,
|
||||
udpListener: listener,
|
||||
udpAddr4: udpAddr4,
|
||||
udpAddr6: udpAddr6,
|
||||
netns: options.NetNs,
|
||||
networkManager: networkManager,
|
||||
networkStrategy: networkStrategy,
|
||||
defaultNetworkStrategy: defaultNetworkStrategy,
|
||||
networkType: networkType,
|
||||
fallbackNetworkType: fallbackNetworkType,
|
||||
networkFallbackDelay: networkFallbackDelay,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefault bool) control.Func {
|
||||
if networkManager == nil {
|
||||
return control.RoutingMark(mark)
|
||||
}
|
||||
return func(network, address string, conn syscall.RawConn) error {
|
||||
if networkManager.AutoRedirectOutputMark() != 0 {
|
||||
if isDefault {
|
||||
return E.New("`route.default_mark` is conflict with `tun.auto_redirect`")
|
||||
} else {
|
||||
return E.New("`routing_mark` is conflict with `tun.auto_redirect`")
|
||||
}
|
||||
}
|
||||
return control.RoutingMark(mark)(network, address, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
||||
if !address.IsValid() {
|
||||
return nil, E.New("invalid address")
|
||||
} else if address.IsFqdn() {
|
||||
return nil, E.New("domain not resolved")
|
||||
}
|
||||
if d.networkStrategy == nil {
|
||||
return trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkUDP:
|
||||
if !address.IsIPv6() {
|
||||
return trackConn(d.udpDialer4.DialContext(ctx, network, address.String()))
|
||||
return d.udpDialer4.DialContext(ctx, network, address.String())
|
||||
} else {
|
||||
return trackConn(d.udpDialer6.DialContext(ctx, network, address.String()))
|
||||
return d.udpDialer6.DialContext(ctx, network, address.String())
|
||||
}
|
||||
}
|
||||
if !address.IsIPv6() {
|
||||
return trackConn(DialSlowContext(&d.dialer4, ctx, network, address))
|
||||
return DialSlowContext(&d.dialer4, ctx, network, address)
|
||||
} else {
|
||||
return trackConn(DialSlowContext(&d.dialer6, ctx, network, address))
|
||||
return DialSlowContext(&d.dialer6, ctx, network, address)
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||
if strategy == nil {
|
||||
strategy = d.networkStrategy
|
||||
}
|
||||
if strategy == nil {
|
||||
return d.DialContext(ctx, network, address)
|
||||
}
|
||||
if len(interfaceType) == 0 {
|
||||
interfaceType = d.networkType
|
||||
}
|
||||
if len(fallbackInterfaceType) == 0 {
|
||||
fallbackInterfaceType = d.fallbackNetworkType
|
||||
}
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = d.networkFallbackDelay
|
||||
}
|
||||
var dialer net.Dialer
|
||||
if N.NetworkName(network) == N.NetworkTCP {
|
||||
dialer = dialerFromTCPDialer(d.dialer4)
|
||||
} else {
|
||||
dialer = d.udpDialer4
|
||||
}
|
||||
fastFallback := time.Now().Sub(d.networkLastFallback.Load()) < C.TCPTimeout
|
||||
var (
|
||||
conn net.Conn
|
||||
isPrimary bool
|
||||
err error
|
||||
)
|
||||
if !fastFallback {
|
||||
conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
} else {
|
||||
conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store)
|
||||
}
|
||||
if err != nil {
|
||||
// bind interface failed on legacy xiaomi systems
|
||||
if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) {
|
||||
d.networkStrategy = nil
|
||||
return d.DialContext(ctx, network, address)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !fastFallback && !isPrimary {
|
||||
d.networkLastFallback.Store(time.Now())
|
||||
}
|
||||
return trackConn(conn, nil)
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
if d.networkStrategy == nil {
|
||||
return trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
|
||||
if destination.IsIPv6() {
|
||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
|
||||
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
|
||||
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4))
|
||||
return d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4)
|
||||
} else {
|
||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4))
|
||||
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||
if strategy == nil {
|
||||
strategy = d.networkStrategy
|
||||
}
|
||||
if strategy == nil {
|
||||
return d.ListenPacket(ctx, destination)
|
||||
}
|
||||
if len(interfaceType) == 0 {
|
||||
interfaceType = d.networkType
|
||||
}
|
||||
if len(fallbackInterfaceType) == 0 {
|
||||
fallbackInterfaceType = d.fallbackNetworkType
|
||||
}
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = d.networkFallbackDelay
|
||||
}
|
||||
network := N.NetworkUDP
|
||||
if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||
network += "4"
|
||||
}
|
||||
packetConn, err := d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", *strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
if err != nil {
|
||||
// bind interface failed on legacy xiaomi systems
|
||||
if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) {
|
||||
d.networkStrategy = nil
|
||||
return d.ListenPacket(ctx, destination)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return trackPacketConn(packetConn, nil)
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
|
||||
return d.udpListener.ListenPacket(context.Background(), network, address)
|
||||
udpListener := d.udpListener
|
||||
udpListener.Control = control.Append(udpListener.Control, func(network, address string, conn syscall.RawConn) error {
|
||||
for _, wgControlFn := range WgControlFns {
|
||||
err := wgControlFn(network, address, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return udpListener.ListenPacket(context.Background(), network, address)
|
||||
}
|
||||
|
||||
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
||||
|
@ -13,3 +13,7 @@ type tcpDialer = tfo.Dialer
|
||||
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
||||
return tfo.Dialer{Dialer: dialer, DisableTFO: !tfoEnabled}, nil
|
||||
}
|
||||
|
||||
func dialerFromTCPDialer(dialer tcpDialer) net.Dialer {
|
||||
return dialer.Dialer
|
||||
}
|
||||
|
@ -16,3 +16,7 @@ func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
||||
}
|
||||
return dialer, nil
|
||||
}
|
||||
|
||||
func dialerFromTCPDialer(dialer tcpDialer) net.Dialer {
|
||||
return dialer
|
||||
}
|
||||
|
244
common/dialer/default_parallel_interface.go
Normal file
244
common/dialer/default_parallel_interface.go
Normal file
@ -0,0 +1,244 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, bool, error) {
|
||||
primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType)
|
||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||
return nil, false, E.New("no available network interface")
|
||||
}
|
||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = N.DefaultFallbackDelay
|
||||
}
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
type dialResult struct {
|
||||
net.Conn
|
||||
error
|
||||
primary bool
|
||||
}
|
||||
results := make(chan dialResult) // unbuffered
|
||||
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
||||
perNetDialer := dialer
|
||||
if defaultInterface == nil || iif.Index != defaultInterface.Index {
|
||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||
}
|
||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
select {
|
||||
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}:
|
||||
case <-returned:
|
||||
}
|
||||
} else {
|
||||
select {
|
||||
case results <- dialResult{Conn: conn, primary: primary}:
|
||||
case <-returned:
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
primaryCtx, primaryCancel := context.WithCancel(ctx)
|
||||
defer primaryCancel()
|
||||
for _, iif := range primaryInterfaces {
|
||||
go startRacer(primaryCtx, true, iif)
|
||||
}
|
||||
var (
|
||||
fallbackTimer *time.Timer
|
||||
fallbackChan <-chan time.Time
|
||||
)
|
||||
if len(fallbackInterfaces) > 0 {
|
||||
fallbackTimer = time.NewTimer(fallbackDelay)
|
||||
defer fallbackTimer.Stop()
|
||||
fallbackChan = fallbackTimer.C
|
||||
}
|
||||
var errors []error
|
||||
for {
|
||||
select {
|
||||
case <-fallbackChan:
|
||||
fallbackCtx, fallbackCancel := context.WithCancel(ctx)
|
||||
defer fallbackCancel()
|
||||
for _, iif := range fallbackInterfaces {
|
||||
go startRacer(fallbackCtx, false, iif)
|
||||
}
|
||||
case res := <-results:
|
||||
if res.error == nil {
|
||||
return res.Conn, res.primary, nil
|
||||
}
|
||||
errors = append(errors, res.error)
|
||||
if len(errors) == len(primaryInterfaces)+len(fallbackInterfaces) {
|
||||
return nil, false, E.Errors(errors...)
|
||||
}
|
||||
if res.primary && fallbackTimer != nil && fallbackTimer.Stop() {
|
||||
fallbackTimer.Reset(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration, resetFastFallback func(time.Time)) (net.Conn, bool, error) {
|
||||
primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType)
|
||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||
return nil, false, E.New("no available network interface")
|
||||
}
|
||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = N.DefaultFallbackDelay
|
||||
}
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
type dialResult struct {
|
||||
net.Conn
|
||||
error
|
||||
primary bool
|
||||
}
|
||||
startAt := time.Now()
|
||||
results := make(chan dialResult) // unbuffered
|
||||
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
||||
perNetDialer := dialer
|
||||
if defaultInterface == nil || iif.Index != defaultInterface.Index {
|
||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||
}
|
||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
select {
|
||||
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}:
|
||||
case <-returned:
|
||||
}
|
||||
} else {
|
||||
select {
|
||||
case results <- dialResult{Conn: conn, primary: primary}:
|
||||
case <-returned:
|
||||
if primary && time.Since(startAt) <= fallbackDelay {
|
||||
resetFastFallback(time.Time{})
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, iif := range primaryInterfaces {
|
||||
go startRacer(ctx, true, iif)
|
||||
}
|
||||
fallbackCtx, fallbackCancel := context.WithCancel(ctx)
|
||||
defer fallbackCancel()
|
||||
for _, iif := range fallbackInterfaces {
|
||||
go startRacer(fallbackCtx, false, iif)
|
||||
}
|
||||
var errors []error
|
||||
for {
|
||||
select {
|
||||
case res := <-results:
|
||||
if res.error == nil {
|
||||
return res.Conn, res.primary, nil
|
||||
}
|
||||
errors = append(errors, res.error)
|
||||
if len(errors) == len(primaryInterfaces)+len(fallbackInterfaces) {
|
||||
return nil, false, E.Errors(errors...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listener net.ListenConfig, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||
primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType)
|
||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||
return nil, E.New("no available network interface")
|
||||
}
|
||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||
var errors []error
|
||||
for _, primaryInterface := range primaryInterfaces {
|
||||
perNetListener := listener
|
||||
if defaultInterface == nil || primaryInterface.Index != defaultInterface.Index {
|
||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index))
|
||||
}
|
||||
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Index, ")"))
|
||||
}
|
||||
for _, fallbackInterface := range fallbackInterfaces {
|
||||
perNetListener := listener
|
||||
if defaultInterface == nil || fallbackInterface.Index != defaultInterface.Index {
|
||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index))
|
||||
}
|
||||
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Index, ")"))
|
||||
}
|
||||
return nil, E.Errors(errors...)
|
||||
}
|
||||
|
||||
func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) {
|
||||
interfaces := networkManager.NetworkInterfaces()
|
||||
switch strategy {
|
||||
case C.NetworkStrategyDefault:
|
||||
if len(interfaceType) == 0 {
|
||||
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
|
||||
if defaultIf != nil {
|
||||
for _, iif := range interfaces {
|
||||
if iif.Index == defaultIf.Index {
|
||||
primaryInterfaces = append(primaryInterfaces, iif)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
primaryInterfaces = interfaces
|
||||
}
|
||||
} else {
|
||||
primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
||||
return common.Contains(interfaceType, it.Type)
|
||||
})
|
||||
}
|
||||
case C.NetworkStrategyHybrid:
|
||||
if len(interfaceType) == 0 {
|
||||
primaryInterfaces = interfaces
|
||||
} else {
|
||||
primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
||||
return common.Contains(interfaceType, it.Type)
|
||||
})
|
||||
}
|
||||
case C.NetworkStrategyFallback:
|
||||
if len(interfaceType) == 0 {
|
||||
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
|
||||
if defaultIf != nil {
|
||||
for _, iif := range interfaces {
|
||||
if iif.Index == defaultIf.Index {
|
||||
primaryInterfaces = append(primaryInterfaces, iif)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
primaryInterfaces = interfaces
|
||||
}
|
||||
} else {
|
||||
primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
||||
return common.Contains(interfaceType, it.Type)
|
||||
})
|
||||
}
|
||||
if len(fallbackInterfaceType) == 0 {
|
||||
fallbackInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
||||
return !common.Any(primaryInterfaces, func(iif adapter.NetworkInterface) bool {
|
||||
return it.Index == iif.Index
|
||||
})
|
||||
})
|
||||
} else {
|
||||
fallbackInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
|
||||
return common.Contains(fallbackInterfaceType, iif.Type)
|
||||
})
|
||||
}
|
||||
}
|
||||
return primaryInterfaces, fallbackInterfaces
|
||||
}
|
161
common/dialer/default_parallel_network.go
Normal file
161
common/dialer/default_parallel_network.go
Normal file
@ -0,0 +1,161 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||
if len(destinationAddresses) == 0 {
|
||||
if !destination.IsIP() {
|
||||
panic("invalid usage")
|
||||
}
|
||||
destinationAddresses = []netip.Addr{destination.Addr}
|
||||
}
|
||||
if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
|
||||
return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
}
|
||||
var errors []error
|
||||
if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel {
|
||||
for _, address := range destinationAddresses {
|
||||
conn, err := parallelDialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
errors = append(errors, err)
|
||||
}
|
||||
} else {
|
||||
for _, address := range destinationAddresses {
|
||||
conn, err := dialer.DialContext(ctx, network, M.SocksaddrFrom(address, destination.Port))
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
return nil, E.Errors(errors...)
|
||||
}
|
||||
|
||||
func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||
if len(destinationAddresses) == 0 {
|
||||
if !destination.IsIP() {
|
||||
panic("invalid usage")
|
||||
}
|
||||
destinationAddresses = []netip.Addr{destination.Addr}
|
||||
}
|
||||
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = N.DefaultFallbackDelay
|
||||
}
|
||||
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
|
||||
addresses4 := common.Filter(destinationAddresses, func(address netip.Addr) bool {
|
||||
return address.Is4() || address.Is4In6()
|
||||
})
|
||||
addresses6 := common.Filter(destinationAddresses, func(address netip.Addr) bool {
|
||||
return address.Is6() && !address.Is4In6()
|
||||
})
|
||||
if len(addresses4) == 0 || len(addresses6) == 0 {
|
||||
return DialSerialNetwork(ctx, dialer, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
}
|
||||
var primaries, fallbacks []netip.Addr
|
||||
if preferIPv6 {
|
||||
primaries = addresses6
|
||||
fallbacks = addresses4
|
||||
} else {
|
||||
primaries = addresses4
|
||||
fallbacks = addresses6
|
||||
}
|
||||
type dialResult struct {
|
||||
net.Conn
|
||||
error
|
||||
primary bool
|
||||
done bool
|
||||
}
|
||||
results := make(chan dialResult) // unbuffered
|
||||
startRacer := func(ctx context.Context, primary bool) {
|
||||
ras := primaries
|
||||
if !primary {
|
||||
ras = fallbacks
|
||||
}
|
||||
c, err := DialSerialNetwork(ctx, dialer, network, destination, ras, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
select {
|
||||
case results <- dialResult{Conn: c, error: err, primary: primary, done: true}:
|
||||
case <-returned:
|
||||
if c != nil {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
var primary, fallback dialResult
|
||||
primaryCtx, primaryCancel := context.WithCancel(ctx)
|
||||
defer primaryCancel()
|
||||
go startRacer(primaryCtx, true)
|
||||
fallbackTimer := time.NewTimer(fallbackDelay)
|
||||
defer fallbackTimer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-fallbackTimer.C:
|
||||
fallbackCtx, fallbackCancel := context.WithCancel(ctx)
|
||||
defer fallbackCancel()
|
||||
go startRacer(fallbackCtx, false)
|
||||
|
||||
case res := <-results:
|
||||
if res.error == nil {
|
||||
return res.Conn, nil
|
||||
}
|
||||
if res.primary {
|
||||
primary = res
|
||||
} else {
|
||||
fallback = res
|
||||
}
|
||||
if primary.done && fallback.done {
|
||||
return nil, primary.error
|
||||
}
|
||||
if res.primary && fallbackTimer.Stop() {
|
||||
fallbackTimer.Reset(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ListenSerialNetworkPacket(ctx context.Context, dialer N.Dialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {
|
||||
if len(destinationAddresses) == 0 {
|
||||
if !destination.IsIP() {
|
||||
panic("invalid usage")
|
||||
}
|
||||
destinationAddresses = []netip.Addr{destination.Addr}
|
||||
}
|
||||
if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
|
||||
return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
}
|
||||
var errors []error
|
||||
if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel {
|
||||
for _, address := range destinationAddresses {
|
||||
conn, err := parallelDialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
if err == nil {
|
||||
return conn, address, nil
|
||||
}
|
||||
errors = append(errors, err)
|
||||
}
|
||||
} else {
|
||||
for _, address := range destinationAddresses {
|
||||
conn, err := dialer.ListenPacket(ctx, M.SocksaddrFrom(address, destination.Port))
|
||||
if err == nil {
|
||||
return conn, address, nil
|
||||
}
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
return nil, netip.Addr{}, E.Errors(errors...)
|
||||
}
|
@ -6,37 +6,61 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type DirectDialer interface {
|
||||
IsEmpty() bool
|
||||
}
|
||||
|
||||
type DetourDialer struct {
|
||||
router adapter.Router
|
||||
outboundManager adapter.OutboundManager
|
||||
detour string
|
||||
legacyDNSDialer bool
|
||||
dialer N.Dialer
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
}
|
||||
|
||||
func NewDetour(router adapter.Router, detour string) N.Dialer {
|
||||
return &DetourDialer{router: router, detour: detour}
|
||||
func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNSDialer bool) N.Dialer {
|
||||
return &DetourDialer{
|
||||
outboundManager: outboundManager,
|
||||
detour: detour,
|
||||
legacyDNSDialer: legacyDNSDialer,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DetourDialer) Start() error {
|
||||
_, err := d.Dialer()
|
||||
return err
|
||||
func InitializeDetour(dialer N.Dialer) error {
|
||||
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
||||
if !isDetour {
|
||||
return nil
|
||||
}
|
||||
return common.Error(detourDialer.Dialer())
|
||||
}
|
||||
|
||||
func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
||||
d.initOnce.Do(func() {
|
||||
var loaded bool
|
||||
d.dialer, loaded = d.router.Outbound(d.detour)
|
||||
d.initOnce.Do(d.init)
|
||||
return d.dialer, d.initErr
|
||||
}
|
||||
|
||||
func (d *DetourDialer) init() {
|
||||
dialer, loaded := d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
return
|
||||
}
|
||||
})
|
||||
return d.dialer, d.initErr
|
||||
if !d.legacyDNSDialer {
|
||||
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
||||
if directDialer.IsEmpty() {
|
||||
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
d.dialer = dialer
|
||||
}
|
||||
|
||||
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
|
@ -1,40 +1,147 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-dns"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) {
|
||||
if options.IsWireGuardListener {
|
||||
return NewDefault(router, options)
|
||||
}
|
||||
if router == nil {
|
||||
return NewDefault(nil, options)
|
||||
}
|
||||
type Options struct {
|
||||
Context context.Context
|
||||
Options option.DialerOptions
|
||||
RemoteIsDomain bool
|
||||
DirectResolver bool
|
||||
ResolverOnDetour bool
|
||||
NewDialer bool
|
||||
LegacyDNSDialer bool
|
||||
DirectOutbound bool
|
||||
}
|
||||
|
||||
// TODO: merge with NewWithOptions
|
||||
func New(ctx context.Context, options option.DialerOptions, remoteIsDomain bool) (N.Dialer, error) {
|
||||
return NewWithOptions(Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
RemoteIsDomain: remoteIsDomain,
|
||||
})
|
||||
}
|
||||
|
||||
func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
dialOptions := options.Options
|
||||
var (
|
||||
dialer N.Dialer
|
||||
err error
|
||||
)
|
||||
if options.Detour == "" {
|
||||
dialer, err = NewDefault(router, options)
|
||||
if dialOptions.Detour != "" {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
||||
} else {
|
||||
dialer, err = NewDefault(options.Context, dialOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
dialer = NewDetour(router, options.Detour)
|
||||
}
|
||||
if options.Detour == "" {
|
||||
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||
var defaultOptions adapter.NetworkOptions
|
||||
if networkManager != nil {
|
||||
defaultOptions = networkManager.DefaultOptions()
|
||||
}
|
||||
var (
|
||||
server string
|
||||
dnsQueryOptions adapter.DNSQueryOptions
|
||||
resolveFallbackDelay time.Duration
|
||||
)
|
||||
if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" {
|
||||
var transport adapter.DNSTransport
|
||||
if !options.DirectResolver {
|
||||
var loaded bool
|
||||
transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server)
|
||||
if !loaded {
|
||||
return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server)
|
||||
}
|
||||
}
|
||||
var strategy C.DomainStrategy
|
||||
if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||
strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy)
|
||||
} else if
|
||||
//nolint:staticcheck
|
||||
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||
//nolint:staticcheck
|
||||
strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
||||
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
|
||||
}
|
||||
server = dialOptions.DomainResolver.Server
|
||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: strategy,
|
||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
}
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else if options.DirectResolver {
|
||||
return nil, E.New("missing domain resolver for domain server address")
|
||||
} else {
|
||||
if defaultOptions.DomainResolver != "" {
|
||||
dnsQueryOptions = defaultOptions.DomainResolveOptions
|
||||
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
|
||||
if !loaded {
|
||||
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
|
||||
}
|
||||
dnsQueryOptions.Transport = transport
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else {
|
||||
transports := dnsTransport.Transports()
|
||||
if len(transports) < 2 {
|
||||
dnsQueryOptions.Transport = dnsTransport.Default()
|
||||
} else if options.NewDialer {
|
||||
return nil, E.New("missing domain resolver for domain server address")
|
||||
} else if !options.DirectOutbound {
|
||||
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||
}
|
||||
}
|
||||
if
|
||||
//nolint:staticcheck
|
||||
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||
//nolint:staticcheck
|
||||
dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
||||
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
|
||||
}
|
||||
}
|
||||
dialer = NewResolveDialer(
|
||||
router,
|
||||
options.Context,
|
||||
dialer,
|
||||
options.Detour == "" && !options.TCPFastOpen,
|
||||
dns.DomainStrategy(options.DomainStrategy),
|
||||
time.Duration(options.FallbackDelay))
|
||||
dialOptions.Detour == "" && !dialOptions.TCPFastOpen,
|
||||
server,
|
||||
dnsQueryOptions,
|
||||
resolveFallbackDelay,
|
||||
)
|
||||
}
|
||||
return dialer, nil
|
||||
}
|
||||
|
||||
type ParallelInterfaceDialer interface {
|
||||
N.Dialer
|
||||
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||
ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error)
|
||||
}
|
||||
|
||||
type ParallelNetworkDialer interface {
|
||||
DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||
ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
|
||||
}
|
||||
|
@ -3,75 +3,124 @@ package dialer
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type ResolveDialer struct {
|
||||
var (
|
||||
_ N.Dialer = (*resolveDialer)(nil)
|
||||
_ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil)
|
||||
)
|
||||
|
||||
type ResolveDialer interface {
|
||||
N.Dialer
|
||||
QueryOptions() adapter.DNSQueryOptions
|
||||
}
|
||||
|
||||
type ParallelInterfaceResolveDialer interface {
|
||||
ParallelInterfaceDialer
|
||||
QueryOptions() adapter.DNSQueryOptions
|
||||
}
|
||||
|
||||
type resolveDialer struct {
|
||||
transport adapter.DNSTransportManager
|
||||
router adapter.DNSRouter
|
||||
dialer N.Dialer
|
||||
parallel bool
|
||||
router adapter.Router
|
||||
strategy dns.DomainStrategy
|
||||
server string
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
queryOptions adapter.DNSQueryOptions
|
||||
fallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func NewResolveDialer(router adapter.Router, dialer N.Dialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) *ResolveDialer {
|
||||
return &ResolveDialer{
|
||||
dialer,
|
||||
parallel,
|
||||
router,
|
||||
strategy,
|
||||
fallbackDelay,
|
||||
func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer {
|
||||
if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel {
|
||||
return &resolveParallelNetworkDialer{
|
||||
resolveDialer{
|
||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||
router: service.FromContext[adapter.DNSRouter](ctx),
|
||||
dialer: dialer,
|
||||
parallel: parallel,
|
||||
server: server,
|
||||
queryOptions: queryOptions,
|
||||
fallbackDelay: fallbackDelay,
|
||||
},
|
||||
parallelDialer,
|
||||
}
|
||||
}
|
||||
return &resolveDialer{
|
||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||
router: service.FromContext[adapter.DNSRouter](ctx),
|
||||
dialer: dialer,
|
||||
parallel: parallel,
|
||||
server: server,
|
||||
queryOptions: queryOptions,
|
||||
fallbackDelay: fallbackDelay,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ResolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
type resolveParallelNetworkDialer struct {
|
||||
resolveDialer
|
||||
dialer ParallelInterfaceDialer
|
||||
}
|
||||
|
||||
func (d *resolveDialer) initialize() error {
|
||||
d.initOnce.Do(d.initServer)
|
||||
return d.initErr
|
||||
}
|
||||
|
||||
func (d *resolveDialer) initServer() {
|
||||
if d.server == "" {
|
||||
return
|
||||
}
|
||||
transport, loaded := d.transport.Transport(d.server)
|
||||
if !loaded {
|
||||
d.initErr = E.New("domain resolver not found: " + d.server)
|
||||
return
|
||||
}
|
||||
d.queryOptions.Transport = transport
|
||||
}
|
||||
|
||||
func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
err := d.initialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
return d.dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
metadata.Destination = destination
|
||||
metadata.Domain = ""
|
||||
var addresses []netip.Addr
|
||||
var err error
|
||||
if d.strategy == dns.DomainStrategyAsIS {
|
||||
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
||||
} else {
|
||||
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
||||
}
|
||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.parallel {
|
||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||
} else {
|
||||
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ResolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
err := d.initialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
return d.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
metadata.Destination = destination
|
||||
metadata.Domain = ""
|
||||
var addresses []netip.Addr
|
||||
var err error
|
||||
if d.strategy == dns.DomainStrategyAsIS {
|
||||
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
||||
} else {
|
||||
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
||||
}
|
||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -82,6 +131,64 @@ func (d *ResolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||
}
|
||||
|
||||
func (d *ResolveDialer) Upstream() any {
|
||||
func (d *resolveDialer) QueryOptions() adapter.DNSQueryOptions {
|
||||
return d.queryOptions
|
||||
}
|
||||
|
||||
func (d *resolveDialer) Upstream() any {
|
||||
return d.dialer
|
||||
}
|
||||
|
||||
func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||
err := d.initialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
return d.dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = d.fallbackDelay
|
||||
}
|
||||
if d.parallel {
|
||||
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
} else {
|
||||
return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||
err := d.initialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
return d.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = d.fallbackDelay
|
||||
}
|
||||
conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||
}
|
||||
|
||||
func (d *resolveParallelNetworkDialer) QueryOptions() adapter.DNSQueryOptions {
|
||||
return d.queryOptions
|
||||
}
|
||||
|
||||
func (d *resolveParallelNetworkDialer) Upstream() any {
|
||||
return d.dialer
|
||||
}
|
||||
|
@ -7,32 +7,27 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type RouterDialer struct {
|
||||
router adapter.Router
|
||||
type DefaultOutboundDialer struct {
|
||||
outbound adapter.OutboundManager
|
||||
}
|
||||
|
||||
func NewRouter(router adapter.Router) N.Dialer {
|
||||
return &RouterDialer{router: router}
|
||||
}
|
||||
|
||||
func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
dialer, err := d.router.DefaultOutbound(network)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func NewDefaultOutbound(ctx context.Context) N.Dialer {
|
||||
return &DefaultOutboundDialer{
|
||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||
}
|
||||
return dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
|
||||
func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
dialer, err := d.router.DefaultOutbound(N.NetworkUDP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dialer.ListenPacket(ctx, destination)
|
||||
func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
return d.outbound.Default().DialContext(ctx, network, destination)
|
||||
}
|
||||
|
||||
func (d *RouterDialer) Upstream() any {
|
||||
return d.router
|
||||
func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return d.outbound.Default().ListenPacket(ctx, destination)
|
||||
}
|
||||
|
||||
func (d *DefaultOutboundDialer) Upstream() any {
|
||||
return d.outbound.Default()
|
||||
}
|
||||
|
@ -10,9 +10,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
@ -26,7 +24,9 @@ type slowOpenConn struct {
|
||||
destination M.Socksaddr
|
||||
conn net.Conn
|
||||
create chan struct{}
|
||||
done chan struct{}
|
||||
access sync.Mutex
|
||||
closeOnce sync.Once
|
||||
err error
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, des
|
||||
network: network,
|
||||
destination: destination,
|
||||
create: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -55,8 +56,8 @@ func (c *slowOpenConn) Read(b []byte) (n int, err error) {
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
case <-c.done:
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
}
|
||||
return c.conn.Read(b)
|
||||
@ -74,12 +75,15 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||
return 0, c.err
|
||||
}
|
||||
return c.conn.Write(b)
|
||||
case <-c.done:
|
||||
return 0, os.ErrClosed
|
||||
default:
|
||||
}
|
||||
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
|
||||
conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
|
||||
if err != nil {
|
||||
c.conn = nil
|
||||
c.err = E.Cause(err, "dial tcp fast open")
|
||||
c.err = err
|
||||
} else {
|
||||
c.conn = conn
|
||||
}
|
||||
n = len(b)
|
||||
close(c.create)
|
||||
@ -87,7 +91,13 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) Close() error {
|
||||
return common.Close(c.conn)
|
||||
c.closeOnce.Do(func() {
|
||||
close(c.done)
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *slowOpenConn) LocalAddr() net.Addr {
|
||||
@ -152,8 +162,8 @@ func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
|
||||
if c.err != nil {
|
||||
return 0, c.err
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
return 0, c.ctx.Err()
|
||||
case <-c.done:
|
||||
return 0, c.err
|
||||
}
|
||||
}
|
||||
return bufio.Copy(w, c.conn)
|
||||
|
@ -2,8 +2,12 @@ package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/control"
|
||||
)
|
||||
|
||||
type WireGuardListener interface {
|
||||
ListenPacketCompat(network, address string) (net.PacketConn, error)
|
||||
}
|
||||
|
||||
var WgControlFns []control.Func
|
||||
|
@ -1,11 +0,0 @@
|
||||
//go:build with_wireguard
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"github.com/sagernet/wireguard-go/conn"
|
||||
)
|
||||
|
||||
var _ WireGuardListener = (conn.Listener)(nil)
|
||||
|
||||
var wgControlFns = conn.ControlFns
|
@ -1,9 +0,0 @@
|
||||
//go:build !with_wireguard
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing/common/control"
|
||||
)
|
||||
|
||||
var wgControlFns []control.Func
|
@ -1,158 +0,0 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// IEC Sizes.
|
||||
// kibis of bits
|
||||
const (
|
||||
Byte = 1 << (iota * 10)
|
||||
KiByte
|
||||
MiByte
|
||||
GiByte
|
||||
TiByte
|
||||
PiByte
|
||||
EiByte
|
||||
)
|
||||
|
||||
// SI Sizes.
|
||||
const (
|
||||
IByte = 1
|
||||
KByte = IByte * 1000
|
||||
MByte = KByte * 1000
|
||||
GByte = MByte * 1000
|
||||
TByte = GByte * 1000
|
||||
PByte = TByte * 1000
|
||||
EByte = PByte * 1000
|
||||
)
|
||||
|
||||
var defaultSizeTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"kib": KiByte,
|
||||
"kb": KByte,
|
||||
"mib": MiByte,
|
||||
"mb": MByte,
|
||||
"gib": GiByte,
|
||||
"gb": GByte,
|
||||
"tib": TiByte,
|
||||
"tb": TByte,
|
||||
"pib": PiByte,
|
||||
"pb": PByte,
|
||||
"eib": EiByte,
|
||||
"eb": EByte,
|
||||
// Without suffix
|
||||
"": Byte,
|
||||
"ki": KiByte,
|
||||
"k": KByte,
|
||||
"mi": MiByte,
|
||||
"m": MByte,
|
||||
"gi": GiByte,
|
||||
"g": GByte,
|
||||
"ti": TiByte,
|
||||
"t": TByte,
|
||||
"pi": PiByte,
|
||||
"p": PByte,
|
||||
"ei": EiByte,
|
||||
"e": EByte,
|
||||
}
|
||||
|
||||
var memorysSizeTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"kb": KiByte,
|
||||
"mb": MiByte,
|
||||
"gb": GiByte,
|
||||
"tb": TiByte,
|
||||
"pb": PiByte,
|
||||
"eb": EiByte,
|
||||
"": Byte,
|
||||
"k": KiByte,
|
||||
"m": MiByte,
|
||||
"g": GiByte,
|
||||
"t": TiByte,
|
||||
"p": PiByte,
|
||||
"e": EiByte,
|
||||
}
|
||||
|
||||
var (
|
||||
defaultSizes = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
iSizes = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
)
|
||||
|
||||
func Bytes(s uint64) string {
|
||||
return humanateBytes(s, 1000, defaultSizes)
|
||||
}
|
||||
|
||||
func MemoryBytes(s uint64) string {
|
||||
return humanateBytes(s, 1024, defaultSizes)
|
||||
}
|
||||
|
||||
func IBytes(s uint64) string {
|
||||
return humanateBytes(s, 1024, iSizes)
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
func ParseBytes(s string) (uint64, error) {
|
||||
return parseBytes0(s, defaultSizeTable)
|
||||
}
|
||||
|
||||
func ParseMemoryBytes(s string) (uint64, error) {
|
||||
return parseBytes0(s, memorysSizeTable)
|
||||
}
|
||||
|
||||
func parseBytes0(s string, sizeTable map[string]uint64) (uint64, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(num, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := sizeTable[extra]; ok {
|
||||
f *= float64(m)
|
||||
if f >= math.MaxUint64 {
|
||||
return 0, fmt.Errorf("too large: %v", s)
|
||||
}
|
||||
return uint64(f), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
171
common/listener/listener.go
Normal file
171
common/listener/listener.go
Normal file
@ -0,0 +1,171 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/settings"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/vishvananda/netns"
|
||||
)
|
||||
|
||||
type Listener struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
network []string
|
||||
listenOptions option.ListenOptions
|
||||
connHandler adapter.ConnectionHandlerEx
|
||||
packetHandler adapter.PacketHandlerEx
|
||||
oobPacketHandler adapter.OOBPacketHandlerEx
|
||||
threadUnsafePacketWriter bool
|
||||
disablePacketOutput bool
|
||||
setSystemProxy bool
|
||||
systemProxySOCKS bool
|
||||
tproxy bool
|
||||
|
||||
tcpListener net.Listener
|
||||
systemProxy settings.SystemProxy
|
||||
udpConn *net.UDPConn
|
||||
udpAddr M.Socksaddr
|
||||
packetOutbound chan *N.PacketBuffer
|
||||
packetOutboundClosed chan struct{}
|
||||
shutdown atomic.Bool
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
Network []string
|
||||
Listen option.ListenOptions
|
||||
ConnectionHandler adapter.ConnectionHandlerEx
|
||||
PacketHandler adapter.PacketHandlerEx
|
||||
OOBPacketHandler adapter.OOBPacketHandlerEx
|
||||
ThreadUnsafePacketWriter bool
|
||||
DisablePacketOutput bool
|
||||
SetSystemProxy bool
|
||||
SystemProxySOCKS bool
|
||||
TProxy bool
|
||||
}
|
||||
|
||||
func New(
|
||||
options Options,
|
||||
) *Listener {
|
||||
return &Listener{
|
||||
ctx: options.Context,
|
||||
logger: options.Logger,
|
||||
network: options.Network,
|
||||
listenOptions: options.Listen,
|
||||
connHandler: options.ConnectionHandler,
|
||||
packetHandler: options.PacketHandler,
|
||||
oobPacketHandler: options.OOBPacketHandler,
|
||||
threadUnsafePacketWriter: options.ThreadUnsafePacketWriter,
|
||||
disablePacketOutput: options.DisablePacketOutput,
|
||||
setSystemProxy: options.SetSystemProxy,
|
||||
systemProxySOCKS: options.SystemProxySOCKS,
|
||||
tproxy: options.TProxy,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) Start() error {
|
||||
if common.Contains(l.network, N.NetworkTCP) {
|
||||
_, err := l.ListenTCP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go l.loopTCPIn()
|
||||
}
|
||||
if common.Contains(l.network, N.NetworkUDP) {
|
||||
_, err := l.ListenUDP()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.packetOutboundClosed = make(chan struct{})
|
||||
l.packetOutbound = make(chan *N.PacketBuffer, 64)
|
||||
go l.loopUDPIn()
|
||||
if !l.disablePacketOutput {
|
||||
go l.loopUDPOut()
|
||||
}
|
||||
}
|
||||
if l.setSystemProxy {
|
||||
listenPort := M.SocksaddrFromNet(l.tcpListener.Addr()).Port
|
||||
var listenAddrString string
|
||||
listenAddr := l.listenOptions.Listen.Build(netip.IPv4Unspecified())
|
||||
if listenAddr.IsUnspecified() {
|
||||
listenAddrString = "127.0.0.1"
|
||||
} else {
|
||||
listenAddrString = listenAddr.String()
|
||||
}
|
||||
systemProxy, err := settings.NewSystemProxy(l.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), l.systemProxySOCKS)
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize system proxy")
|
||||
}
|
||||
err = systemProxy.Enable()
|
||||
if err != nil {
|
||||
return E.Cause(err, "set system proxy")
|
||||
}
|
||||
l.systemProxy = systemProxy
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
l.shutdown.Store(true)
|
||||
var err error
|
||||
if l.systemProxy != nil && l.systemProxy.IsEnabled() {
|
||||
err = l.systemProxy.Disable()
|
||||
}
|
||||
return E.Errors(err, common.Close(
|
||||
l.tcpListener,
|
||||
common.PtrOrNil(l.udpConn),
|
||||
))
|
||||
}
|
||||
|
||||
func (l *Listener) TCPListener() net.Listener {
|
||||
return l.tcpListener
|
||||
}
|
||||
|
||||
func (l *Listener) UDPConn() *net.UDPConn {
|
||||
return l.udpConn
|
||||
}
|
||||
|
||||
func (l *Listener) ListenOptions() option.ListenOptions {
|
||||
return l.listenOptions
|
||||
}
|
||||
|
||||
func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (T, error) {
|
||||
if nameOrPath != "" {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
currentNs, err := netns.Get()
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "get current netns")
|
||||
}
|
||||
defer netns.Set(currentNs)
|
||||
var targetNs netns.NsHandle
|
||||
if strings.HasPrefix(nameOrPath, "/") {
|
||||
targetNs, err = netns.GetFromPath(nameOrPath)
|
||||
} else {
|
||||
targetNs, err = netns.GetFromName(nameOrPath)
|
||||
}
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath)
|
||||
}
|
||||
defer targetNs.Close()
|
||||
err = netns.Set(targetNs)
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath)
|
||||
}
|
||||
}
|
||||
return block()
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
//go:build go1.21
|
||||
|
||||
package inbound
|
||||
package listener
|
||||
|
||||
import "net"
|
||||
|
16
common/listener/listener_go123.go
Normal file
16
common/listener/listener_go123.go
Normal file
@ -0,0 +1,16 @@
|
||||
//go:build go1.23
|
||||
|
||||
package listener
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
|
||||
listener.KeepAliveConfig = net.KeepAliveConfig{
|
||||
Enable: true,
|
||||
Idle: idle,
|
||||
Interval: interval,
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
//go:build !go1.21
|
||||
|
||||
package inbound
|
||||
package listener
|
||||
|
||||
import "net"
|
||||
|
15
common/listener/listener_nongo123.go
Normal file
15
common/listener/listener_nongo123.go
Normal file
@ -0,0 +1,15 @@
|
||||
//go:build !go1.23
|
||||
|
||||
package listener
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/control"
|
||||
)
|
||||
|
||||
func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
|
||||
listener.KeepAlive = idle
|
||||
listener.Control = control.Append(listener.Control, control.SetKeepAlivePeriod(idle, interval))
|
||||
}
|
108
common/listener/listener_tcp.go
Normal file
108
common/listener/listener_tcp.go
Normal file
@ -0,0 +1,108 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/redir"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/metacubex/tfo-go"
|
||||
)
|
||||
|
||||
func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
//nolint:staticcheck
|
||||
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
|
||||
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
|
||||
}
|
||||
var err error
|
||||
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
|
||||
var listenConfig net.ListenConfig
|
||||
if l.listenOptions.BindInterface != "" {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
|
||||
}
|
||||
if l.listenOptions.RoutingMark != 0 {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
|
||||
}
|
||||
if l.listenOptions.ReuseAddr {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
||||
}
|
||||
if l.listenOptions.TCPKeepAlive >= 0 {
|
||||
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
|
||||
if keepIdle == 0 {
|
||||
keepIdle = C.TCPKeepAliveInitial
|
||||
}
|
||||
keepInterval := time.Duration(l.listenOptions.TCPKeepAliveInterval)
|
||||
if keepInterval == 0 {
|
||||
keepInterval = C.TCPKeepAliveInterval
|
||||
}
|
||||
setKeepAliveConfig(&listenConfig, keepIdle, keepInterval)
|
||||
}
|
||||
if l.listenOptions.TCPMultiPath {
|
||||
if !go121Available {
|
||||
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
|
||||
}
|
||||
setMultiPathTCP(&listenConfig)
|
||||
}
|
||||
if l.tproxy {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
|
||||
return control.Raw(conn, func(fd uintptr) error {
|
||||
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), false)
|
||||
})
|
||||
})
|
||||
}
|
||||
tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) {
|
||||
if l.listenOptions.TCPFastOpen {
|
||||
var tfoConfig tfo.ListenConfig
|
||||
tfoConfig.ListenConfig = listenConfig
|
||||
return tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
|
||||
} else {
|
||||
return listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.logger.Info("tcp server started at ", tcpListener.Addr())
|
||||
l.tcpListener = tcpListener
|
||||
return tcpListener, err
|
||||
}
|
||||
|
||||
func (l *Listener) loopTCPIn() {
|
||||
tcpListener := l.tcpListener
|
||||
var metadata adapter.InboundContext
|
||||
for {
|
||||
conn, err := tcpListener.Accept()
|
||||
if err != nil {
|
||||
//nolint:staticcheck
|
||||
if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() {
|
||||
l.logger.Error(err)
|
||||
continue
|
||||
}
|
||||
if l.shutdown.Load() && E.IsClosed(err) {
|
||||
return
|
||||
}
|
||||
l.tcpListener.Close()
|
||||
l.logger.Error("tcp listener closed: ", err)
|
||||
continue
|
||||
}
|
||||
//nolint:staticcheck
|
||||
metadata.InboundDetour = l.listenOptions.Detour
|
||||
//nolint:staticcheck
|
||||
metadata.InboundOptions = l.listenOptions.InboundOptions
|
||||
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
|
||||
metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
|
||||
ctx := log.ContextWithNewID(l.ctx)
|
||||
l.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
|
||||
go l.connHandler.NewConnectionEx(ctx, conn, metadata, nil)
|
||||
}
|
||||
}
|
207
common/listener/listener_udp.go
Normal file
207
common/listener/listener_udp.go
Normal file
@ -0,0 +1,207 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/redir"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
||||
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
|
||||
var listenConfig net.ListenConfig
|
||||
if l.listenOptions.BindInterface != "" {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
|
||||
}
|
||||
if l.listenOptions.RoutingMark != 0 {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
|
||||
}
|
||||
if l.listenOptions.ReuseAddr {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
||||
}
|
||||
var udpFragment bool
|
||||
if l.listenOptions.UDPFragment != nil {
|
||||
udpFragment = *l.listenOptions.UDPFragment
|
||||
} else {
|
||||
udpFragment = l.listenOptions.UDPFragmentDefault
|
||||
}
|
||||
if !udpFragment {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment())
|
||||
}
|
||||
if l.tproxy {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
|
||||
return control.Raw(conn, func(fd uintptr) error {
|
||||
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), true)
|
||||
})
|
||||
})
|
||||
}
|
||||
udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||
return listenConfig.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.udpConn = udpConn.(*net.UDPConn)
|
||||
l.udpAddr = bindAddr
|
||||
l.logger.Info("udp server started at ", udpConn.LocalAddr())
|
||||
return udpConn, err
|
||||
}
|
||||
|
||||
func (l *Listener) DialContext(dialer net.Dialer, ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
return ListenNetworkNamespace[net.Conn](l.listenOptions.NetNs, func() (net.Conn, error) {
|
||||
if l.listenOptions.BindInterface != "" {
|
||||
dialer.Control = control.Append(dialer.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
|
||||
}
|
||||
if l.listenOptions.RoutingMark != 0 {
|
||||
dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
|
||||
}
|
||||
if l.listenOptions.ReuseAddr {
|
||||
dialer.Control = control.Append(dialer.Control, control.ReuseAddr())
|
||||
}
|
||||
return dialer.DialContext(ctx, network, address)
|
||||
})
|
||||
}
|
||||
|
||||
func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) {
|
||||
return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||
if l.listenOptions.BindInterface != "" {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.BindToInterface(service.FromContext[adapter.NetworkManager](l.ctx).InterfaceFinder(), l.listenOptions.BindInterface, -1))
|
||||
}
|
||||
if l.listenOptions.RoutingMark != 0 {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.RoutingMark(uint32(l.listenOptions.RoutingMark)))
|
||||
}
|
||||
if l.listenOptions.ReuseAddr {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
||||
}
|
||||
return listenConfig.ListenPacket(ctx, network, address)
|
||||
})
|
||||
}
|
||||
|
||||
func (l *Listener) UDPAddr() M.Socksaddr {
|
||||
return l.udpAddr
|
||||
}
|
||||
|
||||
func (l *Listener) PacketWriter() N.PacketWriter {
|
||||
return (*packetWriter)(l)
|
||||
}
|
||||
|
||||
func (l *Listener) loopUDPIn() {
|
||||
defer close(l.packetOutboundClosed)
|
||||
var buffer *buf.Buffer
|
||||
if !l.threadUnsafePacketWriter {
|
||||
buffer = buf.NewPacket()
|
||||
defer buffer.Release()
|
||||
buffer.IncRef()
|
||||
defer buffer.DecRef()
|
||||
}
|
||||
if l.oobPacketHandler != nil {
|
||||
oob := make([]byte, 1024)
|
||||
for {
|
||||
if l.threadUnsafePacketWriter {
|
||||
buffer = buf.NewPacket()
|
||||
} else {
|
||||
buffer.Reset()
|
||||
}
|
||||
n, oobN, _, addr, err := l.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob)
|
||||
if err != nil {
|
||||
if l.threadUnsafePacketWriter {
|
||||
buffer.Release()
|
||||
}
|
||||
if l.shutdown.Load() && E.IsClosed(err) {
|
||||
return
|
||||
}
|
||||
l.udpConn.Close()
|
||||
l.logger.Error("udp listener closed: ", err)
|
||||
return
|
||||
}
|
||||
buffer.Truncate(n)
|
||||
l.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap())
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
if l.threadUnsafePacketWriter {
|
||||
buffer = buf.NewPacket()
|
||||
} else {
|
||||
buffer.Reset()
|
||||
}
|
||||
n, addr, err := l.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes())
|
||||
if err != nil {
|
||||
if l.threadUnsafePacketWriter {
|
||||
buffer.Release()
|
||||
}
|
||||
if l.shutdown.Load() && E.IsClosed(err) {
|
||||
return
|
||||
}
|
||||
l.udpConn.Close()
|
||||
l.logger.Error("udp listener closed: ", err)
|
||||
return
|
||||
}
|
||||
buffer.Truncate(n)
|
||||
l.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) loopUDPOut() {
|
||||
for {
|
||||
select {
|
||||
case packet := <-l.packetOutbound:
|
||||
destination := packet.Destination.AddrPort()
|
||||
_, err := l.udpConn.WriteToUDPAddrPort(packet.Buffer.Bytes(), destination)
|
||||
packet.Buffer.Release()
|
||||
N.PutPacketBuffer(packet)
|
||||
if err != nil {
|
||||
if l.shutdown.Load() && E.IsClosed(err) {
|
||||
return
|
||||
}
|
||||
l.udpConn.Close()
|
||||
l.logger.Error("udp listener write back: ", destination, ": ", err)
|
||||
return
|
||||
}
|
||||
continue
|
||||
case <-l.packetOutboundClosed:
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case packet := <-l.packetOutbound:
|
||||
packet.Buffer.Release()
|
||||
N.PutPacketBuffer(packet)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type packetWriter Listener
|
||||
|
||||
func (w *packetWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||
packet := N.NewPacketBuffer()
|
||||
packet.Buffer = buffer
|
||||
packet.Destination = destination
|
||||
select {
|
||||
case w.packetOutbound <- packet:
|
||||
return nil
|
||||
default:
|
||||
buffer.Release()
|
||||
N.PutPacketBuffer(packet)
|
||||
if w.shutdown.Load() {
|
||||
return os.ErrClosed
|
||||
}
|
||||
w.logger.Trace("dropped packet to ", destination)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *packetWriter) WriteIsThreadUnsafe() {
|
||||
}
|
@ -15,11 +15,11 @@ import (
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
router adapter.ConnectionRouter
|
||||
router adapter.ConnectionRouterEx
|
||||
service *mux.Service
|
||||
}
|
||||
|
||||
func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouter, error) {
|
||||
func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouterEx, error) {
|
||||
if !options.Enabled {
|
||||
return router, nil
|
||||
}
|
||||
@ -42,7 +42,7 @@ func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.Context
|
||||
return log.ContextWithNewID(ctx)
|
||||
},
|
||||
Logger: logger,
|
||||
Handler: adapter.NewRouteContextHandler(router, logger),
|
||||
HandlerEx: adapter.NewRouteContextHandlerEx(router),
|
||||
Padding: options.Padding,
|
||||
Brutal: brutalOptions,
|
||||
})
|
||||
@ -52,14 +52,29 @@ func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.Context
|
||||
return &Router{router, service}, nil
|
||||
}
|
||||
|
||||
// Deprecated: Use RouteConnectionEx instead.
|
||||
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||
if metadata.Destination == mux.Destination {
|
||||
// TODO: check if WithContext is necessary
|
||||
return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata))
|
||||
} else {
|
||||
return r.router.RouteConnection(ctx, conn, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Use RoutePacketConnectionEx instead.
|
||||
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||
return r.router.RoutePacketConnection(ctx, conn, metadata)
|
||||
}
|
||||
|
||||
func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
if metadata.Destination == mux.Destination {
|
||||
r.service.NewConnectionEx(adapter.WithContext(ctx, &metadata), conn, metadata.Source, metadata.Destination, onClose)
|
||||
return
|
||||
}
|
||||
r.router.RouteConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
vmess "github.com/sagernet/sing-vmess"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type V2RayLegacyRouter struct {
|
||||
router adapter.ConnectionRouter
|
||||
logger logger.ContextLogger
|
||||
}
|
||||
|
||||
func NewV2RayLegacyRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) adapter.ConnectionRouter {
|
||||
return &V2RayLegacyRouter{router, logger}
|
||||
}
|
||||
|
||||
func (r *V2RayLegacyRouter) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||
if metadata.Destination.Fqdn == vmess.MuxDestination.Fqdn {
|
||||
r.logger.InfoContext(ctx, "inbound legacy multiplex connection")
|
||||
return vmess.HandleMuxConnection(ctx, conn, adapter.NewRouteHandler(metadata, r.router, r.logger))
|
||||
}
|
||||
return r.router.RouteConnection(ctx, conn, metadata)
|
||||
}
|
||||
|
||||
func (r *V2RayLegacyRouter) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||
return r.router.RoutePacketConnection(ctx, conn, metadata)
|
||||
}
|
@ -23,6 +23,7 @@ type Config struct {
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
ProcessID uint32
|
||||
ProcessPath string
|
||||
PackageName string
|
||||
User string
|
||||
|
@ -2,14 +2,11 @@ package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/winiphlpapi"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
@ -26,209 +23,39 @@ func NewSearcher(_ Config) (Searcher, error) {
|
||||
return &windowsSearcher{}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
|
||||
procGetExtendedTcpTable = modiphlpapi.NewProc("GetExtendedTcpTable")
|
||||
procGetExtendedUdpTable = modiphlpapi.NewProc("GetExtendedUdpTable")
|
||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW")
|
||||
)
|
||||
|
||||
func initWin32API() error {
|
||||
err := modiphlpapi.Load()
|
||||
if err != nil {
|
||||
return E.Cause(err, "load iphlpapi.dll")
|
||||
}
|
||||
|
||||
err = procGetExtendedTcpTable.Find()
|
||||
if err != nil {
|
||||
return E.Cause(err, "load iphlpapi::GetExtendedTcpTable")
|
||||
}
|
||||
|
||||
err = procGetExtendedUdpTable.Find()
|
||||
if err != nil {
|
||||
return E.Cause(err, "load iphlpapi::GetExtendedUdpTable")
|
||||
}
|
||||
|
||||
err = modkernel32.Load()
|
||||
if err != nil {
|
||||
return E.Cause(err, "load kernel32.dll")
|
||||
}
|
||||
|
||||
err = procQueryFullProcessImageNameW.Find()
|
||||
if err != nil {
|
||||
return E.Cause(err, "load kernel32::QueryFullProcessImageNameW")
|
||||
}
|
||||
|
||||
return nil
|
||||
return winiphlpapi.LoadExtendedTable()
|
||||
}
|
||||
|
||||
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
||||
pid, err := winiphlpapi.FindPid(network, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Info{ProcessPath: processName, UserId: -1}, nil
|
||||
}
|
||||
|
||||
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
|
||||
family := windows.AF_INET
|
||||
if ip.Is6() {
|
||||
family = windows.AF_INET6
|
||||
}
|
||||
|
||||
const (
|
||||
tcpTablePidConn = 4
|
||||
udpTablePid = 1
|
||||
)
|
||||
|
||||
var class int
|
||||
var fn uintptr
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
fn = procGetExtendedTcpTable.Addr()
|
||||
class = tcpTablePidConn
|
||||
case N.NetworkUDP:
|
||||
fn = procGetExtendedUdpTable.Addr()
|
||||
class = udpTablePid
|
||||
default:
|
||||
return "", os.ErrInvalid
|
||||
}
|
||||
|
||||
buf, err := getTransportTable(fn, family, class)
|
||||
path, err := getProcessPath(pid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return &Info{ProcessID: pid, UserId: -1}, err
|
||||
}
|
||||
|
||||
s := newSearcher(family == windows.AF_INET, network == N.NetworkTCP)
|
||||
|
||||
pid, err := s.Search(buf, ip, uint16(srcPort))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return getExecPathFromPID(pid)
|
||||
return &Info{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
|
||||
}
|
||||
|
||||
type searcher struct {
|
||||
itemSize int
|
||||
port int
|
||||
ip int
|
||||
ipSize int
|
||||
pid int
|
||||
tcpState int
|
||||
}
|
||||
|
||||
func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) {
|
||||
n := int(readNativeUint32(b[:4]))
|
||||
itemSize := s.itemSize
|
||||
for i := 0; i < n; i++ {
|
||||
row := b[4+itemSize*i : 4+itemSize*(i+1)]
|
||||
|
||||
if s.tcpState >= 0 {
|
||||
tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4])
|
||||
// MIB_TCP_STATE_ESTAB, only check established connections for TCP
|
||||
if tcpState != 5 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian.
|
||||
// this field can be illustrated as follows depends on different machine endianess:
|
||||
// little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB)
|
||||
// big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB)
|
||||
// so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32
|
||||
srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4])))
|
||||
if srcPort != port {
|
||||
continue
|
||||
}
|
||||
|
||||
srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize])
|
||||
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
|
||||
if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) {
|
||||
continue
|
||||
}
|
||||
|
||||
pid := readNativeUint32(row[s.pid : s.pid+4])
|
||||
return pid, nil
|
||||
}
|
||||
return 0, ErrNotFound
|
||||
}
|
||||
|
||||
func newSearcher(isV4, isTCP bool) *searcher {
|
||||
var itemSize, port, ip, ipSize, pid int
|
||||
tcpState := -1
|
||||
switch {
|
||||
case isV4 && isTCP:
|
||||
// struct MIB_TCPROW_OWNER_PID
|
||||
itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0
|
||||
case isV4 && !isTCP:
|
||||
// struct MIB_UDPROW_OWNER_PID
|
||||
itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8
|
||||
case !isV4 && isTCP:
|
||||
// struct MIB_TCP6ROW_OWNER_PID
|
||||
itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48
|
||||
case !isV4 && !isTCP:
|
||||
// struct MIB_UDP6ROW_OWNER_PID
|
||||
itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24
|
||||
}
|
||||
|
||||
return &searcher{
|
||||
itemSize: itemSize,
|
||||
port: port,
|
||||
ip: ip,
|
||||
ipSize: ipSize,
|
||||
pid: pid,
|
||||
tcpState: tcpState,
|
||||
}
|
||||
}
|
||||
|
||||
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
|
||||
for size, buf := uint32(8), make([]byte, 8); ; {
|
||||
ptr := unsafe.Pointer(&buf[0])
|
||||
err, _, _ := syscall.SyscallN(fn, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
|
||||
|
||||
switch err {
|
||||
case 0:
|
||||
return buf, nil
|
||||
case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER):
|
||||
buf = make([]byte, size)
|
||||
default:
|
||||
return nil, fmt.Errorf("syscall error: %d", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readNativeUint32(b []byte) uint32 {
|
||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||
}
|
||||
|
||||
func getExecPathFromPID(pid uint32) (string, error) {
|
||||
// kernel process starts with a colon in order to distinguish with normal processes
|
||||
func getProcessPath(pid uint32) (string, error) {
|
||||
switch pid {
|
||||
case 0:
|
||||
// reserved pid for system idle process
|
||||
return ":System Idle Process", nil
|
||||
case 4:
|
||||
// reserved pid for windows kernel image
|
||||
return ":System", nil
|
||||
}
|
||||
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer windows.CloseHandle(h)
|
||||
|
||||
defer windows.CloseHandle(handle)
|
||||
size := uint32(syscall.MAX_LONG_PATH)
|
||||
buf := make([]uint16, syscall.MAX_LONG_PATH)
|
||||
size := uint32(len(buf))
|
||||
r1, _, err := syscall.SyscallN(
|
||||
procQueryFullProcessImageNameW.Addr(),
|
||||
uintptr(h),
|
||||
uintptr(0),
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
uintptr(unsafe.Pointer(&size)),
|
||||
)
|
||||
if r1 == 0 {
|
||||
err = windows.QueryFullProcessImageName(handle, 0, &buf[0], &size)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return syscall.UTF16ToString(buf[:size]), nil
|
||||
return windows.UTF16ToString(buf[:size]), nil
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user