mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-01 19:44:15 +03:00
Compare commits
131 Commits
v1.13.2-ex
...
v1.13.11-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf8fe79a7a | ||
|
|
398b6387ab | ||
|
|
04908a6a67 | ||
|
|
88a80e961b | ||
|
|
05b82f63da | ||
|
|
00c08995c7 | ||
|
|
553cfa1f9f | ||
|
|
f102ef1d94 | ||
|
|
3312b8da50 | ||
|
|
09f9f114aa | ||
|
|
a3fc14f35f | ||
|
|
8947cb243e | ||
|
|
83d3a6d4e1 | ||
|
|
71f6a2ab4e | ||
|
|
d942ecc904 | ||
|
|
c3de6a25fb | ||
|
|
b3523abad5 | ||
|
|
60dd7ea5c9 | ||
|
|
e4bc459975 | ||
|
|
3124cdd661 | ||
|
|
b3606e33a6 | ||
|
|
9b72b352d5 | ||
|
|
fb61987d93 | ||
|
|
a80ef94f09 | ||
|
|
3a236d9c3c | ||
|
|
ca76c56377 | ||
|
|
0bd109d7bc | ||
|
|
1995ba4279 | ||
|
|
9b155ba467 | ||
|
|
7ed5ef6da4 | ||
|
|
bb3ad9c694 | ||
|
|
d5adb54bc6 | ||
|
|
1cfcea769f | ||
|
|
f43fc797d4 | ||
|
|
8e3176b789 | ||
|
|
025b947a24 | ||
|
|
76fa3c2e5e | ||
|
|
53db1f178c | ||
|
|
55ec8abf17 | ||
|
|
5a957fd750 | ||
|
|
7c3d8cf8db | ||
|
|
2d33dee415 | ||
|
|
813b634d08 | ||
|
|
d9b435fb62 | ||
|
|
354b4b040e | ||
|
|
7ffdc48b49 | ||
|
|
e15bdf11eb | ||
|
|
e3bcb06c3e | ||
|
|
84d2280960 | ||
|
|
4fd2532b0a | ||
|
|
02ccde6c71 | ||
|
|
e98b4ad449 | ||
|
|
d09182614c | ||
|
|
6381de7bab | ||
|
|
b0c6762bc1 | ||
|
|
7425100bac | ||
|
|
d454aa0fdf | ||
|
|
a3623eb41a | ||
|
|
72bc4c1f87 | ||
|
|
9ac1e2ff32 | ||
|
|
0045103d14 | ||
|
|
d2a933784c | ||
|
|
3f05a37f65 | ||
|
|
b8e5a71450 | ||
|
|
c13faa8e3c | ||
|
|
7623bcd19e | ||
|
|
795d1c2892 | ||
|
|
6913b11e0a | ||
|
|
1e57c06295 | ||
|
|
ea464cef8d | ||
|
|
a8e3cd3256 | ||
|
|
686cf1f304 | ||
|
|
9fbfb87723 | ||
|
|
d2fa21d07b | ||
|
|
d3768cca36 | ||
|
|
0889ddd001 | ||
|
|
f46fbf188a | ||
|
|
f2d15139f5 | ||
|
|
041646b728 | ||
|
|
b990de2e12 | ||
|
|
fe585157d2 | ||
|
|
eed6a36e5d | ||
|
|
eb0f38544c | ||
|
|
cb3131b3d4 | ||
|
|
20bf40e822 | ||
|
|
54468a1a2a | ||
|
|
8289bbd846 | ||
|
|
49c450d942 | ||
|
|
861aff60f0 | ||
|
|
a7ee943216 | ||
|
|
8bb4c4dd32 | ||
|
|
67621ee6ba | ||
|
|
a09ffe6a0f | ||
|
|
e0be8743f6 | ||
|
|
0b04528803 | ||
|
|
65875e6dac | ||
|
|
2df1cffb0f | ||
|
|
bc6ca6e2ea | ||
|
|
4d6fb1d38d | ||
|
|
305b930d90 | ||
|
|
bc3884ca91 | ||
|
|
df0bf927e4 | ||
|
|
efe20ea51c | ||
|
|
e21a72fcd1 | ||
|
|
e1477bd065 | ||
|
|
aa495fce38 | ||
|
|
9cd60c28c0 | ||
|
|
2ba896c5ac | ||
|
|
1d388547ee | ||
|
|
e343cec4d5 | ||
|
|
d58efc5d01 | ||
|
|
4b26ab16fb | ||
|
|
0e27312eda | ||
|
|
0503006f48 | ||
|
|
517f5152e7 | ||
|
|
b1b7aa81cd | ||
|
|
195e941c35 | ||
|
|
35bc351564 | ||
|
|
290dbed7b8 | ||
|
|
d7a8207f44 | ||
|
|
57c5ca13eb | ||
|
|
7fc33134fb | ||
|
|
881ab6d436 | ||
|
|
0443b93328 | ||
|
|
75557830a8 | ||
|
|
9d5273ba1e | ||
|
|
5f2a65f01b | ||
|
|
06a519db27 | ||
|
|
65e73fe817 | ||
|
|
c0aa3480c5 | ||
|
|
69f6c75dd7 |
@@ -4,6 +4,7 @@
|
|||||||
--license GPL-3.0-or-later
|
--license GPL-3.0-or-later
|
||||||
--description "The universal proxy platform."
|
--description "The universal proxy platform."
|
||||||
--url "https://sing-box.sagernet.org/"
|
--url "https://sing-box.sagernet.org/"
|
||||||
|
--vendor SagerNet
|
||||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||||
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||||
--no-deb-generate-changes
|
--no-deb-generate-changes
|
||||||
|
|||||||
2
.github/CRONET_GO_VERSION
vendored
2
.github/CRONET_GO_VERSION
vendored
@@ -1 +1 @@
|
|||||||
cba7b9ac0399055aa49fbdc57c03c374f58e1597
|
e4926ba205fae5351e3d3eeafff7e7029654424a
|
||||||
|
|||||||
94
.github/build_alpine_apk.sh
vendored
Executable file
94
.github/build_alpine_apk.sh
vendored
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
prepare_apk_root() {
|
||||||
|
# apk mkpkg resolves owner/group names through --root/etc/{passwd,group}.
|
||||||
|
APK_ROOT_DIR=$(mktemp -d)
|
||||||
|
mkdir -p "$APK_ROOT_DIR/etc"
|
||||||
|
cat > "$APK_ROOT_DIR/etc/passwd" <<EOF
|
||||||
|
root:x:$(id -u):$(id -g):root:/root:/sbin/nologin
|
||||||
|
EOF
|
||||||
|
cat > "$APK_ROOT_DIR/etc/group" <<EOF
|
||||||
|
root:x:$(id -g):root
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ARCHITECTURE="$1"
|
||||||
|
VERSION="$2"
|
||||||
|
BINARY_PATH="$3"
|
||||||
|
OUTPUT_PATH="$4"
|
||||||
|
|
||||||
|
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
|
||||||
|
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||||
|
|
||||||
|
# Convert version to APK format:
|
||||||
|
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
|
||||||
|
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
|
||||||
|
# 1.13.0 -> 1.13.0-r0
|
||||||
|
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
|
||||||
|
APK_VERSION="${APK_VERSION}-r0"
|
||||||
|
|
||||||
|
ROOT_DIR=$(mktemp -d)
|
||||||
|
prepare_apk_root
|
||||||
|
trap 'rm -rf "$ROOT_DIR" "$APK_ROOT_DIR"' EXIT
|
||||||
|
|
||||||
|
# Binary
|
||||||
|
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
|
||||||
|
|
||||||
|
# Config files
|
||||||
|
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
|
||||||
|
install -Dm755 "$PROJECT/release/config/sing-box.initd" "$ROOT_DIR/etc/init.d/sing-box"
|
||||||
|
install -Dm644 "$PROJECT/release/config/sing-box.confd" "$ROOT_DIR/etc/conf.d/sing-box"
|
||||||
|
|
||||||
|
# Service files
|
||||||
|
install -Dm644 "$PROJECT/release/config/sing-box.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box.service"
|
||||||
|
install -Dm644 "$PROJECT/release/config/sing-box@.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box@.service"
|
||||||
|
|
||||||
|
# Completions
|
||||||
|
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
|
||||||
|
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
|
||||||
|
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
|
||||||
|
|
||||||
|
# License
|
||||||
|
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
|
||||||
|
|
||||||
|
# APK metadata
|
||||||
|
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
|
||||||
|
mkdir -p "$PACKAGES_DIR"
|
||||||
|
|
||||||
|
# .conffiles
|
||||||
|
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
|
||||||
|
/etc/conf.d/sing-box
|
||||||
|
/etc/init.d/sing-box
|
||||||
|
/etc/sing-box/config.json
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# .conffiles_static (sha256 checksums)
|
||||||
|
while IFS= read -r conffile; do
|
||||||
|
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
|
||||||
|
echo "$conffile $sha256"
|
||||||
|
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
|
||||||
|
|
||||||
|
# .list (all files, excluding lib/apk/packages/ metadata)
|
||||||
|
(cd "$ROOT_DIR" && find . -type f -o -type l) \
|
||||||
|
| sed 's|^\./|/|' \
|
||||||
|
| grep -v '^/lib/apk/packages/' \
|
||||||
|
| sort > "$PACKAGES_DIR/.list"
|
||||||
|
|
||||||
|
# Build APK
|
||||||
|
apk --root "$APK_ROOT_DIR" mkpkg \
|
||||||
|
--info "name:sing-box" \
|
||||||
|
--info "version:${APK_VERSION}" \
|
||||||
|
--info "description:The universal proxy platform." \
|
||||||
|
--info "arch:${ARCHITECTURE}" \
|
||||||
|
--info "license:GPL-3.0-or-later with name use or association addition" \
|
||||||
|
--info "origin:sing-box" \
|
||||||
|
--info "url:https://sing-box.sagernet.org/" \
|
||||||
|
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
|
||||||
|
--files "$ROOT_DIR" \
|
||||||
|
--output "$OUTPUT_PATH"
|
||||||
93
.github/build_openwrt_apk.sh
vendored
Executable file
93
.github/build_openwrt_apk.sh
vendored
Executable file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
prepare_apk_root() {
|
||||||
|
# apk mkpkg resolves owner/group names through --root/etc/{passwd,group}.
|
||||||
|
APK_ROOT_DIR=$(mktemp -d)
|
||||||
|
mkdir -p "$APK_ROOT_DIR/etc"
|
||||||
|
cat > "$APK_ROOT_DIR/etc/passwd" <<EOF
|
||||||
|
root:x:$(id -u):$(id -g):root:/root:/sbin/nologin
|
||||||
|
EOF
|
||||||
|
cat > "$APK_ROOT_DIR/etc/group" <<EOF
|
||||||
|
root:x:$(id -g):root
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
ARCHITECTURE="$1"
|
||||||
|
VERSION="$2"
|
||||||
|
BINARY_PATH="$3"
|
||||||
|
OUTPUT_PATH="$4"
|
||||||
|
|
||||||
|
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
|
||||||
|
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
|
||||||
|
|
||||||
|
# Convert version to APK format:
|
||||||
|
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
|
||||||
|
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
|
||||||
|
# 1.13.0 -> 1.13.0-r0
|
||||||
|
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
|
||||||
|
APK_VERSION="${APK_VERSION}-r0"
|
||||||
|
|
||||||
|
ROOT_DIR=$(mktemp -d)
|
||||||
|
prepare_apk_root
|
||||||
|
trap 'rm -rf "$ROOT_DIR" "$APK_ROOT_DIR"' EXIT
|
||||||
|
|
||||||
|
# Binary
|
||||||
|
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
|
||||||
|
|
||||||
|
# Config files
|
||||||
|
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
|
||||||
|
install -Dm644 "$PROJECT/release/config/openwrt.conf" "$ROOT_DIR/etc/config/sing-box"
|
||||||
|
install -Dm755 "$PROJECT/release/config/openwrt.init" "$ROOT_DIR/etc/init.d/sing-box"
|
||||||
|
install -Dm644 "$PROJECT/release/config/openwrt.keep" "$ROOT_DIR/lib/upgrade/keep.d/sing-box"
|
||||||
|
|
||||||
|
# Completions
|
||||||
|
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
|
||||||
|
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
|
||||||
|
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
|
||||||
|
|
||||||
|
# License
|
||||||
|
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
|
||||||
|
|
||||||
|
# APK metadata
|
||||||
|
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
|
||||||
|
mkdir -p "$PACKAGES_DIR"
|
||||||
|
|
||||||
|
# .conffiles
|
||||||
|
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
|
||||||
|
/etc/config/sing-box
|
||||||
|
/etc/sing-box/config.json
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# .conffiles_static (sha256 checksums)
|
||||||
|
while IFS= read -r conffile; do
|
||||||
|
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
|
||||||
|
echo "$conffile $sha256"
|
||||||
|
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
|
||||||
|
|
||||||
|
# .list (all files, excluding lib/apk/packages/ metadata)
|
||||||
|
(cd "$ROOT_DIR" && find . -type f -o -type l) \
|
||||||
|
| sed 's|^\./|/|' \
|
||||||
|
| grep -v '^/lib/apk/packages/' \
|
||||||
|
| sort > "$PACKAGES_DIR/.list"
|
||||||
|
|
||||||
|
# Build APK
|
||||||
|
apk --root "$APK_ROOT_DIR" mkpkg \
|
||||||
|
--info "name:sing-box" \
|
||||||
|
--info "version:${APK_VERSION}" \
|
||||||
|
--info "description:The universal proxy platform." \
|
||||||
|
--info "arch:${ARCHITECTURE}" \
|
||||||
|
--info "license:GPL-3.0-or-later" \
|
||||||
|
--info "origin:sing-box" \
|
||||||
|
--info "url:https://sing-box.sagernet.org/" \
|
||||||
|
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
|
||||||
|
--info "depends:ca-bundle kmod-inet-diag kmod-tun firewall4 kmod-nft-queue" \
|
||||||
|
--info "provider-priority:100" \
|
||||||
|
--script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \
|
||||||
|
--files "$ROOT_DIR" \
|
||||||
|
--output "$OUTPUT_PATH"
|
||||||
33
.github/detect_track.sh
vendored
Executable file
33
.github/detect_track.sh
vendored
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
branches=$(git branch -r --contains HEAD)
|
||||||
|
if echo "$branches" | grep -q 'origin/stable'; then
|
||||||
|
track=stable
|
||||||
|
elif echo "$branches" | grep -q 'origin/testing'; then
|
||||||
|
track=testing
|
||||||
|
elif echo "$branches" | grep -q 'origin/oldstable'; then
|
||||||
|
track=oldstable
|
||||||
|
else
|
||||||
|
echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$track" == "stable" ]]; then
|
||||||
|
tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
|
||||||
|
if [[ -n "$tag" && "$tag" == *"-"* ]]; then
|
||||||
|
track=beta
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$track" in
|
||||||
|
stable) name=sing-box; docker_tag=latest ;;
|
||||||
|
beta) name=sing-box-beta; docker_tag=latest-beta ;;
|
||||||
|
testing) name=sing-box-testing; docker_tag=latest-testing ;;
|
||||||
|
oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2
|
||||||
|
echo "TRACK=${track}" >> "$GITHUB_ENV"
|
||||||
|
echo "NAME=${name}" >> "$GITHUB_ENV"
|
||||||
|
echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV"
|
||||||
45
.github/setup_go_for_macos1013.sh
vendored
Executable file
45
.github/setup_go_for_macos1013.sh
vendored
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="1.25.9"
|
||||||
|
PATCH_COMMITS=(
|
||||||
|
"afe69d3cec1c6dcf0f1797b20546795730850070"
|
||||||
|
"1ed289b0cf87dc5aae9c6fe1aa5f200a83412938"
|
||||||
|
)
|
||||||
|
CURL_ARGS=(
|
||||||
|
-fL
|
||||||
|
--silent
|
||||||
|
--show-error
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||||
|
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$HOME/go"
|
||||||
|
cd "$HOME/go"
|
||||||
|
wget "https://dl.google.com/go/go${VERSION}.darwin-arm64.tar.gz"
|
||||||
|
tar -xzf "go${VERSION}.darwin-arm64.tar.gz"
|
||||||
|
#cp -a go go_bootstrap
|
||||||
|
mv go go_osx
|
||||||
|
cd go_osx
|
||||||
|
|
||||||
|
# these patch URLs only work on golang1.25.x
|
||||||
|
# that means after golang1.26 release it must be changed
|
||||||
|
# see: https://github.com/SagerNet/go/commits/release-branch.go1.25/
|
||||||
|
# revert:
|
||||||
|
# 33d3f603c1: "cmd/link/internal/ld: use 12.0.0 OS/SDK versions for macOS linking"
|
||||||
|
# 937368f84e: "crypto/x509: change how we retrieve chains on darwin"
|
||||||
|
|
||||||
|
for patch_commit in "${PATCH_COMMITS[@]}"; do
|
||||||
|
curl "${CURL_ARGS[@]}" "https://github.com/SagerNet/go/commit/${patch_commit}.diff" | patch --verbose -p 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Rebuild is not needed: we build with CGO_ENABLED=1, so Apple's external
|
||||||
|
# linker handles LC_BUILD_VERSION via MACOSX_DEPLOYMENT_TARGET, and the
|
||||||
|
# stdlib (crypto/x509) is compiled from patched src automatically.
|
||||||
|
#cd src
|
||||||
|
#GOROOT_BOOTSTRAP="$HOME/go/go_bootstrap" ./make.bash
|
||||||
|
#cd ../..
|
||||||
|
#rm -rf go_bootstrap "go${VERSION}.darwin-arm64.tar.gz"
|
||||||
39
.github/setup_go_for_windows7.sh
vendored
39
.github/setup_go_for_windows7.sh
vendored
@@ -1,16 +1,35 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
VERSION="1.25.7"
|
set -euo pipefail
|
||||||
|
|
||||||
mkdir -p $HOME/go
|
VERSION="1.25.9"
|
||||||
cd $HOME/go
|
PATCH_COMMITS=(
|
||||||
|
"466f6c7a29bc098b0d4c987b803c779222894a11"
|
||||||
|
"1bdabae205052afe1dadb2ad6f1ba612cdbc532a"
|
||||||
|
"a90777dcf692dd2168577853ba743b4338721b06"
|
||||||
|
"f6bddda4e8ff58a957462a1a09562924d5f3d05c"
|
||||||
|
"bed309eff415bcb3c77dd4bc3277b682b89a388d"
|
||||||
|
"34b899c2fb39b092db4fa67c4417e41dc046be4b"
|
||||||
|
)
|
||||||
|
CURL_ARGS=(
|
||||||
|
-fL
|
||||||
|
--silent
|
||||||
|
--show-error
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||||
|
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$HOME/go"
|
||||||
|
cd "$HOME/go"
|
||||||
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
|
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
|
||||||
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
|
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
|
||||||
mv go go_win7
|
mv go go_win7
|
||||||
cd go_win7
|
cd go_win7
|
||||||
|
|
||||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||||
# this patch file only works on golang1.25.x
|
# these patch URLs only work on golang1.25.x
|
||||||
# that means after golang1.26 release it must be changed
|
# that means after golang1.26 release it must be changed
|
||||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
||||||
# revert:
|
# revert:
|
||||||
@@ -18,10 +37,10 @@ cd go_win7
|
|||||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||||
|
# fixes:
|
||||||
|
# bed309eff415bcb3c77dd4bc3277b682b89a388d: "Fix os.RemoveAll not working on Windows7"
|
||||||
|
# 34b899c2fb39b092db4fa67c4417e41dc046be4b: "Revert \"os: remove 5ms sleep on Windows in (*Process).Wait\""
|
||||||
|
|
||||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
for patch_commit in "${PATCH_COMMITS[@]}"; do
|
||||||
|
curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1
|
||||||
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
|
done
|
||||||
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
|
|
||||||
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
|
|
||||||
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1
|
|
||||||
|
|||||||
86
.github/workflows/build.yml
vendored
86
.github/workflows/build.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.7
|
go-version: ~1.25.9
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -72,27 +72,27 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- { os: linux, arch: amd64, variant: purego, naive: true }
|
- { os: linux, arch: amd64, variant: purego, naive: true }
|
||||||
- { os: linux, arch: amd64, variant: glibc, naive: true }
|
- { os: linux, arch: amd64, variant: glibc, naive: true }
|
||||||
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
|
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, alpine: x86_64, openwrt: "x86_64" }
|
||||||
|
|
||||||
- { os: linux, arch: arm64, variant: purego, naive: true }
|
- { os: linux, arch: arm64, variant: purego, naive: true }
|
||||||
- { os: linux, arch: arm64, variant: glibc, naive: true }
|
- { os: linux, arch: arm64, variant: glibc, naive: true }
|
||||||
- { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
|
- { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, alpine: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
|
||||||
|
|
||||||
- { os: linux, arch: "386", go386: sse2 }
|
- { os: linux, arch: "386", go386: sse2 }
|
||||||
- { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 }
|
- { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 }
|
||||||
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
|
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, alpine: x86, openwrt: "i386_pentium4" }
|
||||||
|
|
||||||
- { os: linux, arch: arm, goarm: "7" }
|
- { os: linux, arch: arm, goarm: "7" }
|
||||||
- { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" }
|
- { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" }
|
||||||
- { os: linux, arch: arm, variant: musl, naive: true, 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: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, alpine: armv7, 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: mipsle, gomips: hardfloat, naive: true, variant: glibc }
|
- { os: linux, arch: mipsle, gomips: hardfloat, naive: true, variant: glibc }
|
||||||
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, variant: musl, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
|
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, variant: musl, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
|
||||||
- { os: linux, arch: mips64le, gomips: hardfloat, naive: true, variant: glibc, debian: mips64el, rpm: mips64el }
|
- { os: linux, arch: mips64le, gomips: hardfloat, naive: true, variant: glibc, debian: mips64el, rpm: mips64el }
|
||||||
- { os: linux, arch: riscv64, naive: true, variant: glibc }
|
- { os: linux, arch: riscv64, naive: true, variant: glibc }
|
||||||
- { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
|
- { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, alpine: riscv64, openwrt: "riscv64_generic" }
|
||||||
- { os: linux, arch: loong64, naive: true, variant: glibc }
|
- { os: linux, arch: loong64, naive: true, variant: glibc }
|
||||||
- { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
- { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, alpine: loongarch64, openwrt: "loongarch64_generic" }
|
||||||
|
|
||||||
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
|
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
|
||||||
- { 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: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
|
||||||
@@ -121,15 +121,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
|
if: ${{ ! matrix.legacy_win7 }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.7
|
go-version: ~1.25.9
|
||||||
- name: Setup Go 1.24
|
|
||||||
if: matrix.legacy_go124
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: ~1.24.10
|
|
||||||
- name: Cache Go for Windows 7
|
- name: Cache Go for Windows 7
|
||||||
if: matrix.legacy_win7
|
if: matrix.legacy_win7
|
||||||
id: cache-go-for-windows7
|
id: cache-go-for-windows7
|
||||||
@@ -137,9 +132,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/go_win7
|
~/go/go_win7
|
||||||
key: go_win7_1255
|
key: go_win7_1258
|
||||||
- name: Setup Go for Windows 7
|
- name: Setup Go for Windows 7
|
||||||
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
run: |-
|
run: |-
|
||||||
.github/setup_go_for_windows7.sh
|
.github/setup_go_for_windows7.sh
|
||||||
- name: Setup Go for Windows 7
|
- name: Setup Go for Windows 7
|
||||||
@@ -399,6 +396,30 @@ jobs:
|
|||||||
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
|
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
|
||||||
done
|
done
|
||||||
rm "dist/openwrt.deb"
|
rm "dist/openwrt.deb"
|
||||||
|
- name: Install apk-tools
|
||||||
|
if: matrix.openwrt != '' || matrix.alpine != ''
|
||||||
|
run: |-
|
||||||
|
docker run --rm -v /usr/local/bin:/mnt alpine:edge sh -c "apk add --no-cache apk-tools-static && cp /sbin/apk.static /mnt/apk && chmod +x /mnt/apk"
|
||||||
|
- name: Package OpenWrt APK
|
||||||
|
if: matrix.openwrt != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
for architecture in ${{ matrix.openwrt }}; do
|
||||||
|
.github/build_openwrt_apk.sh \
|
||||||
|
"$architecture" \
|
||||||
|
"${{ needs.calculate_version.outputs.version }}" \
|
||||||
|
"dist/sing-box" \
|
||||||
|
"dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.apk"
|
||||||
|
done
|
||||||
|
- name: Package Alpine APK
|
||||||
|
if: matrix.alpine != ''
|
||||||
|
run: |-
|
||||||
|
set -xeuo pipefail
|
||||||
|
.github/build_alpine_apk.sh \
|
||||||
|
"${{ matrix.alpine }}" \
|
||||||
|
"${{ needs.calculate_version.outputs.version }}" \
|
||||||
|
"dist/sing-box" \
|
||||||
|
"dist/sing-box_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.alpine }}.apk"
|
||||||
- name: Archive
|
- name: Archive
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
@@ -434,22 +455,36 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- { arch: amd64 }
|
- { arch: amd64 }
|
||||||
- { arch: arm64 }
|
- { arch: arm64 }
|
||||||
- { arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
|
- { arch: amd64, legacy_osx: true, legacy_name: "macos-10.13" }
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
if: ${{ ! matrix.legacy_go124 }}
|
if: ${{ ! matrix.legacy_osx }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.25.3
|
go-version: ^1.25.3
|
||||||
- name: Setup Go 1.24
|
- name: Cache Go for macOS 10.13
|
||||||
if: matrix.legacy_go124
|
if: matrix.legacy_osx
|
||||||
uses: actions/setup-go@v5
|
id: cache-go-for-macos1013
|
||||||
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
go-version: ~1.24.6
|
path: |
|
||||||
|
~/go/go_osx
|
||||||
|
key: go_osx_1258
|
||||||
|
- name: Setup Go for macOS 10.13
|
||||||
|
if: matrix.legacy_osx && steps.cache-go-for-macos1013.outputs.cache-hit != 'true'
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: |-
|
||||||
|
.github/setup_go_for_macos1013.sh
|
||||||
|
- name: Setup Go for macOS 10.13
|
||||||
|
if: matrix.legacy_osx
|
||||||
|
run: |-
|
||||||
|
echo "PATH=$HOME/go/go_osx/bin:$PATH" >> $GITHUB_ENV
|
||||||
|
echo "GOROOT=$HOME/go/go_osx" >> $GITHUB_ENV
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
run: |-
|
run: |-
|
||||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||||
@@ -457,7 +492,7 @@ jobs:
|
|||||||
- name: Set build tags
|
- name: Set build tags
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
if [[ "${{ matrix.legacy_go124 }}" != "true" ]]; then
|
if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then
|
||||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
|
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
|
||||||
else
|
else
|
||||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
||||||
@@ -477,6 +512,7 @@ jobs:
|
|||||||
CGO_ENABLED: "1"
|
CGO_ENABLED: "1"
|
||||||
GOOS: darwin
|
GOOS: darwin
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.legacy_osx && '10.13' || '' }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Set name
|
- name: Set name
|
||||||
run: |-
|
run: |-
|
||||||
@@ -605,7 +641,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.7
|
go-version: ~1.25.9
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -695,7 +731,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.7
|
go-version: ~1.25.9
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -794,7 +830,7 @@ jobs:
|
|||||||
if: matrix.if
|
if: matrix.if
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.7
|
go-version: ~1.25.9
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
|
|||||||
21
.github/workflows/docker.yml
vendored
21
.github/workflows/docker.yml
vendored
@@ -19,7 +19,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
build_binary:
|
build_binary:
|
||||||
name: Build binary
|
name: Build binary
|
||||||
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
@@ -56,7 +55,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.7
|
go-version: ~1.25.9
|
||||||
- name: Clone cronet-go
|
- name: Clone cronet-go
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
run: |
|
run: |
|
||||||
@@ -260,13 +259,13 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "ref=$ref"
|
echo "ref=$ref"
|
||||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||||
if [[ $ref == *"-"* ]]; then
|
- name: Checkout
|
||||||
latest=latest-beta
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
else
|
with:
|
||||||
latest=latest
|
ref: ${{ steps.ref.outputs.ref }}
|
||||||
fi
|
fetch-depth: 0
|
||||||
echo "latest=$latest"
|
- name: Detect track
|
||||||
echo "latest=$latest" >> $GITHUB_OUTPUT
|
run: bash .github/detect_track.sh
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
@@ -286,11 +285,11 @@ jobs:
|
|||||||
working-directory: /tmp/digests
|
working-directory: /tmp/digests
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create \
|
docker buildx imagetools create \
|
||||||
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
|
-t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \
|
||||||
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
|
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
|
||||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||||
- name: Inspect image
|
- name: Inspect image
|
||||||
if: github.event_name != 'push'
|
if: github.event_name != 'push'
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
|
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}
|
||||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
||||||
|
|||||||
20
.github/workflows/linux.yml
vendored
20
.github/workflows/linux.yml
vendored
@@ -11,11 +11,6 @@ on:
|
|||||||
description: "Version name"
|
description: "Version name"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
forceBeta:
|
|
||||||
description: "Force beta"
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
@@ -23,7 +18,6 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
calculate_version:
|
calculate_version:
|
||||||
name: Calculate version
|
name: Calculate version
|
||||||
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.outputs.outputs.version }}
|
version: ${{ steps.outputs.outputs.version }}
|
||||||
@@ -35,7 +29,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.7
|
go-version: ~1.25.9
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -78,7 +72,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.7
|
go-version: ~1.25.9
|
||||||
- name: Clone cronet-go
|
- name: Clone cronet-go
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
run: |
|
run: |
|
||||||
@@ -168,14 +162,8 @@ jobs:
|
|||||||
- name: Set mtime
|
- name: Set mtime
|
||||||
run: |-
|
run: |-
|
||||||
TZ=UTC touch -t '197001010000' dist/sing-box
|
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||||
- name: Set name
|
- name: Detect track
|
||||||
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
|
run: bash .github/detect_track.sh
|
||||||
run: |-
|
|
||||||
echo "NAME=sing-box" >> "$GITHUB_ENV"
|
|
||||||
- name: Set beta name
|
|
||||||
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta
|
|
||||||
run: |-
|
|
||||||
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
|
||||||
- name: Set version
|
- name: Set version
|
||||||
run: |-
|
run: |-
|
||||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||||
|
|||||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,6 +0,0 @@
|
|||||||
[submodule "clients/apple"]
|
|
||||||
path = clients/apple
|
|
||||||
url = https://github.com/SagerNet/sing-box-for-apple.git
|
|
||||||
[submodule "clients/android"]
|
|
||||||
path = clients/android
|
|
||||||
url = https://github.com/SagerNet/sing-box-for-android.git
|
|
||||||
|
|||||||
197
.goreleaser.yaml
Normal file
197
.goreleaser.yaml
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
version: 2
|
||||||
|
project_name: sing-box
|
||||||
|
builds:
|
||||||
|
- &template
|
||||||
|
id: main
|
||||||
|
main: ./cmd/sing-box
|
||||||
|
flags:
|
||||||
|
- -v
|
||||||
|
- -trimpath
|
||||||
|
ldflags:
|
||||||
|
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||||
|
- -s
|
||||||
|
- -buildid=
|
||||||
|
tags:
|
||||||
|
- with_gvisor
|
||||||
|
- with_quic
|
||||||
|
- with_dhcp
|
||||||
|
- with_wireguard
|
||||||
|
- with_utls
|
||||||
|
- with_acme
|
||||||
|
- with_clash_api
|
||||||
|
- with_tailscale
|
||||||
|
- with_masque
|
||||||
|
- with_mtproxy
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
- GOTOOLCHAIN=local
|
||||||
|
targets:
|
||||||
|
- linux_386
|
||||||
|
- linux_amd64_v1
|
||||||
|
- linux_arm64
|
||||||
|
- linux_arm_6
|
||||||
|
- linux_arm_7
|
||||||
|
- linux_s390x
|
||||||
|
- linux_riscv64
|
||||||
|
- linux_mips
|
||||||
|
- linux_mips_softfloat
|
||||||
|
- linux_mipsle
|
||||||
|
- linux_mipsle_softfloat
|
||||||
|
- linux_mips64
|
||||||
|
- linux_mips64le
|
||||||
|
- windows_amd64_v1
|
||||||
|
- windows_386
|
||||||
|
- windows_arm64
|
||||||
|
- darwin_amd64_v1
|
||||||
|
- darwin_arm64
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
- id: manager
|
||||||
|
main: ./cmd/sing-box
|
||||||
|
flags:
|
||||||
|
- -v
|
||||||
|
- -trimpath
|
||||||
|
ldflags:
|
||||||
|
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||||
|
- -s
|
||||||
|
- -buildid=
|
||||||
|
tags:
|
||||||
|
- with_gvisor
|
||||||
|
- with_quic
|
||||||
|
- with_dhcp
|
||||||
|
- with_wireguard
|
||||||
|
- with_utls
|
||||||
|
- with_acme
|
||||||
|
- with_clash_api
|
||||||
|
- with_tailscale
|
||||||
|
- with_masque
|
||||||
|
- with_mtproxy
|
||||||
|
- with_manager
|
||||||
|
- with_admin_panel
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
- GOTOOLCHAIN=local
|
||||||
|
targets:
|
||||||
|
- linux_386
|
||||||
|
- linux_amd64_v1
|
||||||
|
- linux_arm64
|
||||||
|
- linux_arm_6
|
||||||
|
- linux_arm_7
|
||||||
|
- linux_s390x
|
||||||
|
- linux_riscv64
|
||||||
|
- linux_mips
|
||||||
|
- linux_mips_softfloat
|
||||||
|
- linux_mipsle
|
||||||
|
- linux_mipsle_softfloat
|
||||||
|
- linux_mips64
|
||||||
|
- linux_mips64le
|
||||||
|
- windows_amd64_v1
|
||||||
|
- windows_386
|
||||||
|
- windows_arm64
|
||||||
|
- darwin_amd64_v1
|
||||||
|
- darwin_arm64
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
- id: legacy
|
||||||
|
<<: *template
|
||||||
|
tags:
|
||||||
|
- with_gvisor
|
||||||
|
- with_quic
|
||||||
|
- with_dhcp
|
||||||
|
- with_wireguard
|
||||||
|
- with_utls
|
||||||
|
- with_acme
|
||||||
|
- with_clash_api
|
||||||
|
- with_tailscale
|
||||||
|
- with_masque
|
||||||
|
- with_mtproxy
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
targets:
|
||||||
|
- windows_amd64_v1
|
||||||
|
- windows_386
|
||||||
|
- id: android
|
||||||
|
<<: *template
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=1
|
||||||
|
- GOTOOLCHAIN=local
|
||||||
|
overrides:
|
||||||
|
- goos: android
|
||||||
|
goarch: arm
|
||||||
|
goarm: 7
|
||||||
|
env:
|
||||||
|
- CC=armv7a-linux-androideabi21-clang
|
||||||
|
- CXX=armv7a-linux-androideabi21-clang++
|
||||||
|
- goos: android
|
||||||
|
goarch: arm64
|
||||||
|
env:
|
||||||
|
- CC=aarch64-linux-android21-clang
|
||||||
|
- CXX=aarch64-linux-android21-clang++
|
||||||
|
- goos: android
|
||||||
|
goarch: 386
|
||||||
|
env:
|
||||||
|
- CC=i686-linux-android21-clang
|
||||||
|
- CXX=i686-linux-android21-clang++
|
||||||
|
- goos: android
|
||||||
|
goarch: amd64
|
||||||
|
goamd64: v1
|
||||||
|
env:
|
||||||
|
- CC=x86_64-linux-android21-clang
|
||||||
|
- CXX=x86_64-linux-android21-clang++
|
||||||
|
targets:
|
||||||
|
- android_arm_7
|
||||||
|
- android_arm64
|
||||||
|
- android_386
|
||||||
|
- android_amd64
|
||||||
|
archives:
|
||||||
|
- &template
|
||||||
|
id: archive
|
||||||
|
builds:
|
||||||
|
- main
|
||||||
|
- android
|
||||||
|
formats:
|
||||||
|
- tar.gz
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
|
wrap_in_directory: true
|
||||||
|
files:
|
||||||
|
- LICENSE
|
||||||
|
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
|
- id: archive_with_manager
|
||||||
|
builds:
|
||||||
|
- manager
|
||||||
|
formats:
|
||||||
|
- tar.gz
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
formats:
|
||||||
|
- zip
|
||||||
|
wrap_in_directory: true
|
||||||
|
files:
|
||||||
|
- LICENSE
|
||||||
|
name_template: '{{ .ProjectName }}-{{ .Version }}-with-manager-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
|
||||||
|
- id: archive-legacy
|
||||||
|
<<: *template
|
||||||
|
builds:
|
||||||
|
- legacy
|
||||||
|
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
|
||||||
|
source:
|
||||||
|
enabled: false
|
||||||
|
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
|
||||||
|
prefix_template: '{{ .ProjectName }}-{{ .Version }}/'
|
||||||
|
checksum:
|
||||||
|
disable: true
|
||||||
|
name_template: '{{ .ProjectName }}-{{ .Version }}.checksum'
|
||||||
|
signs:
|
||||||
|
- artifacts: checksum
|
||||||
|
release:
|
||||||
|
github:
|
||||||
|
owner: shtorm-7
|
||||||
|
name: sing-box-extended
|
||||||
|
draft: true
|
||||||
|
prerelease: auto
|
||||||
|
mode: replace
|
||||||
|
ids:
|
||||||
|
- archive
|
||||||
|
- package
|
||||||
|
skip_upload: true
|
||||||
8
Makefile
8
Makefile
@@ -70,14 +70,10 @@ update_certificates:
|
|||||||
go run ./cmd/internal/update_certificates
|
go run ./cmd/internal/update_certificates
|
||||||
|
|
||||||
release:
|
release:
|
||||||
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
go run ./cmd/internal/build goreleaser release --skip=validate --clean -p 3 --skip publish
|
||||||
mkdir dist/release
|
mkdir dist/release
|
||||||
mv dist/*.tar.gz \
|
mv dist/*.tar.gz \
|
||||||
dist/*.zip \
|
dist/*.zip \
|
||||||
dist/*.deb \
|
|
||||||
dist/*.rpm \
|
|
||||||
dist/*_amd64.pkg.tar.zst \
|
|
||||||
dist/*_arm64.pkg.tar.zst \
|
|
||||||
dist/release
|
dist/release
|
||||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||||
rm -r dist/release
|
rm -r dist/release
|
||||||
@@ -101,7 +97,7 @@ upload_android:
|
|||||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||||
rm -rf dist/release_android
|
rm -rf dist/release_android
|
||||||
|
|
||||||
release_android: lib_android update_android_version build_android upload_android
|
release_android: lib_android update_android_version build_android
|
||||||
|
|
||||||
publish_android:
|
publish_android:
|
||||||
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
|
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -2,22 +2,47 @@
|
|||||||
|
|
||||||
Sing-box with extended features.
|
Sing-box with extended features.
|
||||||
|
|
||||||
## Features
|
## 🔥 Features
|
||||||
|
|
||||||
* Amnezia 1.5
|
### 🌐 Protocols
|
||||||
* WARP
|
- **WARP**
|
||||||
* Tunneling
|
- **Masque**
|
||||||
* Mieru
|
- **MTProxy**
|
||||||
* XHTTP
|
- **Mieru**
|
||||||
* SDNS (DNSCrypt)
|
- **VPN**
|
||||||
* Extended Wireguard options
|
- **Bond**
|
||||||
* Unified delay
|
- **Fallback**
|
||||||
|
|
||||||
## Examples
|
### 🚦 Limiters
|
||||||
|
- **Bandwidth Limiter**
|
||||||
|
- **Connection Limiter**
|
||||||
|
|
||||||
|
### 🛡 Encryption & Obfuscation
|
||||||
|
- **Amnezia 2.0**
|
||||||
|
- **VLESS encryption**
|
||||||
|
|
||||||
|
### 🔄 Transports
|
||||||
|
- **mKCP**
|
||||||
|
- **XHTTP**
|
||||||
|
|
||||||
|
### 🛠 Services
|
||||||
|
- **Admin Panel**
|
||||||
|
- **Manager**
|
||||||
|
- **Node Manager**
|
||||||
|
|
||||||
|
### ⚙ Miscellaneous
|
||||||
|
- **Link parser**
|
||||||
|
- **SDNS (DNSCrypt)**
|
||||||
|
- **Extended WireGuard options**
|
||||||
|
- **Unified Delay**
|
||||||
|
|
||||||
|
## 📚 Examples
|
||||||
|
|
||||||
|
Configuration examples are available here:
|
||||||
|
|
||||||
https://github.com/shtorm-7/sing-box-extended/tree/extended/examples
|
https://github.com/shtorm-7/sing-box-extended/tree/extended/examples
|
||||||
|
|
||||||
## Support the project
|
## Support the Project
|
||||||
|
|
||||||
If you want to support the project, you can donate to the following addresses.
|
If you want to support the project, you can donate to the following addresses.
|
||||||
|
|
||||||
@@ -42,7 +67,7 @@ bc1qqx97p8k4dchqkyd47s4vf74hrqdfnmhqvcja7x
|
|||||||
0xAcc5919C22F2B3fAa0ec7E8BaD142da5B375FBF6
|
0xAcc5919C22F2B3fAa0ec7E8BaD142da5B375FBF6
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## 📄 License
|
||||||
|
|
||||||
```
|
```
|
||||||
Copyright (C) 2022 by nekohasekai <contact-sagernet@sekai.icu>
|
Copyright (C) 2022 by nekohasekai <contact-sagernet@sekai.icu>
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ type DNSTransport interface {
|
|||||||
Type() string
|
Type() string
|
||||||
Tag() string
|
Tag() string
|
||||||
Dependencies() []string
|
Dependencies() []string
|
||||||
|
// Reset closes the transport's existing connections so later requests use fresh connections.
|
||||||
|
// Exchanges that are currently using those connections may fail.
|
||||||
Reset()
|
Reset()
|
||||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
@@ -12,7 +11,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.EndpointManager = (*Manager)(nil)
|
var _ adapter.EndpointManager = (*Manager)(nil)
|
||||||
@@ -51,13 +49,12 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
|||||||
m.access.Unlock()
|
m.access.Unlock()
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
|
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
|
||||||
m.logger.Trace(stage, " ", name)
|
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||||
startTime := time.Now()
|
|
||||||
err := adapter.LegacyStart(endpoint, stage)
|
err := adapter.LegacyStart(endpoint, stage)
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage, " ", name)
|
return E.Cause(err, stage, " ", name)
|
||||||
}
|
}
|
||||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -75,14 +72,13 @@ func (m *Manager) Close() error {
|
|||||||
var err error
|
var err error
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
|
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
|
||||||
m.logger.Trace("close ", name)
|
done := adapter.LogElapsed(m.logger, "close ", name)
|
||||||
startTime := time.Now()
|
|
||||||
monitor.Start("close ", name)
|
monitor.Start("close ", name)
|
||||||
err = E.Append(err, endpoint.Close(), func(err error) error {
|
err = E.Append(err, endpoint.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", name)
|
return E.Cause(err, "close ", name)
|
||||||
})
|
})
|
||||||
monitor.Finish()
|
monitor.Finish()
|
||||||
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
done()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -133,13 +129,12 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
|||||||
if m.started {
|
if m.started {
|
||||||
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
|
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
|
||||||
for _, stage := range adapter.ListStartStages {
|
for _, stage := range adapter.ListStartStages {
|
||||||
m.logger.Trace(stage, " ", name)
|
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||||
startTime := time.Now()
|
|
||||||
err = adapter.LegacyStart(endpoint, stage)
|
err = adapter.LegacyStart(endpoint, stage)
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage, " ", name)
|
return E.Cause(err, stage, " ", name)
|
||||||
}
|
}
|
||||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {
|
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ type CacheFile interface {
|
|||||||
RDRCStore
|
RDRCStore
|
||||||
|
|
||||||
StoreWARPConfig() bool
|
StoreWARPConfig() bool
|
||||||
|
StoreMASQUEConfig() bool
|
||||||
|
|
||||||
LoadMode() string
|
LoadMode() string
|
||||||
StoreMode(mode string) error
|
StoreMode(mode string) error
|
||||||
@@ -59,6 +60,10 @@ type CacheFile interface {
|
|||||||
SaveRuleSet(tag string, set *SavedBinary) error
|
SaveRuleSet(tag string, set *SavedBinary) error
|
||||||
LoadWARPConfig(tag string) *SavedBinary
|
LoadWARPConfig(tag string) *SavedBinary
|
||||||
SaveWARPConfig(tag string, set *SavedBinary) error
|
SaveWARPConfig(tag string, set *SavedBinary) error
|
||||||
|
LoadMASQUEConfig(tag string) *SavedBinary
|
||||||
|
SaveMASQUEConfig(tag string, set *SavedBinary) error
|
||||||
|
LoadSubscription(tag string) *SavedBinary
|
||||||
|
SaveSubscription(tag string, sub *SavedBinary) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedBinary struct {
|
type SavedBinary struct {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type UDPInjectableInbound interface {
|
|||||||
type InboundRegistry interface {
|
type InboundRegistry interface {
|
||||||
option.InboundOptionsRegistry
|
option.InboundOptionsRegistry
|
||||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
|
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
|
||||||
|
UnsafeCreate(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type InboundManager interface {
|
type InboundManager interface {
|
||||||
@@ -41,16 +42,15 @@ type InboundManager interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InboundContext struct {
|
type InboundContext struct {
|
||||||
Inbound string
|
Inbound string
|
||||||
InboundType string
|
InboundType string
|
||||||
IPVersion uint8
|
IPVersion uint8
|
||||||
Network string
|
Network string
|
||||||
Source M.Socksaddr
|
Source M.Socksaddr
|
||||||
Destination M.Socksaddr
|
Destination M.Socksaddr
|
||||||
TunnelSource string
|
Gateway *netip.Addr
|
||||||
TunnelDestination string
|
User string
|
||||||
User string
|
Outbound string
|
||||||
Outbound string
|
|
||||||
|
|
||||||
// sniffer
|
// sniffer
|
||||||
|
|
||||||
@@ -103,6 +103,10 @@ type InboundContext struct {
|
|||||||
func (c *InboundContext) ResetRuleCache() {
|
func (c *InboundContext) ResetRuleCache() {
|
||||||
c.IPCIDRMatchSource = false
|
c.IPCIDRMatchSource = false
|
||||||
c.IPCIDRAcceptEmpty = false
|
c.IPCIDRAcceptEmpty = false
|
||||||
|
c.ResetRuleMatchCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *InboundContext) ResetRuleMatchCache() {
|
||||||
c.SourceAddressMatch = false
|
c.SourceAddressMatch = false
|
||||||
c.SourcePortMatch = false
|
c.SourcePortMatch = false
|
||||||
c.DestinationAddressMatch = false
|
c.DestinationAddressMatch = false
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
@@ -12,7 +11,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.InboundManager = (*Manager)(nil)
|
var _ adapter.InboundManager = (*Manager)(nil)
|
||||||
@@ -48,13 +46,12 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
|||||||
m.access.Unlock()
|
m.access.Unlock()
|
||||||
for _, inbound := range inbounds {
|
for _, inbound := range inbounds {
|
||||||
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
|
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
|
||||||
m.logger.Trace(stage, " ", name)
|
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||||
startTime := time.Now()
|
|
||||||
err := adapter.LegacyStart(inbound, stage)
|
err := adapter.LegacyStart(inbound, stage)
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage, " ", name)
|
return E.Cause(err, stage, " ", name)
|
||||||
}
|
}
|
||||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -72,14 +69,13 @@ func (m *Manager) Close() error {
|
|||||||
var err error
|
var err error
|
||||||
for _, inbound := range inbounds {
|
for _, inbound := range inbounds {
|
||||||
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
|
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
|
||||||
m.logger.Trace("close ", name)
|
done := adapter.LogElapsed(m.logger, "close ", name)
|
||||||
startTime := time.Now()
|
|
||||||
monitor.Start("close ", name)
|
monitor.Start("close ", name)
|
||||||
err = E.Append(err, inbound.Close(), func(err error) error {
|
err = E.Append(err, inbound.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", name)
|
return E.Cause(err, "close ", name)
|
||||||
})
|
})
|
||||||
monitor.Finish()
|
monitor.Finish()
|
||||||
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
done()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -133,13 +129,12 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
|||||||
if m.started {
|
if m.started {
|
||||||
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
|
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
|
||||||
for _, stage := range adapter.ListStartStages {
|
for _, stage := range adapter.ListStartStages {
|
||||||
m.logger.Trace(stage, " ", name)
|
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||||
startTime := time.Now()
|
|
||||||
err = adapter.LegacyStart(inbound, stage)
|
err = adapter.LegacyStart(inbound, stage)
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage, " ", name)
|
return E.Cause(err, stage, " ", name)
|
||||||
}
|
}
|
||||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if existsInbound, loaded := m.inboundByTag[tag]; loaded {
|
if existsInbound, loaded := m.inboundByTag[tag]; loaded {
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ func (m *Registry) CreateOptions(outboundType string) (any, bool) {
|
|||||||
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
|
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()
|
m.access.Lock()
|
||||||
defer m.access.Unlock()
|
defer m.access.Unlock()
|
||||||
|
return m.UnsafeCreate(ctx, router, logger, tag, outboundType, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Registry) UnsafeCreate(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
|
||||||
constructor, loaded := m.constructor[outboundType]
|
constructor, loaded := m.constructor[outboundType]
|
||||||
if !loaded {
|
if !loaded {
|
||||||
return nil, E.New("outbound type not found: " + outboundType)
|
return nil, E.New("outbound type not found: " + outboundType)
|
||||||
|
|||||||
@@ -77,26 +77,38 @@ func getServiceName(service any) string {
|
|||||||
func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) error {
|
func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) error {
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
name := getServiceName(service)
|
name := getServiceName(service)
|
||||||
logger.Trace(stage, " ", name)
|
done := LogElapsed(logger, stage, " ", name)
|
||||||
startTime := time.Now()
|
|
||||||
err := service.Start(stage)
|
err := service.Start(stage)
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error {
|
func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error {
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
logger.Trace(stage, " ", service.Name())
|
done := LogElapsed(logger, stage, " ", service.Name())
|
||||||
startTime := time.Now()
|
|
||||||
err := service.Start(stage)
|
err := service.Start(stage)
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage.String(), " ", service.Name())
|
return E.Cause(err, stage.String(), " ", service.Name())
|
||||||
}
|
}
|
||||||
logger.Trace(stage, " ", service.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LogElapsed(logger log.ContextLogger, description ...any) func() {
|
||||||
|
prefix := F.ToString(description...)
|
||||||
|
startTime := time.Now()
|
||||||
|
timer := time.AfterFunc(time.Second, func() {
|
||||||
|
logger.Trace(prefix, "...")
|
||||||
|
})
|
||||||
|
return func() {
|
||||||
|
if timer.Stop() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Trace(prefix, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
@@ -51,6 +54,24 @@ type WIFIState struct {
|
|||||||
BSSID string
|
BSSID string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NormalizeWIFIBSSID(bssid string) string {
|
||||||
|
bssid = strings.TrimSpace(bssid)
|
||||||
|
if bssid == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
parsed, err := net.ParseMAC(bssid)
|
||||||
|
if err == nil && len(parsed) == 6 {
|
||||||
|
return parsed.String()
|
||||||
|
}
|
||||||
|
if len(bssid) == 12 {
|
||||||
|
decoded, err := hex.DecodeString(bssid)
|
||||||
|
if err == nil {
|
||||||
|
return net.HardwareAddr(decoded).String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bssid
|
||||||
|
}
|
||||||
|
|
||||||
type NetworkInterface struct {
|
type NetworkInterface struct {
|
||||||
control.Interface
|
control.Interface
|
||||||
Type C.InterfaceType
|
Type C.InterfaceType
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type DirectRouteOutbound interface {
|
|||||||
type OutboundRegistry interface {
|
type OutboundRegistry interface {
|
||||||
option.OutboundOptionsRegistry
|
option.OutboundOptionsRegistry
|
||||||
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||||
|
UnsafeCreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutboundManager interface {
|
type OutboundManager interface {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
@@ -14,7 +13,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,13 +82,12 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
|||||||
m.access.Unlock()
|
m.access.Unlock()
|
||||||
for _, outbound := range outbounds {
|
for _, outbound := range outbounds {
|
||||||
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
||||||
m.logger.Trace(stage, " ", name)
|
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||||
startTime := time.Now()
|
|
||||||
err := adapter.LegacyStart(outbound, stage)
|
err := adapter.LegacyStart(outbound, stage)
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage, " ", name)
|
return E.Cause(err, stage, " ", name)
|
||||||
}
|
}
|
||||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -117,27 +114,25 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
|
|||||||
canContinue = true
|
canContinue = true
|
||||||
name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]"
|
name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]"
|
||||||
if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
|
if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
|
||||||
m.logger.Trace("start ", name)
|
done := adapter.LogElapsed(m.logger, "start ", name)
|
||||||
startTime := time.Now()
|
|
||||||
monitor.Start("start ", name)
|
monitor.Start("start ", name)
|
||||||
err := starter.Start(adapter.StartStateStart)
|
err := starter.Start(adapter.StartStateStart)
|
||||||
monitor.Finish()
|
monitor.Finish()
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "start ", name)
|
return E.Cause(err, "start ", name)
|
||||||
}
|
}
|
||||||
m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
} else if starter, isStarter := outboundToStart.(interface {
|
} else if starter, isStarter := outboundToStart.(interface {
|
||||||
Start() error
|
Start() error
|
||||||
}); isStarter {
|
}); isStarter {
|
||||||
m.logger.Trace("start ", name)
|
done := adapter.LogElapsed(m.logger, "start ", name)
|
||||||
startTime := time.Now()
|
|
||||||
monitor.Start("start ", name)
|
monitor.Start("start ", name)
|
||||||
err := starter.Start()
|
err := starter.Start()
|
||||||
monitor.Finish()
|
monitor.Finish()
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "start ", name)
|
return E.Cause(err, "start ", name)
|
||||||
}
|
}
|
||||||
m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(started) == len(outbounds) {
|
if len(started) == len(outbounds) {
|
||||||
@@ -185,14 +180,13 @@ func (m *Manager) Close() error {
|
|||||||
for _, outbound := range outbounds {
|
for _, outbound := range outbounds {
|
||||||
if closer, isCloser := outbound.(io.Closer); isCloser {
|
if closer, isCloser := outbound.(io.Closer); isCloser {
|
||||||
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
||||||
m.logger.Trace("close ", name)
|
done := adapter.LogElapsed(m.logger, "close ", name)
|
||||||
startTime := time.Now()
|
|
||||||
monitor.Start("close ", name)
|
monitor.Start("close ", name)
|
||||||
err = E.Append(err, closer.Close(), func(err error) error {
|
err = E.Append(err, closer.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", name)
|
return E.Cause(err, "close ", name)
|
||||||
})
|
})
|
||||||
monitor.Finish()
|
monitor.Finish()
|
||||||
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
done()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -275,13 +269,12 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
|
|||||||
if m.started {
|
if m.started {
|
||||||
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
|
||||||
for _, stage := range adapter.ListStartStages {
|
for _, stage := range adapter.ListStartStages {
|
||||||
m.logger.Trace(stage, " ", name)
|
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||||
startTime := time.Now()
|
|
||||||
err = adapter.LegacyStart(outbound, stage)
|
err = adapter.LegacyStart(outbound, stage)
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage, " ", name)
|
return E.Cause(err, stage, " ", name)
|
||||||
}
|
}
|
||||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.access.Lock()
|
m.access.Lock()
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ func (r *Registry) CreateOptions(outboundType string) (any, bool) {
|
|||||||
func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
|
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()
|
r.access.Lock()
|
||||||
defer r.access.Unlock()
|
defer r.access.Unlock()
|
||||||
|
return r.UnsafeCreateOutbound(ctx, router, logger, tag, outboundType, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) UnsafeCreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
|
||||||
constructor, loaded := r.constructors[outboundType]
|
constructor, loaded := r.constructors[outboundType]
|
||||||
if !loaded {
|
if !loaded {
|
||||||
return nil, E.New("outbound type not found: " + outboundType)
|
return nil, E.New("outbound type not found: " + outboundType)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-tun"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
@@ -36,6 +38,8 @@ type PlatformInterface interface {
|
|||||||
|
|
||||||
UsePlatformNotification() bool
|
UsePlatformNotification() bool
|
||||||
SendNotification(notification *Notification) error
|
SendNotification(notification *Notification) error
|
||||||
|
|
||||||
|
MyInterfaceAddress() []netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindConnectionOwnerRequest struct {
|
type FindConnectionOwnerRequest struct {
|
||||||
@@ -47,11 +51,11 @@ type FindConnectionOwnerRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ConnectionOwner struct {
|
type ConnectionOwner struct {
|
||||||
ProcessID uint32
|
ProcessID uint32
|
||||||
UserId int32
|
UserId int32
|
||||||
UserName string
|
UserName string
|
||||||
ProcessPath string
|
ProcessPath string
|
||||||
AndroidPackageName string
|
AndroidPackageNames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
|
|||||||
51
adapter/provider.go
Normal file
51
adapter/provider.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
Type() string
|
||||||
|
Tag() string
|
||||||
|
Outbounds() []Outbound
|
||||||
|
Outbound(tag string) (Outbound, bool)
|
||||||
|
UpdatedAt() time.Time
|
||||||
|
HealthCheck(ctx context.Context) (map[string]uint16, error)
|
||||||
|
RegisterCallback(callback ProviderUpdateCallback) *list.Element[ProviderUpdateCallback]
|
||||||
|
UnregisterCallback(element *list.Element[ProviderUpdateCallback])
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderUpdater interface {
|
||||||
|
Update() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderSubscriptionInfo interface {
|
||||||
|
SubscriptionInfo() SubscriptionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderRegistry interface {
|
||||||
|
option.ProviderOptionsRegistry
|
||||||
|
CreateProvider(ctx context.Context, router Router, logFactory log.Factory, tag string, providerType string, options any) (Provider, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderManager interface {
|
||||||
|
Lifecycle
|
||||||
|
Providers() []Provider
|
||||||
|
Get(tag string) (Provider, bool)
|
||||||
|
Remove(tag string) error
|
||||||
|
Create(ctx context.Context, router Router, logFactory log.Factory, tag string, providerType string, options any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionInfo struct {
|
||||||
|
Upload int64
|
||||||
|
Download int64
|
||||||
|
Total int64
|
||||||
|
Expire int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProviderUpdateCallback = func(tag string) error
|
||||||
267
adapter/provider/adapter.go
Normal file
267
adapter/provider/adapter.go
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/urltest"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common/batch"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Adapter struct {
|
||||||
|
ctx context.Context
|
||||||
|
outbound adapter.OutboundManager
|
||||||
|
router adapter.Router
|
||||||
|
logFactory log.Factory
|
||||||
|
logger log.ContextLogger
|
||||||
|
providerType string
|
||||||
|
providerTag string
|
||||||
|
outbounds []adapter.Outbound
|
||||||
|
outboundsByTag map[string]adapter.Outbound
|
||||||
|
ticker *time.Ticker
|
||||||
|
checking atomic.Bool
|
||||||
|
history adapter.URLTestHistoryStorage
|
||||||
|
callbackAccess sync.Mutex
|
||||||
|
callbacks list.List[adapter.ProviderUpdateCallback]
|
||||||
|
|
||||||
|
link string
|
||||||
|
enabled bool
|
||||||
|
timeout time.Duration
|
||||||
|
interval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdapter(ctx context.Context, router adapter.Router, outbound adapter.OutboundManager, logFactory log.Factory, logger log.ContextLogger, providerTag string, providerType string, options option.ProviderHealthCheckOptions) Adapter {
|
||||||
|
timeout := time.Duration(options.Timeout)
|
||||||
|
if timeout == 0 {
|
||||||
|
timeout = 3 * time.Second
|
||||||
|
}
|
||||||
|
interval := time.Duration(options.Interval)
|
||||||
|
if interval == 0 {
|
||||||
|
interval = 10 * time.Minute
|
||||||
|
}
|
||||||
|
if interval < time.Minute {
|
||||||
|
interval = time.Minute
|
||||||
|
}
|
||||||
|
return Adapter{
|
||||||
|
ctx: ctx,
|
||||||
|
outbound: outbound,
|
||||||
|
router: router,
|
||||||
|
logFactory: logFactory,
|
||||||
|
logger: logger,
|
||||||
|
providerType: providerType,
|
||||||
|
providerTag: providerTag,
|
||||||
|
|
||||||
|
enabled: options.Enabled,
|
||||||
|
link: options.URL,
|
||||||
|
timeout: timeout,
|
||||||
|
interval: interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Start() error {
|
||||||
|
a.history = service.FromContext[adapter.URLTestHistoryStorage](a.ctx)
|
||||||
|
if a.history == nil {
|
||||||
|
if clashServer := service.FromContext[adapter.ClashServer](a.ctx); clashServer != nil {
|
||||||
|
a.history = clashServer.HistoryStorage()
|
||||||
|
} else {
|
||||||
|
a.history = urltest.NewHistoryStorage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go a.loopCheck()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Type() string {
|
||||||
|
return a.providerType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Tag() string {
|
||||||
|
return a.providerTag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Outbounds() []adapter.Outbound {
|
||||||
|
return a.outbounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Outbound(tag string) (adapter.Outbound, bool) {
|
||||||
|
if a.outboundsByTag == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
detour, ok := a.outboundsByTag[tag]
|
||||||
|
return detour, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) UpdateOutbounds(oldOpts []option.Outbound, newOpts []option.Outbound) {
|
||||||
|
a.removeUseless(newOpts)
|
||||||
|
var (
|
||||||
|
oldOptByTag = make(map[string]option.Outbound)
|
||||||
|
outbounds = make([]adapter.Outbound, 0, len(newOpts))
|
||||||
|
outboundsByTag = make(map[string]adapter.Outbound)
|
||||||
|
)
|
||||||
|
for _, opt := range oldOpts {
|
||||||
|
oldOptByTag[opt.Tag] = opt
|
||||||
|
}
|
||||||
|
for i, opt := range newOpts {
|
||||||
|
var tag string
|
||||||
|
if opt.Tag != "" {
|
||||||
|
tag = F.ToString(a.providerTag, "/", opt.Tag)
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(a.providerTag, "/", i)
|
||||||
|
}
|
||||||
|
outbound, exist := a.outbound.Outbound(tag)
|
||||||
|
if !exist || !reflect.DeepEqual(opt, oldOptByTag[opt.Tag]) {
|
||||||
|
err := a.outbound.Create(
|
||||||
|
adapter.WithContext(a.ctx, &adapter.InboundContext{
|
||||||
|
Outbound: tag,
|
||||||
|
}),
|
||||||
|
a.router,
|
||||||
|
a.logFactory.NewLogger(F.ToString("outbound/", opt.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
opt.Type,
|
||||||
|
opt.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Warn(err, " in ", tag, ", skip create this outbound")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
outbound, _ = a.outbound.Outbound(tag)
|
||||||
|
}
|
||||||
|
outbounds = append(outbounds, outbound)
|
||||||
|
outboundsByTag[tag] = outbound
|
||||||
|
}
|
||||||
|
if a.enabled && a.history != nil {
|
||||||
|
go a.HealthCheck(a.ctx)
|
||||||
|
}
|
||||||
|
a.outbounds = outbounds
|
||||||
|
a.outboundsByTag = outboundsByTag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) HealthCheck(ctx context.Context) (map[string]uint16, error) {
|
||||||
|
if a.ticker != nil {
|
||||||
|
a.ticker.Reset(a.interval)
|
||||||
|
}
|
||||||
|
return a.healthcheck(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) RegisterCallback(callback adapter.ProviderUpdateCallback) *list.Element[adapter.ProviderUpdateCallback] {
|
||||||
|
a.callbackAccess.Lock()
|
||||||
|
defer a.callbackAccess.Unlock()
|
||||||
|
return a.callbacks.PushBack(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) UnregisterCallback(element *list.Element[adapter.ProviderUpdateCallback]) {
|
||||||
|
a.callbackAccess.Lock()
|
||||||
|
defer a.callbackAccess.Unlock()
|
||||||
|
a.callbacks.Remove(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) UpdateGroups() {
|
||||||
|
for element := a.callbacks.Front(); element != nil; element = element.Next() {
|
||||||
|
element.Value(a.providerTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Close() error {
|
||||||
|
if a.ticker != nil {
|
||||||
|
a.ticker.Stop()
|
||||||
|
}
|
||||||
|
outbounds := a.outbounds
|
||||||
|
a.outbounds = nil
|
||||||
|
var err error
|
||||||
|
for _, ob := range outbounds {
|
||||||
|
if err2 := a.outbound.Remove(ob.Tag()); err2 != nil {
|
||||||
|
err = E.Append(err, err2, func(err error) error {
|
||||||
|
return E.Cause(err, "close outbound [", ob.Tag(), "]")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) loopCheck() {
|
||||||
|
if !a.enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.ticker = time.NewTicker(a.interval)
|
||||||
|
a.healthcheck(a.ctx)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-a.ticker.C:
|
||||||
|
a.healthcheck(a.ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) healthcheck(ctx context.Context) (map[string]uint16, error) {
|
||||||
|
result := make(map[string]uint16)
|
||||||
|
if a.checking.Swap(true) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
defer a.checking.Store(false)
|
||||||
|
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
|
||||||
|
var resultAccess sync.Mutex
|
||||||
|
checked := make(map[string]bool)
|
||||||
|
for _, detour := range a.outbounds {
|
||||||
|
tag := detour.Tag()
|
||||||
|
if checked[tag] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
checked[tag] = true
|
||||||
|
b.Go(tag, func() (any, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(a.ctx, a.timeout)
|
||||||
|
defer cancel()
|
||||||
|
t, err := urltest.URLTest(ctx, a.link, detour)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Debug("outbound ", tag, " unavailable: ", err)
|
||||||
|
a.history.DeleteURLTestHistory(tag)
|
||||||
|
} else {
|
||||||
|
a.logger.Debug("outbound ", tag, " available: ", t, "ms")
|
||||||
|
a.history.StoreURLTestHistory(tag, &adapter.URLTestHistory{
|
||||||
|
Time: time.Now(),
|
||||||
|
Delay: t,
|
||||||
|
})
|
||||||
|
resultAccess.Lock()
|
||||||
|
result[tag] = t
|
||||||
|
resultAccess.Unlock()
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
b.Wait()
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) removeUseless(newOpts []option.Outbound) {
|
||||||
|
if len(a.outbounds) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exists := make(map[string]bool)
|
||||||
|
for i, opt := range newOpts {
|
||||||
|
var tag string
|
||||||
|
if opt.Tag != "" {
|
||||||
|
tag = F.ToString(a.providerTag, "/", opt.Tag)
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(a.providerTag, "/", i)
|
||||||
|
}
|
||||||
|
exists[tag] = true
|
||||||
|
}
|
||||||
|
for _, opt := range a.outbounds {
|
||||||
|
if !exists[opt.Tag()] {
|
||||||
|
if err := a.outbound.Remove(opt.Tag()); err != nil {
|
||||||
|
a.logger.Error(err, "close outbound [", opt.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
adapter/provider/manager.go
Normal file
157
adapter/provider/manager.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.ProviderManager = (*Manager)(nil)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
logger log.ContextLogger
|
||||||
|
registry adapter.ProviderRegistry
|
||||||
|
access sync.Mutex
|
||||||
|
started bool
|
||||||
|
stage adapter.StartStage
|
||||||
|
providers []adapter.Provider
|
||||||
|
providerByTag map[string]adapter.Provider
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(logger logger.ContextLogger, registry adapter.ProviderRegistry) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
logger: logger,
|
||||||
|
registry: registry,
|
||||||
|
providerByTag: make(map[string]adapter.Provider),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Initialize() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||||
|
m.access.Lock()
|
||||||
|
if m.started && m.stage >= stage {
|
||||||
|
panic("already started")
|
||||||
|
}
|
||||||
|
m.started = true
|
||||||
|
m.stage = stage
|
||||||
|
providers := m.providers
|
||||||
|
m.access.Unlock()
|
||||||
|
for _, provider := range providers {
|
||||||
|
err := adapter.LegacyStart(provider, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() error {
|
||||||
|
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||||
|
m.access.Lock()
|
||||||
|
if !m.started {
|
||||||
|
m.access.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.started = false
|
||||||
|
providers := m.providers
|
||||||
|
m.providers = nil
|
||||||
|
m.access.Unlock()
|
||||||
|
var err error
|
||||||
|
for _, provider := range providers {
|
||||||
|
if closer, isCloser := provider.(io.Closer); isCloser {
|
||||||
|
monitor.Start("close provider/", provider.Type(), "[", provider.Tag(), "]")
|
||||||
|
err = E.Append(err, closer.Close(), func(err error) error {
|
||||||
|
return E.Cause(err, "close provider/", provider.Type(), "[", provider.Tag(), "]")
|
||||||
|
})
|
||||||
|
monitor.Finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Providers() []adapter.Provider {
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
return m.providers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Get(tag string) (adapter.Provider, bool) {
|
||||||
|
m.access.Lock()
|
||||||
|
provider, found := m.providerByTag[tag]
|
||||||
|
m.access.Unlock()
|
||||||
|
return provider, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Remove(tag string) error {
|
||||||
|
m.access.Lock()
|
||||||
|
provider, found := m.providerByTag[tag]
|
||||||
|
if !found {
|
||||||
|
m.access.Unlock()
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
delete(m.providerByTag, tag)
|
||||||
|
index := common.Index(m.providers, func(it adapter.Provider) bool {
|
||||||
|
return it == provider
|
||||||
|
})
|
||||||
|
if index == -1 {
|
||||||
|
panic("invalid provider index")
|
||||||
|
}
|
||||||
|
m.providers = append(m.providers[:index], m.providers[index+1:]...)
|
||||||
|
started := m.started
|
||||||
|
m.access.Unlock()
|
||||||
|
if started {
|
||||||
|
return common.Close(provider)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Create(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) error {
|
||||||
|
if tag == "" {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := m.registry.CreateProvider(ctx, router, logFactory, tag, providerType, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
|
if m.started {
|
||||||
|
for _, stage := range adapter.ListStartStages {
|
||||||
|
err = adapter.LegacyStart(provider, stage)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if existsProvider, loaded := m.providerByTag[tag]; loaded {
|
||||||
|
if m.started {
|
||||||
|
err = common.Close(existsProvider)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "close provider", provider.Type(), "[", existsProvider.Tag(), "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existsIndex := common.Index(m.providers, func(it adapter.Provider) bool {
|
||||||
|
return it == existsProvider
|
||||||
|
})
|
||||||
|
if existsIndex == -1 {
|
||||||
|
panic("invalid provider index")
|
||||||
|
}
|
||||||
|
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
|
||||||
|
}
|
||||||
|
m.providers = append(m.providers, provider)
|
||||||
|
m.providerByTag[tag] = provider
|
||||||
|
return nil
|
||||||
|
}
|
||||||
72
adapter/provider/registry.go
Normal file
72
adapter/provider/registry.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options T) (adapter.Provider, error)
|
||||||
|
|
||||||
|
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
|
||||||
|
registry.register(providerType, func() any {
|
||||||
|
return new(Options)
|
||||||
|
}, func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, rawOptions any) (adapter.Provider, error) {
|
||||||
|
var options *Options
|
||||||
|
if rawOptions != nil {
|
||||||
|
options = rawOptions.(*Options)
|
||||||
|
}
|
||||||
|
return constructor(ctx, router, logFactory, tag, common.PtrValueOrDefault(options))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.ProviderRegistry = (*Registry)(nil)
|
||||||
|
|
||||||
|
type (
|
||||||
|
optionsConstructorFunc func() any
|
||||||
|
constructorFunc func(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options any) (adapter.Provider, error)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
access sync.Mutex
|
||||||
|
optionsType map[string]optionsConstructorFunc
|
||||||
|
constructors map[string]constructorFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry() *Registry {
|
||||||
|
return &Registry{
|
||||||
|
optionsType: make(map[string]optionsConstructorFunc),
|
||||||
|
constructors: make(map[string]constructorFunc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) CreateOptions(providerType string) (any, bool) {
|
||||||
|
r.access.Lock()
|
||||||
|
defer r.access.Unlock()
|
||||||
|
optionsConstructor, loaded := r.optionsType[providerType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return optionsConstructor(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) CreateProvider(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) (adapter.Provider, error) {
|
||||||
|
r.access.Lock()
|
||||||
|
defer r.access.Unlock()
|
||||||
|
constructor, loaded := r.constructors[providerType]
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("provider type not found: '" + providerType + "'")
|
||||||
|
}
|
||||||
|
return constructor(ctx, router, logFactory, tag, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||||
|
r.access.Lock()
|
||||||
|
defer r.access.Unlock()
|
||||||
|
r.optionsType[providerType] = optionsConstructor
|
||||||
|
r.constructors[providerType] = constructor
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
@@ -12,7 +11,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.ServiceManager = (*Manager)(nil)
|
var _ adapter.ServiceManager = (*Manager)(nil)
|
||||||
@@ -46,13 +44,12 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
|||||||
m.access.Unlock()
|
m.access.Unlock()
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
name := "service/" + service.Type() + "[" + service.Tag() + "]"
|
name := "service/" + service.Type() + "[" + service.Tag() + "]"
|
||||||
m.logger.Trace(stage, " ", name)
|
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||||
startTime := time.Now()
|
|
||||||
err := adapter.LegacyStart(service, stage)
|
err := adapter.LegacyStart(service, stage)
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage, " ", name)
|
return E.Cause(err, stage, " ", name)
|
||||||
}
|
}
|
||||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -70,14 +67,13 @@ func (m *Manager) Close() error {
|
|||||||
var err error
|
var err error
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
name := "service/" + service.Type() + "[" + service.Tag() + "]"
|
name := "service/" + service.Type() + "[" + service.Tag() + "]"
|
||||||
m.logger.Trace("close ", name)
|
done := adapter.LogElapsed(m.logger, "close ", name)
|
||||||
startTime := time.Now()
|
|
||||||
monitor.Start("close ", name)
|
monitor.Start("close ", name)
|
||||||
err = E.Append(err, service.Close(), func(err error) error {
|
err = E.Append(err, service.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", name)
|
return E.Cause(err, "close ", name)
|
||||||
})
|
})
|
||||||
monitor.Finish()
|
monitor.Finish()
|
||||||
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
done()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -128,13 +124,12 @@ func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag stri
|
|||||||
if m.started {
|
if m.started {
|
||||||
name := "service/" + service.Type() + "[" + service.Tag() + "]"
|
name := "service/" + service.Type() + "[" + service.Tag() + "]"
|
||||||
for _, stage := range adapter.ListStartStages {
|
for _, stage := range adapter.ListStartStages {
|
||||||
m.logger.Trace(stage, " ", name)
|
done := adapter.LogElapsed(m.logger, stage, " ", name)
|
||||||
startTime := time.Now()
|
|
||||||
err = adapter.LegacyStart(service, stage)
|
err = adapter.LegacyStart(service, stage)
|
||||||
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage, " ", name)
|
return E.Cause(err, stage, " ", name)
|
||||||
}
|
}
|
||||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if existsService, loaded := m.serviceByTag[tag]; loaded {
|
if existsService, loaded := m.serviceByTag[tag]; loaded {
|
||||||
|
|||||||
73
box.go
73
box.go
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
"github.com/sagernet/sing-box/adapter/outbound"
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
|
"github.com/sagernet/sing-box/adapter/provider"
|
||||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||||
"github.com/sagernet/sing-box/common/certificate"
|
"github.com/sagernet/sing-box/common/certificate"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
@@ -20,7 +21,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/common/urltest"
|
"github.com/sagernet/sing-box/common/urltest"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/dns"
|
"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"
|
||||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@@ -45,6 +45,7 @@ type Box struct {
|
|||||||
endpoint *endpoint.Manager
|
endpoint *endpoint.Manager
|
||||||
inbound *inbound.Manager
|
inbound *inbound.Manager
|
||||||
outbound *outbound.Manager
|
outbound *outbound.Manager
|
||||||
|
provider *provider.Manager
|
||||||
service *boxService.Manager
|
service *boxService.Manager
|
||||||
dnsTransport *dns.TransportManager
|
dnsTransport *dns.TransportManager
|
||||||
dnsRouter *dns.Router
|
dnsRouter *dns.Router
|
||||||
@@ -65,6 +66,7 @@ func Context(
|
|||||||
inboundRegistry adapter.InboundRegistry,
|
inboundRegistry adapter.InboundRegistry,
|
||||||
outboundRegistry adapter.OutboundRegistry,
|
outboundRegistry adapter.OutboundRegistry,
|
||||||
endpointRegistry adapter.EndpointRegistry,
|
endpointRegistry adapter.EndpointRegistry,
|
||||||
|
providerRegistry adapter.ProviderRegistry,
|
||||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||||
serviceRegistry adapter.ServiceRegistry,
|
serviceRegistry adapter.ServiceRegistry,
|
||||||
) context.Context {
|
) context.Context {
|
||||||
@@ -83,6 +85,11 @@ func Context(
|
|||||||
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
||||||
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
||||||
}
|
}
|
||||||
|
if service.FromContext[option.ProviderOptionsRegistry](ctx) == nil ||
|
||||||
|
service.FromContext[adapter.ProviderRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.ProviderOptionsRegistry](ctx, providerRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.ProviderRegistry](ctx, providerRegistry)
|
||||||
|
}
|
||||||
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
|
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
|
||||||
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
||||||
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
||||||
@@ -105,6 +112,7 @@ func New(options Options) (*Box, error) {
|
|||||||
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
||||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||||
|
providerRegistry := service.FromContext[adapter.ProviderRegistry](ctx)
|
||||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||||
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||||
|
|
||||||
@@ -117,6 +125,9 @@ func New(options Options) (*Box, error) {
|
|||||||
if outboundRegistry == nil {
|
if outboundRegistry == nil {
|
||||||
return nil, E.New("missing outbound registry in context")
|
return nil, E.New("missing outbound registry in context")
|
||||||
}
|
}
|
||||||
|
if providerRegistry == nil {
|
||||||
|
return nil, E.New("missing provider registry in context")
|
||||||
|
}
|
||||||
if dnsTransportRegistry == nil {
|
if dnsTransportRegistry == nil {
|
||||||
return nil, E.New("missing DNS transport registry in context")
|
return nil, E.New("missing DNS transport registry in context")
|
||||||
}
|
}
|
||||||
@@ -161,6 +172,7 @@ func New(options Options) (*Box, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create log factory")
|
return nil, E.Cause(err, "create log factory")
|
||||||
}
|
}
|
||||||
|
service.MustRegister[log.Factory](ctx, logFactory)
|
||||||
|
|
||||||
var internalServices []adapter.LifecycleService
|
var internalServices []adapter.LifecycleService
|
||||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||||
@@ -181,11 +193,13 @@ func New(options Options) (*Box, error) {
|
|||||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||||
|
providerManager := provider.NewManager(logFactory.NewLogger("provider"), providerRegistry)
|
||||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||||
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||||
|
service.MustRegister[adapter.ProviderManager](ctx, providerManager)
|
||||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||||
@@ -276,6 +290,10 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
options.Outbounds = append(options.Outbounds, option.Outbound{
|
||||||
|
Tag: "Compatible",
|
||||||
|
Type: C.TypeDirect,
|
||||||
|
})
|
||||||
for i, outboundOptions := range options.Outbounds {
|
for i, outboundOptions := range options.Outbounds {
|
||||||
var tag string
|
var tag string
|
||||||
if outboundOptions.Tag != "" {
|
if outboundOptions.Tag != "" {
|
||||||
@@ -302,6 +320,25 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for i, providerOptions := range options.Providers {
|
||||||
|
var tag string
|
||||||
|
if providerOptions.Tag != "" {
|
||||||
|
tag = providerOptions.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
err = providerManager.Create(
|
||||||
|
ctx,
|
||||||
|
router,
|
||||||
|
logFactory,
|
||||||
|
tag,
|
||||||
|
providerOptions.Type,
|
||||||
|
providerOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize provider[", i, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
for i, serviceOptions := range options.Services {
|
for i, serviceOptions := range options.Services {
|
||||||
var tag string
|
var tag string
|
||||||
if serviceOptions.Tag != "" {
|
if serviceOptions.Tag != "" {
|
||||||
@@ -330,11 +367,12 @@ func New(options Options) (*Box, error) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||||
return local.NewTransport(
|
return dnsTransportRegistry.CreateDNSTransport(
|
||||||
ctx,
|
ctx,
|
||||||
logFactory.NewLogger("dns/local"),
|
logFactory.NewLogger("dns/local"),
|
||||||
"local",
|
"local",
|
||||||
option.LocalDNSServerOptions{},
|
C.DNSTypeLocal,
|
||||||
|
&option.LocalDNSServerOptions{},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
@@ -391,6 +429,7 @@ func New(options Options) (*Box, error) {
|
|||||||
endpoint: endpointManager,
|
endpoint: endpointManager,
|
||||||
inbound: inboundManager,
|
inbound: inboundManager,
|
||||||
outbound: outboundManager,
|
outbound: outboundManager,
|
||||||
|
provider: providerManager,
|
||||||
dnsTransport: dnsTransportManager,
|
dnsTransport: dnsTransportManager,
|
||||||
service: serviceManager,
|
service: serviceManager,
|
||||||
dnsRouter: dnsRouter,
|
dnsRouter: dnsRouter,
|
||||||
@@ -454,11 +493,11 @@ func (s *Box) preStart() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.provider, s.service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.provider, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -478,7 +517,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
|
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.provider, s.service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -486,7 +525,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.provider, s.service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -512,6 +551,7 @@ func (s *Box) Close() error {
|
|||||||
{"service", s.service},
|
{"service", s.service},
|
||||||
{"endpoint", s.endpoint},
|
{"endpoint", s.endpoint},
|
||||||
{"inbound", s.inbound},
|
{"inbound", s.inbound},
|
||||||
|
{"provider", s.provider},
|
||||||
{"outbound", s.outbound},
|
{"outbound", s.outbound},
|
||||||
{"router", s.router},
|
{"router", s.router},
|
||||||
{"connection", s.connection},
|
{"connection", s.connection},
|
||||||
@@ -519,27 +559,24 @@ func (s *Box) Close() error {
|
|||||||
{"dns-transport", s.dnsTransport},
|
{"dns-transport", s.dnsTransport},
|
||||||
{"network", s.network},
|
{"network", s.network},
|
||||||
} {
|
} {
|
||||||
s.logger.Trace("close ", closeItem.name)
|
done := adapter.LogElapsed(s.logger, "close ", closeItem.name)
|
||||||
startTime := time.Now()
|
|
||||||
err = E.Append(err, closeItem.service.Close(), func(err error) error {
|
err = E.Append(err, closeItem.service.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", closeItem.name)
|
return E.Cause(err, "close ", closeItem.name)
|
||||||
})
|
})
|
||||||
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
done()
|
||||||
}
|
}
|
||||||
for _, lifecycleService := range s.internalService {
|
for _, lifecycleService := range s.internalService {
|
||||||
s.logger.Trace("close ", lifecycleService.Name())
|
done := adapter.LogElapsed(s.logger, "close ", lifecycleService.Name())
|
||||||
startTime := time.Now()
|
|
||||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close ", lifecycleService.Name())
|
return E.Cause(err, "close ", lifecycleService.Name())
|
||||||
})
|
})
|
||||||
s.logger.Trace("close ", lifecycleService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
done()
|
||||||
}
|
}
|
||||||
s.logger.Trace("close logger")
|
done := adapter.LogElapsed(s.logger, "close logger")
|
||||||
startTime := time.Now()
|
|
||||||
err = E.Append(err, s.logFactory.Close(), func(err error) error {
|
err = E.Append(err, s.logFactory.Close(), func(err error) error {
|
||||||
return E.Cause(err, "close logger")
|
return E.Cause(err, "close logger")
|
||||||
})
|
})
|
||||||
s.logger.Trace("close logger completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
done()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,6 +596,10 @@ func (s *Box) Outbound() adapter.OutboundManager {
|
|||||||
return s.outbound
|
return s.outbound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Box) Endpoint() adapter.EndpointManager {
|
||||||
|
return s.endpoint
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Box) LogFactory() log.Factory {
|
func (s *Box) LogFactory() log.Factory {
|
||||||
return s.logFactory
|
return s.logFactory
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build with_quic
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
package cloudflare
|
package cloudflare
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CloudflareApi struct {
|
type CloudflareApi struct {
|
||||||
@@ -25,50 +22,93 @@ func NewCloudflareApi(opts ...CloudflareApiOption) *CloudflareApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *CloudflareApi) CreateProfile(ctx context.Context, publicKey string) (*CloudflareProfile, error) {
|
func (api *CloudflareApi) CreateProfile(ctx context.Context, publicKey string) (*CloudflareProfile, error) {
|
||||||
request, err := http.NewRequest("POST", "https://api.cloudflareclient.com/v0i1909051800/reg", strings.NewReader(
|
serial, err := GenerateRandomAndroidSerial()
|
||||||
fmt.Sprintf(
|
if err != nil {
|
||||||
"{\"install_id\":\"\",\"tos\":\"%s\",\"key\":\"%s\",\"fcm_token\":\"\",\"type\":\"ios\",\"locale\":\"en_US\"}",
|
return nil, fmt.Errorf("failed to generate serial: %v", err)
|
||||||
time.Now().Format("2006-01-02T15:04:05.000Z"),
|
}
|
||||||
publicKey,
|
data := Registration{
|
||||||
),
|
Key: publicKey,
|
||||||
))
|
InstallID: "",
|
||||||
|
FcmToken: "",
|
||||||
|
Tos: TimeAsCfString(time.Now()),
|
||||||
|
Model: "PC",
|
||||||
|
Serial: serial,
|
||||||
|
OsVersion: "",
|
||||||
|
KeyType: KeyTypeWg,
|
||||||
|
TunType: TunTypeWg,
|
||||||
|
Locale: "en-US",
|
||||||
|
}
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal json: %v", err)
|
||||||
|
}
|
||||||
|
request, err := http.NewRequest("POST", ApiUrl+"/"+ApiVersion+"/reg", bytes.NewBuffer(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
for k, v := range Headers {
|
||||||
|
request.Header.Set(k, v)
|
||||||
|
}
|
||||||
response, err := api.client.Do(request.WithContext(ctx))
|
response, err := api.client.Do(request.WithContext(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
if response.StatusCode != 200 {
|
if response.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("status code is not 200")
|
return nil, fmt.Errorf("failed to register: %v", response.StatusCode)
|
||||||
}
|
|
||||||
content, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
profile := new(CloudflareProfile)
|
profile := new(CloudflareProfile)
|
||||||
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
|
return profile, json.NewDecoder(response.Body).Decode(profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *CloudflareApi) GetProfile(ctx context.Context, authToken string, id string) (*CloudflareProfile, error) {
|
func (api *CloudflareApi) EnrollKey(ctx context.Context, authToken string, id string, keyType, tunType, publicKey string) (*CloudflareProfile, error) {
|
||||||
request, err := http.NewRequest("GET", "https://api.cloudflareclient.com/v0i1909051800/reg/"+id, nil)
|
deviceUpdate := DeviceUpdate{
|
||||||
|
Name: "PC",
|
||||||
|
Key: publicKey,
|
||||||
|
KeyType: keyType,
|
||||||
|
TunType: tunType,
|
||||||
|
}
|
||||||
|
jsonData, err := json.Marshal(deviceUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal json: %v", err)
|
||||||
|
}
|
||||||
|
request, err := http.NewRequest("PATCH", ApiUrl+"/"+ApiVersion+"/reg/"+id, bytes.NewBuffer(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
for k, v := range Headers {
|
||||||
|
request.Header.Set(k, v)
|
||||||
|
}
|
||||||
request.Header.Set("Authorization", "Bearer "+authToken)
|
request.Header.Set("Authorization", "Bearer "+authToken)
|
||||||
response, err := api.client.Do(request.WithContext(ctx))
|
response, err := api.client.Do(request.WithContext(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
if response.StatusCode != 200 {
|
if response.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("status code is not 200")
|
return nil, fmt.Errorf("failed to enroll key: %v", response.StatusCode)
|
||||||
}
|
}
|
||||||
content, err := io.ReadAll(response.Body)
|
profile := new(CloudflareProfile)
|
||||||
|
return profile, json.NewDecoder(response.Body).Decode(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *CloudflareApi) GetProfile(ctx context.Context, authToken string, id string) (*CloudflareProfile, error) {
|
||||||
|
request, err := http.NewRequest("GET", ApiUrl+"/"+ApiVersion+"/reg/"+id, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
for k, v := range Headers {
|
||||||
|
request.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
request.Header.Set("Authorization", "Bearer "+authToken)
|
||||||
|
response, err := api.client.Do(request.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to get profile: %v", response.StatusCode)
|
||||||
|
}
|
||||||
profile := new(CloudflareProfile)
|
profile := new(CloudflareProfile)
|
||||||
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
|
return profile, json.NewDecoder(response.Body).Decode(profile)
|
||||||
}
|
}
|
||||||
|
|||||||
25
common/cloudflare/constant.go
Normal file
25
common/cloudflare/constant.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package cloudflare
|
||||||
|
|
||||||
|
const (
|
||||||
|
ApiUrl = "https://api.cloudflareclient.com"
|
||||||
|
ApiVersion = "v0a4471"
|
||||||
|
ConnectSNI = "consumer-masque.cloudflareclient.com"
|
||||||
|
// unused for now
|
||||||
|
ZeroTierSNI = "zt-masque.cloudflareclient.com"
|
||||||
|
ConnectURI = "https://cloudflareaccess.com"
|
||||||
|
DefaultModel = "PC"
|
||||||
|
KeyTypeWg = "curve25519"
|
||||||
|
TunTypeWg = "wireguard"
|
||||||
|
KeyTypeMasque = "secp256r1"
|
||||||
|
TunTypeMasque = "masque"
|
||||||
|
DefaultLocale = "en_US"
|
||||||
|
DefaultEndpointH2V4 = "162.159.198.2"
|
||||||
|
DefaultEndpointH2V6 = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
var Headers = map[string]string{
|
||||||
|
"User-Agent": "WARP for Android",
|
||||||
|
"CF-Client-Version": "a-6.35-4471",
|
||||||
|
"Content-Type": "application/json; charset=UTF-8",
|
||||||
|
"Connection": "Keep-Alive",
|
||||||
|
}
|
||||||
132
common/cloudflare/models.go
Normal file
132
common/cloudflare/models.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package cloudflare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Registration struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
InstallID string `json:"install_id"`
|
||||||
|
FcmToken string `json:"fcm_token"`
|
||||||
|
Tos string `json:"tos"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Serial string `json:"serial_number"`
|
||||||
|
OsVersion string `json:"os_version"`
|
||||||
|
KeyType string `json:"key_type"`
|
||||||
|
TunType string `json:"tunnel_type"`
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloudflareProfile struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
KeyType string `json:"key_type"`
|
||||||
|
TunType string `json:"tunnel_type"`
|
||||||
|
Account Account `json:"account"`
|
||||||
|
Config Config `json:"config"`
|
||||||
|
// WarpEnabled not set for ZeroTier
|
||||||
|
WarpEnabled bool `json:"warp_enabled,omitempty"`
|
||||||
|
// Waitlist not set for ZeroTier
|
||||||
|
Waitlist bool `json:"waitlist_enabled,omitempty"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
Updated string `json:"updated"`
|
||||||
|
// Tos not set for ZeroTier
|
||||||
|
Tos string `json:"tos,omitempty"`
|
||||||
|
// Place not set for ZeroTier
|
||||||
|
Place int `json:"place,omitempty"`
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
// Enabled not set for ZeroTier
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
InstallID string `json:"install_id"`
|
||||||
|
// Token only set for /reg call
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
FcmToken string `json:"fcm_token"`
|
||||||
|
// SerialNumber not set for ZeroTier
|
||||||
|
SerialNumber string `json:"serial_number,omitempty"`
|
||||||
|
Policy Policy `json:"policy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
AccountType string `json:"account_type"`
|
||||||
|
// Created not set for ZeroTier
|
||||||
|
Created string `json:"created,omitempty"`
|
||||||
|
// Updated not set for ZeroTier
|
||||||
|
Updated string `json:"updated,omitempty"`
|
||||||
|
// Managed only set for ZeroTier
|
||||||
|
Managed string `json:"managed,omitempty"`
|
||||||
|
// Organization only set for ZeroTier
|
||||||
|
Organization string `json:"organization,omitempty"`
|
||||||
|
// PremiumData not set for ZeroTier
|
||||||
|
PremiumData int `json:"premium_data,omitempty"`
|
||||||
|
// Quota not set for ZeroTier
|
||||||
|
Quota int `json:"quota,omitempty"`
|
||||||
|
// WarpPlus not set for ZeroTier
|
||||||
|
WarpPlus bool `json:"warp_plus,omitempty"`
|
||||||
|
// ReferralCode not set for ZeroTier
|
||||||
|
ReferralCount int `json:"referral_count,omitempty"`
|
||||||
|
// ReferralRenewalCount not set for ZeroTier
|
||||||
|
ReferralRenewalCount int `json:"referral_renewal_countdown,omitempty"`
|
||||||
|
// Role not set for ZeroTier
|
||||||
|
Role string `json:"role,omitempty"`
|
||||||
|
// License not set for ZeroTier
|
||||||
|
License string `json:"license,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
Peers []Peer `json:"peers"`
|
||||||
|
Interface struct {
|
||||||
|
Addresses struct {
|
||||||
|
V4 string `json:"v4"`
|
||||||
|
V6 string `json:"v6"`
|
||||||
|
} `json:"addresses"`
|
||||||
|
} `json:"interface"`
|
||||||
|
Services struct {
|
||||||
|
HTTPProxy string `json:"http_proxy"`
|
||||||
|
} `json:"services"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Peer struct {
|
||||||
|
PublicKey string `json:"public_key"`
|
||||||
|
Endpoint struct {
|
||||||
|
V4 string `json:"v4"`
|
||||||
|
V6 string `json:"v6"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Ports []int `json:"ports"`
|
||||||
|
} `json:"endpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Policy struct {
|
||||||
|
TunnelProtocol string `json:"tunnel_protocol"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceUpdate struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
KeyType string `json:"key_type"`
|
||||||
|
TunType string `json:"tunnel_type"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIError struct {
|
||||||
|
Result interface{} `json:"result"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Errors []ErrorInfo `json:"errors"`
|
||||||
|
Messages []string `json:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorInfo struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *APIError) Error() string {
|
||||||
|
errors := make([]string, len(e.Errors))
|
||||||
|
for i, err := range e.Errors {
|
||||||
|
errors[i] = err.Message
|
||||||
|
}
|
||||||
|
return strings.Join(errors, ",")
|
||||||
|
}
|
||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CloudflareApiOption func(api *CloudflareApi)
|
type CloudflareApiOption func(api *CloudflareApi)
|
||||||
|
|
||||||
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) CloudflareApiOption {
|
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) CloudflareApiOption {
|
||||||
return func(api *CloudflareApi) {
|
return func(api *CloudflareApi) {
|
||||||
|
api.client.Timeout = 30 * time.Second
|
||||||
api.client.Transport = &http.Transport{
|
api.client.Transport = &http.Transport{
|
||||||
DialContext: dialContext,
|
DialContext: dialContext,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
package cloudflare
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type CloudflareProfile struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
Account struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
AccountType string `json:"account_type"`
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
Updated time.Time `json:"updated"`
|
|
||||||
PremiumData int `json:"premium_data"`
|
|
||||||
Quota int `json:"quota"`
|
|
||||||
Usage int `json:"usage"`
|
|
||||||
WARPPlus bool `json:"warp_plus"`
|
|
||||||
ReferralCount int `json:"referral_count"`
|
|
||||||
ReferralRenewalCountdown int `json:"referral_renewal_countdown"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
License string `json:"license"`
|
|
||||||
TTL time.Time `json:"ttl"`
|
|
||||||
} `json:"account"`
|
|
||||||
Config struct {
|
|
||||||
ClientID string `json:"client_id"`
|
|
||||||
Interface struct {
|
|
||||||
Addresses struct {
|
|
||||||
V4 string `json:"v4"`
|
|
||||||
V6 string `json:"v6"`
|
|
||||||
} `json:"addresses"`
|
|
||||||
} `json:"interface"`
|
|
||||||
Peers []struct {
|
|
||||||
PublicKey string `json:"public_key"`
|
|
||||||
Endpoint struct {
|
|
||||||
V4 string `json:"v4"`
|
|
||||||
V6 string `json:"v6"`
|
|
||||||
Host string `json:"host"`
|
|
||||||
Ports []int `json:"ports"`
|
|
||||||
} `json:"endpoint"`
|
|
||||||
} `json:"peers"`
|
|
||||||
Services struct {
|
|
||||||
HTTPProxy string `json:"http_proxy"`
|
|
||||||
} `json:"services"`
|
|
||||||
Metrics struct {
|
|
||||||
Ping int `json:"ping"`
|
|
||||||
Report int `json:"report"`
|
|
||||||
} `json:"metrics"`
|
|
||||||
} `json:"config"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
WARPEnabled bool `json:"warp_enabled"`
|
|
||||||
WaitlistEnabled bool `json:"waitlist_enabled"`
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
Updated time.Time `json:"updated"`
|
|
||||||
Tos time.Time `json:"tos"`
|
|
||||||
Place int `json:"place"`
|
|
||||||
Locale string `json:"locale"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
InstallID string `json:"install_id"`
|
|
||||||
FcmToken string `json:"fcm_token"`
|
|
||||||
Policy struct {
|
|
||||||
TunnelProtocol string `json:"tunnel_protocol"`
|
|
||||||
} `json:"policy"`
|
|
||||||
}
|
|
||||||
19
common/cloudflare/utils.go
Normal file
19
common/cloudflare/utils.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package cloudflare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateRandomAndroidSerial() (string, error) {
|
||||||
|
serial := make([]byte, 8)
|
||||||
|
if _, err := rand.Read(serial); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(serial), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeAsCfString(t time.Time) string {
|
||||||
|
return t.Format("2006-01-02T15:04:05.000-07:00")
|
||||||
|
}
|
||||||
@@ -149,7 +149,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
} else {
|
} else {
|
||||||
dialer.Timeout = C.TCPConnectTimeout
|
dialer.Timeout = C.TCPConnectTimeout
|
||||||
}
|
}
|
||||||
if !options.DisableTCPKeepAlive {
|
if options.DisableTCPKeepAlive {
|
||||||
|
dialer.KeepAlive = -1
|
||||||
|
dialer.KeepAliveConfig.Enable = false
|
||||||
|
} else {
|
||||||
keepIdle := time.Duration(options.TCPKeepAlive)
|
keepIdle := time.Duration(options.TCPKeepAlive)
|
||||||
if keepIdle == 0 {
|
if keepIdle == 0 {
|
||||||
keepIdle = C.TCPKeepAliveInitial
|
keepIdle = C.TCPKeepAliveInitial
|
||||||
@@ -239,7 +242,7 @@ func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefaul
|
|||||||
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
||||||
if !address.IsValid() {
|
if !address.IsValid() {
|
||||||
return nil, E.New("invalid address")
|
return nil, E.New("invalid address")
|
||||||
} else if address.IsFqdn() {
|
} else if address.IsDomain() {
|
||||||
return nil, E.New("domain not resolved")
|
return nil, E.New("domain not resolved")
|
||||||
}
|
}
|
||||||
if d.networkStrategy == nil {
|
if d.networkStrategy == nil {
|
||||||
@@ -329,9 +332,9 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
|||||||
|
|
||||||
func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
|
func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
|
||||||
if !destination.Is6() {
|
if !destination.Is6() {
|
||||||
return d.dialer6.Dialer
|
|
||||||
} else {
|
|
||||||
return d.dialer4.Dialer
|
return d.dialer4.Dialer
|
||||||
|
} else {
|
||||||
|
return d.dialer6.Dialer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -184,6 +184,12 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
|
|||||||
|
|
||||||
func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) {
|
func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) {
|
||||||
interfaces := networkManager.NetworkInterfaces()
|
interfaces := networkManager.NetworkInterfaces()
|
||||||
|
myInterface := networkManager.InterfaceMonitor().MyInterface()
|
||||||
|
if myInterface != "" {
|
||||||
|
interfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
||||||
|
return it.Name != myInterface
|
||||||
|
})
|
||||||
|
}
|
||||||
switch strategy {
|
switch strategy {
|
||||||
case C.NetworkStrategyDefault:
|
case C.NetworkStrategyDefault:
|
||||||
if len(interfaceType) == 0 {
|
if len(interfaceType) == 0 {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func (d *resolveDialer) DialContext(ctx context.Context, network string, destina
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsDomain() {
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
@@ -116,7 +116,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsDomain() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
@@ -144,7 +144,7 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsDomain() {
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
@@ -167,7 +167,7 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsDomain() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
|
|||||||
@@ -11,3 +11,13 @@ func ContextWithIsExternalConnection(ctx context.Context) context.Context {
|
|||||||
func IsExternalConnectionFromContext(ctx context.Context) bool {
|
func IsExternalConnectionFromContext(ctx context.Context) bool {
|
||||||
return ctx.Value(contextKeyIsExternalConnection{}) != nil
|
return ctx.Value(contextKeyIsExternalConnection{}) != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type contextKeyIsProviderConnection struct{}
|
||||||
|
|
||||||
|
func ContextWithIsProviderConnection(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyIsProviderConnection{}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsProviderConnectionFromContext(ctx context.Context) bool {
|
||||||
|
return ctx.Value(contextKeyIsProviderConnection{}) != nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,30 +17,31 @@ type Group struct {
|
|||||||
type groupConnItem struct {
|
type groupConnItem struct {
|
||||||
conn io.Closer
|
conn io.Closer
|
||||||
isExternal bool
|
isExternal bool
|
||||||
|
isProvider bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGroup() *Group {
|
func NewGroup() *Group {
|
||||||
return &Group{}
|
return &Group{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Group) NewConn(conn net.Conn, isExternal bool) net.Conn {
|
func (g *Group) NewConn(conn net.Conn, isExternal bool, isProvider bool) net.Conn {
|
||||||
g.access.Lock()
|
g.access.Lock()
|
||||||
defer g.access.Unlock()
|
defer g.access.Unlock()
|
||||||
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
|
item := g.connections.PushBack(&groupConnItem{conn, isExternal, isProvider})
|
||||||
return &Conn{Conn: conn, group: g, element: item}
|
return &Conn{Conn: conn, group: g, element: item}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketConn {
|
func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool, isProvider bool) net.PacketConn {
|
||||||
g.access.Lock()
|
g.access.Lock()
|
||||||
defer g.access.Unlock()
|
defer g.access.Unlock()
|
||||||
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
|
item := g.connections.PushBack(&groupConnItem{conn, isExternal, isProvider})
|
||||||
return &PacketConn{PacketConn: conn, group: g, element: item}
|
return &PacketConn{PacketConn: conn, group: g, element: item}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool) N.PacketConn {
|
func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool, isProvider bool) N.PacketConn {
|
||||||
g.access.Lock()
|
g.access.Lock()
|
||||||
defer g.access.Unlock()
|
defer g.access.Unlock()
|
||||||
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
|
item := g.connections.PushBack(&groupConnItem{conn, isExternal, isProvider})
|
||||||
return &SingPacketConn{PacketConn: conn, group: g, element: item}
|
return &SingPacketConn{PacketConn: conn, group: g, element: item}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
54
common/kmutex/mutex.go
Normal file
54
common/kmutex/mutex.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package kmutex
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type Kmutex[T comparable] struct {
|
||||||
|
l sync.Locker
|
||||||
|
s map[T]*klock
|
||||||
|
}
|
||||||
|
|
||||||
|
type klock struct {
|
||||||
|
cond *sync.Cond
|
||||||
|
ref uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func New[T comparable]() *Kmutex[T] {
|
||||||
|
l := sync.Mutex{}
|
||||||
|
return &Kmutex[T]{
|
||||||
|
l: &l,
|
||||||
|
s: make(map[T]*klock),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (km *Kmutex[T]) Unlock(key T) {
|
||||||
|
km.l.Lock()
|
||||||
|
defer km.l.Unlock()
|
||||||
|
kl, ok := km.s[key]
|
||||||
|
if !ok || kl.ref == 0 {
|
||||||
|
panic("unlock of unlocked kmutex")
|
||||||
|
}
|
||||||
|
kl.ref--
|
||||||
|
if kl.ref == 0 {
|
||||||
|
delete(km.s, key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kl.cond.Signal()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (km *Kmutex[T]) Lock(key T) {
|
||||||
|
km.l.Lock()
|
||||||
|
defer km.l.Unlock()
|
||||||
|
for {
|
||||||
|
kl, ok := km.s[key]
|
||||||
|
if !ok {
|
||||||
|
km.s[key] = &klock{
|
||||||
|
cond: sync.NewCond(km.l),
|
||||||
|
ref: 1,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kl.ref++
|
||||||
|
kl.cond.Wait()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
96
common/kmutex/mutex_test.go
Normal file
96
common/kmutex/mutex_test.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package kmutex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Number of unique resources to access
|
||||||
|
const number = 100
|
||||||
|
|
||||||
|
func makeIds(count int) []int {
|
||||||
|
ids := make([]int, count)
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
ids[i] = i
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKmutex(t *testing.T) {
|
||||||
|
km := New[int]()
|
||||||
|
ids := makeIds(number)
|
||||||
|
resources := make([]int, number)
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
lc := make(chan int)
|
||||||
|
uc := make(chan int)
|
||||||
|
// Start 10n goroutines accessing n resources 10 times each
|
||||||
|
for i := 0; i < 10*number; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(k int) {
|
||||||
|
for j := 0; j < 10; j++ {
|
||||||
|
lc <- k
|
||||||
|
km.Lock(ids[k])
|
||||||
|
// read and write resource to check for race
|
||||||
|
resources[k] = resources[k] + 1
|
||||||
|
km.Unlock(ids[k])
|
||||||
|
uc <- k
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}(i % len(ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
to := time.After(time.Second)
|
||||||
|
counts := make(map[int]int)
|
||||||
|
var lCount, ulCount int
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case k := <-lc:
|
||||||
|
counts[k] = counts[k] + 1
|
||||||
|
lCount++
|
||||||
|
case k := <-uc:
|
||||||
|
counts[k] = counts[k] - 1
|
||||||
|
ulCount++
|
||||||
|
case <-to:
|
||||||
|
t.Fatal("timed out waiting for results")
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
expectCount := 100 * number
|
||||||
|
if lCount == expectCount && ulCount == expectCount {
|
||||||
|
// Have all results
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, c := range counts {
|
||||||
|
if c != 0 {
|
||||||
|
t.Errorf("Key %d count != 0: %d\n", k, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkKmutex1000(b *testing.B) {
|
||||||
|
km := New[int]()
|
||||||
|
ids := makeIds(number)
|
||||||
|
resources := make([]int, number)
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
// Start 1000 goroutines accessing 100 resources N times each
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(k int) {
|
||||||
|
for j := 0; j < b.N; j++ {
|
||||||
|
km.Lock(ids[k])
|
||||||
|
// read and write resource to check for race
|
||||||
|
resources[k] = resources[k] + 1
|
||||||
|
km.Unlock(ids[k])
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}(i % len(ids))
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Conn) Read(b []byte) (int, error) {
|
func (c *Conn) Read(b []byte) (int, error) {
|
||||||
@@ -229,7 +230,7 @@ func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) {
|
|||||||
record := c.rawConn.RawInput.Next(recordHeaderLen + n)
|
record := c.rawConn.RawInput.Next(recordHeaderLen + n)
|
||||||
data, typ, err = c.rawConn.In.Decrypt(record)
|
data, typ, err = c.rawConn.In.Decrypt(record)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = c.rawConn.In.SetErrorLocked(c.sendAlert(uint8(err.(tls.AlertError))))
|
err = c.rawConn.In.SetErrorLocked(c.sendAlert(*(*uint8)((*[2]unsafe.Pointer)(unsafe.Pointer(&err))[1])))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return common.DefaultValue[T](), E.Cause(err, "get current netns")
|
return common.DefaultValue[T](), E.Cause(err, "get current netns")
|
||||||
}
|
}
|
||||||
|
defer currentNs.Close()
|
||||||
defer netns.Set(currentNs)
|
defer netns.Set(currentNs)
|
||||||
var targetNs netns.NsHandle
|
var targetNs netns.NsHandle
|
||||||
if strings.HasPrefix(nameOrPath, "/") {
|
if strings.HasPrefix(nameOrPath, "/") {
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
|||||||
if l.listenOptions.ReuseAddr {
|
if l.listenOptions.ReuseAddr {
|
||||||
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
||||||
}
|
}
|
||||||
if !l.listenOptions.DisableTCPKeepAlive {
|
if l.listenOptions.DisableTCPKeepAlive {
|
||||||
|
listenConfig.KeepAlive = -1
|
||||||
|
listenConfig.KeepAliveConfig.Enable = false
|
||||||
|
} else {
|
||||||
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
|
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
|
||||||
if keepIdle == 0 {
|
if keepIdle == 0 {
|
||||||
keepIdle = C.TCPKeepAliveInitial
|
keepIdle = C.TCPKeepAliveInitial
|
||||||
|
|||||||
98
common/migrate/source/raw.go
Normal file
98
common/migrate/source/raw.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4/source"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RawDriver struct {
|
||||||
|
migrations *source.Migrations
|
||||||
|
rawMigrations map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRawDriver(rawMigrations map[string]string) *RawDriver {
|
||||||
|
return &RawDriver{rawMigrations: rawMigrations}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RawDriver) Init() error {
|
||||||
|
ms := source.NewMigrations()
|
||||||
|
for key := range d.rawMigrations {
|
||||||
|
m, err := source.DefaultParse(key)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !ms.Append(m) {
|
||||||
|
return source.ErrDuplicateMigration{
|
||||||
|
Migration: *m,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.migrations = ms
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RawDriver) Open(url string) (source.Driver, error) {
|
||||||
|
return nil, E.New("open() cannot be called")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RawDriver) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RawDriver) First() (version uint, err error) {
|
||||||
|
if version, ok := d.migrations.First(); ok {
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
return 0, &fs.PathError{
|
||||||
|
Op: "first",
|
||||||
|
Err: fs.ErrNotExist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RawDriver) Prev(version uint) (prevVersion uint, err error) {
|
||||||
|
if version, ok := d.migrations.Prev(version); ok {
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
return 0, &fs.PathError{
|
||||||
|
Op: "prev for version " + strconv.FormatUint(uint64(version), 10),
|
||||||
|
Err: fs.ErrNotExist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RawDriver) Next(version uint) (nextVersion uint, err error) {
|
||||||
|
if version, ok := d.migrations.Next(version); ok {
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
return 0, &fs.PathError{
|
||||||
|
Op: "next for version " + strconv.FormatUint(uint64(version), 10),
|
||||||
|
Err: fs.ErrNotExist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RawDriver) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||||
|
if m, ok := d.migrations.Up(version); ok {
|
||||||
|
body := ioutil.NopCloser(strings.NewReader(d.rawMigrations[m.Raw]))
|
||||||
|
return body, m.Identifier, nil
|
||||||
|
}
|
||||||
|
return nil, "", &fs.PathError{
|
||||||
|
Op: "read up for version " + strconv.FormatUint(uint64(version), 10),
|
||||||
|
Err: fs.ErrNotExist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *RawDriver) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
|
||||||
|
if m, ok := d.migrations.Down(version); ok {
|
||||||
|
body := ioutil.NopCloser(strings.NewReader(d.rawMigrations[m.Raw]))
|
||||||
|
return body, m.Identifier, nil
|
||||||
|
}
|
||||||
|
return nil, "", &fs.PathError{
|
||||||
|
Op: "read down for version " + strconv.FormatUint(uint64(version), 10),
|
||||||
|
Err: fs.ErrNotExist,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,9 @@ func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.Conte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
service, err := mux.NewService(mux.ServiceOptions{
|
service, err := mux.NewService(mux.ServiceOptions{
|
||||||
|
NewConnectionContext: func(ctx context.Context, conn net.Conn) context.Context {
|
||||||
|
return log.ContextWithNewMuxID(ctx)
|
||||||
|
},
|
||||||
NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context {
|
NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context {
|
||||||
return log.ContextWithNewID(ctx)
|
return log.ContextWithNewID(ctx)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
type Searcher interface {
|
type Searcher interface {
|
||||||
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
|
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
|
||||||
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrNotFound = E.New("process not found")
|
var ErrNotFound = E.New("process not found")
|
||||||
@@ -28,7 +29,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if info.UserId != -1 {
|
if info.UserId != -1 && info.UserName == "" {
|
||||||
osUser, _ := user.LookupId(F.ToString(info.UserId))
|
osUser, _ := user.LookupId(F.ToString(info.UserId))
|
||||||
if osUser != nil {
|
if osUser != nil {
|
||||||
info.UserName = osUser.Username
|
info.UserName = osUser.Username
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-tun"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Searcher = (*androidSearcher)(nil)
|
var _ Searcher = (*androidSearcher)(nil)
|
||||||
@@ -18,22 +19,30 @@ func NewSearcher(config Config) (Searcher, error) {
|
|||||||
return &androidSearcher{config.PackageManager}, nil
|
return &androidSearcher{config.PackageManager}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *androidSearcher) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||||
_, uid, err := resolveSocketByNetlink(network, source, destination)
|
family, protocol, err := socketDiagSettings(network, source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
|
_, uid, err := querySocketDiagOnce(family, protocol, source)
|
||||||
return &adapter.ConnectionOwner{
|
if err != nil {
|
||||||
UserId: int32(uid),
|
return nil, err
|
||||||
AndroidPackageName: sharedPackage,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
|
appID := uid % 100000
|
||||||
return &adapter.ConnectionOwner{
|
var packageNames []string
|
||||||
UserId: int32(uid),
|
if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded {
|
||||||
AndroidPackageName: packageName,
|
packageNames = append(packageNames, sharedPackage)
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
return &adapter.ConnectionOwner{UserId: int32(uid)}, nil
|
if packages, loaded := s.packageManager.PackagesByID(appID); loaded {
|
||||||
|
packageNames = append(packageNames, packages...)
|
||||||
|
}
|
||||||
|
packageNames = common.Uniq(packageNames)
|
||||||
|
return &adapter.ConnectionOwner{
|
||||||
|
UserId: int32(uid),
|
||||||
|
AndroidPackageNames: packageNames,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Searcher = (*darwinSearcher)(nil)
|
var _ Searcher = (*darwinSearcher)(nil)
|
||||||
@@ -24,12 +20,12 @@ func NewSearcher(_ Config) (Searcher, error) {
|
|||||||
return &darwinSearcher{}, nil
|
return &darwinSearcher{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *darwinSearcher) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||||
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
return FindDarwinConnectionOwner(network, source, destination)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var structSize = func() int {
|
var structSize = func() int {
|
||||||
@@ -47,107 +43,3 @@ var structSize = func() int {
|
|||||||
return 384
|
return 384
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
|
||||||
var spath string
|
|
||||||
switch network {
|
|
||||||
case N.NetworkTCP:
|
|
||||||
spath = "net.inet.tcp.pcblist_n"
|
|
||||||
case N.NetworkUDP:
|
|
||||||
spath = "net.inet.udp.pcblist_n"
|
|
||||||
default:
|
|
||||||
return "", os.ErrInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
isIPv4 := ip.Is4()
|
|
||||||
|
|
||||||
value, err := unix.SysctlRaw(spath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := value
|
|
||||||
|
|
||||||
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
|
|
||||||
// size/offset are round up (aligned) to 8 bytes in darwin
|
|
||||||
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
|
|
||||||
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
|
|
||||||
itemSize := structSize
|
|
||||||
if network == N.NetworkTCP {
|
|
||||||
// rup8(sizeof(xtcpcb_n))
|
|
||||||
itemSize += 208
|
|
||||||
}
|
|
||||||
|
|
||||||
var fallbackUDPProcess string
|
|
||||||
// skip the first xinpgen(24 bytes) block
|
|
||||||
for i := 24; i+itemSize <= len(buf); i += itemSize {
|
|
||||||
// offset of xinpcb_n and xsocket_n
|
|
||||||
inp, so := i, i+104
|
|
||||||
|
|
||||||
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
|
|
||||||
if uint16(port) != srcPort {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// xinpcb_n.inp_vflag
|
|
||||||
flag := buf[inp+44]
|
|
||||||
|
|
||||||
var srcIP netip.Addr
|
|
||||||
srcIsIPv4 := false
|
|
||||||
switch {
|
|
||||||
case flag&0x1 > 0 && isIPv4:
|
|
||||||
// ipv4
|
|
||||||
srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80]))
|
|
||||||
srcIsIPv4 = true
|
|
||||||
case flag&0x2 > 0 && !isIPv4:
|
|
||||||
// ipv6
|
|
||||||
srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80]))
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip == srcIP {
|
|
||||||
// xsocket_n.so_last_pid
|
|
||||||
pid := readNativeUint32(buf[so+68 : so+72])
|
|
||||||
return getExecPathFromPID(pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// udp packet connection may be not equal with srcIP
|
|
||||||
if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
|
|
||||||
pid := readNativeUint32(buf[so+68 : so+72])
|
|
||||||
fallbackUDPProcess, _ = getExecPathFromPID(pid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 {
|
|
||||||
return fallbackUDPProcess, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExecPathFromPID(pid uint32) (string, error) {
|
|
||||||
const (
|
|
||||||
procpidpathinfo = 0xb
|
|
||||||
procpidpathinfosize = 1024
|
|
||||||
proccallnumpidinfo = 0x2
|
|
||||||
)
|
|
||||||
buf := make([]byte, procpidpathinfosize)
|
|
||||||
_, _, errno := syscall.Syscall6(
|
|
||||||
syscall.SYS_PROC_INFO,
|
|
||||||
proccallnumpidinfo,
|
|
||||||
uintptr(pid),
|
|
||||||
procpidpathinfo,
|
|
||||||
0,
|
|
||||||
uintptr(unsafe.Pointer(&buf[0])),
|
|
||||||
procpidpathinfosize)
|
|
||||||
if errno != 0 {
|
|
||||||
return "", errno
|
|
||||||
}
|
|
||||||
|
|
||||||
return unix.ByteSliceToString(buf), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readNativeUint32(b []byte) uint32 {
|
|
||||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
|
||||||
}
|
|
||||||
|
|||||||
269
common/process/searcher_darwin_shared.go
Normal file
269
common/process/searcher_darwin_shared.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
darwinSnapshotTTL = 200 * time.Millisecond
|
||||||
|
|
||||||
|
darwinXinpgenSize = 24
|
||||||
|
darwinXsocketOffset = 104
|
||||||
|
darwinXinpcbForeignPort = 16
|
||||||
|
darwinXinpcbLocalPort = 18
|
||||||
|
darwinXinpcbVFlag = 44
|
||||||
|
darwinXinpcbForeignAddr = 48
|
||||||
|
darwinXinpcbLocalAddr = 64
|
||||||
|
darwinXinpcbIPv4Addr = 12
|
||||||
|
darwinXsocketUID = 64
|
||||||
|
darwinXsocketLastPID = 68
|
||||||
|
darwinTCPExtraStructSize = 208
|
||||||
|
)
|
||||||
|
|
||||||
|
type darwinConnectionEntry struct {
|
||||||
|
localAddr netip.Addr
|
||||||
|
remoteAddr netip.Addr
|
||||||
|
localPort uint16
|
||||||
|
remotePort uint16
|
||||||
|
pid uint32
|
||||||
|
uid int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type darwinConnectionMatchKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
darwinConnectionMatchExact darwinConnectionMatchKind = iota
|
||||||
|
darwinConnectionMatchLocalFallback
|
||||||
|
darwinConnectionMatchWildcardFallback
|
||||||
|
)
|
||||||
|
|
||||||
|
type darwinSnapshot struct {
|
||||||
|
createdAt time.Time
|
||||||
|
entries []darwinConnectionEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
type darwinConnectionFinder struct {
|
||||||
|
access sync.Mutex
|
||||||
|
ttl time.Duration
|
||||||
|
snapshots map[string]darwinSnapshot
|
||||||
|
builder func(string) (darwinSnapshot, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sharedDarwinConnectionFinder = newDarwinConnectionFinder(darwinSnapshotTTL)
|
||||||
|
|
||||||
|
func newDarwinConnectionFinder(ttl time.Duration) *darwinConnectionFinder {
|
||||||
|
return &darwinConnectionFinder{
|
||||||
|
ttl: ttl,
|
||||||
|
snapshots: make(map[string]darwinSnapshot),
|
||||||
|
builder: buildDarwinSnapshot,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindDarwinConnectionOwner(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||||
|
return sharedDarwinConnectionFinder.find(network, source, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *darwinConnectionFinder) find(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||||
|
networkName := N.NetworkName(network)
|
||||||
|
source = normalizeDarwinAddrPort(source)
|
||||||
|
destination = normalizeDarwinAddrPort(destination)
|
||||||
|
var lastOwner *adapter.ConnectionOwner
|
||||||
|
for attempt := 0; attempt < 2; attempt++ {
|
||||||
|
snapshot, fromCache, err := f.loadSnapshot(networkName, attempt > 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entry, matchKind, err := matchDarwinConnectionEntry(snapshot.entries, networkName, source, destination)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrNotFound && fromCache {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if fromCache && matchKind != darwinConnectionMatchExact {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
owner := &adapter.ConnectionOwner{
|
||||||
|
UserId: entry.uid,
|
||||||
|
}
|
||||||
|
lastOwner = owner
|
||||||
|
if entry.pid == 0 {
|
||||||
|
return owner, nil
|
||||||
|
}
|
||||||
|
processPath, err := getExecPathFromPID(entry.pid)
|
||||||
|
if err == nil {
|
||||||
|
owner.ProcessPath = processPath
|
||||||
|
return owner, nil
|
||||||
|
}
|
||||||
|
if fromCache {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return owner, nil
|
||||||
|
}
|
||||||
|
if lastOwner != nil {
|
||||||
|
return lastOwner, nil
|
||||||
|
}
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *darwinConnectionFinder) loadSnapshot(network string, forceRefresh bool) (darwinSnapshot, bool, error) {
|
||||||
|
f.access.Lock()
|
||||||
|
defer f.access.Unlock()
|
||||||
|
if !forceRefresh {
|
||||||
|
if snapshot, loaded := f.snapshots[network]; loaded && time.Since(snapshot.createdAt) < f.ttl {
|
||||||
|
return snapshot, true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshot, err := f.builder(network)
|
||||||
|
if err != nil {
|
||||||
|
return darwinSnapshot{}, false, err
|
||||||
|
}
|
||||||
|
f.snapshots[network] = snapshot
|
||||||
|
return snapshot, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDarwinSnapshot(network string) (darwinSnapshot, error) {
|
||||||
|
spath, itemSize, err := darwinSnapshotSettings(network)
|
||||||
|
if err != nil {
|
||||||
|
return darwinSnapshot{}, err
|
||||||
|
}
|
||||||
|
value, err := unix.SysctlRaw(spath)
|
||||||
|
if err != nil {
|
||||||
|
return darwinSnapshot{}, err
|
||||||
|
}
|
||||||
|
return darwinSnapshot{
|
||||||
|
createdAt: time.Now(),
|
||||||
|
entries: parseDarwinSnapshot(value, itemSize),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func darwinSnapshotSettings(network string) (string, int, error) {
|
||||||
|
itemSize := structSize
|
||||||
|
switch network {
|
||||||
|
case N.NetworkTCP:
|
||||||
|
return "net.inet.tcp.pcblist_n", itemSize + darwinTCPExtraStructSize, nil
|
||||||
|
case N.NetworkUDP:
|
||||||
|
return "net.inet.udp.pcblist_n", itemSize, nil
|
||||||
|
default:
|
||||||
|
return "", 0, os.ErrInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDarwinSnapshot(buf []byte, itemSize int) []darwinConnectionEntry {
|
||||||
|
entries := make([]darwinConnectionEntry, 0, (len(buf)-darwinXinpgenSize)/itemSize)
|
||||||
|
for i := darwinXinpgenSize; i+itemSize <= len(buf); i += itemSize {
|
||||||
|
inp := i
|
||||||
|
so := i + darwinXsocketOffset
|
||||||
|
entry, ok := parseDarwinConnectionEntry(buf[inp:so], buf[so:so+structSize-darwinXsocketOffset])
|
||||||
|
if ok {
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDarwinConnectionEntry(inp []byte, so []byte) (darwinConnectionEntry, bool) {
|
||||||
|
if len(inp) < darwinXsocketOffset || len(so) < structSize-darwinXsocketOffset {
|
||||||
|
return darwinConnectionEntry{}, false
|
||||||
|
}
|
||||||
|
entry := darwinConnectionEntry{
|
||||||
|
remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]),
|
||||||
|
localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]),
|
||||||
|
pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]),
|
||||||
|
uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])),
|
||||||
|
}
|
||||||
|
flag := inp[darwinXinpcbVFlag]
|
||||||
|
switch {
|
||||||
|
case flag&0x1 != 0:
|
||||||
|
entry.remoteAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr : darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr+4]))
|
||||||
|
entry.localAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr : darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr+4]))
|
||||||
|
return entry, true
|
||||||
|
case flag&0x2 != 0:
|
||||||
|
entry.remoteAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16]))
|
||||||
|
entry.localAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16]))
|
||||||
|
return entry, true
|
||||||
|
default:
|
||||||
|
return darwinConnectionEntry{}, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchDarwinConnectionEntry(entries []darwinConnectionEntry, network string, source netip.AddrPort, destination netip.AddrPort) (darwinConnectionEntry, darwinConnectionMatchKind, error) {
|
||||||
|
sourceAddr := source.Addr()
|
||||||
|
if !sourceAddr.IsValid() {
|
||||||
|
return darwinConnectionEntry{}, darwinConnectionMatchExact, os.ErrInvalid
|
||||||
|
}
|
||||||
|
var localFallback darwinConnectionEntry
|
||||||
|
var hasLocalFallback bool
|
||||||
|
var wildcardFallback darwinConnectionEntry
|
||||||
|
var hasWildcardFallback bool
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.localPort != source.Port() || sourceAddr.BitLen() != entry.localAddr.BitLen() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if entry.localAddr == sourceAddr && destination.IsValid() && entry.remotePort == destination.Port() && entry.remoteAddr == destination.Addr() {
|
||||||
|
return entry, darwinConnectionMatchExact, nil
|
||||||
|
}
|
||||||
|
if !destination.IsValid() && entry.localAddr == sourceAddr {
|
||||||
|
return entry, darwinConnectionMatchExact, nil
|
||||||
|
}
|
||||||
|
if network != N.NetworkUDP {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !hasLocalFallback && entry.localAddr == sourceAddr {
|
||||||
|
hasLocalFallback = true
|
||||||
|
localFallback = entry
|
||||||
|
}
|
||||||
|
if !hasWildcardFallback && entry.localAddr.IsUnspecified() {
|
||||||
|
hasWildcardFallback = true
|
||||||
|
wildcardFallback = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasLocalFallback {
|
||||||
|
return localFallback, darwinConnectionMatchLocalFallback, nil
|
||||||
|
}
|
||||||
|
if hasWildcardFallback {
|
||||||
|
return wildcardFallback, darwinConnectionMatchWildcardFallback, nil
|
||||||
|
}
|
||||||
|
return darwinConnectionEntry{}, darwinConnectionMatchExact, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeDarwinAddrPort(addrPort netip.AddrPort) netip.AddrPort {
|
||||||
|
if !addrPort.IsValid() {
|
||||||
|
return addrPort
|
||||||
|
}
|
||||||
|
return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExecPathFromPID(pid uint32) (string, error) {
|
||||||
|
const (
|
||||||
|
procpidpathinfo = 0xb
|
||||||
|
procpidpathinfosize = 1024
|
||||||
|
proccallnumpidinfo = 0x2
|
||||||
|
)
|
||||||
|
buf := make([]byte, procpidpathinfosize)
|
||||||
|
_, _, errno := syscall.Syscall6(
|
||||||
|
syscall.SYS_PROC_INFO,
|
||||||
|
proccallnumpidinfo,
|
||||||
|
uintptr(pid),
|
||||||
|
procpidpathinfo,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
procpidpathinfosize)
|
||||||
|
if errno != 0 {
|
||||||
|
return "", errno
|
||||||
|
}
|
||||||
|
return unix.ByteSliceToString(buf), nil
|
||||||
|
}
|
||||||
@@ -4,33 +4,82 @@ package process
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Searcher = (*linuxSearcher)(nil)
|
var _ Searcher = (*linuxSearcher)(nil)
|
||||||
|
|
||||||
type linuxSearcher struct {
|
type linuxSearcher struct {
|
||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
|
diagConns [4]*socketDiagConn
|
||||||
|
processPathCache *uidProcessPathCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSearcher(config Config) (Searcher, error) {
|
func NewSearcher(config Config) (Searcher, error) {
|
||||||
return &linuxSearcher{config.Logger}, nil
|
searcher := &linuxSearcher{
|
||||||
|
logger: config.Logger,
|
||||||
|
processPathCache: newUIDProcessPathCache(time.Second),
|
||||||
|
}
|
||||||
|
for _, family := range []uint8{syscall.AF_INET, syscall.AF_INET6} {
|
||||||
|
for _, protocol := range []uint8{syscall.IPPROTO_TCP, syscall.IPPROTO_UDP} {
|
||||||
|
searcher.diagConns[socketDiagConnIndex(family, protocol)] = newSocketDiagConn(family, protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return searcher, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *linuxSearcher) Close() error {
|
||||||
|
var errs []error
|
||||||
|
for _, conn := range s.diagConns {
|
||||||
|
if conn == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
errs = append(errs, conn.Close())
|
||||||
|
}
|
||||||
|
return E.Errors(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||||
inode, uid, err := resolveSocketByNetlink(network, source, destination)
|
inode, uid, err := s.resolveSocketByNetlink(network, source, destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
processPath, err := resolveProcessNameByProcSearch(inode, uid)
|
processInfo := &adapter.ConnectionOwner{
|
||||||
|
UserId: int32(uid),
|
||||||
|
}
|
||||||
|
processPath, err := s.processPathCache.findProcessPath(inode, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.DebugContext(ctx, "find process path: ", err)
|
s.logger.DebugContext(ctx, "find process path: ", err)
|
||||||
|
} else {
|
||||||
|
processInfo.ProcessPath = processPath
|
||||||
}
|
}
|
||||||
return &adapter.ConnectionOwner{
|
return processInfo, nil
|
||||||
UserId: int32(uid),
|
}
|
||||||
ProcessPath: processPath,
|
|
||||||
}, nil
|
func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||||
|
family, protocol, err := socketDiagSettings(network, source)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
conn := s.diagConns[socketDiagConnIndex(family, protocol)]
|
||||||
|
if conn == nil {
|
||||||
|
return 0, 0, E.New("missing socket diag connection for family=", family, " protocol=", protocol)
|
||||||
|
}
|
||||||
|
if destination.IsValid() && source.Addr().BitLen() == destination.Addr().BitLen() {
|
||||||
|
inode, uid, err = conn.query(source, destination)
|
||||||
|
if err == nil {
|
||||||
|
return inode, uid, nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return querySocketDiagOnce(family, protocol, source)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,43 +3,67 @@
|
|||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"errors"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/contrab/freelru"
|
||||||
|
"github.com/sagernet/sing/contrab/maphash"
|
||||||
)
|
)
|
||||||
|
|
||||||
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
|
|
||||||
var nativeEndian = func() binary.ByteOrder {
|
|
||||||
var x uint32 = 0x01020304
|
|
||||||
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
|
|
||||||
return binary.BigEndian
|
|
||||||
}
|
|
||||||
|
|
||||||
return binary.LittleEndian
|
|
||||||
}()
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
|
sizeOfSocketDiagRequestData = 56
|
||||||
socketDiagByFamily = 20
|
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData
|
||||||
pathProc = "/proc"
|
socketDiagResponseMinSize = 72
|
||||||
|
socketDiagByFamily = 20
|
||||||
|
pathProc = "/proc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
type socketDiagConn struct {
|
||||||
var family uint8
|
access sync.Mutex
|
||||||
var protocol uint8
|
family uint8
|
||||||
|
protocol uint8
|
||||||
|
fd int
|
||||||
|
}
|
||||||
|
|
||||||
|
type uidProcessPathCache struct {
|
||||||
|
cache freelru.Cache[uint32, *uidProcessPaths]
|
||||||
|
}
|
||||||
|
|
||||||
|
type uidProcessPaths struct {
|
||||||
|
entries map[uint32]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSocketDiagConn(family, protocol uint8) *socketDiagConn {
|
||||||
|
return &socketDiagConn{
|
||||||
|
family: family,
|
||||||
|
protocol: protocol,
|
||||||
|
fd: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func socketDiagConnIndex(family, protocol uint8) int {
|
||||||
|
index := 0
|
||||||
|
if protocol == syscall.IPPROTO_UDP {
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
if family == syscall.AF_INET6 {
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
func socketDiagSettings(network string, source netip.AddrPort) (family, protocol uint8, err error) {
|
||||||
switch network {
|
switch network {
|
||||||
case N.NetworkTCP:
|
case N.NetworkTCP:
|
||||||
protocol = syscall.IPPROTO_TCP
|
protocol = syscall.IPPROTO_TCP
|
||||||
@@ -48,151 +72,308 @@ func resolveSocketByNetlink(network string, source netip.AddrPort, destination n
|
|||||||
default:
|
default:
|
||||||
return 0, 0, os.ErrInvalid
|
return 0, 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
switch {
|
||||||
if source.Addr().Is4() {
|
case source.Addr().Is4():
|
||||||
family = syscall.AF_INET
|
family = syscall.AF_INET
|
||||||
} else {
|
case source.Addr().Is6():
|
||||||
family = syscall.AF_INET6
|
family = syscall.AF_INET6
|
||||||
|
default:
|
||||||
|
return 0, 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
return family, protocol, nil
|
||||||
req := packSocketDiagRequest(family, protocol, source)
|
|
||||||
|
|
||||||
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, E.Cause(err, "dial netlink")
|
|
||||||
}
|
|
||||||
defer syscall.Close(socket)
|
|
||||||
|
|
||||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
|
|
||||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
|
|
||||||
|
|
||||||
err = syscall.Connect(socket, &syscall.SockaddrNetlink{
|
|
||||||
Family: syscall.AF_NETLINK,
|
|
||||||
Pad: 0,
|
|
||||||
Pid: 0,
|
|
||||||
Groups: 0,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = syscall.Write(socket, req)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, E.Cause(err, "write netlink request")
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer := buf.New()
|
|
||||||
defer buffer.Release()
|
|
||||||
|
|
||||||
n, err := syscall.Read(socket, buffer.FreeBytes())
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, E.Cause(err, "read netlink response")
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.Truncate(n)
|
|
||||||
|
|
||||||
messages, err := syscall.ParseNetlinkMessage(buffer.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, E.Cause(err, "parse netlink message")
|
|
||||||
} else if len(messages) == 0 {
|
|
||||||
return 0, 0, E.New("unexcepted netlink response")
|
|
||||||
}
|
|
||||||
|
|
||||||
message := messages[0]
|
|
||||||
if message.Header.Type&syscall.NLMSG_ERROR != 0 {
|
|
||||||
return 0, 0, E.New("netlink message: NLMSG_ERROR")
|
|
||||||
}
|
|
||||||
|
|
||||||
inode, uid = unpackSocketDiagResponse(&messages[0])
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte {
|
func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache {
|
||||||
s := make([]byte, 16)
|
cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32))
|
||||||
copy(s, source.Addr().AsSlice())
|
cache.SetLifetime(ttl)
|
||||||
|
return &uidProcessPathCache{cache: cache}
|
||||||
buf := make([]byte, sizeOfSocketDiagRequest)
|
|
||||||
|
|
||||||
nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
|
|
||||||
nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
|
|
||||||
nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
|
|
||||||
nativeEndian.PutUint32(buf[8:12], 0)
|
|
||||||
nativeEndian.PutUint32(buf[12:16], 0)
|
|
||||||
|
|
||||||
buf[16] = family
|
|
||||||
buf[17] = protocol
|
|
||||||
buf[18] = 0
|
|
||||||
buf[19] = 0
|
|
||||||
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint16(buf[24:26], source.Port())
|
|
||||||
binary.BigEndian.PutUint16(buf[26:28], 0)
|
|
||||||
|
|
||||||
copy(buf[28:44], s)
|
|
||||||
copy(buf[44:60], net.IPv6zero)
|
|
||||||
|
|
||||||
nativeEndian.PutUint32(buf[60:64], 0)
|
|
||||||
nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF)
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) {
|
||||||
if len(msg.Data) < 72 {
|
if cached, ok := c.cache.Get(uid); ok {
|
||||||
return 0, 0
|
if processPath, found := cached.entries[targetInode]; found {
|
||||||
|
return processPath, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
processPaths, err := buildProcessPathByUIDCache(uid)
|
||||||
data := msg.Data
|
|
||||||
|
|
||||||
uid = nativeEndian.Uint32(data[64:68])
|
|
||||||
inode = nativeEndian.Uint32(data[68:72])
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
|
|
||||||
files, err := os.ReadDir(pathProc)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
c.cache.Add(uid, &uidProcessPaths{entries: processPaths})
|
||||||
|
processPath, found := processPaths[targetInode]
|
||||||
|
if !found {
|
||||||
|
return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found")
|
||||||
|
}
|
||||||
|
return processPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *socketDiagConn) Close() error {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
return c.closeLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
request := packSocketDiagRequest(c.family, c.protocol, source, destination, false)
|
||||||
|
for attempt := 0; attempt < 2; attempt++ {
|
||||||
|
err = c.ensureOpenLocked()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, E.Cause(err, "dial netlink")
|
||||||
|
}
|
||||||
|
inode, uid, err = querySocketDiag(c.fd, request)
|
||||||
|
if err == nil || errors.Is(err, ErrNotFound) {
|
||||||
|
return inode, uid, err
|
||||||
|
}
|
||||||
|
if !shouldRetrySocketDiag(err) {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
_ = c.closeLocked()
|
||||||
|
}
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) {
|
||||||
|
fd, err := openSocketDiag()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, E.Cause(err, "dial netlink")
|
||||||
|
}
|
||||||
|
defer syscall.Close(fd)
|
||||||
|
return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *socketDiagConn) ensureOpenLocked() error {
|
||||||
|
if c.fd != -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fd, err := openSocketDiag()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.fd = fd
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openSocketDiag() (int, error) {
|
||||||
|
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM|syscall.SOCK_CLOEXEC, syscall.NETLINK_INET_DIAG)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
timeout := &syscall.Timeval{Usec: 100}
|
||||||
|
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil {
|
||||||
|
syscall.Close(fd)
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil {
|
||||||
|
syscall.Close(fd)
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
if err = syscall.Connect(fd, &syscall.SockaddrNetlink{
|
||||||
|
Family: syscall.AF_NETLINK,
|
||||||
|
Pid: 0,
|
||||||
|
Groups: 0,
|
||||||
|
}); err != nil {
|
||||||
|
syscall.Close(fd)
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
return fd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *socketDiagConn) closeLocked() error {
|
||||||
|
if c.fd == -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := syscall.Close(c.fd)
|
||||||
|
c.fd = -1
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort, destination netip.AddrPort, dump bool) []byte {
|
||||||
|
request := make([]byte, sizeOfSocketDiagRequest)
|
||||||
|
|
||||||
|
binary.NativeEndian.PutUint32(request[0:4], sizeOfSocketDiagRequest)
|
||||||
|
binary.NativeEndian.PutUint16(request[4:6], socketDiagByFamily)
|
||||||
|
flags := uint16(syscall.NLM_F_REQUEST)
|
||||||
|
if dump {
|
||||||
|
flags |= syscall.NLM_F_DUMP
|
||||||
|
}
|
||||||
|
binary.NativeEndian.PutUint16(request[6:8], flags)
|
||||||
|
binary.NativeEndian.PutUint32(request[8:12], 0)
|
||||||
|
binary.NativeEndian.PutUint32(request[12:16], 0)
|
||||||
|
|
||||||
|
request[16] = family
|
||||||
|
request[17] = protocol
|
||||||
|
request[18] = 0
|
||||||
|
request[19] = 0
|
||||||
|
if dump {
|
||||||
|
binary.NativeEndian.PutUint32(request[20:24], 0xFFFFFFFF)
|
||||||
|
}
|
||||||
|
requestSource := source
|
||||||
|
requestDestination := destination
|
||||||
|
if protocol == syscall.IPPROTO_UDP && !dump && destination.IsValid() {
|
||||||
|
// udp_dump_one expects the exact-match endpoints reversed for historical reasons.
|
||||||
|
requestSource, requestDestination = destination, source
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint16(request[24:26], requestSource.Port())
|
||||||
|
binary.BigEndian.PutUint16(request[26:28], requestDestination.Port())
|
||||||
|
if family == syscall.AF_INET6 {
|
||||||
|
copy(request[28:44], requestSource.Addr().AsSlice())
|
||||||
|
if requestDestination.IsValid() {
|
||||||
|
copy(request[44:60], requestDestination.Addr().AsSlice())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
copy(request[28:32], requestSource.Addr().AsSlice())
|
||||||
|
if requestDestination.IsValid() {
|
||||||
|
copy(request[44:48], requestDestination.Addr().AsSlice())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binary.NativeEndian.PutUint32(request[60:64], 0)
|
||||||
|
binary.NativeEndian.PutUint64(request[64:72], 0xFFFFFFFFFFFFFFFF)
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) {
|
||||||
|
_, err = syscall.Write(fd, request)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, E.Cause(err, "write netlink request")
|
||||||
|
}
|
||||||
|
buffer := make([]byte, 64<<10)
|
||||||
|
n, err := syscall.Read(fd, buffer)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, E.Cause(err, "read netlink response")
|
||||||
|
}
|
||||||
|
messages, err := syscall.ParseNetlinkMessage(buffer[:n])
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, E.Cause(err, "parse netlink message")
|
||||||
|
}
|
||||||
|
return unpackSocketDiagMessages(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) {
|
||||||
|
for _, message := range messages {
|
||||||
|
switch message.Header.Type {
|
||||||
|
case syscall.NLMSG_DONE:
|
||||||
|
continue
|
||||||
|
case syscall.NLMSG_ERROR:
|
||||||
|
err = unpackSocketDiagError(&message)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
case socketDiagByFamily:
|
||||||
|
inode, uid = unpackSocketDiagResponse(&message)
|
||||||
|
if inode != 0 || uid != 0 {
|
||||||
|
return inode, uid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, 0, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
||||||
|
if len(msg.Data) < socketDiagResponseMinSize {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
uid = binary.NativeEndian.Uint32(msg.Data[64:68])
|
||||||
|
inode = binary.NativeEndian.Uint32(msg.Data[68:72])
|
||||||
|
return inode, uid
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackSocketDiagError(msg *syscall.NetlinkMessage) error {
|
||||||
|
if len(msg.Data) < 4 {
|
||||||
|
return E.New("netlink message: NLMSG_ERROR")
|
||||||
|
}
|
||||||
|
errno := int32(binary.NativeEndian.Uint32(msg.Data[:4]))
|
||||||
|
if errno == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if errno < 0 {
|
||||||
|
errno = -errno
|
||||||
|
}
|
||||||
|
sysErr := syscall.Errno(errno)
|
||||||
|
switch sysErr {
|
||||||
|
case syscall.ENOENT, syscall.ESRCH:
|
||||||
|
return ErrNotFound
|
||||||
|
default:
|
||||||
|
return E.New("netlink message: ", sysErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRetrySocketDiag(err error) bool {
|
||||||
|
return err != nil && !errors.Is(err, ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildProcessPathByUIDCache(uid uint32) (map[uint32]string, error) {
|
||||||
|
files, err := os.ReadDir(pathProc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
buffer := make([]byte, syscall.PathMax)
|
buffer := make([]byte, syscall.PathMax)
|
||||||
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
|
processPaths := make(map[uint32]string)
|
||||||
|
for _, file := range files {
|
||||||
for _, f := range files {
|
if !file.IsDir() || !isPid(file.Name()) {
|
||||||
if !f.IsDir() || !isPid(f.Name()) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
info, err := file.Info()
|
||||||
info, err := f.Info()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
if isIgnorableProcError(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
processPath := filepath.Join(pathProc, file.Name())
|
||||||
processPath := path.Join(pathProc, f.Name())
|
fdPath := filepath.Join(processPath, "fd")
|
||||||
fdPath := path.Join(processPath, "fd")
|
exePath, err := os.Readlink(filepath.Join(processPath, "exe"))
|
||||||
|
if err != nil {
|
||||||
|
if isIgnorableProcError(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
fds, err := os.ReadDir(fdPath)
|
fds, err := os.ReadDir(fdPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fd := range fds {
|
for _, fd := range fds {
|
||||||
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
|
n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
inode, ok := parseSocketInode(buffer[:n])
|
||||||
if bytes.Equal(buffer[:n], socket) {
|
if !ok {
|
||||||
return os.Readlink(path.Join(processPath, "exe"))
|
continue
|
||||||
|
}
|
||||||
|
if _, loaded := processPaths[inode]; !loaded {
|
||||||
|
processPaths[inode] = exePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return processPaths, nil
|
||||||
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
|
func isIgnorableProcError(err error) bool {
|
||||||
|
return os.IsNotExist(err) || os.IsPermission(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSocketInode(link []byte) (uint32, bool) {
|
||||||
|
const socketPrefix = "socket:["
|
||||||
|
if len(link) <= len(socketPrefix) || string(link[:len(socketPrefix)]) != socketPrefix || link[len(link)-1] != ']' {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
var inode uint64
|
||||||
|
for _, char := range link[len(socketPrefix) : len(link)-1] {
|
||||||
|
if char < '0' || char > '9' {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
inode = inode*10 + uint64(char-'0')
|
||||||
|
if inode > uint64(^uint32(0)) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uint32(inode), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func isPid(s string) bool {
|
func isPid(s string) bool {
|
||||||
|
|||||||
60
common/process/searcher_linux_shared_test.go
Normal file
60
common/process/searcher_linux_shared_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQuerySocketDiagUDPExact(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client, err := net.DialUDP("udp4", nil, server.LocalAddr().(*net.UDPAddr))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
err = client.SetDeadline(time.Now().Add(time.Second))
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = client.Write([]byte{0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = server.SetReadDeadline(time.Now().Add(time.Second))
|
||||||
|
require.NoError(t, err)
|
||||||
|
buffer := make([]byte, 1)
|
||||||
|
_, _, err = server.ReadFromUDP(buffer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
source := addrPortFromUDPAddr(t, client.LocalAddr())
|
||||||
|
destination := addrPortFromUDPAddr(t, client.RemoteAddr())
|
||||||
|
|
||||||
|
fd, err := openSocketDiag()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer syscall.Close(fd)
|
||||||
|
|
||||||
|
inode, uid, err := querySocketDiag(fd, packSocketDiagRequest(syscall.AF_INET, syscall.IPPROTO_UDP, source, destination, false))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotZero(t, inode)
|
||||||
|
require.EqualValues(t, os.Getuid(), uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addrPortFromUDPAddr(t *testing.T, addr net.Addr) netip.AddrPort {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
udpAddr, ok := addr.(*net.UDPAddr)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
ip, ok := netip.AddrFromSlice(udpAddr.IP)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
return netip.AddrPortFrom(ip.Unmap(), uint16(udpAddr.Port))
|
||||||
|
}
|
||||||
@@ -28,6 +28,10 @@ func initWin32API() error {
|
|||||||
return winiphlpapi.LoadExtendedTable()
|
return winiphlpapi.LoadExtendedTable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *windowsSearcher) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||||
pid, err := winiphlpapi.FindPid(network, source)
|
pid, err := winiphlpapi.FindPid(network, source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
74
common/tls/masque_client.go
Normal file
74
common/tls/masque_client.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go/http3"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMASQUEClient(ctx context.Context, logger logger.ContextLogger, serverName string, cert [][]byte, privateKey *ecdsa.PrivateKey, peerPublicKey *ecdsa.PublicKey, options option.MASQUEOutboundTLSOptions) (Config, error) {
|
||||||
|
var tlsConfig tls.Config
|
||||||
|
tlsConfig.ServerName = serverName
|
||||||
|
tlsConfig.InsecureSkipVerify = true
|
||||||
|
tlsConfig.NextProtos = []string{http3.NextProtoH3}
|
||||||
|
tlsConfig.Certificates = []tls.Certificate{
|
||||||
|
{
|
||||||
|
Certificate: cert,
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if options.CipherSuites != nil {
|
||||||
|
find:
|
||||||
|
for _, cipherSuite := range options.CipherSuites {
|
||||||
|
for _, tlsCipherSuite := range tls.CipherSuites() {
|
||||||
|
if cipherSuite == tlsCipherSuite.Name {
|
||||||
|
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
|
||||||
|
continue find
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, curve := range options.CurvePreferences {
|
||||||
|
tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curve))
|
||||||
|
}
|
||||||
|
if !options.Insecure {
|
||||||
|
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||||
|
if len(rawCerts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cert, err := x509.ParseCertificate(rawCerts[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, ok := cert.PublicKey.(*ecdsa.PublicKey); !ok {
|
||||||
|
return x509.ErrUnsupportedAlgorithm
|
||||||
|
}
|
||||||
|
if !cert.PublicKey.(*ecdsa.PublicKey).Equal(peerPublicKey) {
|
||||||
|
return x509.CertificateInvalidError{Cert: cert, Reason: 10, Detail: "remote endpoint has a different public key than what we trust in config.json"}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||||
|
if options.KernelRx || options.KernelTx {
|
||||||
|
if !C.IsLinux {
|
||||||
|
return nil, E.New("kTLS is only supported on Linux")
|
||||||
|
}
|
||||||
|
config = &KTLSClientConfig{
|
||||||
|
Config: config,
|
||||||
|
logger: logger,
|
||||||
|
kernelTx: options.KernelTx,
|
||||||
|
kernelRx: options.KernelRx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
68
common/utils.go
Normal file
68
common/utils.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/json/badoption"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StringToType[T any](str string) T {
|
||||||
|
var value T
|
||||||
|
v := reflect.ValueOf(&value).Elem()
|
||||||
|
switch any(value).(type) {
|
||||||
|
case badoption.Duration:
|
||||||
|
d, err := time.ParseDuration(str)
|
||||||
|
if err != nil {
|
||||||
|
v.SetInt(StringToType[int64](str))
|
||||||
|
} else {
|
||||||
|
v.Set(reflect.ValueOf(d))
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
case badoption.HTTPHeader:
|
||||||
|
headers := badoption.HTTPHeader{}
|
||||||
|
reg := regexp.MustCompile(`^[ \t]*?(\S+?):[ \t]*?(\S+?)[ \t]*?$`)
|
||||||
|
for _, header := range strings.Split(str, "\n") {
|
||||||
|
result := reg.FindStringSubmatch(header)
|
||||||
|
if result != nil {
|
||||||
|
key := result[1]
|
||||||
|
headers[key] = strings.Split(result[2], ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.Set(reflect.ValueOf(headers))
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
i, _ := strconv.ParseInt(str, 10, 64)
|
||||||
|
v.SetInt(i)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
i, _ := strconv.ParseUint(str, 10, 64)
|
||||||
|
v.SetUint(i)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
f, _ := strconv.ParseFloat(str, 64)
|
||||||
|
v.SetFloat(f)
|
||||||
|
case reflect.Bool:
|
||||||
|
b, _ := strconv.ParseBool(str)
|
||||||
|
v.SetBool(b)
|
||||||
|
default:
|
||||||
|
panic("unsupported type")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeBase64URLSafe(content string) (string, error) {
|
||||||
|
s := strings.ReplaceAll(content, " ", "-")
|
||||||
|
s = strings.ReplaceAll(s, "/", "_")
|
||||||
|
s = strings.ReplaceAll(s, "+", "-")
|
||||||
|
s = strings.ReplaceAll(s, "=", "")
|
||||||
|
result, err := base64.RawURLEncoding.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
return string(result), nil
|
||||||
|
}
|
||||||
25
common/vision/hook.go
Normal file
25
common/vision/hook.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package vision
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hook func(net.Conn)
|
||||||
|
|
||||||
|
type hookKey struct{}
|
||||||
|
|
||||||
|
func WithHook(ctx context.Context, hook Hook) context.Context {
|
||||||
|
if hook == nil {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, hookKey{}, hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HookFromContext(ctx context.Context) (Hook, bool) {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
hook, ok := ctx.Value(hookKey{}).(Hook)
|
||||||
|
return hook, ok
|
||||||
|
}
|
||||||
18
common/xray/cpuid/cpuid.go
Normal file
18
common/xray/cpuid/cpuid.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package cpuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"golang.org/x/sys/cpu"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Keep in sync with crypto/tls/cipher_suites.go.
|
||||||
|
hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ
|
||||||
|
hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
|
||||||
|
hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasGHASH
|
||||||
|
hasGCMAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le"
|
||||||
|
|
||||||
|
// HasAESGCM indicates whether the CPU has AES-GCM hardware acceleration.
|
||||||
|
HasAESGCM = hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X || hasGCMAsmPPC64
|
||||||
|
)
|
||||||
@@ -71,6 +71,13 @@ func (c *Range) UnmarshalJSON(content []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Range) String() string {
|
||||||
|
if c.From == c.To {
|
||||||
|
return strconv.FormatInt(int64(c.From), 10)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d-%d", c.From, c.To)
|
||||||
|
}
|
||||||
|
|
||||||
func (c Range) Rand() int32 {
|
func (c Range) Rand() int32 {
|
||||||
return int32(crypto.RandBetween(int64(c.From), int64(c.To)))
|
return int32(crypto.RandBetween(int64(c.From), int64(c.To)))
|
||||||
}
|
}
|
||||||
|
|||||||
20
constant/provider.go
Normal file
20
constant/provider.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderTypeInline = "inline"
|
||||||
|
ProviderTypeLocal = "local"
|
||||||
|
ProviderTypeRemote = "remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProviderDisplayName(providerType string) string {
|
||||||
|
switch providerType {
|
||||||
|
case ProviderTypeInline:
|
||||||
|
return "Inline"
|
||||||
|
case ProviderTypeLocal:
|
||||||
|
return "Local"
|
||||||
|
case ProviderTypeRemote:
|
||||||
|
return "Remote"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +1,56 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeTun = "tun"
|
TypeTun = "tun"
|
||||||
TypeRedirect = "redirect"
|
TypeRedirect = "redirect"
|
||||||
TypeTProxy = "tproxy"
|
TypeTProxy = "tproxy"
|
||||||
TypeDirect = "direct"
|
TypeDirect = "direct"
|
||||||
TypeBlock = "block"
|
TypeBlock = "block"
|
||||||
TypeDNS = "dns"
|
TypeDNS = "dns"
|
||||||
TypeSOCKS = "socks"
|
TypeSOCKS = "socks"
|
||||||
TypeHTTP = "http"
|
TypeHTTP = "http"
|
||||||
TypeMixed = "mixed"
|
TypeMixed = "mixed"
|
||||||
TypeShadowsocks = "shadowsocks"
|
TypeShadowsocks = "shadowsocks"
|
||||||
TypeVMess = "vmess"
|
TypeVMess = "vmess"
|
||||||
TypeTrojan = "trojan"
|
TypeTrojan = "trojan"
|
||||||
TypeNaive = "naive"
|
TypeNaive = "naive"
|
||||||
TypeWireGuard = "wireguard"
|
TypeWireGuard = "wireguard"
|
||||||
TypeWARP = "warp"
|
TypeWARP = "warp"
|
||||||
TypeHysteria = "hysteria"
|
TypeMASQUE = "masque"
|
||||||
TypeTor = "tor"
|
TypeMTProxy = "mtproxy"
|
||||||
TypeSSH = "ssh"
|
TypeParser = "parser"
|
||||||
TypeShadowTLS = "shadowtls"
|
TypeHysteria = "hysteria"
|
||||||
TypeMieru = "mieru"
|
TypeTor = "tor"
|
||||||
TypeAnyTLS = "anytls"
|
TypeSSH = "ssh"
|
||||||
TypeShadowsocksR = "shadowsocksr"
|
TypeShadowTLS = "shadowtls"
|
||||||
TypeVLESS = "vless"
|
TypeMieru = "mieru"
|
||||||
TypeTUIC = "tuic"
|
TypeAnyTLS = "anytls"
|
||||||
TypeHysteria2 = "hysteria2"
|
TypeShadowsocksR = "shadowsocksr"
|
||||||
TypeTunnelClient = "tunnel_client"
|
TypeVLESS = "vless"
|
||||||
TypeTunnelServer = "tunnel_server"
|
TypeTUIC = "tuic"
|
||||||
TypeTailscale = "tailscale"
|
TypeHysteria2 = "hysteria2"
|
||||||
TypeDERP = "derp"
|
TypeBond = "bond"
|
||||||
TypeResolved = "resolved"
|
TypeVPNServer = "vpn-server"
|
||||||
TypeSSMAPI = "ssm-api"
|
TypeVPNClient = "vpn-client"
|
||||||
TypeCCM = "ccm"
|
TypeTailscale = "tailscale"
|
||||||
TypeOCM = "ocm"
|
TypeConnectionLimiter = "connection-limiter"
|
||||||
TypeOOMKiller = "oom-killer"
|
TypeBandwidthLimiter = "bandwidth-limiter"
|
||||||
|
TypeTrafficLimiter = "traffic-limiter"
|
||||||
|
TypeAdminPanel = "admin-panel"
|
||||||
|
TypeNodeManagerServer = "node-manager-server"
|
||||||
|
TypeNodeManagerClient = "node-manager-client"
|
||||||
|
TypeDERP = "derp"
|
||||||
|
TypeManager = "manager"
|
||||||
|
TypeNode = "node"
|
||||||
|
TypeResolved = "resolved"
|
||||||
|
TypeSSMAPI = "ssm-api"
|
||||||
|
TypeCCM = "ccm"
|
||||||
|
TypeOCM = "ocm"
|
||||||
|
TypeOOMKiller = "oom-killer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
TypeFallback = "fallback"
|
||||||
TypeSelector = "selector"
|
TypeSelector = "selector"
|
||||||
TypeURLTest = "urltest"
|
TypeURLTest = "urltest"
|
||||||
)
|
)
|
||||||
@@ -74,6 +87,12 @@ func ProxyDisplayName(proxyType string) string {
|
|||||||
return "WireGuard"
|
return "WireGuard"
|
||||||
case TypeWARP:
|
case TypeWARP:
|
||||||
return "WARP"
|
return "WARP"
|
||||||
|
case TypeMASQUE:
|
||||||
|
return "MASQUE"
|
||||||
|
case TypeMTProxy:
|
||||||
|
return "MTProxy"
|
||||||
|
case TypeParser:
|
||||||
|
return "Parser"
|
||||||
case TypeHysteria:
|
case TypeHysteria:
|
||||||
return "Hysteria"
|
return "Hysteria"
|
||||||
case TypeTor:
|
case TypeTor:
|
||||||
@@ -90,20 +109,24 @@ func ProxyDisplayName(proxyType string) string {
|
|||||||
return "TUIC"
|
return "TUIC"
|
||||||
case TypeHysteria2:
|
case TypeHysteria2:
|
||||||
return "Hysteria2"
|
return "Hysteria2"
|
||||||
|
case TypeBond:
|
||||||
|
return "Bond"
|
||||||
case TypeMieru:
|
case TypeMieru:
|
||||||
return "Mieru"
|
return "Mieru"
|
||||||
case TypeAnyTLS:
|
case TypeAnyTLS:
|
||||||
return "AnyTLS"
|
return "AnyTLS"
|
||||||
|
case TypeFallback:
|
||||||
|
return "Fallback"
|
||||||
case TypeTailscale:
|
case TypeTailscale:
|
||||||
return "Tailscale"
|
return "Tailscale"
|
||||||
case TypeSelector:
|
case TypeSelector:
|
||||||
return "Selector"
|
return "Selector"
|
||||||
case TypeURLTest:
|
case TypeURLTest:
|
||||||
return "URLTest"
|
return "URLTest"
|
||||||
case TypeTunnelClient:
|
case TypeVPNClient:
|
||||||
return "Tunnel Client"
|
return "VPN Client"
|
||||||
case TypeTunnelServer:
|
case TypeVPNServer:
|
||||||
return "Tunnel Server"
|
return "VPN Server"
|
||||||
default:
|
default:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ const (
|
|||||||
V2RayTransportTypeGRPC = "grpc"
|
V2RayTransportTypeGRPC = "grpc"
|
||||||
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
|
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
|
||||||
V2RayTransportTypeXHTTP = "xhttp"
|
V2RayTransportTypeXHTTP = "xhttp"
|
||||||
|
V2RayTransportTypeKCP = "mkcp"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package constant
|
|
||||||
|
|
||||||
type WARPConfig struct {
|
|
||||||
PrivateKey string `json:"private_key"`
|
|
||||||
Interface struct {
|
|
||||||
Addresses struct {
|
|
||||||
V4 string `json:"v4"`
|
|
||||||
V6 string `json:"v6"`
|
|
||||||
} `json:"addresses"`
|
|
||||||
} `json:"interface"`
|
|
||||||
Peers []struct {
|
|
||||||
PublicKey string `json:"public_key"`
|
|
||||||
Endpoint struct {
|
|
||||||
V4 string `json:"v4"`
|
|
||||||
V6 string `json:"v6"`
|
|
||||||
Host string `json:"host"`
|
|
||||||
Ports []int `json:"ports"`
|
|
||||||
} `json:"endpoint"`
|
|
||||||
} `json:"peers"`
|
|
||||||
}
|
|
||||||
@@ -69,7 +69,7 @@ type OverrideOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
|
func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
|
||||||
ctx := s.ctx
|
ctx := service.ExtendContext(s.ctx)
|
||||||
service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
|
service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
|
||||||
ctx, cancel := context.WithCancel(include.Context(ctx))
|
ctx, cancel := context.WithCancel(include.Context(ctx))
|
||||||
options, err := parseConfig(ctx, profileContent)
|
options, err := parseConfig(ctx, profileContent)
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ func (s *StartedService) waitForStarted(ctx context.Context) error {
|
|||||||
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
|
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
|
||||||
s.serviceAccess.Lock()
|
s.serviceAccess.Lock()
|
||||||
switch s.serviceStatus.Status {
|
switch s.serviceStatus.Status {
|
||||||
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING:
|
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING, ServiceStatus_FATAL:
|
||||||
default:
|
default:
|
||||||
s.serviceAccess.Unlock()
|
s.serviceAccess.Unlock()
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
@@ -226,13 +226,14 @@ func (s *StartedService) CloseService() error {
|
|||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
s.updateStatus(ServiceStatus_STOPPING)
|
s.updateStatus(ServiceStatus_STOPPING)
|
||||||
if s.instance != nil {
|
instance := s.instance
|
||||||
err := s.instance.Close()
|
s.instance = nil
|
||||||
|
if instance != nil {
|
||||||
|
err := instance.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return s.updateStatusError(err)
|
return s.updateStatusError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.instance = nil
|
|
||||||
s.startedAt = time.Time{}
|
s.startedAt = time.Time{}
|
||||||
s.updateStatus(ServiceStatus_IDLE)
|
s.updateStatus(ServiceStatus_IDLE)
|
||||||
s.serviceAccess.Unlock()
|
s.serviceAccess.Unlock()
|
||||||
@@ -949,11 +950,11 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
|
|||||||
var processInfo *ProcessInfo
|
var processInfo *ProcessInfo
|
||||||
if metadata.Metadata.ProcessInfo != nil {
|
if metadata.Metadata.ProcessInfo != nil {
|
||||||
processInfo = &ProcessInfo{
|
processInfo = &ProcessInfo{
|
||||||
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
|
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
|
||||||
UserId: metadata.Metadata.ProcessInfo.UserId,
|
UserId: metadata.Metadata.ProcessInfo.UserId,
|
||||||
UserName: metadata.Metadata.ProcessInfo.UserName,
|
UserName: metadata.Metadata.ProcessInfo.UserName,
|
||||||
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
|
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
|
||||||
PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName,
|
PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &Connection{
|
return &Connection{
|
||||||
|
|||||||
@@ -1460,7 +1460,7 @@ type ProcessInfo struct {
|
|||||||
UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"`
|
UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"`
|
||||||
UserName string `protobuf:"bytes,3,opt,name=userName,proto3" json:"userName,omitempty"`
|
UserName string `protobuf:"bytes,3,opt,name=userName,proto3" json:"userName,omitempty"`
|
||||||
ProcessPath string `protobuf:"bytes,4,opt,name=processPath,proto3" json:"processPath,omitempty"`
|
ProcessPath string `protobuf:"bytes,4,opt,name=processPath,proto3" json:"processPath,omitempty"`
|
||||||
PackageName string `protobuf:"bytes,5,opt,name=packageName,proto3" json:"packageName,omitempty"`
|
PackageNames []string `protobuf:"bytes,5,rep,name=packageNames,proto3" json:"packageNames,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -1523,11 +1523,11 @@ func (x *ProcessInfo) GetProcessPath() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ProcessInfo) GetPackageName() string {
|
func (x *ProcessInfo) GetPackageNames() []string {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.PackageName
|
return x.PackageNames
|
||||||
}
|
}
|
||||||
return ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type CloseConnectionRequest struct {
|
type CloseConnectionRequest struct {
|
||||||
@@ -1884,13 +1884,13 @@ const file_daemon_started_service_proto_rawDesc = "" +
|
|||||||
"\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" +
|
"\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" +
|
||||||
"\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" +
|
"\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" +
|
||||||
"\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" +
|
"\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" +
|
||||||
"\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa3\x01\n" +
|
"\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa5\x01\n" +
|
||||||
"\vProcessInfo\x12\x1c\n" +
|
"\vProcessInfo\x12\x1c\n" +
|
||||||
"\tprocessId\x18\x01 \x01(\rR\tprocessId\x12\x16\n" +
|
"\tprocessId\x18\x01 \x01(\rR\tprocessId\x12\x16\n" +
|
||||||
"\x06userId\x18\x02 \x01(\x05R\x06userId\x12\x1a\n" +
|
"\x06userId\x18\x02 \x01(\x05R\x06userId\x12\x1a\n" +
|
||||||
"\buserName\x18\x03 \x01(\tR\buserName\x12 \n" +
|
"\buserName\x18\x03 \x01(\tR\buserName\x12 \n" +
|
||||||
"\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12 \n" +
|
"\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12\"\n" +
|
||||||
"\vpackageName\x18\x05 \x01(\tR\vpackageName\"(\n" +
|
"\fpackageNames\x18\x05 \x03(\tR\fpackageNames\"(\n" +
|
||||||
"\x16CloseConnectionRequest\x12\x0e\n" +
|
"\x16CloseConnectionRequest\x12\x0e\n" +
|
||||||
"\x02id\x18\x01 \x01(\tR\x02id\"K\n" +
|
"\x02id\x18\x01 \x01(\tR\x02id\"K\n" +
|
||||||
"\x12DeprecatedWarnings\x125\n" +
|
"\x12DeprecatedWarnings\x125\n" +
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ message ProcessInfo {
|
|||||||
int32 userId = 2;
|
int32 userId = 2;
|
||||||
string userName = 3;
|
string userName = 3;
|
||||||
string processPath = 4;
|
string processPath = 4;
|
||||||
string packageName = 5;
|
repeated string packageNames = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CloseConnectionRequest {
|
message CloseConnectionRequest {
|
||||||
|
|||||||
@@ -283,6 +283,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
if timeToLive == 0 {
|
if timeToLive == 0 {
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
for _, record := range recordList {
|
for _, record := range recordList {
|
||||||
|
if record.Header().Rrtype == dns.TypeOPT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||||
timeToLive = record.Header().Ttl
|
timeToLive = record.Header().Ttl
|
||||||
}
|
}
|
||||||
@@ -294,6 +297,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
}
|
}
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
for _, record := range recordList {
|
for _, record := range recordList {
|
||||||
|
if record.Header().Rrtype == dns.TypeOPT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
record.Header().Ttl = timeToLive
|
record.Header().Ttl = timeToLive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,16 +330,20 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
|||||||
} else {
|
} else {
|
||||||
strategy = options.Strategy
|
strategy = options.Strategy
|
||||||
}
|
}
|
||||||
|
lookupOptions := options
|
||||||
|
if options.LookupStrategy != C.DomainStrategyAsIS {
|
||||||
|
lookupOptions.Strategy = strategy
|
||||||
|
}
|
||||||
if strategy == C.DomainStrategyIPv4Only {
|
if strategy == C.DomainStrategyIPv4Only {
|
||||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
|
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker)
|
||||||
} else if strategy == C.DomainStrategyIPv6Only {
|
} else if strategy == C.DomainStrategyIPv6Only {
|
||||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
|
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker)
|
||||||
}
|
}
|
||||||
var response4 []netip.Addr
|
var response4 []netip.Addr
|
||||||
var response6 []netip.Addr
|
var response6 []netip.Addr
|
||||||
var group task.Group
|
var group task.Group
|
||||||
group.Append("exchange4", func(ctx context.Context) error {
|
group.Append("exchange4", func(ctx context.Context) error {
|
||||||
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
|
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -341,7 +351,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
group.Append("exchange6", func(ctx context.Context) error {
|
group.Append("exchange6", func(ctx context.Context) error {
|
||||||
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
|
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -377,21 +387,21 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
|
|||||||
}
|
}
|
||||||
if c.disableExpire {
|
if c.disableExpire {
|
||||||
if !c.independentCache {
|
if !c.independentCache {
|
||||||
c.cache.Add(question, message)
|
c.cache.Add(question, message.Copy())
|
||||||
} else {
|
} else {
|
||||||
c.transportCache.Add(transportCacheKey{
|
c.transportCache.Add(transportCacheKey{
|
||||||
Question: question,
|
Question: question,
|
||||||
transportTag: transport.Tag(),
|
transportTag: transport.Tag(),
|
||||||
}, message)
|
}, message.Copy())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !c.independentCache {
|
if !c.independentCache {
|
||||||
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
|
c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||||
} else {
|
} else {
|
||||||
c.transportCache.AddWithLifetime(transportCacheKey{
|
c.transportCache.AddWithLifetime(transportCacheKey{
|
||||||
Question: question,
|
Question: question,
|
||||||
transportTag: transport.Tag(),
|
transportTag: transport.Tag(),
|
||||||
}, message, time.Second*time.Duration(timeToLive))
|
}, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,6 +492,9 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
|
|||||||
var originTTL int
|
var originTTL int
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
for _, record := range recordList {
|
for _, record := range recordList {
|
||||||
|
if record.Header().Rrtype == dns.TypeOPT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
|
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
|
||||||
originTTL = int(record.Header().Ttl)
|
originTTL = int(record.Header().Ttl)
|
||||||
}
|
}
|
||||||
@@ -496,12 +509,18 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
|
|||||||
duration := uint32(originTTL - nowTTL)
|
duration := uint32(originTTL - nowTTL)
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
for _, record := range recordList {
|
for _, record := range recordList {
|
||||||
|
if record.Header().Rrtype == dns.TypeOPT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
record.Header().Ttl = record.Header().Ttl - duration
|
record.Header().Ttl = record.Header().Ttl - duration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
for _, record := range recordList {
|
for _, record := range recordList {
|
||||||
|
if record.Header().Rrtype == dns.TypeOPT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
record.Header().Ttl = uint32(nowTTL)
|
record.Header().Ttl = uint32(nowTTL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,16 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r.transport.Default(), nil, -1
|
transport := r.transport.Default()
|
||||||
|
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||||
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
|
options.Strategy = legacyTransport.LegacyStrategy()
|
||||||
|
}
|
||||||
|
if !options.ClientSubnet.IsValid() {
|
||||||
|
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transport, nil, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
|
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
|
||||||
@@ -345,7 +354,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
|||||||
transport := options.Transport
|
transport := options.Transport
|
||||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
options.Strategy = r.defaultDomainStrategy
|
options.Strategy = legacyTransport.LegacyStrategy()
|
||||||
}
|
}
|
||||||
if !options.ClientSubnet.IsValid() {
|
if !options.ClientSubnet.IsValid() {
|
||||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TransportState int
|
|
||||||
|
|
||||||
const (
|
|
||||||
StateNew TransportState = iota
|
|
||||||
StateStarted
|
|
||||||
StateClosing
|
|
||||||
StateClosed
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrTransportClosed = os.ErrClosed
|
|
||||||
ErrConnectionReset = E.New("connection reset")
|
|
||||||
)
|
|
||||||
|
|
||||||
type BaseTransport struct {
|
|
||||||
dns.TransportAdapter
|
|
||||||
Logger logger.ContextLogger
|
|
||||||
|
|
||||||
mutex sync.Mutex
|
|
||||||
state TransportState
|
|
||||||
inFlight int32
|
|
||||||
queriesComplete chan struct{}
|
|
||||||
closeCtx context.Context
|
|
||||||
closeCancel context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBaseTransport(adapter dns.TransportAdapter, logger logger.ContextLogger) *BaseTransport {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
return &BaseTransport{
|
|
||||||
TransportAdapter: adapter,
|
|
||||||
Logger: logger,
|
|
||||||
state: StateNew,
|
|
||||||
closeCtx: ctx,
|
|
||||||
closeCancel: cancel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *BaseTransport) State() TransportState {
|
|
||||||
t.mutex.Lock()
|
|
||||||
defer t.mutex.Unlock()
|
|
||||||
return t.state
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *BaseTransport) SetStarted() error {
|
|
||||||
t.mutex.Lock()
|
|
||||||
defer t.mutex.Unlock()
|
|
||||||
switch t.state {
|
|
||||||
case StateNew:
|
|
||||||
t.state = StateStarted
|
|
||||||
return nil
|
|
||||||
case StateStarted:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return ErrTransportClosed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *BaseTransport) BeginQuery() bool {
|
|
||||||
t.mutex.Lock()
|
|
||||||
defer t.mutex.Unlock()
|
|
||||||
if t.state != StateStarted {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
t.inFlight++
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *BaseTransport) EndQuery() {
|
|
||||||
t.mutex.Lock()
|
|
||||||
if t.inFlight > 0 {
|
|
||||||
t.inFlight--
|
|
||||||
}
|
|
||||||
if t.inFlight == 0 && t.queriesComplete != nil {
|
|
||||||
close(t.queriesComplete)
|
|
||||||
t.queriesComplete = nil
|
|
||||||
}
|
|
||||||
t.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *BaseTransport) CloseContext() context.Context {
|
|
||||||
return t.closeCtx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *BaseTransport) Shutdown(ctx context.Context) error {
|
|
||||||
t.mutex.Lock()
|
|
||||||
|
|
||||||
if t.state >= StateClosing {
|
|
||||||
t.mutex.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.state == StateNew {
|
|
||||||
t.state = StateClosed
|
|
||||||
t.mutex.Unlock()
|
|
||||||
t.closeCancel()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
t.state = StateClosing
|
|
||||||
|
|
||||||
if t.inFlight == 0 {
|
|
||||||
t.state = StateClosed
|
|
||||||
t.mutex.Unlock()
|
|
||||||
t.closeCancel()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
t.queriesComplete = make(chan struct{})
|
|
||||||
queriesComplete := t.queriesComplete
|
|
||||||
t.mutex.Unlock()
|
|
||||||
|
|
||||||
t.closeCancel()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-queriesComplete:
|
|
||||||
t.mutex.Lock()
|
|
||||||
t.state = StateClosed
|
|
||||||
t.mutex.Unlock()
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
t.mutex.Lock()
|
|
||||||
t.state = StateClosed
|
|
||||||
t.mutex.Unlock()
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *BaseTransport) Close() error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout)
|
|
||||||
defer cancel()
|
|
||||||
return t.Shutdown(ctx)
|
|
||||||
}
|
|
||||||
547
dns/transport/conn_pool.go
Normal file
547
dns/transport/conn_pool.go
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
package transport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConnPoolMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnPoolSingle ConnPoolMode = iota
|
||||||
|
ConnPoolOrdered
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConnPoolOptions[T comparable] struct {
|
||||||
|
Mode ConnPoolMode
|
||||||
|
IsAlive func(T) bool
|
||||||
|
Close func(T, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnPool[T comparable] struct {
|
||||||
|
options ConnPoolOptions[T]
|
||||||
|
|
||||||
|
access sync.Mutex
|
||||||
|
closed bool
|
||||||
|
state *connPoolState[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
type connPoolState[T comparable] struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelCauseFunc
|
||||||
|
|
||||||
|
all map[T]struct{}
|
||||||
|
|
||||||
|
idle list.List[T]
|
||||||
|
idleElements map[T]*list.Element[T]
|
||||||
|
|
||||||
|
shared T
|
||||||
|
hasShared bool
|
||||||
|
sharedClaimed bool
|
||||||
|
sharedCtx context.Context
|
||||||
|
sharedCancel context.CancelCauseFunc
|
||||||
|
|
||||||
|
connecting *connPoolConnect[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
type connPoolConnect[T comparable] struct {
|
||||||
|
done chan struct{}
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type connPoolDialContext struct {
|
||||||
|
context.Context
|
||||||
|
parent context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c connPoolDialContext) Deadline() (time.Time, bool) {
|
||||||
|
return c.parent.Deadline()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c connPoolDialContext) Value(key any) any {
|
||||||
|
return c.parent.Value(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnPool[T comparable](options ConnPoolOptions[T]) *ConnPool[T] {
|
||||||
|
return &ConnPool[T]{
|
||||||
|
options: options,
|
||||||
|
state: newConnPoolState[T](options.Mode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConnPoolState[T comparable](mode ConnPoolMode) *connPoolState[T] {
|
||||||
|
ctx, cancel := context.WithCancelCause(context.Background())
|
||||||
|
state := &connPoolState[T]{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
all: make(map[T]struct{}),
|
||||||
|
}
|
||||||
|
if mode == ConnPoolOrdered {
|
||||||
|
state.idleElements = make(map[T]*list.Element[T])
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) Acquire(ctx context.Context, dial func(context.Context) (T, error)) (T, bool, error) {
|
||||||
|
switch p.options.Mode {
|
||||||
|
case ConnPoolSingle:
|
||||||
|
conn, _, created, err := p.acquireShared(ctx, dial)
|
||||||
|
return conn, created, err
|
||||||
|
case ConnPoolOrdered:
|
||||||
|
return p.acquireOrdered(ctx, dial)
|
||||||
|
default:
|
||||||
|
var zero T
|
||||||
|
return zero, false, net.ErrClosed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) AcquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) {
|
||||||
|
if p.options.Mode != ConnPoolSingle {
|
||||||
|
var zero T
|
||||||
|
return zero, nil, false, net.ErrClosed
|
||||||
|
}
|
||||||
|
return p.acquireShared(ctx, dial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) Release(conn T, reuse bool) {
|
||||||
|
var (
|
||||||
|
closeConn bool
|
||||||
|
closeErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
p.access.Lock()
|
||||||
|
if p.closed || p.state == nil {
|
||||||
|
closeConn = true
|
||||||
|
closeErr = net.ErrClosed
|
||||||
|
p.access.Unlock()
|
||||||
|
if closeConn {
|
||||||
|
p.options.Close(conn, closeErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState := p.state
|
||||||
|
_, tracked := currentState.all[conn]
|
||||||
|
if !tracked {
|
||||||
|
closeConn = true
|
||||||
|
closeErr = p.closeCause(currentState)
|
||||||
|
p.access.Unlock()
|
||||||
|
if closeConn {
|
||||||
|
p.options.Close(conn, closeErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reuse || !p.options.IsAlive(conn) {
|
||||||
|
delete(currentState.all, conn)
|
||||||
|
switch p.options.Mode {
|
||||||
|
case ConnPoolSingle:
|
||||||
|
if currentState.hasShared && currentState.shared == conn {
|
||||||
|
var zero T
|
||||||
|
currentState.shared = zero
|
||||||
|
currentState.hasShared = false
|
||||||
|
currentState.sharedClaimed = false
|
||||||
|
currentState.sharedCtx = nil
|
||||||
|
if currentState.sharedCancel != nil {
|
||||||
|
currentState.sharedCancel(net.ErrClosed)
|
||||||
|
currentState.sharedCancel = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ConnPoolOrdered:
|
||||||
|
if element, loaded := currentState.idleElements[conn]; loaded {
|
||||||
|
currentState.idle.Remove(element)
|
||||||
|
delete(currentState.idleElements, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closeConn = true
|
||||||
|
closeErr = net.ErrClosed
|
||||||
|
p.access.Unlock()
|
||||||
|
if closeConn {
|
||||||
|
p.options.Close(conn, closeErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.options.Mode == ConnPoolOrdered {
|
||||||
|
if _, loaded := currentState.idleElements[conn]; !loaded {
|
||||||
|
currentState.idleElements[conn] = currentState.idle.PushBack(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.access.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) Invalidate(conn T, cause error) {
|
||||||
|
p.access.Lock()
|
||||||
|
if p.closed || p.state == nil {
|
||||||
|
p.access.Unlock()
|
||||||
|
p.options.Close(conn, cause)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState := p.state
|
||||||
|
_, tracked := currentState.all[conn]
|
||||||
|
if !tracked {
|
||||||
|
p.access.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(currentState.all, conn)
|
||||||
|
switch p.options.Mode {
|
||||||
|
case ConnPoolSingle:
|
||||||
|
if currentState.hasShared && currentState.shared == conn {
|
||||||
|
var zero T
|
||||||
|
currentState.shared = zero
|
||||||
|
currentState.hasShared = false
|
||||||
|
currentState.sharedClaimed = false
|
||||||
|
currentState.sharedCtx = nil
|
||||||
|
if currentState.sharedCancel != nil {
|
||||||
|
currentState.sharedCancel(cause)
|
||||||
|
currentState.sharedCancel = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ConnPoolOrdered:
|
||||||
|
if element, loaded := currentState.idleElements[conn]; loaded {
|
||||||
|
currentState.idle.Remove(element)
|
||||||
|
delete(currentState.idleElements, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.access.Unlock()
|
||||||
|
|
||||||
|
p.options.Close(conn, cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) Reset() {
|
||||||
|
p.access.Lock()
|
||||||
|
if p.closed {
|
||||||
|
p.access.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oldState := p.state
|
||||||
|
p.state = newConnPoolState[T](p.options.Mode)
|
||||||
|
p.access.Unlock()
|
||||||
|
|
||||||
|
p.closeState(oldState, net.ErrClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) Close() error {
|
||||||
|
p.access.Lock()
|
||||||
|
if p.closed {
|
||||||
|
p.access.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.closed = true
|
||||||
|
oldState := p.state
|
||||||
|
p.state = nil
|
||||||
|
p.access.Unlock()
|
||||||
|
|
||||||
|
p.closeState(oldState, net.ErrClosed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) acquireOrdered(ctx context.Context, dial func(context.Context) (T, error)) (T, bool, error) {
|
||||||
|
var zero T
|
||||||
|
for {
|
||||||
|
var (
|
||||||
|
staleConn T
|
||||||
|
hasStale bool
|
||||||
|
)
|
||||||
|
|
||||||
|
p.access.Lock()
|
||||||
|
if p.closed {
|
||||||
|
p.access.Unlock()
|
||||||
|
return zero, false, net.ErrClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState := p.state
|
||||||
|
if element := currentState.idle.Front(); element != nil {
|
||||||
|
conn := currentState.idle.Remove(element)
|
||||||
|
delete(currentState.idleElements, conn)
|
||||||
|
if p.options.IsAlive(conn) {
|
||||||
|
p.access.Unlock()
|
||||||
|
return conn, false, nil
|
||||||
|
}
|
||||||
|
delete(currentState.all, conn)
|
||||||
|
staleConn = conn
|
||||||
|
hasStale = true
|
||||||
|
}
|
||||||
|
p.access.Unlock()
|
||||||
|
|
||||||
|
if hasStale {
|
||||||
|
p.options.Close(staleConn, net.ErrClosed)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := p.dial(ctx, currentState, dial)
|
||||||
|
if err != nil {
|
||||||
|
return zero, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.access.Lock()
|
||||||
|
if p.closed {
|
||||||
|
p.access.Unlock()
|
||||||
|
p.options.Close(conn, net.ErrClosed)
|
||||||
|
return zero, false, net.ErrClosed
|
||||||
|
}
|
||||||
|
if p.state != currentState {
|
||||||
|
cause := p.closeCause(currentState)
|
||||||
|
p.access.Unlock()
|
||||||
|
p.options.Close(conn, cause)
|
||||||
|
return zero, false, cause
|
||||||
|
}
|
||||||
|
currentState.all[conn] = struct{}{}
|
||||||
|
p.access.Unlock()
|
||||||
|
return conn, true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) {
|
||||||
|
var zero T
|
||||||
|
for {
|
||||||
|
var (
|
||||||
|
staleConn T
|
||||||
|
hasStale bool
|
||||||
|
state *connPoolConnect[T]
|
||||||
|
current *connPoolState[T]
|
||||||
|
startDial bool
|
||||||
|
)
|
||||||
|
|
||||||
|
p.access.Lock()
|
||||||
|
if p.closed {
|
||||||
|
p.access.Unlock()
|
||||||
|
return zero, nil, false, net.ErrClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
current = p.state
|
||||||
|
if current.hasShared {
|
||||||
|
conn := current.shared
|
||||||
|
if p.options.IsAlive(conn) {
|
||||||
|
created := !current.sharedClaimed
|
||||||
|
current.sharedClaimed = true
|
||||||
|
connCtx := current.sharedCtx
|
||||||
|
p.access.Unlock()
|
||||||
|
return conn, connCtx, created, nil
|
||||||
|
}
|
||||||
|
delete(current.all, conn)
|
||||||
|
var zeroConn T
|
||||||
|
current.shared = zeroConn
|
||||||
|
current.hasShared = false
|
||||||
|
current.sharedClaimed = false
|
||||||
|
current.sharedCtx = nil
|
||||||
|
if current.sharedCancel != nil {
|
||||||
|
current.sharedCancel(net.ErrClosed)
|
||||||
|
current.sharedCancel = nil
|
||||||
|
}
|
||||||
|
staleConn = conn
|
||||||
|
hasStale = true
|
||||||
|
p.access.Unlock()
|
||||||
|
p.options.Close(staleConn, net.ErrClosed)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.connecting == nil {
|
||||||
|
current.connecting = &connPoolConnect[T]{
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
startDial = true
|
||||||
|
}
|
||||||
|
state = current.connecting
|
||||||
|
p.access.Unlock()
|
||||||
|
|
||||||
|
if hasStale {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if startDial {
|
||||||
|
go p.connectSingle(current, state, ctx, dial)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-state.done:
|
||||||
|
conn, connCtx, created, retry, err := p.collectShared(current, state, startDial)
|
||||||
|
if retry {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return conn, connCtx, created, err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return zero, nil, false, ctx.Err()
|
||||||
|
case <-current.ctx.Done():
|
||||||
|
p.access.Lock()
|
||||||
|
closed := p.closed
|
||||||
|
p.access.Unlock()
|
||||||
|
if closed {
|
||||||
|
return zero, nil, false, net.ErrClosed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) connectSingle(current *connPoolState[T], state *connPoolConnect[T], ctx context.Context, dial func(context.Context) (T, error)) {
|
||||||
|
conn, err := p.dial(ctx, current, dial)
|
||||||
|
if err != nil {
|
||||||
|
p.access.Lock()
|
||||||
|
if current.connecting == state {
|
||||||
|
current.connecting = nil
|
||||||
|
}
|
||||||
|
state.err = err
|
||||||
|
p.access.Unlock()
|
||||||
|
close(state.done)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var closeErr error
|
||||||
|
|
||||||
|
p.access.Lock()
|
||||||
|
if current.connecting == state {
|
||||||
|
current.connecting = nil
|
||||||
|
}
|
||||||
|
if p.closed {
|
||||||
|
closeErr = net.ErrClosed
|
||||||
|
state.err = closeErr
|
||||||
|
} else if p.state != current {
|
||||||
|
closeErr = p.closeCause(current)
|
||||||
|
state.err = closeErr
|
||||||
|
} else {
|
||||||
|
sharedCtx, sharedCancel := context.WithCancelCause(current.ctx)
|
||||||
|
current.shared = conn
|
||||||
|
current.hasShared = true
|
||||||
|
current.sharedClaimed = false
|
||||||
|
current.sharedCtx = sharedCtx
|
||||||
|
current.sharedCancel = sharedCancel
|
||||||
|
current.all[conn] = struct{}{}
|
||||||
|
}
|
||||||
|
p.access.Unlock()
|
||||||
|
|
||||||
|
if closeErr != nil {
|
||||||
|
p.options.Close(conn, closeErr)
|
||||||
|
}
|
||||||
|
close(state.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolConnect[T], startDial bool) (T, context.Context, bool, bool, error) {
|
||||||
|
var zero T
|
||||||
|
|
||||||
|
p.access.Lock()
|
||||||
|
if state.err != nil {
|
||||||
|
err := state.err
|
||||||
|
p.access.Unlock()
|
||||||
|
if startDial {
|
||||||
|
return zero, nil, false, false, err
|
||||||
|
}
|
||||||
|
return zero, nil, false, true, nil
|
||||||
|
}
|
||||||
|
if p.closed {
|
||||||
|
p.access.Unlock()
|
||||||
|
return zero, nil, false, false, net.ErrClosed
|
||||||
|
}
|
||||||
|
if p.state != current {
|
||||||
|
cause := p.closeCause(current)
|
||||||
|
p.access.Unlock()
|
||||||
|
return zero, nil, false, false, cause
|
||||||
|
}
|
||||||
|
if !current.hasShared {
|
||||||
|
p.access.Unlock()
|
||||||
|
return zero, nil, false, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := current.shared
|
||||||
|
if !p.options.IsAlive(conn) {
|
||||||
|
delete(current.all, conn)
|
||||||
|
var zeroConn T
|
||||||
|
current.shared = zeroConn
|
||||||
|
current.hasShared = false
|
||||||
|
current.sharedClaimed = false
|
||||||
|
current.sharedCtx = nil
|
||||||
|
if current.sharedCancel != nil {
|
||||||
|
current.sharedCancel(net.ErrClosed)
|
||||||
|
current.sharedCancel = nil
|
||||||
|
}
|
||||||
|
p.access.Unlock()
|
||||||
|
p.options.Close(conn, net.ErrClosed)
|
||||||
|
return zero, nil, false, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
created := !current.sharedClaimed
|
||||||
|
current.sharedClaimed = true
|
||||||
|
connCtx := current.sharedCtx
|
||||||
|
p.access.Unlock()
|
||||||
|
return conn, connCtx, created, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) dial(ctx context.Context, current *connPoolState[T], dial func(context.Context) (T, error)) (T, error) {
|
||||||
|
var zero T
|
||||||
|
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
if cause := context.Cause(current.ctx); cause != nil {
|
||||||
|
return zero, cause
|
||||||
|
}
|
||||||
|
|
||||||
|
dialCtx, cancel := context.WithCancelCause(current.ctx)
|
||||||
|
var (
|
||||||
|
stateAccess sync.Mutex
|
||||||
|
dialComplete bool
|
||||||
|
)
|
||||||
|
stopCancel := context.AfterFunc(ctx, func() {
|
||||||
|
stateAccess.Lock()
|
||||||
|
if !dialComplete {
|
||||||
|
cancel(context.Cause(ctx))
|
||||||
|
}
|
||||||
|
stateAccess.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
stateAccess.Lock()
|
||||||
|
dialComplete = true
|
||||||
|
stateAccess.Unlock()
|
||||||
|
stopCancel()
|
||||||
|
cancel(context.Cause(ctx))
|
||||||
|
return zero, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := dial(connPoolDialContext{
|
||||||
|
Context: dialCtx,
|
||||||
|
parent: ctx,
|
||||||
|
})
|
||||||
|
stateAccess.Lock()
|
||||||
|
dialComplete = true
|
||||||
|
stateAccess.Unlock()
|
||||||
|
stopCancel()
|
||||||
|
if err != nil {
|
||||||
|
if cause := context.Cause(dialCtx); cause != nil {
|
||||||
|
return zero, cause
|
||||||
|
}
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
if cause := context.Cause(dialCtx); cause != nil {
|
||||||
|
p.options.Close(conn, cause)
|
||||||
|
return zero, cause
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) closeState(state *connPoolState[T], cause error) {
|
||||||
|
if state == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.cancel(cause)
|
||||||
|
if state.sharedCancel != nil {
|
||||||
|
state.sharedCancel(cause)
|
||||||
|
}
|
||||||
|
for conn := range state.all {
|
||||||
|
p.options.Close(conn, cause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ConnPool[T]) closeCause(state *connPoolState[T]) error {
|
||||||
|
_ = state
|
||||||
|
return net.ErrClosed
|
||||||
|
}
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConnectorCallbacks[T any] struct {
|
|
||||||
IsClosed func(connection T) bool
|
|
||||||
Close func(connection T)
|
|
||||||
Reset func(connection T)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Connector[T any] struct {
|
|
||||||
dial func(ctx context.Context) (T, error)
|
|
||||||
callbacks ConnectorCallbacks[T]
|
|
||||||
|
|
||||||
access sync.Mutex
|
|
||||||
connection T
|
|
||||||
hasConnection bool
|
|
||||||
connectionCancel context.CancelFunc
|
|
||||||
connecting chan struct{}
|
|
||||||
|
|
||||||
closeCtx context.Context
|
|
||||||
closed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConnector[T any](closeCtx context.Context, dial func(context.Context) (T, error), callbacks ConnectorCallbacks[T]) *Connector[T] {
|
|
||||||
return &Connector[T]{
|
|
||||||
dial: dial,
|
|
||||||
callbacks: callbacks,
|
|
||||||
closeCtx: closeCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSingleflightConnector(closeCtx context.Context, dial func(context.Context) (*Connection, error)) *Connector[*Connection] {
|
|
||||||
return NewConnector(closeCtx, dial, ConnectorCallbacks[*Connection]{
|
|
||||||
IsClosed: func(connection *Connection) bool {
|
|
||||||
return connection.IsClosed()
|
|
||||||
},
|
|
||||||
Close: func(connection *Connection) {
|
|
||||||
connection.CloseWithError(ErrTransportClosed)
|
|
||||||
},
|
|
||||||
Reset: func(connection *Connection) {
|
|
||||||
connection.CloseWithError(ErrConnectionReset)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type contextKeyConnecting struct{}
|
|
||||||
|
|
||||||
var errRecursiveConnectorDial = E.New("recursive connector dial")
|
|
||||||
|
|
||||||
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
|
|
||||||
var zero T
|
|
||||||
for {
|
|
||||||
c.access.Lock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
c.access.Unlock()
|
|
||||||
return zero, ErrTransportClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.hasConnection && !c.callbacks.IsClosed(c.connection) {
|
|
||||||
connection := c.connection
|
|
||||||
c.access.Unlock()
|
|
||||||
return connection, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.hasConnection = false
|
|
||||||
if c.connectionCancel != nil {
|
|
||||||
c.connectionCancel()
|
|
||||||
c.connectionCancel = nil
|
|
||||||
}
|
|
||||||
if isRecursiveConnectorDial(ctx, c) {
|
|
||||||
c.access.Unlock()
|
|
||||||
return zero, errRecursiveConnectorDial
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.connecting != nil {
|
|
||||||
connecting := c.connecting
|
|
||||||
c.access.Unlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-connecting:
|
|
||||||
continue
|
|
||||||
case <-ctx.Done():
|
|
||||||
return zero, ctx.Err()
|
|
||||||
case <-c.closeCtx.Done():
|
|
||||||
return zero, ErrTransportClosed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
c.access.Unlock()
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.connecting = make(chan struct{})
|
|
||||||
c.access.Unlock()
|
|
||||||
|
|
||||||
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
|
|
||||||
connection, cancel, err := c.dialWithCancellation(dialContext)
|
|
||||||
|
|
||||||
c.access.Lock()
|
|
||||||
close(c.connecting)
|
|
||||||
c.connecting = nil
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c.access.Unlock()
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
cancel()
|
|
||||||
c.callbacks.Close(connection)
|
|
||||||
c.access.Unlock()
|
|
||||||
return zero, ErrTransportClosed
|
|
||||||
}
|
|
||||||
if err = ctx.Err(); err != nil {
|
|
||||||
cancel()
|
|
||||||
c.callbacks.Close(connection)
|
|
||||||
c.access.Unlock()
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.connection = connection
|
|
||||||
c.hasConnection = true
|
|
||||||
c.connectionCancel = cancel
|
|
||||||
result := c.connection
|
|
||||||
c.access.Unlock()
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T]) bool {
|
|
||||||
dialConnector, loaded := ctx.Value(contextKeyConnecting{}).(*Connector[T])
|
|
||||||
return loaded && dialConnector == connector
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) {
|
|
||||||
var zero T
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return zero, nil, err
|
|
||||||
}
|
|
||||||
connCtx, cancel := context.WithCancel(c.closeCtx)
|
|
||||||
|
|
||||||
var (
|
|
||||||
stateAccess sync.Mutex
|
|
||||||
dialComplete bool
|
|
||||||
)
|
|
||||||
stopCancel := context.AfterFunc(ctx, func() {
|
|
||||||
stateAccess.Lock()
|
|
||||||
if !dialComplete {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
stateAccess.Unlock()
|
|
||||||
})
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
stateAccess.Lock()
|
|
||||||
dialComplete = true
|
|
||||||
stateAccess.Unlock()
|
|
||||||
stopCancel()
|
|
||||||
cancel()
|
|
||||||
return zero, nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
connection, err := c.dial(valueContext{connCtx, ctx})
|
|
||||||
stateAccess.Lock()
|
|
||||||
dialComplete = true
|
|
||||||
stateAccess.Unlock()
|
|
||||||
stopCancel()
|
|
||||||
if err != nil {
|
|
||||||
cancel()
|
|
||||||
return zero, nil, err
|
|
||||||
}
|
|
||||||
return connection, cancel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type valueContext struct {
|
|
||||||
context.Context
|
|
||||||
parent context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v valueContext) Value(key any) any {
|
|
||||||
return v.parent.Value(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v valueContext) Deadline() (time.Time, bool) {
|
|
||||||
return v.parent.Deadline()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connector[T]) Close() error {
|
|
||||||
c.access.Lock()
|
|
||||||
defer c.access.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
c.closed = true
|
|
||||||
|
|
||||||
if c.connectionCancel != nil {
|
|
||||||
c.connectionCancel()
|
|
||||||
c.connectionCancel = nil
|
|
||||||
}
|
|
||||||
if c.hasConnection {
|
|
||||||
c.callbacks.Close(c.connection)
|
|
||||||
c.hasConnection = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connector[T]) Reset() {
|
|
||||||
c.access.Lock()
|
|
||||||
defer c.access.Unlock()
|
|
||||||
|
|
||||||
if c.connectionCancel != nil {
|
|
||||||
c.connectionCancel()
|
|
||||||
c.connectionCancel = nil
|
|
||||||
}
|
|
||||||
if c.hasConnection {
|
|
||||||
c.callbacks.Reset(c.connection)
|
|
||||||
c.hasConnection = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Connection struct {
|
|
||||||
net.Conn
|
|
||||||
|
|
||||||
closeOnce sync.Once
|
|
||||||
done chan struct{}
|
|
||||||
closeError error
|
|
||||||
}
|
|
||||||
|
|
||||||
func WrapConnection(conn net.Conn) *Connection {
|
|
||||||
return &Connection{
|
|
||||||
Conn: conn,
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connection) Done() <-chan struct{} {
|
|
||||||
return c.done
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connection) IsClosed() bool {
|
|
||||||
select {
|
|
||||||
case <-c.done:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connection) CloseError() error {
|
|
||||||
select {
|
|
||||||
case <-c.done:
|
|
||||||
if c.closeError != nil {
|
|
||||||
return c.closeError
|
|
||||||
}
|
|
||||||
return ErrTransportClosed
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connection) Close() error {
|
|
||||||
return c.CloseWithError(ErrTransportClosed)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Connection) CloseWithError(err error) error {
|
|
||||||
var returnError error
|
|
||||||
c.closeOnce.Do(func() {
|
|
||||||
c.closeError = err
|
|
||||||
returnError = c.Conn.Close()
|
|
||||||
close(c.done)
|
|
||||||
})
|
|
||||||
return returnError
|
|
||||||
}
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testConnectorConnection struct{}
|
|
||||||
|
|
||||||
func TestConnectorRecursiveGetFailsFast(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dialCount atomic.Int32
|
|
||||||
closeCount atomic.Int32
|
|
||||||
connector *Connector[*testConnectorConnection]
|
|
||||||
)
|
|
||||||
|
|
||||||
dial := func(ctx context.Context) (*testConnectorConnection, error) {
|
|
||||||
dialCount.Add(1)
|
|
||||||
_, err := connector.Get(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &testConnectorConnection{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
connector = NewConnector(context.Background(), dial, ConnectorCallbacks[*testConnectorConnection]{
|
|
||||||
IsClosed: func(connection *testConnectorConnection) bool {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
Close: func(connection *testConnectorConnection) {
|
|
||||||
closeCount.Add(1)
|
|
||||||
},
|
|
||||||
Reset: func(connection *testConnectorConnection) {
|
|
||||||
closeCount.Add(1)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
_, err := connector.Get(context.Background())
|
|
||||||
require.ErrorIs(t, err, errRecursiveConnectorDial)
|
|
||||||
require.EqualValues(t, 1, dialCount.Load())
|
|
||||||
require.EqualValues(t, 0, closeCount.Load())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectorRecursiveGetAcrossConnectorsAllowed(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
outerDialCount atomic.Int32
|
|
||||||
innerDialCount atomic.Int32
|
|
||||||
outerConnector *Connector[*testConnectorConnection]
|
|
||||||
innerConnector *Connector[*testConnectorConnection]
|
|
||||||
)
|
|
||||||
|
|
||||||
innerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
|
||||||
innerDialCount.Add(1)
|
|
||||||
return &testConnectorConnection{}, nil
|
|
||||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
|
||||||
IsClosed: func(connection *testConnectorConnection) bool {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
Close: func(connection *testConnectorConnection) {},
|
|
||||||
Reset: func(connection *testConnectorConnection) {},
|
|
||||||
})
|
|
||||||
|
|
||||||
outerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
|
||||||
outerDialCount.Add(1)
|
|
||||||
_, err := innerConnector.Get(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &testConnectorConnection{}, nil
|
|
||||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
|
||||||
IsClosed: func(connection *testConnectorConnection) bool {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
Close: func(connection *testConnectorConnection) {},
|
|
||||||
Reset: func(connection *testConnectorConnection) {},
|
|
||||||
})
|
|
||||||
|
|
||||||
_, err := outerConnector.Get(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.EqualValues(t, 1, outerDialCount.Load())
|
|
||||||
require.EqualValues(t, 1, innerDialCount.Load())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectorDialContextPreservesValueAndDeadline(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
type contextKey struct{}
|
|
||||||
|
|
||||||
var (
|
|
||||||
dialValue any
|
|
||||||
dialDeadline time.Time
|
|
||||||
dialHasDeadline bool
|
|
||||||
)
|
|
||||||
|
|
||||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
|
||||||
dialValue = ctx.Value(contextKey{})
|
|
||||||
dialDeadline, dialHasDeadline = ctx.Deadline()
|
|
||||||
return &testConnectorConnection{}, nil
|
|
||||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
|
||||||
IsClosed: func(connection *testConnectorConnection) bool {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
Close: func(connection *testConnectorConnection) {},
|
|
||||||
Reset: func(connection *testConnectorConnection) {},
|
|
||||||
})
|
|
||||||
|
|
||||||
deadline := time.Now().Add(time.Minute)
|
|
||||||
requestContext, cancel := context.WithDeadline(context.WithValue(context.Background(), contextKey{}, "test-value"), deadline)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, err := connector.Get(requestContext)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "test-value", dialValue)
|
|
||||||
require.True(t, dialHasDeadline)
|
|
||||||
require.WithinDuration(t, deadline, dialDeadline, time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectorDialSkipsCanceledRequest(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var dialCount atomic.Int32
|
|
||||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
|
||||||
dialCount.Add(1)
|
|
||||||
return &testConnectorConnection{}, nil
|
|
||||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
|
||||||
IsClosed: func(connection *testConnectorConnection) bool {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
Close: func(connection *testConnectorConnection) {},
|
|
||||||
Reset: func(connection *testConnectorConnection) {},
|
|
||||||
})
|
|
||||||
|
|
||||||
requestContext, cancel := context.WithCancel(context.Background())
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
_, err := connector.Get(requestContext)
|
|
||||||
require.ErrorIs(t, err, context.Canceled)
|
|
||||||
require.EqualValues(t, 0, dialCount.Load())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
dialCount atomic.Int32
|
|
||||||
closeCount atomic.Int32
|
|
||||||
)
|
|
||||||
dialStarted := make(chan struct{}, 1)
|
|
||||||
releaseDial := make(chan struct{})
|
|
||||||
|
|
||||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
|
||||||
dialCount.Add(1)
|
|
||||||
select {
|
|
||||||
case dialStarted <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
<-releaseDial
|
|
||||||
return &testConnectorConnection{}, nil
|
|
||||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
|
||||||
IsClosed: func(connection *testConnectorConnection) bool {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
Close: func(connection *testConnectorConnection) {
|
|
||||||
closeCount.Add(1)
|
|
||||||
},
|
|
||||||
Reset: func(connection *testConnectorConnection) {},
|
|
||||||
})
|
|
||||||
|
|
||||||
requestContext, cancel := context.WithCancel(context.Background())
|
|
||||||
result := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
_, err := connector.Get(requestContext)
|
|
||||||
result <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-dialStarted
|
|
||||||
cancel()
|
|
||||||
close(releaseDial)
|
|
||||||
|
|
||||||
err := <-result
|
|
||||||
require.ErrorIs(t, err, context.Canceled)
|
|
||||||
require.EqualValues(t, 1, dialCount.Load())
|
|
||||||
require.EqualValues(t, 1, closeCount.Load())
|
|
||||||
|
|
||||||
_, err = connector.Get(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.EqualValues(t, 2, dialCount.Load())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var dialContext context.Context
|
|
||||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
|
||||||
dialContext = ctx
|
|
||||||
return &testConnectorConnection{}, nil
|
|
||||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
|
||||||
IsClosed: func(connection *testConnectorConnection) bool {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
Close: func(connection *testConnectorConnection) {},
|
|
||||||
Reset: func(connection *testConnectorConnection) {},
|
|
||||||
})
|
|
||||||
|
|
||||||
requestContext, cancel := context.WithCancel(context.Background())
|
|
||||||
_, err := connector.Get(requestContext)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, dialContext)
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-dialContext.Done():
|
|
||||||
t.Fatal("dial context canceled by request context after successful dial")
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
}
|
|
||||||
|
|
||||||
err = connector.Close()
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectorDialContextCanceledOnClose(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var dialContext context.Context
|
|
||||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
|
||||||
dialContext = ctx
|
|
||||||
return &testConnectorConnection{}, nil
|
|
||||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
|
||||||
IsClosed: func(connection *testConnectorConnection) bool {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
Close: func(connection *testConnectorConnection) {},
|
|
||||||
Reset: func(connection *testConnectorConnection) {},
|
|
||||||
})
|
|
||||||
|
|
||||||
_, err := connector.Get(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, dialContext)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-dialContext.Done():
|
|
||||||
t.Fatal("dial context canceled before connector close")
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
err = connector.Close()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-dialContext.Done():
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatal("dial context not canceled after connector close")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/dns/transport"
|
"github.com/sagernet/sing-box/dns/transport"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -40,13 +39,6 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr,
|
|||||||
results := make(chan queryResult)
|
results := make(chan queryResult)
|
||||||
startRacer := func(ctx context.Context, fqdn string) {
|
startRacer := func(ctx context.Context, fqdn string) {
|
||||||
response, err := t.tryOneName(ctx, servers, fqdn, message)
|
response, err := t.tryOneName(ctx, servers, fqdn, message)
|
||||||
if err == nil {
|
|
||||||
if response.Rcode != mDNS.RcodeSuccess {
|
|
||||||
err = dns.RcodeError(response.Rcode)
|
|
||||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
|
||||||
err = dns.RcodeSuccess
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case results <- queryResult{response, err}:
|
case results <- queryResult{response, err}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
|
|||||||
@@ -23,16 +23,25 @@ var _ adapter.FakeIPTransport = (*Transport)(nil)
|
|||||||
|
|
||||||
type Transport struct {
|
type Transport struct {
|
||||||
dns.TransportAdapter
|
dns.TransportAdapter
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
store adapter.FakeIPStore
|
store adapter.FakeIPStore
|
||||||
|
inet4Enabled bool
|
||||||
|
inet6Enabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) {
|
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
store := NewStore(ctx, logger, options.Inet4Range.Build(netip.Prefix{}), options.Inet6Range.Build(netip.Prefix{}))
|
inet4Range := options.Inet4Range.Build(netip.Prefix{})
|
||||||
|
inet6Range := options.Inet6Range.Build(netip.Prefix{})
|
||||||
|
if !inet4Range.IsValid() && !inet6Range.IsValid() {
|
||||||
|
return nil, E.New("at least one of inet4_range or inet6_range must be set")
|
||||||
|
}
|
||||||
|
store := NewStore(ctx, logger, inet4Range, inet6Range)
|
||||||
return &Transport{
|
return &Transport{
|
||||||
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil),
|
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
store: store,
|
store: store,
|
||||||
|
inet4Enabled: inet4Range.IsValid(),
|
||||||
|
inet6Enabled: inet6Range.IsValid(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +64,9 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
|
|||||||
if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA {
|
if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA {
|
||||||
return nil, E.New("only IP queries are supported by fakeip")
|
return nil, E.New("only IP queries are supported by fakeip")
|
||||||
}
|
}
|
||||||
|
if question.Qtype == mDNS.TypeA && !t.inet4Enabled || question.Qtype == mDNS.TypeAAAA && !t.inet6Enabled {
|
||||||
|
return dns.FixedResponseStatus(message, mDNS.RcodeSuccess), nil
|
||||||
|
}
|
||||||
address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA)
|
address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -81,10 +81,7 @@ func (t *Transport) Reset() {
|
|||||||
|
|
||||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
if t.resolved != nil {
|
if t.resolved != nil {
|
||||||
resolverObject := t.resolved.Object()
|
return t.resolved.Exchange(ctx, message)
|
||||||
if resolverObject != nil {
|
|
||||||
return t.resolved.Exchange(resolverObject, ctx, message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
question := message.Question[0]
|
question := message.Question[0]
|
||||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||||
|
|||||||
@@ -9,6 +9,5 @@ import (
|
|||||||
type ResolvedResolver interface {
|
type ResolvedResolver interface {
|
||||||
Start() error
|
Start() error
|
||||||
Close() error
|
Close() error
|
||||||
Object() any
|
Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
|
||||||
Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,26 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
dnsTransport "github.com/sagernet/sing-box/dns/transport"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/service/resolved"
|
"github.com/sagernet/sing-box/service/resolved"
|
||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-tun"
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/control"
|
"github.com/sagernet/sing/common/control"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
@@ -49,13 +56,23 @@ type DBusResolvedResolver struct {
|
|||||||
interfaceMonitor tun.DefaultInterfaceMonitor
|
interfaceMonitor tun.DefaultInterfaceMonitor
|
||||||
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
|
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
|
||||||
systemBus *dbus.Conn
|
systemBus *dbus.Conn
|
||||||
resoledObject atomic.Pointer[ResolvedObject]
|
savedServerSet atomic.Pointer[resolvedServerSet]
|
||||||
closeOnce sync.Once
|
closeOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResolvedObject struct {
|
type resolvedServerSet struct {
|
||||||
dbus.BusObject
|
servers []resolvedServer
|
||||||
InterfaceIndex int32
|
}
|
||||||
|
|
||||||
|
type resolvedServer struct {
|
||||||
|
primaryTransport adapter.DNSTransport
|
||||||
|
fallbackTransport adapter.DNSTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
type resolvedServerSpecification struct {
|
||||||
|
address netip.Addr
|
||||||
|
port uint16
|
||||||
|
serverName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
|
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
|
||||||
@@ -82,17 +99,31 @@ func (t *DBusResolvedResolver) Start() error {
|
|||||||
"org.freedesktop.DBus",
|
"org.freedesktop.DBus",
|
||||||
"NameOwnerChanged",
|
"NameOwnerChanged",
|
||||||
dbus.WithMatchSender("org.freedesktop.DBus"),
|
dbus.WithMatchSender("org.freedesktop.DBus"),
|
||||||
dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"),
|
dbus.WithMatchArg(0, "org.freedesktop.resolve1"),
|
||||||
).Err
|
).Err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "configure resolved restart listener")
|
return E.Cause(err, "configure resolved restart listener")
|
||||||
}
|
}
|
||||||
|
err = t.systemBus.BusObject().AddMatchSignal(
|
||||||
|
"org.freedesktop.DBus.Properties",
|
||||||
|
"PropertiesChanged",
|
||||||
|
dbus.WithMatchSender("org.freedesktop.resolve1"),
|
||||||
|
dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"),
|
||||||
|
).Err
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "configure resolved properties listener")
|
||||||
|
}
|
||||||
go t.loopUpdateStatus()
|
go t.loopUpdateStatus()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *DBusResolvedResolver) Close() error {
|
func (t *DBusResolvedResolver) Close() error {
|
||||||
|
var closeErr error
|
||||||
t.closeOnce.Do(func() {
|
t.closeOnce.Do(func() {
|
||||||
|
serverSet := t.savedServerSet.Swap(nil)
|
||||||
|
if serverSet != nil {
|
||||||
|
closeErr = serverSet.Close()
|
||||||
|
}
|
||||||
if t.interfaceCallback != nil {
|
if t.interfaceCallback != nil {
|
||||||
t.interfaceMonitor.UnregisterCallback(t.interfaceCallback)
|
t.interfaceMonitor.UnregisterCallback(t.interfaceCallback)
|
||||||
}
|
}
|
||||||
@@ -100,99 +131,97 @@ func (t *DBusResolvedResolver) Close() error {
|
|||||||
_ = t.systemBus.Close()
|
_ = t.systemBus.Close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return nil
|
return closeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *DBusResolvedResolver) Object() any {
|
func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
return common.PtrOrNil(t.resoledObject.Load())
|
serverSet := t.savedServerSet.Load()
|
||||||
}
|
if serverSet == nil {
|
||||||
|
var err error
|
||||||
func (t *DBusResolvedResolver) Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
serverSet, err = t.checkResolved(context.Background())
|
||||||
question := message.Question[0]
|
if err != nil {
|
||||||
resolvedObject := object.(*ResolvedObject)
|
return nil, err
|
||||||
call := resolvedObject.CallWithContext(
|
}
|
||||||
ctx,
|
previousServerSet := t.savedServerSet.Swap(serverSet)
|
||||||
"org.freedesktop.resolve1.Manager.ResolveRecord",
|
if previousServerSet != nil {
|
||||||
0,
|
_ = previousServerSet.Close()
|
||||||
resolvedObject.InterfaceIndex,
|
|
||||||
question.Name,
|
|
||||||
question.Qclass,
|
|
||||||
question.Qtype,
|
|
||||||
uint64(0),
|
|
||||||
)
|
|
||||||
if call.Err != nil {
|
|
||||||
var dbusError dbus.Error
|
|
||||||
if errors.As(call.Err, &dbusError) && dbusError.Name == "org.freedesktop.resolve1.NoNameServers" {
|
|
||||||
t.updateStatus()
|
|
||||||
}
|
}
|
||||||
return nil, E.Cause(call.Err, " resolve record via resolved")
|
|
||||||
}
|
}
|
||||||
var (
|
response, err := t.exchangeServerSet(ctx, message, serverSet)
|
||||||
records []resolved.ResourceRecord
|
if err == nil {
|
||||||
outflags uint64
|
return response, nil
|
||||||
)
|
}
|
||||||
err := call.Store(&records, &outflags)
|
t.updateStatus()
|
||||||
if err != nil {
|
refreshedServerSet := t.savedServerSet.Load()
|
||||||
|
if refreshedServerSet == nil || refreshedServerSet == serverSet {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
response := &mDNS.Msg{
|
return t.exchangeServerSet(ctx, message, refreshedServerSet)
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Response: true,
|
|
||||||
Authoritative: true,
|
|
||||||
RecursionDesired: true,
|
|
||||||
RecursionAvailable: true,
|
|
||||||
Rcode: mDNS.RcodeSuccess,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{question},
|
|
||||||
}
|
|
||||||
for _, record := range records {
|
|
||||||
var rr mDNS.RR
|
|
||||||
rr, _, err = mDNS.UnpackRR(record.Data, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "unpack resource record")
|
|
||||||
}
|
|
||||||
response.Answer = append(response.Answer, rr)
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *DBusResolvedResolver) loopUpdateStatus() {
|
func (t *DBusResolvedResolver) loopUpdateStatus() {
|
||||||
signalChan := make(chan *dbus.Signal, 1)
|
signalChan := make(chan *dbus.Signal, 1)
|
||||||
t.systemBus.Signal(signalChan)
|
t.systemBus.Signal(signalChan)
|
||||||
for signal := range signalChan {
|
for signal := range signalChan {
|
||||||
var restarted bool
|
switch signal.Name {
|
||||||
if signal.Name == "org.freedesktop.DBus.NameOwnerChanged" {
|
case "org.freedesktop.DBus.NameOwnerChanged":
|
||||||
if len(signal.Body) != 3 || signal.Body[2].(string) == "" {
|
if len(signal.Body) != 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newOwner, loaded := signal.Body[2].(string)
|
||||||
|
if !loaded || newOwner == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.updateStatus()
|
||||||
|
case "org.freedesktop.DBus.Properties.PropertiesChanged":
|
||||||
|
if !shouldUpdateResolvedServerSet(signal) {
|
||||||
continue
|
continue
|
||||||
} else {
|
|
||||||
restarted = true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if restarted {
|
|
||||||
t.updateStatus()
|
t.updateStatus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *DBusResolvedResolver) updateStatus() {
|
func (t *DBusResolvedResolver) updateStatus() {
|
||||||
dbusObject, err := t.checkResolved(context.Background())
|
serverSet, err := t.checkResolved(context.Background())
|
||||||
oldValue := t.resoledObject.Swap(dbusObject)
|
oldServerSet := t.savedServerSet.Swap(serverSet)
|
||||||
|
if oldServerSet != nil {
|
||||||
|
_ = oldServerSet.Close()
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var dbusErr dbus.Error
|
var dbusErr dbus.Error
|
||||||
if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwnerCould" {
|
if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" {
|
||||||
t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable"))
|
t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable"))
|
||||||
}
|
}
|
||||||
if oldValue != nil {
|
if oldServerSet != nil {
|
||||||
t.logger.Debug("systemd-resolved service is gone")
|
t.logger.Debug("systemd-resolved service is gone")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
} else if oldValue == nil {
|
} else if oldServerSet == nil {
|
||||||
t.logger.Debug("using systemd-resolved service as resolver")
|
t.logger.Debug("using systemd-resolved service as resolver")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObject, error) {
|
func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet) (*mDNS.Msg, error) {
|
||||||
|
if serverSet == nil || len(serverSet.servers) == 0 {
|
||||||
|
return nil, E.New("link has no DNS servers configured")
|
||||||
|
}
|
||||||
|
var lastError error
|
||||||
|
for _, server := range serverSet.servers {
|
||||||
|
response, err := server.primaryTransport.Exchange(ctx, message)
|
||||||
|
if err != nil && server.fallbackTransport != nil {
|
||||||
|
response, err = server.fallbackTransport.Exchange(ctx, message)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
lastError = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
return nil, lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) {
|
||||||
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
|
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
|
||||||
err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err
|
err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -220,16 +249,19 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
|
|||||||
if linkObject == nil {
|
if linkObject == nil {
|
||||||
return nil, E.New("missing link object for default interface")
|
return nil, E.New("missing link object for default interface")
|
||||||
}
|
}
|
||||||
dnsProp, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
|
dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var linkDNS []resolved.LinkDNS
|
linkDNSEx, err := loadResolvedLinkDNSEx(linkObject)
|
||||||
err = dnsProp.Store(&linkDNS)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(linkDNS) == 0 {
|
linkDNS, err := loadResolvedLinkDNS(linkObject)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(linkDNSEx) == 0 && len(linkDNS) == 0 {
|
||||||
for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() {
|
for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() {
|
||||||
if inbound.Type() == C.TypeTun {
|
if inbound.Type() == C.TypeTun {
|
||||||
return nil, E.New("No appropriate name servers or networks for name found")
|
return nil, E.New("No appropriate name servers or networks for name found")
|
||||||
@@ -237,12 +269,233 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje
|
|||||||
}
|
}
|
||||||
return nil, E.New("link has no DNS servers configured")
|
return nil, E.New("link has no DNS servers configured")
|
||||||
}
|
}
|
||||||
return &ResolvedObject{
|
serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{
|
||||||
BusObject: dbusObject,
|
BindInterface: defaultInterface.Name,
|
||||||
InterfaceIndex: int32(defaultInterface.Index),
|
UDPFragmentDefault: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var serverSpecifications []resolvedServerSpecification
|
||||||
|
if len(linkDNSEx) > 0 {
|
||||||
|
for _, entry := range linkDNSEx {
|
||||||
|
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, entry.Port, entry.Name)
|
||||||
|
if !loaded {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
serverSpecifications = append(serverSpecifications, serverSpecification)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, entry := range linkDNS {
|
||||||
|
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, 0, "")
|
||||||
|
if !loaded {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
serverSpecifications = append(serverSpecifications, serverSpecification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(serverSpecifications) == 0 {
|
||||||
|
return nil, E.New("no valid DNS servers on link")
|
||||||
|
}
|
||||||
|
serverSet := &resolvedServerSet{
|
||||||
|
servers: make([]resolvedServer, 0, len(serverSpecifications)),
|
||||||
|
}
|
||||||
|
for _, serverSpecification := range serverSpecifications {
|
||||||
|
server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification)
|
||||||
|
if createErr != nil {
|
||||||
|
_ = serverSet.Close()
|
||||||
|
return nil, createErr
|
||||||
|
}
|
||||||
|
serverSet.servers = append(serverSet.servers, server)
|
||||||
|
}
|
||||||
|
return serverSet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) {
|
||||||
|
if dnsOverTLSMode == "yes" {
|
||||||
|
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
|
||||||
|
if err != nil {
|
||||||
|
return resolvedServer{}, err
|
||||||
|
}
|
||||||
|
return resolvedServer{
|
||||||
|
primaryTransport: primaryTransport,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if dnsOverTLSMode == "opportunistic" {
|
||||||
|
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
|
||||||
|
if err != nil {
|
||||||
|
return resolvedServer{}, err
|
||||||
|
}
|
||||||
|
fallbackTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
|
||||||
|
if err != nil {
|
||||||
|
_ = primaryTransport.Close()
|
||||||
|
return resolvedServer{}, err
|
||||||
|
}
|
||||||
|
return resolvedServer{
|
||||||
|
primaryTransport: primaryTransport,
|
||||||
|
fallbackTransport: fallbackTransport,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
|
||||||
|
if err != nil {
|
||||||
|
return resolvedServer{}, err
|
||||||
|
}
|
||||||
|
return resolvedServer{
|
||||||
|
primaryTransport: primaryTransport,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) {
|
||||||
|
serverAddress := M.SocksaddrFrom(serverSpecification.address, resolvedServerPort(serverSpecification.port, useTLS))
|
||||||
|
if useTLS {
|
||||||
|
tlsAddress := serverSpecification.address
|
||||||
|
if tlsAddress.Zone() != "" {
|
||||||
|
tlsAddress = tlsAddress.WithZone("")
|
||||||
|
}
|
||||||
|
serverName := serverSpecification.serverName
|
||||||
|
if serverName == "" {
|
||||||
|
serverName = tlsAddress.String()
|
||||||
|
}
|
||||||
|
tlsConfig, err := tls.NewClient(t.ctx, t.logger, tlsAddress.String(), option.OutboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: serverName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
serverTransport := dnsTransport.NewTLSRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeTLS, "", nil), serverDialer, serverAddress, tlsConfig)
|
||||||
|
err = serverTransport.Start(adapter.StartStateStart)
|
||||||
|
if err != nil {
|
||||||
|
_ = serverTransport.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return serverTransport, nil
|
||||||
|
}
|
||||||
|
serverTransport := dnsTransport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), serverDialer, serverAddress)
|
||||||
|
err := serverTransport.Start(adapter.StartStateStart)
|
||||||
|
if err != nil {
|
||||||
|
_ = serverTransport.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return serverTransport, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *resolvedServerSet) Close() error {
|
||||||
|
var errors []error
|
||||||
|
for _, server := range s.servers {
|
||||||
|
errors = append(errors, server.primaryTransport.Close())
|
||||||
|
if server.fallbackTransport != nil {
|
||||||
|
errors = append(errors, server.fallbackTransport.Close())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return E.Errors(errors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, port uint16, serverName string) (resolvedServerSpecification, bool) {
|
||||||
|
address, loaded := netip.AddrFromSlice(rawAddress)
|
||||||
|
if !loaded {
|
||||||
|
return resolvedServerSpecification{}, false
|
||||||
|
}
|
||||||
|
if address.Is6() && address.IsLinkLocalUnicast() && address.Zone() == "" {
|
||||||
|
address = address.WithZone(interfaceName)
|
||||||
|
}
|
||||||
|
return resolvedServerSpecification{
|
||||||
|
address: address,
|
||||||
|
port: port,
|
||||||
|
serverName: serverName,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvedServerPort(port uint16, useTLS bool) uint16 {
|
||||||
|
if port > 0 {
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
if useTLS {
|
||||||
|
return 853
|
||||||
|
}
|
||||||
|
return 53
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) {
|
||||||
|
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
|
||||||
|
if err != nil {
|
||||||
|
if isResolvedUnknownPropertyError(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var linkDNS []resolved.LinkDNS
|
||||||
|
err = dnsProperty.Store(&linkDNS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return linkDNS, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadResolvedLinkDNSEx(linkObject dbus.BusObject) ([]resolved.LinkDNSEx, error) {
|
||||||
|
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSEx")
|
||||||
|
if err != nil {
|
||||||
|
if isResolvedUnknownPropertyError(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var linkDNSEx []resolved.LinkDNSEx
|
||||||
|
err = dnsProperty.Store(&linkDNSEx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return linkDNSEx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadResolvedLinkDNSOverTLS(linkObject dbus.BusObject) (string, error) {
|
||||||
|
dnsOverTLSProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSOverTLS")
|
||||||
|
if err != nil {
|
||||||
|
if isResolvedUnknownPropertyError(err) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var dnsOverTLSMode string
|
||||||
|
err = dnsOverTLSProperty.Store(&dnsOverTLSMode)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dnsOverTLSMode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isResolvedUnknownPropertyError(err error) bool {
|
||||||
|
var dbusError dbus.Error
|
||||||
|
return errors.As(err, &dbusError) && dbusError.Name == "org.freedesktop.DBus.Error.UnknownProperty"
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool {
|
||||||
|
if len(signal.Body) != 3 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
changedProperties, loaded := signal.Body[1].(map[string]dbus.Variant)
|
||||||
|
if !loaded {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for propertyName := range changedProperties {
|
||||||
|
switch propertyName {
|
||||||
|
case "DNS", "DNSEx", "DNSOverTLS":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalidatedProperties, loaded := signal.Body[2].([]string)
|
||||||
|
if !loaded {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, propertyName := range invalidatedProperties {
|
||||||
|
switch propertyName {
|
||||||
|
case "DNS", "DNSEx", "DNSOverTLS":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) {
|
func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) {
|
||||||
t.updateStatus()
|
t.updateStatus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/dns/transport"
|
"github.com/sagernet/sing-box/dns/transport"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -49,13 +48,6 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
|
|||||||
results := make(chan queryResult)
|
results := make(chan queryResult)
|
||||||
startRacer := func(ctx context.Context, fqdn string) {
|
startRacer := func(ctx context.Context, fqdn string) {
|
||||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
||||||
if err == nil {
|
|
||||||
if response.Rcode != mDNS.RcodeSuccess {
|
|
||||||
err = dns.RcodeError(response.Rcode)
|
|
||||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
|
||||||
err = E.New(fqdn, ": empty result")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case results <- queryResult{response, err}:
|
case results <- queryResult{response, err}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
|
|||||||
@@ -31,14 +31,13 @@ func RegisterTransport(registry *dns.TransportRegistry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Transport struct {
|
type Transport struct {
|
||||||
*transport.BaseTransport
|
dns.TransportAdapter
|
||||||
|
|
||||||
ctx context.Context
|
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
serverAddr M.Socksaddr
|
serverAddr M.Socksaddr
|
||||||
tlsConfig tls.Config
|
tlsConfig tls.Config
|
||||||
|
|
||||||
connector *transport.Connector[*quic.Conn]
|
connection *transport.ConnPool[*quic.Conn]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
|
func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
@@ -63,93 +62,76 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
|
|||||||
return nil, E.New("invalid server address: ", serverAddr)
|
return nil, E.New("invalid server address: ", serverAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
t := &Transport{
|
return &Transport{
|
||||||
BaseTransport: transport.NewBaseTransport(
|
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
|
||||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
|
dialer: transportDialer,
|
||||||
logger,
|
serverAddr: serverAddr,
|
||||||
),
|
tlsConfig: tlsConfig,
|
||||||
ctx: ctx,
|
connection: transport.NewConnPool(transport.ConnPoolOptions[*quic.Conn]{
|
||||||
dialer: transportDialer,
|
Mode: transport.ConnPoolSingle,
|
||||||
serverAddr: serverAddr,
|
IsAlive: func(conn *quic.Conn) bool {
|
||||||
tlsConfig: tlsConfig,
|
return conn != nil && !common.Done(conn.Context())
|
||||||
}
|
},
|
||||||
|
Close: func(conn *quic.Conn, _ error) {
|
||||||
t.connector = transport.NewConnector(t.CloseContext(), t.dial, transport.ConnectorCallbacks[*quic.Conn]{
|
conn.CloseWithError(0, "")
|
||||||
IsClosed: func(connection *quic.Conn) bool {
|
},
|
||||||
return common.Done(connection.Context())
|
}),
|
||||||
},
|
}, nil
|
||||||
Close: func(connection *quic.Conn) {
|
|
||||||
connection.CloseWithError(0, "")
|
|
||||||
},
|
|
||||||
Reset: func(connection *quic.Conn) {
|
|
||||||
connection.CloseWithError(0, "")
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) dial(ctx context.Context) (*quic.Conn, error) {
|
|
||||||
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "dial UDP connection")
|
|
||||||
}
|
|
||||||
earlyConnection, err := sQUIC.DialEarly(
|
|
||||||
ctx,
|
|
||||||
bufio.NewUnbindPacketConn(conn),
|
|
||||||
t.serverAddr.UDPAddr(),
|
|
||||||
t.tlsConfig,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, E.Cause(err, "establish QUIC connection")
|
|
||||||
}
|
|
||||||
return earlyConnection, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||||
if stage != adapter.StartStateStart {
|
if stage != adapter.StartStateStart {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := t.SetStarted()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return dialer.InitializeDetour(t.dialer)
|
return dialer.InitializeDetour(t.dialer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Close() error {
|
func (t *Transport) Close() error {
|
||||||
return E.Errors(t.BaseTransport.Close(), t.connector.Close())
|
return t.connection.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Reset() {
|
func (t *Transport) Reset() {
|
||||||
t.connector.Reset()
|
t.connection.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
if !t.BeginQuery() {
|
|
||||||
return nil, transport.ErrTransportClosed
|
|
||||||
}
|
|
||||||
defer t.EndQuery()
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
conn *quic.Conn
|
conn *quic.Conn
|
||||||
err error
|
err error
|
||||||
response *mDNS.Msg
|
response *mDNS.Msg
|
||||||
)
|
)
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; i < 2; i++ {
|
||||||
conn, err = t.connector.Get(ctx)
|
conn, _, err = t.connection.Acquire(ctx, func(ctx context.Context) (*quic.Conn, error) {
|
||||||
|
rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "dial UDP connection")
|
||||||
|
}
|
||||||
|
earlyConnection, err := sQUIC.DialEarly(
|
||||||
|
ctx,
|
||||||
|
bufio.NewUnbindPacketConn(rawConn),
|
||||||
|
t.serverAddr.UDPAddr(),
|
||||||
|
t.tlsConfig,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
rawConn.Close()
|
||||||
|
return nil, E.Cause(err, "establish QUIC connection")
|
||||||
|
}
|
||||||
|
return earlyConnection, nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
response, err = t.exchange(ctx, message, conn)
|
response, err = t.exchange(ctx, message, conn)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
t.connection.Release(conn, true)
|
||||||
return response, nil
|
return response, nil
|
||||||
} else if !isQUICRetryError(err) {
|
} else if !isQUICRetryError(err) {
|
||||||
|
t.connection.Release(conn, true)
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
t.connector.Reset()
|
t.connection.Release(conn, true)
|
||||||
|
t.Reset()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package transport
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -17,7 +16,6 @@ import (
|
|||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
@@ -29,13 +27,13 @@ func RegisterTLS(registry *dns.TransportRegistry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TLSTransport struct {
|
type TLSTransport struct {
|
||||||
*BaseTransport
|
dns.TransportAdapter
|
||||||
|
logger logger.ContextLogger
|
||||||
|
|
||||||
dialer tls.Dialer
|
dialer tls.Dialer
|
||||||
serverAddr M.Socksaddr
|
serverAddr M.Socksaddr
|
||||||
tlsConfig tls.Config
|
tlsConfig tls.Config
|
||||||
access sync.Mutex
|
connections *ConnPool[*tlsDNSConn]
|
||||||
connections list.List[*tlsDNSConn]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type tlsDNSConn struct {
|
type tlsDNSConn struct {
|
||||||
@@ -66,10 +64,20 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
|
|||||||
|
|
||||||
func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport {
|
func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport {
|
||||||
return &TLSTransport{
|
return &TLSTransport{
|
||||||
BaseTransport: NewBaseTransport(adapter, logger),
|
TransportAdapter: adapter,
|
||||||
dialer: tls.NewDialer(dialer, tlsConfig),
|
logger: logger,
|
||||||
serverAddr: serverAddr,
|
dialer: tls.NewDialer(dialer, tlsConfig),
|
||||||
tlsConfig: tlsConfig,
|
serverAddr: serverAddr,
|
||||||
|
tlsConfig: tlsConfig,
|
||||||
|
connections: NewConnPool(ConnPoolOptions[*tlsDNSConn]{
|
||||||
|
Mode: ConnPoolOrdered,
|
||||||
|
IsAlive: func(conn *tlsDNSConn) bool {
|
||||||
|
return conn != nil
|
||||||
|
},
|
||||||
|
Close: func(conn *tlsDNSConn, _ error) {
|
||||||
|
conn.Close()
|
||||||
|
},
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,53 +85,43 @@ func (t *TLSTransport) Start(stage adapter.StartStage) error {
|
|||||||
if stage != adapter.StartStateStart {
|
if stage != adapter.StartStateStart {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := t.SetStarted()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return dialer.InitializeDetour(t.dialer)
|
return dialer.InitializeDetour(t.dialer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLSTransport) Close() error {
|
func (t *TLSTransport) Close() error {
|
||||||
t.access.Lock()
|
return t.connections.Close()
|
||||||
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
|
|
||||||
connection.Value.Close()
|
|
||||||
}
|
|
||||||
t.connections.Init()
|
|
||||||
t.access.Unlock()
|
|
||||||
return t.BaseTransport.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLSTransport) Reset() {
|
func (t *TLSTransport) Reset() {
|
||||||
t.access.Lock()
|
t.connections.Reset()
|
||||||
defer t.access.Unlock()
|
|
||||||
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
|
|
||||||
connection.Value.Close()
|
|
||||||
}
|
|
||||||
t.connections.Init()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
if !t.BeginQuery() {
|
var lastErr error
|
||||||
return nil, ErrTransportClosed
|
for attempt := 0; attempt < 2; attempt++ {
|
||||||
}
|
conn, created, err := t.connections.Acquire(ctx, func(ctx context.Context) (*tlsDNSConn, error) {
|
||||||
defer t.EndQuery()
|
tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr)
|
||||||
|
if err != nil {
|
||||||
t.access.Lock()
|
return nil, E.Cause(err, "dial TLS connection")
|
||||||
conn := t.connections.PopFront()
|
}
|
||||||
t.access.Unlock()
|
return &tlsDNSConn{Conn: tlsConn}, nil
|
||||||
if conn != nil {
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
response, err := t.exchange(ctx, message, conn)
|
response, err := t.exchange(ctx, message, conn)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
t.connections.Release(conn, true)
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
t.Logger.DebugContext(ctx, "discarded pooled connection: ", err)
|
lastErr = err
|
||||||
|
t.logger.DebugContext(ctx, "discarded pooled connection: ", err)
|
||||||
|
t.connections.Release(conn, false)
|
||||||
|
if created {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr)
|
return nil, lastErr
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "dial TLS connection")
|
|
||||||
}
|
|
||||||
return t.exchange(ctx, message, &tlsDNSConn{Conn: tlsConn})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) {
|
func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) {
|
||||||
@@ -133,22 +131,12 @@ func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tl
|
|||||||
conn.queryId++
|
conn.queryId++
|
||||||
err := WriteMessage(conn, conn.queryId, message)
|
err := WriteMessage(conn, conn.queryId, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
|
||||||
return nil, E.Cause(err, "write request")
|
return nil, E.Cause(err, "write request")
|
||||||
}
|
}
|
||||||
response, err := ReadMessage(conn)
|
response, err := ReadMessage(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
|
||||||
return nil, E.Cause(err, "read response")
|
return nil, E.Cause(err, "read response")
|
||||||
}
|
}
|
||||||
t.access.Lock()
|
|
||||||
if t.State() >= StateClosing {
|
|
||||||
t.access.Unlock()
|
|
||||||
conn.Close()
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
conn.SetDeadline(time.Time{})
|
conn.SetDeadline(time.Time{})
|
||||||
t.connections.PushBack(conn)
|
|
||||||
t.access.Unlock()
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package transport
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
@@ -27,13 +28,14 @@ func RegisterUDP(registry *dns.TransportRegistry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UDPTransport struct {
|
type UDPTransport struct {
|
||||||
*BaseTransport
|
dns.TransportAdapter
|
||||||
|
logger logger.ContextLogger
|
||||||
|
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
serverAddr M.Socksaddr
|
serverAddr M.Socksaddr
|
||||||
udpSize atomic.Int32
|
udpSize atomic.Int32
|
||||||
|
|
||||||
connector *Connector[*Connection]
|
connection *ConnPool[net.Conn]
|
||||||
|
|
||||||
callbackAccess sync.RWMutex
|
callbackAccess sync.RWMutex
|
||||||
queryId uint16
|
queryId uint16
|
||||||
@@ -63,43 +65,38 @@ func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options o
|
|||||||
|
|
||||||
func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialerInstance N.Dialer, serverAddr M.Socksaddr) *UDPTransport {
|
func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialerInstance N.Dialer, serverAddr M.Socksaddr) *UDPTransport {
|
||||||
t := &UDPTransport{
|
t := &UDPTransport{
|
||||||
BaseTransport: NewBaseTransport(adapter, logger),
|
TransportAdapter: adapter,
|
||||||
dialer: dialerInstance,
|
logger: logger,
|
||||||
serverAddr: serverAddr,
|
dialer: dialerInstance,
|
||||||
callbacks: make(map[uint16]*udpCallback),
|
serverAddr: serverAddr,
|
||||||
|
callbacks: make(map[uint16]*udpCallback),
|
||||||
|
connection: NewConnPool(ConnPoolOptions[net.Conn]{
|
||||||
|
Mode: ConnPoolSingle,
|
||||||
|
IsAlive: func(conn net.Conn) bool {
|
||||||
|
return conn != nil
|
||||||
|
},
|
||||||
|
Close: func(conn net.Conn, cause error) {
|
||||||
|
conn.Close()
|
||||||
|
},
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
t.udpSize.Store(2048)
|
t.udpSize.Store(2048)
|
||||||
t.connector = NewSingleflightConnector(t.CloseContext(), t.dial)
|
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *UDPTransport) dial(ctx context.Context) (*Connection, error) {
|
|
||||||
rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "dial UDP connection")
|
|
||||||
}
|
|
||||||
conn := WrapConnection(rawConn)
|
|
||||||
go t.recvLoop(conn)
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPTransport) Start(stage adapter.StartStage) error {
|
func (t *UDPTransport) Start(stage adapter.StartStage) error {
|
||||||
if stage != adapter.StartStateStart {
|
if stage != adapter.StartStateStart {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := t.SetStarted()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return dialer.InitializeDetour(t.dialer)
|
return dialer.InitializeDetour(t.dialer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *UDPTransport) Close() error {
|
func (t *UDPTransport) Close() error {
|
||||||
return E.Errors(t.BaseTransport.Close(), t.connector.Close())
|
return t.connection.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *UDPTransport) Reset() {
|
func (t *UDPTransport) Reset() {
|
||||||
t.connector.Reset()
|
t.connection.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *UDPTransport) nextAvailableQueryId() (uint16, error) {
|
func (t *UDPTransport) nextAvailableQueryId() (uint16, error) {
|
||||||
@@ -116,17 +113,12 @@ func (t *UDPTransport) nextAvailableQueryId() (uint16, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
if !t.BeginQuery() {
|
|
||||||
return nil, ErrTransportClosed
|
|
||||||
}
|
|
||||||
defer t.EndQuery()
|
|
||||||
|
|
||||||
response, err := t.exchange(ctx, message)
|
response, err := t.exchange(ctx, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if response.Truncated {
|
if response.Truncated {
|
||||||
t.Logger.InfoContext(ctx, "response truncated, retrying with TCP")
|
t.logger.InfoContext(ctx, "response truncated, retrying with TCP")
|
||||||
return t.exchangeTCP(ctx, message)
|
return t.exchangeTCP(ctx, message)
|
||||||
}
|
}
|
||||||
return response, nil
|
return response, nil
|
||||||
@@ -158,16 +150,25 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if t.udpSize.CompareAndSwap(current, udpSize) {
|
if t.udpSize.CompareAndSwap(current, udpSize) {
|
||||||
t.connector.Reset()
|
t.Reset()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := t.connector.Get(ctx)
|
conn, connCtx, created, err := t.connection.AcquireShared(ctx, func(ctx context.Context) (net.Conn, error) {
|
||||||
|
rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "dial UDP connection")
|
||||||
|
}
|
||||||
|
return rawConn, nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if created {
|
||||||
|
go t.recvLoop(conn)
|
||||||
|
}
|
||||||
|
|
||||||
callback := &udpCallback{
|
callback := &udpCallback{
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
@@ -177,6 +178,7 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
|
|||||||
queryId, err := t.nextAvailableQueryId()
|
queryId, err := t.nextAvailableQueryId()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.callbackAccess.Unlock()
|
t.callbackAccess.Unlock()
|
||||||
|
t.connection.Release(conn, true)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t.callbacks[queryId] = callback
|
t.callbacks[queryId] = callback
|
||||||
@@ -203,30 +205,30 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
|
|||||||
|
|
||||||
_, err = conn.Write(rawMessage)
|
_, err = conn.Write(rawMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.CloseWithError(err)
|
t.connection.Invalidate(conn, err)
|
||||||
return nil, E.Cause(err, "write request")
|
return nil, E.Cause(err, "write request")
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-callback.done:
|
case <-callback.done:
|
||||||
|
t.connection.Release(conn, true)
|
||||||
callback.response.Id = originalId
|
callback.response.Id = originalId
|
||||||
return callback.response, nil
|
return callback.response, nil
|
||||||
case <-conn.Done():
|
case <-connCtx.Done():
|
||||||
return nil, conn.CloseError()
|
return nil, context.Cause(connCtx)
|
||||||
case <-t.CloseContext().Done():
|
|
||||||
return nil, ErrTransportClosed
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
t.connection.Release(conn, true)
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *UDPTransport) recvLoop(conn *Connection) {
|
func (t *UDPTransport) recvLoop(conn net.Conn) {
|
||||||
for {
|
for {
|
||||||
buffer := buf.NewSize(int(t.udpSize.Load()))
|
buffer := buf.NewSize(int(t.udpSize.Load()))
|
||||||
_, err := buffer.ReadOnceFrom(conn)
|
_, err := buffer.ReadOnceFrom(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
buffer.Release()
|
buffer.Release()
|
||||||
conn.CloseWithError(err)
|
t.connection.Invalidate(conn, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +236,7 @@ func (t *UDPTransport) recvLoop(conn *Connection) {
|
|||||||
err = message.Unpack(buffer.Bytes())
|
err = message.Unpack(buffer.Bytes())
|
||||||
buffer.Release()
|
buffer.Release()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logger.Debug("discarded malformed UDP response: ", err)
|
t.logger.Debug("discarded malformed UDP response: ", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,65 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### 1.13.11
|
||||||
|
|
||||||
|
* Fix process searcher failure introduced in 1.13.9
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.10
|
||||||
|
|
||||||
|
* Fix process searcher failure introduced in 1.13.9
|
||||||
|
|
||||||
|
#### 1.13.9
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.8
|
||||||
|
|
||||||
|
* Update naiveproxy to v147.0.7727.49-1
|
||||||
|
* Fix fake-ip DNS server should return SUCCESS when address type is not configured
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.7
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.6
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.5
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.4
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.3
|
||||||
|
|
||||||
|
* Add OpenWrt and Alpine APK packages to release **1**
|
||||||
|
* Backport to macOS 10.13 High Sierra **2**
|
||||||
|
* OCM service: Add WebSocket support for Responses API **3**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
|
||||||
|
|
||||||
|
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
|
||||||
|
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
|
||||||
|
macOS 10.13 High Sierra, built using Go 1.25 with patches
|
||||||
|
from [SagerNet/go](https://github.com/SagerNet/go).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
See [OCM](/configuration/service/ocm).
|
||||||
|
|
||||||
#### 1.13.2
|
#### 1.13.2
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ icon: material/delete-clock
|
|||||||
|
|
||||||
!!! failure "已在 sing-box 1.12.0 废弃"
|
!!! failure "已在 sing-box 1.12.0 废弃"
|
||||||
|
|
||||||
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
|
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||||
|
|
||||||
### 结构
|
### 结构
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ icon: material/alert-decagram
|
|||||||
(`source_port` || `source_port_range`) &&
|
(`source_port` || `source_port_range`) &&
|
||||||
`other fields`
|
`other fields`
|
||||||
|
|
||||||
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
|
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
|
||||||
|
|
||||||
#### inbound
|
#### inbound
|
||||||
|
|
||||||
@@ -546,4 +546,4 @@ Match any IP with query response.
|
|||||||
|
|
||||||
#### rules
|
#### rules
|
||||||
|
|
||||||
Included rules.
|
Included rules.
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ icon: material/alert-decagram
|
|||||||
(`source_port` || `source_port_range`) &&
|
(`source_port` || `source_port_range`) &&
|
||||||
`other fields`
|
`other fields`
|
||||||
|
|
||||||
另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。
|
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。
|
||||||
|
|
||||||
#### inbound
|
#### inbound
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
!!! failure "已在 sing-box 1.12.0 中被移除"
|
||||||
|
|
||||||
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。
|
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
|
||||||
|
|
||||||
匹配 Geosite。
|
匹配 Geosite。
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
!!! failure "已在 sing-box 1.12.0 中被移除"
|
||||||
|
|
||||||
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
|
||||||
|
|
||||||
匹配源 GeoIP。
|
匹配源 GeoIP。
|
||||||
|
|
||||||
@@ -453,7 +453,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
!!! failure "已在 sing-box 1.12.0 废弃"
|
!!! failure "已在 sing-box 1.12.0 废弃"
|
||||||
|
|
||||||
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver)。
|
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项)。
|
||||||
|
|
||||||
匹配出站。
|
匹配出站。
|
||||||
|
|
||||||
@@ -505,7 +505,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
!!! failure "已在 sing-box 1.12.0 中被移除"
|
||||||
|
|
||||||
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
|
||||||
|
|
||||||
|
|
||||||
与查询响应匹配 GeoIP。
|
与查询响应匹配 GeoIP。
|
||||||
@@ -550,4 +550,4 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
==必填==
|
==必填==
|
||||||
|
|
||||||
包括的规则。
|
包括的规则。
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ DNS 服务器的路径。
|
|||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ DNS 服务器的路径。
|
|||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ icon: material/delete-clock
|
|||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.12.0"
|
!!! failure "Deprecated in sing-box 1.12.0"
|
||||||
|
|
||||||
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
|
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||||
|
|
||||||
!!! quote "sing-box 1.9.0 中的更改"
|
!!! quote "sing-box 1.9.0 中的更改"
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ DNS 服务器的端口。
|
|||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ DNS 服务器的端口。
|
|||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||||
|
|
||||||
### 拨号字段
|
### 拨号字段
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
将拒绝的 DNS 响应缓存存储在缓存文件中。
|
将拒绝的 DNS 响应缓存存储在缓存文件中。
|
||||||
|
|
||||||
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。
|
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。
|
||||||
|
|
||||||
#### rdrc_timeout
|
#### rdrc_timeout
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|
||||||
默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#_5)。
|
默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#构建标记)。
|
||||||
|
|
||||||
### 结构
|
### 结构
|
||||||
|
|
||||||
|
|||||||
@@ -58,4 +58,4 @@ AnyTLS 填充方案行数组。
|
|||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
#### tls
|
#### tls
|
||||||
|
|
||||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||||
|
|
||||||
#### users
|
#### users
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user