mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-14 04:38:28 +10:00
Compare commits
93 Commits
testing
...
v1.13.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a8b7a3d22 | ||
|
|
f40350a13a | ||
|
|
e5d120741c | ||
|
|
255d434407 | ||
|
|
bbfffe832b | ||
|
|
e133463c01 | ||
|
|
7de3a416c7 | ||
|
|
c81078ee99 | ||
|
|
955bdb31e0 | ||
|
|
eb1560eab8 | ||
|
|
f2f787c596 | ||
|
|
4b1b00a4f6 | ||
|
|
f7968c4ff7 | ||
|
|
07730802fb | ||
|
|
0284bf2c34 | ||
|
|
cf9ce3f852 | ||
|
|
6d2fd31692 | ||
|
|
8b46120129 | ||
|
|
2cc0695054 | ||
|
|
cd6aee901e | ||
|
|
edb7079c5f | ||
|
|
e3dbe0dd55 | ||
|
|
5d906a762c | ||
|
|
8d0e6fe300 | ||
|
|
0aff7f7084 | ||
|
|
e945c89a09 | ||
|
|
4dfb1a2b13 | ||
|
|
9254169cda | ||
|
|
9049710033 | ||
|
|
d2a94c3733 | ||
|
|
a45dce0ab9 | ||
|
|
9e24918da9 | ||
|
|
98a1790047 | ||
|
|
def5704801 | ||
|
|
a4807c1c77 | ||
|
|
f3e7ecf8a3 | ||
|
|
66fd4b4497 | ||
|
|
3e8437baeb | ||
|
|
c8d6b1e458 | ||
|
|
e3c861e615 | ||
|
|
111a94df7e | ||
|
|
29a5919783 | ||
|
|
9afee16126 | ||
|
|
cb7ac55010 | ||
|
|
cfe4747c61 | ||
|
|
efb6ff737d | ||
|
|
4e7faac0b4 | ||
|
|
b6ae9fe33d | ||
|
|
f0b8b4ad2c | ||
|
|
d36c78c19c | ||
|
|
2878d84b6a | ||
|
|
afab455c4e | ||
|
|
794b716468 | ||
|
|
2b510b4d00 | ||
|
|
e259c21c61 | ||
|
|
d6533ba12a | ||
|
|
cbc1f56f61 | ||
|
|
51d750b57e | ||
|
|
ad0ad44772 | ||
|
|
3a5f54a39f | ||
|
|
cedd4470c1 | ||
|
|
bb563a1e05 | ||
|
|
0a315a62ad | ||
|
|
e525104b1b | ||
|
|
563efbc78e | ||
|
|
8dee1d95c8 | ||
|
|
7f2e17dd39 | ||
|
|
ffc7e3e1f3 | ||
|
|
693879e216 | ||
|
|
f7c913617d | ||
|
|
4ccdd44fba | ||
|
|
c9938c18eb | ||
|
|
1c2882de7e | ||
|
|
76637ef3ee | ||
|
|
b84ef9c1d0 | ||
|
|
3da06bb372 | ||
|
|
e1c5ffc823 | ||
|
|
6dcead5d6b | ||
|
|
e442da3c56 | ||
|
|
6920d560ae | ||
|
|
4bb1921f60 | ||
|
|
d50a687ac7 | ||
|
|
7bcc390c80 | ||
|
|
1d1c08fc95 | ||
|
|
3f4577a420 | ||
|
|
525e53b59e | ||
|
|
77cbd3490a | ||
|
|
3a77f375c8 | ||
|
|
4ccd02709d | ||
|
|
8069491864 | ||
|
|
5b3f291b4a | ||
|
|
b5595dd5bc | ||
|
|
a91d310987 |
23
.fpm_pacman
23
.fpm_pacman
@@ -1,23 +0,0 @@
|
|||||||
-s dir
|
|
||||||
--name sing-box
|
|
||||||
--category net
|
|
||||||
--license GPL-3.0-or-later
|
|
||||||
--description "The universal proxy platform."
|
|
||||||
--url "https://sing-box.sagernet.org/"
|
|
||||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
|
||||||
--config-files etc/sing-box/config.json
|
|
||||||
--after-install release/config/sing-box.postinst
|
|
||||||
|
|
||||||
release/config/config.json=/etc/sing-box/config.json
|
|
||||||
|
|
||||||
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
|
|
||||||
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
|
|
||||||
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
|
|
||||||
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
|
|
||||||
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
|
||||||
|
|
||||||
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
|
||||||
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
|
||||||
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
|
||||||
|
|
||||||
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
--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 @@
|
|||||||
335e5bef5d88fc4474c9a70b865561f45a67de83
|
92d4602aba0ab6084673af0fe4887dccbc1049a5
|
||||||
|
|||||||
81
.github/build_alpine_apk.sh
vendored
81
.github/build_alpine_apk.sh
vendored
@@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e -o pipefail
|
|
||||||
|
|
||||||
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)
|
|
||||||
trap 'rm -rf "$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 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"
|
|
||||||
80
.github/build_openwrt_apk.sh
vendored
80
.github/build_openwrt_apk.sh
vendored
@@ -1,80 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e -o pipefail
|
|
||||||
|
|
||||||
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)
|
|
||||||
trap 'rm -rf "$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 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
33
.github/detect_track.sh
vendored
@@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
branches=$(git branch -r --contains HEAD)
|
|
||||||
if echo "$branches" | grep -q 'origin/stable'; then
|
|
||||||
track=stable
|
|
||||||
elif echo "$branches" | grep -q 'origin/testing'; then
|
|
||||||
track=testing
|
|
||||||
elif echo "$branches" | grep -q 'origin/oldstable'; then
|
|
||||||
track=oldstable
|
|
||||||
else
|
|
||||||
echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$track" == "stable" ]]; then
|
|
||||||
tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
|
|
||||||
if [[ -n "$tag" && "$tag" == *"-"* ]]; then
|
|
||||||
track=beta
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$track" in
|
|
||||||
stable) name=sing-box; docker_tag=latest ;;
|
|
||||||
beta) name=sing-box-beta; docker_tag=latest-beta ;;
|
|
||||||
testing) name=sing-box-testing; docker_tag=latest-testing ;;
|
|
||||||
oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2
|
|
||||||
echo "TRACK=${track}" >> "$GITHUB_ENV"
|
|
||||||
echo "NAME=${name}" >> "$GITHUB_ENV"
|
|
||||||
echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV"
|
|
||||||
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
@@ -6,7 +6,7 @@
|
|||||||
":disableRateLimiting"
|
":disableRateLimiting"
|
||||||
],
|
],
|
||||||
"baseBranches": [
|
"baseBranches": [
|
||||||
"unstable"
|
"dev-next"
|
||||||
],
|
],
|
||||||
"golang": {
|
"golang": {
|
||||||
"enabled": false
|
"enabled": false
|
||||||
|
|||||||
45
.github/setup_go_for_macos1013.sh
vendored
45
.github/setup_go_for_macos1013.sh
vendored
@@ -1,45 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
VERSION="1.25.8"
|
|
||||||
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,35 +1,16 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -euo pipefail
|
VERSION="1.25.5"
|
||||||
|
|
||||||
VERSION="1.25.8"
|
mkdir -p $HOME/go
|
||||||
PATCH_COMMITS=(
|
cd $HOME/go
|
||||||
"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
|
||||||
# these patch URLs only work on golang1.25.x
|
# this patch file only works 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:
|
||||||
@@ -37,10 +18,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\""
|
|
||||||
|
|
||||||
for patch_commit in "${PATCH_COMMITS[@]}"; do
|
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||||
curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1
|
|
||||||
done
|
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
|
||||||
|
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
|
||||||
|
|||||||
2
.github/update_cronet.sh
vendored
2
.github/update_cronet.sh
vendored
@@ -10,4 +10,4 @@ git -C $PROJECTS/cronet-go fetch origin go
|
|||||||
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
|
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
|
||||||
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
|
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
|
||||||
go mod tidy
|
go mod tidy
|
||||||
git -C $PROJECTS/cronet-go rev-parse origin/go > "$SCRIPT_DIR/CRONET_GO_VERSION"
|
git -C $PROJECTS/cronet-go rev-parse origin/HEAD > "$SCRIPT_DIR/CRONET_GO_VERSION"
|
||||||
|
|||||||
13
.github/update_cronet_dev.sh
vendored
13
.github/update_cronet_dev.sh
vendored
@@ -1,13 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -e -o pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR=$(dirname "$0")
|
|
||||||
PROJECTS=$SCRIPT_DIR/../..
|
|
||||||
|
|
||||||
git -C $PROJECTS/cronet-go fetch origin dev
|
|
||||||
git -C $PROJECTS/cronet-go fetch origin go_dev
|
|
||||||
go get -x github.com/sagernet/cronet-go/all@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
|
|
||||||
go get -x github.com/sagernet/cronet-go@$(git -C $PROJECTS/cronet-go rev-parse origin/go_dev)
|
|
||||||
go mod tidy
|
|
||||||
git -C $PROJECTS/cronet-go rev-parse origin/dev > "$SCRIPT_DIR/CRONET_GO_VERSION"
|
|
||||||
182
.github/workflows/build.yml
vendored
182
.github/workflows/build.yml
vendored
@@ -25,9 +25,8 @@ on:
|
|||||||
- publish-android
|
- publish-android
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- stable
|
- main-next
|
||||||
- testing
|
- dev-next
|
||||||
- unstable
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
||||||
@@ -47,7 +46,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.5
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -72,41 +71,33 @@ 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, alpine: x86_64, openwrt: "x86_64" }
|
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: 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, alpine: 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, 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, alpine: x86, openwrt: "i386_pentium4" }
|
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, 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, 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: 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: 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: 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: 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: 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" }
|
||||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
|
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
|
||||||
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
|
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
|
||||||
- { os: linux, arch: mipsle, gomips: hardfloat, openwrt: "mipsel_24kc_24kf" }
|
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
|
||||||
- { os: linux, arch: mipsle, gomips: softfloat }
|
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
|
||||||
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
|
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
|
||||||
- { os: linux, arch: mips64le, gomips: hardfloat }
|
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
|
||||||
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
|
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
|
||||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||||
- { os: linux, arch: riscv64 }
|
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
|
||||||
- { os: linux, arch: loong64 }
|
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
||||||
|
|
||||||
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
|
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
|
||||||
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
|
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
|
||||||
@@ -121,10 +112,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
if: ${{ ! matrix.legacy_win7 }}
|
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.5
|
||||||
|
- 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
|
||||||
@@ -132,11 +128,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/go_win7
|
~/go/go_win7
|
||||||
key: go_win7_1258
|
key: go_win7_1255
|
||||||
- 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
|
||||||
@@ -160,23 +154,14 @@ jobs:
|
|||||||
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||||||
git -C ~/cronet-go checkout FETCH_HEAD
|
git -C ~/cronet-go checkout FETCH_HEAD
|
||||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||||
- name: Regenerate Debian keyring
|
|
||||||
if: matrix.naive
|
|
||||||
run: |
|
|
||||||
set -xeuo pipefail
|
|
||||||
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
|
|
||||||
cd ~/cronet-go
|
|
||||||
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
|
|
||||||
- name: Cache Chromium toolchain
|
- name: Cache Chromium toolchain
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
id: cache-chromium-toolchain
|
id: cache-chromium-toolchain
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/cronet-go/naiveproxy/src/third_party/llvm-build/
|
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
|
||||||
~/cronet-go/naiveproxy/src/gn/out/
|
~/cronet-go/naiveproxy/src/out/sysroot-build
|
||||||
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
|
|
||||||
~/cronet-go/naiveproxy/src/out/sysroot-build/
|
|
||||||
key: chromium-toolchain-${{ matrix.arch }}-${{ matrix.variant }}-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
key: chromium-toolchain-${{ matrix.arch }}-${{ matrix.variant }}-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
||||||
- name: Download Chromium toolchain
|
- name: Download Chromium toolchain
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
@@ -205,10 +190,9 @@ jobs:
|
|||||||
- name: Set build tags
|
- name: Set build tags
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
|
||||||
if [[ "${{ matrix.naive }}" == "true" ]]; then
|
if [[ "${{ matrix.naive }}" == "true" ]]; then
|
||||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
|
TAGS="${TAGS},with_naive_outbound"
|
||||||
else
|
|
||||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
|
||||||
fi
|
fi
|
||||||
if [[ "${{ matrix.variant }}" == "purego" ]]; then
|
if [[ "${{ matrix.variant }}" == "purego" ]]; then
|
||||||
TAGS="${TAGS},with_purego"
|
TAGS="${TAGS},with_purego"
|
||||||
@@ -216,16 +200,13 @@ jobs:
|
|||||||
TAGS="${TAGS},with_musl"
|
TAGS="${TAGS},with_musl"
|
||||||
fi
|
fi
|
||||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
- name: Set shared ldflags
|
|
||||||
run: |
|
|
||||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
|
|
||||||
- name: Build (purego)
|
- name: Build (purego)
|
||||||
if: matrix.variant == 'purego'
|
if: matrix.variant == 'purego'
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
@@ -247,7 +228,7 @@ jobs:
|
|||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "1"
|
CGO_ENABLED: "1"
|
||||||
@@ -255,8 +236,6 @@ jobs:
|
|||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
GO386: ${{ matrix.go386 }}
|
GO386: ${{ matrix.go386 }}
|
||||||
GOARM: ${{ matrix.goarm }}
|
GOARM: ${{ matrix.goarm }}
|
||||||
GOMIPS: ${{ matrix.gomips }}
|
|
||||||
GOMIPS64: ${{ matrix.gomips }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build (musl)
|
- name: Build (musl)
|
||||||
if: matrix.variant == 'musl'
|
if: matrix.variant == 'musl'
|
||||||
@@ -264,7 +243,7 @@ jobs:
|
|||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "1"
|
CGO_ENABLED: "1"
|
||||||
@@ -272,8 +251,6 @@ jobs:
|
|||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
GO386: ${{ matrix.go386 }}
|
GO386: ${{ matrix.go386 }}
|
||||||
GOARM: ${{ matrix.goarm }}
|
GOARM: ${{ matrix.goarm }}
|
||||||
GOMIPS: ${{ matrix.gomips }}
|
|
||||||
GOMIPS64: ${{ matrix.gomips }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build (non-variant)
|
- name: Build (non-variant)
|
||||||
if: matrix.os != 'android' && matrix.variant == ''
|
if: matrix.os != 'android' && matrix.variant == ''
|
||||||
@@ -281,7 +258,7 @@ jobs:
|
|||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
@@ -301,7 +278,7 @@ jobs:
|
|||||||
export CXX="${CC}++"
|
export CXX="${CC}++"
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "1"
|
CGO_ENABLED: "1"
|
||||||
@@ -375,7 +352,7 @@ jobs:
|
|||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libarchive-tools
|
sudo apt-get install -y libarchive-tools
|
||||||
cp .fpm_pacman .fpm
|
cp .fpm_systemd .fpm
|
||||||
fpm -t pacman \
|
fpm -t pacman \
|
||||||
-v "$PKG_VERSION" \
|
-v "$PKG_VERSION" \
|
||||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
||||||
@@ -396,30 +373,6 @@ 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
|
||||||
@@ -455,36 +408,22 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- { arch: amd64 }
|
- { arch: amd64 }
|
||||||
- { arch: arm64 }
|
- { arch: arm64 }
|
||||||
- { arch: amd64, legacy_osx: true, legacy_name: "macos-10.13" }
|
- { arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
|
||||||
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_osx }}
|
if: ${{ ! matrix.legacy_go124 }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.25.3
|
go-version: ^1.25.3
|
||||||
- name: Cache Go for macOS 10.13
|
- name: Setup Go 1.24
|
||||||
if: matrix.legacy_osx
|
if: matrix.legacy_go124
|
||||||
id: cache-go-for-macos1013
|
uses: actions/setup-go@v5
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
with:
|
||||||
path: |
|
go-version: ~1.24.6
|
||||||
~/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"
|
||||||
@@ -492,27 +431,22 @@ jobs:
|
|||||||
- name: Set build tags
|
- name: Set build tags
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
|
||||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
|
if [[ "${{ matrix.legacy_go124 }}" != "true" ]]; then
|
||||||
else
|
TAGS="${TAGS},with_naive_outbound"
|
||||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
|
||||||
fi
|
fi
|
||||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
- name: Set shared ldflags
|
|
||||||
run: |
|
|
||||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
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: |-
|
||||||
@@ -565,11 +499,9 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
run: |
|
run: |
|
||||||
$TAGS = Get-Content release/DEFAULT_BUILD_TAGS_WINDOWS
|
|
||||||
$LDFLAGS_SHARED = Get-Content release/LDFLAGS
|
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -v -trimpath -o dist/sing-box.exe -tags "$TAGS" `
|
go build -v -trimpath -o dist/sing-box.exe -tags "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0" `
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' $LDFLAGS_SHARED -s -w -buildid=" `
|
-ldflags "-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0" `
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
@@ -579,11 +511,9 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
if: ${{ !matrix.naive }}
|
if: ${{ !matrix.naive }}
|
||||||
run: |
|
run: |
|
||||||
$TAGS = Get-Content release/DEFAULT_BUILD_TAGS_OTHERS
|
|
||||||
$LDFLAGS_SHARED = Get-Content release/LDFLAGS
|
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -v -trimpath -o dist/sing-box.exe -tags "$TAGS" `
|
go build -v -trimpath -o dist/sing-box.exe -tags "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0" `
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' $LDFLAGS_SHARED -s -w -buildid=" `
|
-ldflags "-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0" `
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
@@ -628,7 +558,7 @@ jobs:
|
|||||||
path: "dist"
|
path: "dist"
|
||||||
build_android:
|
build_android:
|
||||||
name: Build Android
|
name: Build Android
|
||||||
if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android') && github.ref != 'refs/heads/oldstable'
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
@@ -641,7 +571,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.5
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -664,12 +594,12 @@ jobs:
|
|||||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
|
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: github.ref == 'refs/heads/testing'
|
if: github.ref == 'refs/heads/dev-next'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -718,7 +648,7 @@ jobs:
|
|||||||
path: 'dist'
|
path: 'dist'
|
||||||
publish_android:
|
publish_android:
|
||||||
name: Publish Android
|
name: Publish Android
|
||||||
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' && github.ref != 'refs/heads/oldstable'
|
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
@@ -731,7 +661,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.5
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -754,12 +684,12 @@ jobs:
|
|||||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
|
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: github.ref == 'refs/heads/testing'
|
if: github.ref == 'refs/heads/dev-next'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -830,7 +760,7 @@ jobs:
|
|||||||
if: matrix.if
|
if: matrix.if
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.5
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
@@ -838,12 +768,12 @@ jobs:
|
|||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: matrix.if && github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
|
if: matrix.if && github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: matrix.if && github.ref == 'refs/heads/testing'
|
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -929,7 +859,7 @@ jobs:
|
|||||||
-authenticationKeyID $ASC_KEY_ID \
|
-authenticationKeyID $ASC_KEY_ID \
|
||||||
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||||
- name: Publish to TestFlight
|
- name: Publish to TestFlight
|
||||||
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/testing'
|
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/dev-next'
|
||||||
run: |-
|
run: |-
|
||||||
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
|
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
|
||||||
- name: Build image
|
- name: Build image
|
||||||
|
|||||||
77
.github/workflows/docker.yml
vendored
77
.github/workflows/docker.yml
vendored
@@ -3,8 +3,8 @@ name: Publish Docker Images
|
|||||||
on:
|
on:
|
||||||
#push:
|
#push:
|
||||||
# branches:
|
# branches:
|
||||||
# - stable
|
# - main-next
|
||||||
# - testing
|
# - dev-next
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
@@ -29,12 +29,10 @@ jobs:
|
|||||||
- { arch: arm64, naive: true, docker_platform: "linux/arm64" }
|
- { arch: arm64, naive: true, docker_platform: "linux/arm64" }
|
||||||
- { arch: "386", naive: true, docker_platform: "linux/386" }
|
- { arch: "386", naive: true, docker_platform: "linux/386" }
|
||||||
- { arch: arm, goarm: "7", naive: true, docker_platform: "linux/arm/v7" }
|
- { arch: arm, goarm: "7", naive: true, docker_platform: "linux/arm/v7" }
|
||||||
- { arch: mipsle, gomips: softfloat, naive: true, docker_platform: "linux/mipsle" }
|
|
||||||
- { arch: riscv64, naive: true, docker_platform: "linux/riscv64" }
|
|
||||||
- { arch: loong64, naive: true, docker_platform: "linux/loong64" }
|
|
||||||
# Non-naive builds
|
# Non-naive builds
|
||||||
- { arch: arm, goarm: "6", docker_platform: "linux/arm/v6" }
|
- { arch: arm, goarm: "6", docker_platform: "linux/arm/v6" }
|
||||||
- { arch: ppc64le, docker_platform: "linux/ppc64le" }
|
- { arch: ppc64le, docker_platform: "linux/ppc64le" }
|
||||||
|
- { arch: riscv64, docker_platform: "linux/riscv64" }
|
||||||
- { arch: s390x, docker_platform: "linux/s390x" }
|
- { arch: s390x, docker_platform: "linux/s390x" }
|
||||||
steps:
|
steps:
|
||||||
- name: Get commit to build
|
- name: Get commit to build
|
||||||
@@ -55,7 +53,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.4
|
||||||
- name: Clone cronet-go
|
- name: Clone cronet-go
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
run: |
|
run: |
|
||||||
@@ -66,23 +64,14 @@ jobs:
|
|||||||
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||||||
git -C ~/cronet-go checkout FETCH_HEAD
|
git -C ~/cronet-go checkout FETCH_HEAD
|
||||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||||
- name: Regenerate Debian keyring
|
|
||||||
if: matrix.naive
|
|
||||||
run: |
|
|
||||||
set -xeuo pipefail
|
|
||||||
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
|
|
||||||
cd ~/cronet-go
|
|
||||||
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
|
|
||||||
- name: Cache Chromium toolchain
|
- name: Cache Chromium toolchain
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
id: cache-chromium-toolchain
|
id: cache-chromium-toolchain
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/cronet-go/naiveproxy/src/third_party/llvm-build/
|
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
|
||||||
~/cronet-go/naiveproxy/src/gn/out/
|
~/cronet-go/naiveproxy/src/out/sysroot-build
|
||||||
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
|
|
||||||
~/cronet-go/naiveproxy/src/out/sysroot-build/
|
|
||||||
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
||||||
- name: Download Chromium toolchain
|
- name: Download Chromium toolchain
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
@@ -104,34 +93,29 @@ jobs:
|
|||||||
- name: Set build tags
|
- name: Set build tags
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
|
||||||
if [[ "${{ matrix.naive }}" == "true" ]]; then
|
if [[ "${{ matrix.naive }}" == "true" ]]; then
|
||||||
TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl"
|
TAGS="${TAGS},with_naive_outbound,with_musl"
|
||||||
else
|
|
||||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
|
||||||
fi
|
fi
|
||||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
- name: Set shared ldflags
|
|
||||||
run: |
|
|
||||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
|
|
||||||
- name: Build (naive)
|
- name: Build (naive)
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
|
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=${VERSION}\" -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "1"
|
CGO_ENABLED: "1"
|
||||||
GOOS: linux
|
GOOS: linux
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
GOARM: ${{ matrix.goarm }}
|
GOARM: ${{ matrix.goarm }}
|
||||||
GOMIPS: ${{ matrix.gomips }}
|
|
||||||
- name: Build (non-naive)
|
- name: Build (non-naive)
|
||||||
if: ${{ ! matrix.naive }}
|
if: ${{ ! matrix.naive }}
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
|
go build -v -trimpath -o sing-box -tags "${BUILD_TAGS}" \
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=${VERSION}\" -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
@@ -164,17 +148,15 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
platform:
|
||||||
- { platform: "linux/amd64" }
|
- linux/amd64
|
||||||
- { platform: "linux/arm/v6" }
|
- linux/arm/v6
|
||||||
- { platform: "linux/arm/v7" }
|
- linux/arm/v7
|
||||||
- { platform: "linux/arm64" }
|
- linux/arm64
|
||||||
- { platform: "linux/386" }
|
- linux/386
|
||||||
# mipsle: no base Docker image available for this platform
|
- linux/ppc64le
|
||||||
- { platform: "linux/ppc64le" }
|
- linux/riscv64
|
||||||
- { platform: "linux/riscv64" }
|
- linux/s390x
|
||||||
- { platform: "linux/s390x" }
|
|
||||||
- { platform: "linux/loong64", base_image: "ghcr.io/loong64/alpine:edge" }
|
|
||||||
steps:
|
steps:
|
||||||
- name: Get commit to build
|
- name: Get commit to build
|
||||||
id: ref
|
id: ref
|
||||||
@@ -227,8 +209,6 @@ jobs:
|
|||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile.binary
|
file: Dockerfile.binary
|
||||||
build-args: |
|
|
||||||
BASE_IMAGE=${{ matrix.base_image || 'alpine' }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
@@ -244,7 +224,6 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
merge:
|
merge:
|
||||||
if: github.event_name != 'push'
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build_docker
|
- build_docker
|
||||||
@@ -259,13 +238,13 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "ref=$ref"
|
echo "ref=$ref"
|
||||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||||
- name: Checkout
|
if [[ $ref == *"-"* ]]; then
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
latest=latest-beta
|
||||||
with:
|
else
|
||||||
ref: ${{ steps.ref.outputs.ref }}
|
latest=latest
|
||||||
fetch-depth: 0
|
fi
|
||||||
- name: Detect track
|
echo "latest=$latest"
|
||||||
run: bash .github/detect_track.sh
|
echo "latest=$latest" >> $GITHUB_OUTPUT
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
@@ -285,11 +264,11 @@ jobs:
|
|||||||
working-directory: /tmp/digests
|
working-directory: /tmp/digests
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create \
|
docker buildx imagetools create \
|
||||||
-t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \
|
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
|
||||||
-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 }}:${{ env.DOCKER_TAG }}
|
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
|
||||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
||||||
|
|||||||
14
.github/workflows/lint.yml
vendored
14
.github/workflows/lint.yml
vendored
@@ -3,20 +3,18 @@ name: Lint
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- oldstable
|
- stable-next
|
||||||
- stable
|
- main-next
|
||||||
- testing
|
- dev-next
|
||||||
- unstable
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- '.github/**'
|
- '.github/**'
|
||||||
- '!.github/workflows/lint.yml'
|
- '!.github/workflows/lint.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- oldstable
|
- stable-next
|
||||||
- stable
|
- main-next
|
||||||
- testing
|
- dev-next
|
||||||
- unstable
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
56
.github/workflows/linux.yml
vendored
56
.github/workflows/linux.yml
vendored
@@ -3,14 +3,19 @@ name: Build Linux Packages
|
|||||||
on:
|
on:
|
||||||
#push:
|
#push:
|
||||||
# branches:
|
# branches:
|
||||||
# - stable
|
# - main-next
|
||||||
# - testing
|
# - dev-next
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
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
|
||||||
@@ -29,7 +34,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.5
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -56,14 +61,14 @@ jobs:
|
|||||||
- { os: linux, arch: arm64, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
- { os: linux, arch: arm64, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
||||||
- { os: linux, arch: "386", naive: true, debian: i386, rpm: i386 }
|
- { os: linux, arch: "386", naive: true, debian: i386, rpm: i386 }
|
||||||
- { os: linux, arch: arm, goarm: "7", naive: true, debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
- { os: linux, arch: arm, goarm: "7", naive: true, debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
||||||
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, debian: mipsel, rpm: mipsel }
|
|
||||||
- { os: linux, arch: riscv64, naive: true, debian: riscv64, rpm: riscv64 }
|
|
||||||
- { os: linux, arch: loong64, naive: true, debian: loongarch64, rpm: loongarch64 }
|
|
||||||
# Non-naive builds (unsupported architectures)
|
# Non-naive builds (unsupported architectures)
|
||||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||||
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||||
|
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
|
||||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||||
|
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
|
||||||
|
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
@@ -72,7 +77,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.5
|
||||||
- name: Clone cronet-go
|
- name: Clone cronet-go
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
run: |
|
run: |
|
||||||
@@ -83,23 +88,14 @@ jobs:
|
|||||||
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||||||
git -C ~/cronet-go checkout FETCH_HEAD
|
git -C ~/cronet-go checkout FETCH_HEAD
|
||||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||||
- name: Regenerate Debian keyring
|
|
||||||
if: matrix.naive
|
|
||||||
run: |
|
|
||||||
set -xeuo pipefail
|
|
||||||
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
|
|
||||||
cd ~/cronet-go
|
|
||||||
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
|
|
||||||
- name: Cache Chromium toolchain
|
- name: Cache Chromium toolchain
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
id: cache-chromium-toolchain
|
id: cache-chromium-toolchain
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/cronet-go/naiveproxy/src/third_party/llvm-build/
|
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
|
||||||
~/cronet-go/naiveproxy/src/gn/out/
|
~/cronet-go/naiveproxy/src/out/sysroot-build
|
||||||
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
|
|
||||||
~/cronet-go/naiveproxy/src/out/sysroot-build/
|
|
||||||
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
||||||
- name: Download Chromium toolchain
|
- name: Download Chromium toolchain
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
@@ -120,30 +116,24 @@ jobs:
|
|||||||
- name: Set build tags
|
- name: Set build tags
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
|
||||||
if [[ "${{ matrix.naive }}" == "true" ]]; then
|
if [[ "${{ matrix.naive }}" == "true" ]]; then
|
||||||
TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl"
|
TAGS="${TAGS},with_naive_outbound,with_musl"
|
||||||
else
|
|
||||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
|
||||||
fi
|
fi
|
||||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
- name: Set shared ldflags
|
|
||||||
run: |
|
|
||||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
|
|
||||||
- name: Build (naive)
|
- name: Build (naive)
|
||||||
if: matrix.naive
|
if: matrix.naive
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "1"
|
CGO_ENABLED: "1"
|
||||||
GOOS: linux
|
GOOS: linux
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
GOARM: ${{ matrix.goarm }}
|
GOARM: ${{ matrix.goarm }}
|
||||||
GOMIPS: ${{ matrix.gomips }}
|
|
||||||
GOMIPS64: ${{ matrix.gomips }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build (non-naive)
|
- name: Build (non-naive)
|
||||||
if: ${{ ! matrix.naive }}
|
if: ${{ ! matrix.naive }}
|
||||||
@@ -151,7 +141,7 @@ jobs:
|
|||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' ${LDFLAGS_SHARED} -s -w -buildid=" \
|
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0' \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
@@ -162,8 +152,14 @@ 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: Detect track
|
- name: Set name
|
||||||
run: bash .github/detect_track.sh
|
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
|
||||||
|
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 }}"
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,9 +12,6 @@
|
|||||||
/*.jar
|
/*.jar
|
||||||
/*.aar
|
/*.aar
|
||||||
/*.xcframework/
|
/*.xcframework/
|
||||||
/experimental/libbox/*.aar
|
|
||||||
/experimental/libbox/*.xcframework/
|
|
||||||
/experimental/libbox/*.nupkg
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/config.d/
|
/config.d/
|
||||||
/venv/
|
/venv/
|
||||||
|
|||||||
@@ -9,11 +9,6 @@ run:
|
|||||||
- with_utls
|
- with_utls
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
- with_tailscale
|
|
||||||
- with_ccm
|
|
||||||
- with_ocm
|
|
||||||
- badlinkname
|
|
||||||
- tfogo_checklinkname0
|
|
||||||
linters:
|
linters:
|
||||||
default: none
|
default: none
|
||||||
enable:
|
enable:
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ RUN set -ex \
|
|||||||
&& apk add git build-base \
|
&& apk add git build-base \
|
||||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||||
&& export TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) \
|
&& go build -v -trimpath -tags \
|
||||||
&& export LDFLAGS_SHARED=$(cat release/LDFLAGS) \
|
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0" \
|
||||||
&& go build -v -trimpath -tags "$TAGS" \
|
|
||||||
-o /go/bin/sing-box \
|
-o /go/bin/sing-box \
|
||||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" $LDFLAGS_SHARED -s -w -buildid=" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
ARG BASE_IMAGE=alpine
|
FROM alpine
|
||||||
FROM ${BASE_IMAGE}
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& if command -v apk > /dev/null; then \
|
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
||||||
apk add --no-cache --upgrade bash tzdata ca-certificates nftables; \
|
|
||||||
else \
|
|
||||||
apt-get update && apt-get install -y --no-install-recommends bash tzdata ca-certificates nftables \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*; \
|
|
||||||
fi
|
|
||||||
COPY sing-box-${TARGETARCH}${TARGETVARIANT} /usr/local/bin/sing-box
|
COPY sing-box-${TARGETARCH}${TARGETVARIANT} /usr/local/bin/sing-box
|
||||||
ENTRYPOINT ["sing-box"]
|
ENTRYPOINT ["sing-box"]
|
||||||
|
|||||||
113
Makefile
113
Makefile
@@ -1,18 +1,15 @@
|
|||||||
NAME = sing-box
|
NAME = sing-box
|
||||||
COMMIT = $(shell git rev-parse --short HEAD)
|
COMMIT = $(shell git rev-parse --short HEAD)
|
||||||
TAGS ?= $(shell cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0
|
||||||
|
|
||||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
|
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
|
||||||
|
|
||||||
LDFLAGS_SHARED = $(shell cat release/LDFLAGS)
|
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0"
|
||||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' $(LDFLAGS_SHARED) -s -w -buildid="
|
|
||||||
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
||||||
MAIN = ./cmd/sing-box
|
MAIN = ./cmd/sing-box
|
||||||
PREFIX ?= $(shell go env GOPATH)
|
PREFIX ?= $(shell go env GOPATH)
|
||||||
SING_FFI ?= sing-ffi
|
|
||||||
LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json
|
|
||||||
|
|
||||||
.PHONY: test release docs build
|
.PHONY: test release docs build
|
||||||
|
|
||||||
@@ -92,12 +89,12 @@ update_android_version:
|
|||||||
go run ./cmd/internal/update_android_version
|
go run ./cmd/internal/update_android_version
|
||||||
|
|
||||||
build_android:
|
build_android:
|
||||||
cd ../sing-box-for-android && ./gradlew :app:clean :app:assembleOtherRelease :app:assembleOtherLegacyRelease && ./gradlew --stop
|
cd ../sing-box-for-android && ./gradlew :app:clean :app:assemblePlayRelease :app:assembleOtherRelease && ./gradlew --stop
|
||||||
|
|
||||||
upload_android:
|
upload_android:
|
||||||
mkdir -p dist/release_android
|
mkdir -p dist/release_android
|
||||||
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*.apk dist/release_android
|
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android
|
||||||
cp ../sing-box-for-android/app/build/outputs/apk/otherLegacy/release/*.apk dist/release_android
|
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*-universal.apk dist/release_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
|
||||||
|
|
||||||
@@ -112,7 +109,7 @@ build_ios:
|
|||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
rm -rf build/SFI.xcarchive && \
|
rm -rf build/SFI.xcarchive && \
|
||||||
xcodebuild clean -scheme SFI && \
|
xcodebuild clean -scheme SFI && \
|
||||||
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates
|
||||||
|
|
||||||
upload_ios_app_store:
|
upload_ios_app_store:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
@@ -133,7 +130,7 @@ release_ios: build_ios upload_ios_app_store
|
|||||||
build_macos:
|
build_macos:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
rm -rf build/SFM.xcarchive && \
|
rm -rf build/SFM.xcarchive && \
|
||||||
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
xcodebuild archive -scheme SFM -configuration Release -archivePath build/SFM.xcarchive -allowProvisioningUpdates
|
||||||
|
|
||||||
upload_macos_app_store:
|
upload_macos_app_store:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
@@ -142,50 +139,54 @@ upload_macos_app_store:
|
|||||||
release_macos: build_macos upload_macos_app_store
|
release_macos: build_macos upload_macos_app_store
|
||||||
|
|
||||||
build_macos_standalone:
|
build_macos_standalone:
|
||||||
$(MAKE) -C ../sing-box-for-apple archive_macos_standalone
|
cd ../sing-box-for-apple && \
|
||||||
|
rm -rf build/SFM.System.xcarchive && \
|
||||||
|
xcodebuild archive -scheme SFM.System -configuration Release -archivePath build/SFM.System.xcarchive -allowProvisioningUpdates
|
||||||
|
|
||||||
build_macos_dmg:
|
build_macos_dmg:
|
||||||
$(MAKE) -C ../sing-box-for-apple build_macos_dmg
|
rm -rf dist/SFM
|
||||||
|
mkdir -p dist/SFM
|
||||||
build_macos_pkg:
|
cd ../sing-box-for-apple && \
|
||||||
$(MAKE) -C ../sing-box-for-apple build_macos_pkg
|
rm -rf build/SFM.System && \
|
||||||
|
rm -rf build/SFM.dmg && \
|
||||||
|
xcodebuild -exportArchive \
|
||||||
|
-archivePath "build/SFM.System.xcarchive" \
|
||||||
|
-exportOptionsPlist SFM.System/Export.plist -allowProvisioningUpdates \
|
||||||
|
-exportPath "build/SFM.System" && \
|
||||||
|
create-dmg \
|
||||||
|
--volname "sing-box" \
|
||||||
|
--volicon "build/SFM.System/SFM.app/Contents/Resources/AppIcon.icns" \
|
||||||
|
--icon "SFM.app" 0 0 \
|
||||||
|
--hide-extension "SFM.app" \
|
||||||
|
--app-drop-link 0 0 \
|
||||||
|
--skip-jenkins \
|
||||||
|
"../sing-box/dist/SFM/SFM.dmg" "build/SFM.System/SFM.app"
|
||||||
|
|
||||||
notarize_macos_dmg:
|
notarize_macos_dmg:
|
||||||
$(MAKE) -C ../sing-box-for-apple notarize_macos_dmg
|
xcrun notarytool submit "dist/SFM/SFM.dmg" --wait \
|
||||||
|
--keychain-profile "notarytool-password" \
|
||||||
notarize_macos_pkg:
|
--no-s3-acceleration
|
||||||
$(MAKE) -C ../sing-box-for-apple notarize_macos_pkg
|
|
||||||
|
|
||||||
upload_macos_dmg:
|
upload_macos_dmg:
|
||||||
mkdir -p dist/SFM
|
cd dist/SFM && \
|
||||||
cp ../sing-box-for-apple/build/SFM-Apple.dmg "dist/SFM/SFM-${VERSION}-Apple.dmg"
|
cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \
|
||||||
cp ../sing-box-for-apple/build/SFM-Intel.dmg "dist/SFM/SFM-${VERSION}-Intel.dmg"
|
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dmg"
|
||||||
cp ../sing-box-for-apple/build/SFM-Universal.dmg "dist/SFM/SFM-${VERSION}-Universal.dmg"
|
|
||||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.dmg"
|
|
||||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.dmg"
|
|
||||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.dmg"
|
|
||||||
|
|
||||||
upload_macos_pkg:
|
|
||||||
mkdir -p dist/SFM
|
|
||||||
cp ../sing-box-for-apple/build/SFM-Apple.pkg "dist/SFM/SFM-${VERSION}-Apple.pkg"
|
|
||||||
cp ../sing-box-for-apple/build/SFM-Intel.pkg "dist/SFM/SFM-${VERSION}-Intel.pkg"
|
|
||||||
cp ../sing-box-for-apple/build/SFM-Universal.pkg "dist/SFM/SFM-${VERSION}-Universal.pkg"
|
|
||||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Apple.pkg"
|
|
||||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Intel.pkg"
|
|
||||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}-Universal.pkg"
|
|
||||||
|
|
||||||
upload_macos_dsyms:
|
upload_macos_dsyms:
|
||||||
mkdir -p dist/SFM
|
pushd ../sing-box-for-apple/build/SFM.System.xcarchive && \
|
||||||
cd ../sing-box-for-apple/build/SFM.System-universal.xcarchive && zip -r SFM.dSYMs.zip dSYMs
|
zip -r SFM.dSYMs.zip dSYMs && \
|
||||||
cp ../sing-box-for-apple/build/SFM.System-universal.xcarchive/SFM.dSYMs.zip "dist/SFM/SFM-${VERSION}.dSYMs.zip"
|
mv SFM.dSYMs.zip ../../../sing-box/dist/SFM && \
|
||||||
ghr --replace --draft --prerelease "v${VERSION}" "dist/SFM/SFM-${VERSION}.dSYMs.zip"
|
popd && \
|
||||||
|
cd dist/SFM && \
|
||||||
|
cp SFM.dSYMs.zip "SFM-${VERSION}-universal.dSYMs.zip" && \
|
||||||
|
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dSYMs.zip"
|
||||||
|
|
||||||
release_macos_standalone: build_macos_pkg notarize_macos_pkg upload_macos_pkg upload_macos_dsyms
|
release_macos_standalone: build_macos_standalone build_macos_dmg notarize_macos_dmg upload_macos_dmg upload_macos_dsyms
|
||||||
|
|
||||||
build_tvos:
|
build_tvos:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
rm -rf build/SFT.xcarchive && \
|
rm -rf build/SFT.xcarchive && \
|
||||||
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates | xcbeautify | grep -A 10 -e "Archive Succeeded" -e "ARCHIVE FAILED" -e "❌"
|
xcodebuild archive -scheme SFT -configuration Release -archivePath build/SFT.xcarchive -allowProvisioningUpdates
|
||||||
|
|
||||||
upload_tvos_app_store:
|
upload_tvos_app_store:
|
||||||
cd ../sing-box-for-apple && \
|
cd ../sing-box-for-apple && \
|
||||||
@@ -209,12 +210,12 @@ update_apple_version:
|
|||||||
update_macos_version:
|
update_macos_version:
|
||||||
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
|
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
|
||||||
|
|
||||||
release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone
|
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
|
||||||
|
|
||||||
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
||||||
|
|
||||||
publish_testflight:
|
publish_testflight:
|
||||||
go run -v ./cmd/internal/app_store_connect publish_testflight $(filter-out $@,$(MAKECMDGOALS))
|
go run -v ./cmd/internal/app_store_connect publish_testflight
|
||||||
|
|
||||||
prepare_app_store:
|
prepare_app_store:
|
||||||
go run -v ./cmd/internal/app_store_connect prepare_app_store
|
go run -v ./cmd/internal/app_store_connect prepare_app_store
|
||||||
@@ -237,21 +238,22 @@ test_stdio:
|
|||||||
lib_android:
|
lib_android:
|
||||||
go run ./cmd/internal/build_libbox -target android
|
go run ./cmd/internal/build_libbox -target android
|
||||||
|
|
||||||
|
lib_android_debug:
|
||||||
|
go run ./cmd/internal/build_libbox -target android -debug
|
||||||
|
|
||||||
lib_apple:
|
lib_apple:
|
||||||
go run ./cmd/internal/build_libbox -target apple
|
go run ./cmd/internal/build_libbox -target apple
|
||||||
|
|
||||||
lib_windows:
|
lib_ios:
|
||||||
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type csharp
|
go run ./cmd/internal/build_libbox -target apple -platform ios -debug
|
||||||
|
|
||||||
lib_android_new:
|
lib:
|
||||||
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type android
|
go run ./cmd/internal/build_libbox -target android
|
||||||
|
go run ./cmd/internal/build_libbox -target ios
|
||||||
lib_apple_new:
|
|
||||||
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple
|
|
||||||
|
|
||||||
lib_install:
|
lib_install:
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.12
|
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.10
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12
|
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.10
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
venv/bin/mkdocs serve
|
venv/bin/mkdocs serve
|
||||||
@@ -260,8 +262,8 @@ publish_docs:
|
|||||||
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
||||||
|
|
||||||
docs_install:
|
docs_install:
|
||||||
python3 -m venv venv
|
python -m venv venv
|
||||||
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.7.2" mkdocs-static-i18n=="1.2.*"
|
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin dist sing-box
|
rm -rf bin dist sing-box
|
||||||
@@ -271,6 +273,3 @@ update:
|
|||||||
git fetch
|
git fetch
|
||||||
git reset FETCH_HEAD --hard
|
git reset FETCH_HEAD --hard
|
||||||
git clean -fdx
|
git clean -fdx
|
||||||
|
|
||||||
%:
|
|
||||||
@:
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
package certificate
|
|
||||||
|
|
||||||
type Adapter struct {
|
|
||||||
providerType string
|
|
||||||
providerTag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAdapter(providerType string, providerTag string) Adapter {
|
|
||||||
return Adapter{
|
|
||||||
providerType: providerType,
|
|
||||||
providerTag: providerTag,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Adapter) Type() string {
|
|
||||||
return a.providerType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Adapter) Tag() string {
|
|
||||||
return a.providerTag
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
package certificate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.CertificateProviderManager = (*Manager)(nil)
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
logger log.ContextLogger
|
|
||||||
registry adapter.CertificateProviderRegistry
|
|
||||||
access sync.Mutex
|
|
||||||
started bool
|
|
||||||
stage adapter.StartStage
|
|
||||||
providers []adapter.CertificateProviderService
|
|
||||||
providerByTag map[string]adapter.CertificateProviderService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewManager(logger log.ContextLogger, registry adapter.CertificateProviderRegistry) *Manager {
|
|
||||||
return &Manager{
|
|
||||||
logger: logger,
|
|
||||||
registry: registry,
|
|
||||||
providerByTag: make(map[string]adapter.CertificateProviderService),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
|
||||||
m.logger.Trace(stage, " ", name)
|
|
||||||
startTime := time.Now()
|
|
||||||
err := adapter.LegacyStart(provider, stage)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, stage, " ", name)
|
|
||||||
}
|
|
||||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Close() error {
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
if !m.started {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
m.started = false
|
|
||||||
providers := m.providers
|
|
||||||
m.providers = nil
|
|
||||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
|
||||||
var err error
|
|
||||||
for _, provider := range providers {
|
|
||||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
|
||||||
m.logger.Trace("close ", name)
|
|
||||||
startTime := time.Now()
|
|
||||||
monitor.Start("close ", name)
|
|
||||||
err = E.Append(err, provider.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close ", name)
|
|
||||||
})
|
|
||||||
monitor.Finish()
|
|
||||||
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) CertificateProviders() []adapter.CertificateProviderService {
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
return m.providers
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) {
|
|
||||||
m.access.Lock()
|
|
||||||
provider, found := m.providerByTag[tag]
|
|
||||||
m.access.Unlock()
|
|
||||||
return provider, found
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) 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.CertificateProviderService) bool {
|
|
||||||
return it == provider
|
|
||||||
})
|
|
||||||
if index == -1 {
|
|
||||||
panic("invalid certificate provider index")
|
|
||||||
}
|
|
||||||
m.providers = append(m.providers[:index], m.providers[index+1:]...)
|
|
||||||
started := m.started
|
|
||||||
m.access.Unlock()
|
|
||||||
if started {
|
|
||||||
return provider.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error {
|
|
||||||
provider, err := m.registry.Create(ctx, logger, tag, providerType, options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
if m.started {
|
|
||||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
|
||||||
for _, stage := range adapter.ListStartStages {
|
|
||||||
m.logger.Trace(stage, " ", name)
|
|
||||||
startTime := time.Now()
|
|
||||||
err = adapter.LegacyStart(provider, stage)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, stage, " ", name)
|
|
||||||
}
|
|
||||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if existsProvider, loaded := m.providerByTag[tag]; loaded {
|
|
||||||
if m.started {
|
|
||||||
err = existsProvider.Close()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "close certificate-provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
existsIndex := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
|
|
||||||
return it == existsProvider
|
|
||||||
})
|
|
||||||
if existsIndex == -1 {
|
|
||||||
panic("invalid certificate provider index")
|
|
||||||
}
|
|
||||||
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
|
|
||||||
}
|
|
||||||
m.providers = append(m.providers, provider)
|
|
||||||
m.providerByTag[tag] = provider
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package certificate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.CertificateProviderService, error)
|
|
||||||
|
|
||||||
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
|
|
||||||
registry.register(providerType, func() any {
|
|
||||||
return new(Options)
|
|
||||||
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.CertificateProviderService, error) {
|
|
||||||
var options *Options
|
|
||||||
if rawOptions != nil {
|
|
||||||
options = rawOptions.(*Options)
|
|
||||||
}
|
|
||||||
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ adapter.CertificateProviderRegistry = (*Registry)(nil)
|
|
||||||
|
|
||||||
type (
|
|
||||||
optionsConstructorFunc func() any
|
|
||||||
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.CertificateProviderService, error)
|
|
||||||
)
|
|
||||||
|
|
||||||
type Registry struct {
|
|
||||||
access sync.Mutex
|
|
||||||
optionsType map[string]optionsConstructorFunc
|
|
||||||
constructor map[string]constructorFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRegistry() *Registry {
|
|
||||||
return &Registry{
|
|
||||||
optionsType: make(map[string]optionsConstructorFunc),
|
|
||||||
constructor: make(map[string]constructorFunc),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Registry) CreateOptions(providerType string) (any, bool) {
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
optionsConstructor, loaded := m.optionsType[providerType]
|
|
||||||
if !loaded {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return optionsConstructor(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (adapter.CertificateProviderService, error) {
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
constructor, loaded := m.constructor[providerType]
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("certificate provider type not found: " + providerType)
|
|
||||||
}
|
|
||||||
return constructor(ctx, logger, tag, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
m.optionsType[providerType] = optionsConstructor
|
|
||||||
m.constructor[providerType] = constructor
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CertificateProvider interface {
|
|
||||||
GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ACMECertificateProvider interface {
|
|
||||||
CertificateProvider
|
|
||||||
GetACMENextProtos() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CertificateProviderService interface {
|
|
||||||
Lifecycle
|
|
||||||
Type() string
|
|
||||||
Tag() string
|
|
||||||
CertificateProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
type CertificateProviderRegistry interface {
|
|
||||||
option.CertificateProviderOptionsRegistry
|
|
||||||
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (CertificateProviderService, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CertificateProviderManager interface {
|
|
||||||
Lifecycle
|
|
||||||
CertificateProviders() []CertificateProviderService
|
|
||||||
Get(tag string) (CertificateProviderService, bool)
|
|
||||||
Remove(tag string) error
|
|
||||||
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error
|
|
||||||
}
|
|
||||||
@@ -9,10 +9,6 @@ import (
|
|||||||
|
|
||||||
type ConnectionManager interface {
|
type ConnectionManager interface {
|
||||||
Lifecycle
|
Lifecycle
|
||||||
Count() int
|
|
||||||
CloseAll()
|
|
||||||
TrackConn(conn net.Conn) net.Conn
|
|
||||||
TrackPacketConn(conn net.PacketConn) net.PacketConn
|
|
||||||
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||||
NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package adapter
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@@ -26,19 +25,18 @@ type DNSRouter interface {
|
|||||||
|
|
||||||
type DNSClient interface {
|
type DNSClient interface {
|
||||||
Start()
|
Start()
|
||||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error)
|
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
|
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||||
ClearCache()
|
ClearCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSQueryOptions struct {
|
type DNSQueryOptions struct {
|
||||||
Transport DNSTransport
|
Transport DNSTransport
|
||||||
Strategy C.DomainStrategy
|
Strategy C.DomainStrategy
|
||||||
LookupStrategy C.DomainStrategy
|
LookupStrategy C.DomainStrategy
|
||||||
DisableCache bool
|
DisableCache bool
|
||||||
DisableOptimisticCache bool
|
RewriteTTL *uint32
|
||||||
RewriteTTL *uint32
|
ClientSubnet netip.Prefix
|
||||||
ClientSubnet netip.Prefix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||||
@@ -51,12 +49,11 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio
|
|||||||
return nil, E.New("domain resolver not found: " + options.Server)
|
return nil, E.New("domain resolver not found: " + options.Server)
|
||||||
}
|
}
|
||||||
return &DNSQueryOptions{
|
return &DNSQueryOptions{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Strategy: C.DomainStrategy(options.Strategy),
|
Strategy: C.DomainStrategy(options.Strategy),
|
||||||
DisableCache: options.DisableCache,
|
DisableCache: options.DisableCache,
|
||||||
DisableOptimisticCache: options.DisableOptimisticCache,
|
RewriteTTL: options.RewriteTTL,
|
||||||
RewriteTTL: options.RewriteTTL,
|
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,13 +63,6 @@ type RDRCStore interface {
|
|||||||
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSCacheStore interface {
|
|
||||||
LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool)
|
|
||||||
SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error
|
|
||||||
SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger)
|
|
||||||
ClearDNSCache() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSTransport interface {
|
type DNSTransport interface {
|
||||||
Lifecycle
|
Lifecycle
|
||||||
Type() string
|
Type() string
|
||||||
@@ -82,6 +72,11 @@ type DNSTransport interface {
|
|||||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LegacyDNSTransport interface {
|
||||||
|
LegacyStrategy() C.DomainStrategy
|
||||||
|
LegacyClientSubnet() netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
type DNSTransportRegistry interface {
|
type DNSTransportRegistry interface {
|
||||||
option.DNSTransportOptionsRegistry
|
option.DNSTransportOptionsRegistry
|
||||||
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/observable"
|
"github.com/sagernet/sing/common/observable"
|
||||||
@@ -47,12 +46,6 @@ type CacheFile interface {
|
|||||||
StoreRDRC() bool
|
StoreRDRC() bool
|
||||||
RDRCStore
|
RDRCStore
|
||||||
|
|
||||||
StoreDNS() bool
|
|
||||||
DNSCacheStore
|
|
||||||
|
|
||||||
SetDisableExpire(disableExpire bool)
|
|
||||||
SetOptimisticTimeout(timeout time.Duration)
|
|
||||||
|
|
||||||
LoadMode() string
|
LoadMode() string
|
||||||
StoreMode(mode string) error
|
StoreMode(mode string) error
|
||||||
LoadSelected(group string) string
|
LoadSelected(group string) string
|
||||||
@@ -75,11 +68,7 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.Content)))
|
err = varbin.Write(&buffer, binary.BigEndian, s.Content)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = buffer.Write(s.Content)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -87,11 +76,7 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.LastEtag)))
|
err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = buffer.WriteString(s.LastEtag)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -105,12 +90,7 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
contentLength, err := binary.ReadUvarint(reader)
|
err = varbin.Read(reader, binary.BigEndian, &s.Content)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.Content = make([]byte, contentLength)
|
|
||||||
_, err = io.ReadFull(reader, s.Content)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -120,16 +100,10 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.LastUpdated = time.Unix(lastUpdated, 0)
|
s.LastUpdated = time.Unix(lastUpdated, 0)
|
||||||
etagLength, err := binary.ReadUvarint(reader)
|
err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
etagBytes := make([]byte, etagLength)
|
|
||||||
_, err = io.ReadFull(reader, etagBytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.LastEtag = string(etagBytes)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HTTPTransport interface {
|
|
||||||
http.RoundTripper
|
|
||||||
CloseIdleConnections()
|
|
||||||
Clone() HTTPTransport
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPClientManager interface {
|
|
||||||
ResolveTransport(ctx context.Context, logger logger.ContextLogger, options option.HTTPClientOptions) (HTTPTransport, error)
|
|
||||||
DefaultTransport() HTTPTransport
|
|
||||||
ResetNetwork()
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -10,8 +9,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Inbound interface {
|
type Inbound interface {
|
||||||
@@ -65,10 +62,13 @@ type InboundContext struct {
|
|||||||
// cache
|
// cache
|
||||||
|
|
||||||
// Deprecated: implement in rule action
|
// Deprecated: implement in rule action
|
||||||
InboundDetour string
|
InboundDetour string
|
||||||
LastInbound string
|
LastInbound string
|
||||||
OriginDestination M.Socksaddr
|
OriginDestination M.Socksaddr
|
||||||
RouteOriginalDestination M.Socksaddr
|
RouteOriginalDestination M.Socksaddr
|
||||||
|
// Deprecated: to be removed
|
||||||
|
//nolint:staticcheck
|
||||||
|
InboundOptions option.InboundOptions
|
||||||
UDPDisableDomainUnmapping bool
|
UDPDisableDomainUnmapping bool
|
||||||
UDPConnect bool
|
UDPConnect bool
|
||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
@@ -81,16 +81,12 @@ type InboundContext struct {
|
|||||||
FallbackNetworkType []C.InterfaceType
|
FallbackNetworkType []C.InterfaceType
|
||||||
FallbackDelay time.Duration
|
FallbackDelay time.Duration
|
||||||
|
|
||||||
DestinationAddresses []netip.Addr
|
DestinationAddresses []netip.Addr
|
||||||
DNSResponse *dns.Msg
|
SourceGeoIPCode string
|
||||||
DestinationAddressMatchFromResponse bool
|
GeoIPCode string
|
||||||
SourceGeoIPCode string
|
ProcessInfo *ConnectionOwner
|
||||||
GeoIPCode string
|
QueryType uint16
|
||||||
ProcessInfo *ConnectionOwner
|
FakeIP bool
|
||||||
SourceMACAddress net.HardwareAddr
|
|
||||||
SourceHostname string
|
|
||||||
QueryType uint16
|
|
||||||
FakeIP bool
|
|
||||||
|
|
||||||
// rule cache
|
// rule cache
|
||||||
|
|
||||||
@@ -108,10 +104,6 @@ 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
|
||||||
@@ -119,51 +111,6 @@ func (c *InboundContext) ResetRuleMatchCache() {
|
|||||||
c.DidMatch = false
|
c.DidMatch = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
|
|
||||||
return DNSResponseAddresses(c.DNSResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DNSResponseAddresses(response *dns.Msg) []netip.Addr {
|
|
||||||
if response == nil || response.Rcode != dns.RcodeSuccess {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
|
||||||
for _, rawRecord := range response.Answer {
|
|
||||||
switch record := rawRecord.(type) {
|
|
||||||
case *dns.A:
|
|
||||||
addr := M.AddrFromIP(record.A)
|
|
||||||
if addr.IsValid() {
|
|
||||||
addresses = append(addresses, addr)
|
|
||||||
}
|
|
||||||
case *dns.AAAA:
|
|
||||||
addr := M.AddrFromIP(record.AAAA)
|
|
||||||
if addr.IsValid() {
|
|
||||||
addresses = append(addresses, addr)
|
|
||||||
}
|
|
||||||
case *dns.HTTPS:
|
|
||||||
for _, value := range record.SVCB.Value {
|
|
||||||
switch hint := value.(type) {
|
|
||||||
case *dns.SVCBIPv4Hint:
|
|
||||||
for _, ip := range hint.Hint {
|
|
||||||
addr := M.AddrFromIP(ip).Unmap()
|
|
||||||
if addr.IsValid() {
|
|
||||||
addresses = append(addresses, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case *dns.SVCBIPv6Hint:
|
|
||||||
for _, ip := range hint.Hint {
|
|
||||||
addr := M.AddrFromIP(ip)
|
|
||||||
if addr.IsValid() {
|
|
||||||
addresses = append(addresses, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return addresses
|
|
||||||
}
|
|
||||||
|
|
||||||
type inboundContextKey struct{}
|
type inboundContextKey struct{}
|
||||||
|
|
||||||
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
|
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ipv4Hint := net.ParseIP("1.1.1.1")
|
|
||||||
require.NotNil(t, ipv4Hint)
|
|
||||||
|
|
||||||
response := &dns.Msg{
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Response: true,
|
|
||||||
Rcode: dns.RcodeSuccess,
|
|
||||||
},
|
|
||||||
Answer: []dns.RR{
|
|
||||||
&dns.HTTPS{
|
|
||||||
SVCB: dns.SVCB{
|
|
||||||
Hdr: dns.RR_Header{
|
|
||||||
Name: dns.Fqdn("example.com"),
|
|
||||||
Rrtype: dns.TypeHTTPS,
|
|
||||||
Class: dns.ClassINET,
|
|
||||||
Ttl: 60,
|
|
||||||
},
|
|
||||||
Priority: 1,
|
|
||||||
Target: ".",
|
|
||||||
Value: []dns.SVCBKeyValue{
|
|
||||||
&dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
addresses := DNSResponseAddresses(response)
|
|
||||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses)
|
|
||||||
require.True(t, addresses[0].Is4())
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NeighborEntry struct {
|
|
||||||
Address netip.Addr
|
|
||||||
MACAddress net.HardwareAddr
|
|
||||||
Hostname string
|
|
||||||
}
|
|
||||||
|
|
||||||
type NeighborResolver interface {
|
|
||||||
LookupMAC(address netip.Addr) (net.HardwareAddr, bool)
|
|
||||||
LookupHostname(address netip.Addr) (string, bool)
|
|
||||||
Start() error
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type NeighborUpdateListener interface {
|
|
||||||
UpdateNeighborTable(entries []NeighborEntry)
|
|
||||||
}
|
|
||||||
@@ -36,10 +36,6 @@ type PlatformInterface interface {
|
|||||||
|
|
||||||
UsePlatformNotification() bool
|
UsePlatformNotification() bool
|
||||||
SendNotification(notification *Notification) error
|
SendNotification(notification *Notification) error
|
||||||
|
|
||||||
UsePlatformNeighborResolver() bool
|
|
||||||
StartNeighborMonitor(listener NeighborUpdateListener) error
|
|
||||||
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindConnectionOwnerRequest struct {
|
type FindConnectionOwnerRequest struct {
|
||||||
@@ -51,11 +47,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
|
||||||
AndroidPackageNames []string
|
AndroidPackageName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
|
|||||||
@@ -2,11 +2,17 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-tun"
|
||||||
|
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/ntp"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
@@ -19,9 +25,6 @@ type Router interface {
|
|||||||
ConnectionRouterEx
|
ConnectionRouterEx
|
||||||
RuleSet(tag string) (RuleSet, bool)
|
RuleSet(tag string) (RuleSet, bool)
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
NeedFindProcess() bool
|
|
||||||
NeedFindNeighbor() bool
|
|
||||||
NeighborResolver() NeighborResolver
|
|
||||||
AppendTracker(tracker ConnectionTracker)
|
AppendTracker(tracker ConnectionTracker)
|
||||||
ResetNetwork()
|
ResetNetwork()
|
||||||
}
|
}
|
||||||
@@ -45,7 +48,7 @@ type ConnectionRouterEx interface {
|
|||||||
|
|
||||||
type RuleSet interface {
|
type RuleSet interface {
|
||||||
Name() string
|
Name() string
|
||||||
StartContext(ctx context.Context) error
|
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||||
PostStart() error
|
PostStart() error
|
||||||
Metadata() RuleSetMetadata
|
Metadata() RuleSetMetadata
|
||||||
ExtractIPSet() []*netipx.IPSet
|
ExtractIPSet() []*netipx.IPSet
|
||||||
@@ -60,14 +63,51 @@ type RuleSet interface {
|
|||||||
|
|
||||||
type RuleSetUpdateCallback func(it RuleSet)
|
type RuleSetUpdateCallback func(it RuleSet)
|
||||||
|
|
||||||
type DNSRuleSetUpdateValidator interface {
|
type RuleSetMetadata struct {
|
||||||
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
|
ContainsProcessRule bool
|
||||||
|
ContainsWIFIRule bool
|
||||||
|
ContainsIPCIDRRule bool
|
||||||
|
}
|
||||||
|
type HTTPStartContext struct {
|
||||||
|
ctx context.Context
|
||||||
|
access sync.Mutex
|
||||||
|
httpClientCache map[string]*http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
|
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||||
type RuleSetMetadata struct {
|
return &HTTPStartContext{
|
||||||
ContainsProcessRule bool
|
ctx: ctx,
|
||||||
ContainsWIFIRule bool
|
httpClientCache: make(map[string]*http.Client),
|
||||||
ContainsIPCIDRRule bool
|
}
|
||||||
ContainsDNSQueryTypeRule bool
|
}
|
||||||
|
|
||||||
|
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
if httpClient, loaded := c.httpClientCache[detour]; loaded {
|
||||||
|
return httpClient
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
TLSHandshakeTimeout: C.TCPTimeout,
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
|
},
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||||
|
RootCAs: RootPoolFromContext(c.ctx),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.httpClientCache[detour] = httpClient
|
||||||
|
return httpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HTTPStartContext) Close() {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
for _, client := range c.httpClientCache {
|
||||||
|
client.CloseIdleConnections()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type HeadlessRule interface {
|
type HeadlessRule interface {
|
||||||
@@ -20,9 +18,8 @@ type Rule interface {
|
|||||||
|
|
||||||
type DNSRule interface {
|
type DNSRule interface {
|
||||||
Rule
|
Rule
|
||||||
LegacyPreMatch(metadata *InboundContext) bool
|
|
||||||
WithAddressLimit() bool
|
WithAddressLimit() bool
|
||||||
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
|
MatchAddressLimit(metadata *InboundContext) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleAction interface {
|
type RuleAction interface {
|
||||||
@@ -32,7 +29,7 @@ type RuleAction interface {
|
|||||||
|
|
||||||
func IsFinalAction(action RuleAction) bool {
|
func IsFinalAction(action RuleAction) bool {
|
||||||
switch action.Type() {
|
switch action.Type() {
|
||||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
|
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
|
||||||
return false
|
return false
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type TailscaleEndpoint interface {
|
|
||||||
SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error
|
|
||||||
StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type TailscalePingResult struct {
|
|
||||||
LatencyMs float64
|
|
||||||
IsDirect bool
|
|
||||||
Endpoint string
|
|
||||||
DERPRegionID int32
|
|
||||||
DERPRegionCode string
|
|
||||||
Error string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TailscaleEndpointStatus struct {
|
|
||||||
BackendState string
|
|
||||||
AuthURL string
|
|
||||||
NetworkName string
|
|
||||||
MagicDNSSuffix string
|
|
||||||
Self *TailscalePeer
|
|
||||||
UserGroups []*TailscaleUserGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
type TailscaleUserGroup struct {
|
|
||||||
UserID int64
|
|
||||||
LoginName string
|
|
||||||
DisplayName string
|
|
||||||
ProfilePicURL string
|
|
||||||
Peers []*TailscalePeer
|
|
||||||
}
|
|
||||||
|
|
||||||
type TailscalePeer struct {
|
|
||||||
HostName string
|
|
||||||
DNSName string
|
|
||||||
OS string
|
|
||||||
TailscaleIPs []string
|
|
||||||
Online bool
|
|
||||||
ExitNode bool
|
|
||||||
ExitNodeOption bool
|
|
||||||
Active bool
|
|
||||||
RxBytes int64
|
|
||||||
TxBytes int64
|
|
||||||
UserID int64
|
|
||||||
KeyExpiry int64
|
|
||||||
}
|
|
||||||
181
box.go
181
box.go
@@ -9,21 +9,19 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
boxCertificate "github.com/sagernet/sing-box/adapter/certificate"
|
|
||||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
"github.com/sagernet/sing-box/adapter/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"
|
||||||
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"
|
||||||
"github.com/sagernet/sing-box/common/httpclient"
|
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
"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"
|
"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/experimental/deprecated"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/protocol/direct"
|
"github.com/sagernet/sing-box/protocol/direct"
|
||||||
@@ -39,22 +37,20 @@ import (
|
|||||||
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||||
|
|
||||||
type Box struct {
|
type Box struct {
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
logFactory log.Factory
|
logFactory log.Factory
|
||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
network *route.NetworkManager
|
network *route.NetworkManager
|
||||||
endpoint *endpoint.Manager
|
endpoint *endpoint.Manager
|
||||||
inbound *inbound.Manager
|
inbound *inbound.Manager
|
||||||
outbound *outbound.Manager
|
outbound *outbound.Manager
|
||||||
service *boxService.Manager
|
service *boxService.Manager
|
||||||
certificateProvider *boxCertificate.Manager
|
dnsTransport *dns.TransportManager
|
||||||
dnsTransport *dns.TransportManager
|
dnsRouter *dns.Router
|
||||||
dnsRouter *dns.Router
|
connection *route.ConnectionManager
|
||||||
connection *route.ConnectionManager
|
router *route.Router
|
||||||
router *route.Router
|
internalService []adapter.LifecycleService
|
||||||
httpClientService adapter.LifecycleService
|
done chan struct{}
|
||||||
internalService []adapter.LifecycleService
|
|
||||||
done chan struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -70,7 +66,6 @@ func Context(
|
|||||||
endpointRegistry adapter.EndpointRegistry,
|
endpointRegistry adapter.EndpointRegistry,
|
||||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||||
serviceRegistry adapter.ServiceRegistry,
|
serviceRegistry adapter.ServiceRegistry,
|
||||||
certificateProviderRegistry adapter.CertificateProviderRegistry,
|
|
||||||
) context.Context {
|
) context.Context {
|
||||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||||
@@ -95,10 +90,6 @@ func Context(
|
|||||||
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||||
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
||||||
}
|
}
|
||||||
if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil {
|
|
||||||
ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry)
|
|
||||||
ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry)
|
|
||||||
}
|
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +106,6 @@ func New(options Options) (*Box, error) {
|
|||||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
outboundRegistry := service.FromContext[adapter.OutboundRegistry](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)
|
||||||
certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
|
|
||||||
|
|
||||||
if endpointRegistry == nil {
|
if endpointRegistry == nil {
|
||||||
return nil, E.New("missing endpoint registry in context")
|
return nil, E.New("missing endpoint registry in context")
|
||||||
@@ -132,16 +122,10 @@ func New(options Options) (*Box, error) {
|
|||||||
if serviceRegistry == nil {
|
if serviceRegistry == nil {
|
||||||
return nil, E.New("missing service registry in context")
|
return nil, E.New("missing service registry in context")
|
||||||
}
|
}
|
||||||
if certificateProviderRegistry == nil {
|
|
||||||
return nil, E.New("missing certificate provider registry in context")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = pause.WithDefaultManager(ctx)
|
ctx = pause.WithDefaultManager(ctx)
|
||||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||||
err := applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var needCacheFile bool
|
var needCacheFile bool
|
||||||
var needClashAPI bool
|
var needClashAPI bool
|
||||||
var needV2RayAPI bool
|
var needV2RayAPI bool
|
||||||
@@ -172,7 +156,6 @@ func New(options Options) (*Box, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var internalServices []adapter.LifecycleService
|
var internalServices []adapter.LifecycleService
|
||||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
|
||||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||||
len(certificateOptions.Certificate) > 0 ||
|
len(certificateOptions.Certificate) > 0 ||
|
||||||
@@ -185,25 +168,21 @@ func New(options Options) (*Box, error) {
|
|||||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||||
internalServices = append(internalServices, certificateStore)
|
internalServices = append(internalServices, certificateStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||||
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)
|
||||||
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)
|
||||||
certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry)
|
|
||||||
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.DNSTransportManager](ctx, dnsTransportManager)
|
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||||
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||||
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "initialize DNS router")
|
|
||||||
}
|
|
||||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||||
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
|
||||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize network manager")
|
return nil, E.Cause(err, "initialize network manager")
|
||||||
@@ -211,10 +190,6 @@ func New(options Options) (*Box, error) {
|
|||||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||||
// Must register after ConnectionManager: the Apple HTTP engine's proxy bridge reads it from the context when Manager.Start resolves the default client.
|
|
||||||
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
|
||||||
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
|
||||||
httpClientService := adapter.LifecycleService(httpClientManager)
|
|
||||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||||
service.MustRegister[adapter.Router](ctx, router)
|
service.MustRegister[adapter.Router](ctx, router)
|
||||||
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
||||||
@@ -294,24 +269,6 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, serviceOptions := range options.Services {
|
|
||||||
var tag string
|
|
||||||
if serviceOptions.Tag != "" {
|
|
||||||
tag = serviceOptions.Tag
|
|
||||||
} else {
|
|
||||||
tag = F.ToString(i)
|
|
||||||
}
|
|
||||||
err = serviceManager.Create(
|
|
||||||
ctx,
|
|
||||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
|
||||||
tag,
|
|
||||||
serviceOptions.Type,
|
|
||||||
serviceOptions.Options,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, outboundOptions := range options.Outbounds {
|
for i, outboundOptions := range options.Outbounds {
|
||||||
var tag string
|
var tag string
|
||||||
if outboundOptions.Tag != "" {
|
if outboundOptions.Tag != "" {
|
||||||
@@ -338,22 +295,22 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, certificateProviderOptions := range options.CertificateProviders {
|
for i, serviceOptions := range options.Services {
|
||||||
var tag string
|
var tag string
|
||||||
if certificateProviderOptions.Tag != "" {
|
if serviceOptions.Tag != "" {
|
||||||
tag = certificateProviderOptions.Tag
|
tag = serviceOptions.Tag
|
||||||
} else {
|
} else {
|
||||||
tag = F.ToString(i)
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
err = certificateProviderManager.Create(
|
err = serviceManager.Create(
|
||||||
ctx,
|
ctx,
|
||||||
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||||
tag,
|
tag,
|
||||||
certificateProviderOptions.Type,
|
serviceOptions.Type,
|
||||||
certificateProviderOptions.Options,
|
serviceOptions.Options,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize certificate provider[", i, "]")
|
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||||
@@ -366,20 +323,13 @@ func New(options Options) (*Box, error) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||||
return dnsTransportRegistry.CreateDNSTransport(
|
return local.NewTransport(
|
||||||
ctx,
|
ctx,
|
||||||
logFactory.NewLogger("dns/local"),
|
logFactory.NewLogger("dns/local"),
|
||||||
"local",
|
"local",
|
||||||
C.DNSTypeLocal,
|
option.LocalDNSServerOptions{},
|
||||||
&option.LocalDNSServerOptions{},
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
httpClientManager.Initialize(func() (*httpclient.Transport, error) {
|
|
||||||
deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient)
|
|
||||||
var httpClientOptions option.HTTPClientOptions
|
|
||||||
httpClientOptions.DefaultOutbound = true
|
|
||||||
return httpclient.NewTransport(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
|
|
||||||
})
|
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
err = platformInterface.Initialize(networkManager)
|
err = platformInterface.Initialize(networkManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -387,7 +337,7 @@ func New(options Options) (*Box, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if needCacheFile {
|
if needCacheFile {
|
||||||
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||||
internalServices = append(internalServices, cacheFile)
|
internalServices = append(internalServices, cacheFile)
|
||||||
}
|
}
|
||||||
@@ -430,22 +380,20 @@ func New(options Options) (*Box, error) {
|
|||||||
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||||
}
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
network: networkManager,
|
network: networkManager,
|
||||||
endpoint: endpointManager,
|
endpoint: endpointManager,
|
||||||
inbound: inboundManager,
|
inbound: inboundManager,
|
||||||
outbound: outboundManager,
|
outbound: outboundManager,
|
||||||
dnsTransport: dnsTransportManager,
|
dnsTransport: dnsTransportManager,
|
||||||
service: serviceManager,
|
service: serviceManager,
|
||||||
certificateProvider: certificateProviderManager,
|
dnsRouter: dnsRouter,
|
||||||
dnsRouter: dnsRouter,
|
connection: connectionManager,
|
||||||
connection: connectionManager,
|
router: router,
|
||||||
router: router,
|
createdAt: createdAt,
|
||||||
httpClientService: httpClientService,
|
logFactory: logFactory,
|
||||||
createdAt: createdAt,
|
logger: logFactory.Logger(),
|
||||||
logFactory: logFactory,
|
internalService: internalServices,
|
||||||
logger: logFactory.Logger(),
|
done: make(chan struct{}),
|
||||||
internalService: internalServices,
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,19 +447,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, s.certificateProvider)
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection)
|
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = adapter.StartNamed(s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -527,19 +467,11 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.endpoint)
|
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.certificateProvider)
|
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)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.service)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -547,7 +479,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.endpoint, s.certificateProvider, s.inbound, 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.service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -571,9 +503,8 @@ func (s *Box) Close() error {
|
|||||||
service adapter.Lifecycle
|
service adapter.Lifecycle
|
||||||
}{
|
}{
|
||||||
{"service", s.service},
|
{"service", s.service},
|
||||||
{"inbound", s.inbound},
|
|
||||||
{"certificate-provider", s.certificateProvider},
|
|
||||||
{"endpoint", s.endpoint},
|
{"endpoint", s.endpoint},
|
||||||
|
{"inbound", s.inbound},
|
||||||
{"outbound", s.outbound},
|
{"outbound", s.outbound},
|
||||||
{"router", s.router},
|
{"router", s.router},
|
||||||
{"connection", s.connection},
|
{"connection", s.connection},
|
||||||
@@ -588,14 +519,6 @@ func (s *Box) Close() error {
|
|||||||
})
|
})
|
||||||
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||||
}
|
}
|
||||||
if s.httpClientService != nil {
|
|
||||||
s.logger.Trace("close ", s.httpClientService.Name())
|
|
||||||
startTime := time.Now()
|
|
||||||
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close ", s.httpClientService.Name())
|
|
||||||
})
|
|
||||||
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
|
||||||
for _, lifecycleService := range s.internalService {
|
for _, lifecycleService := range s.internalService {
|
||||||
s.logger.Trace("close ", lifecycleService.Name())
|
s.logger.Trace("close ", lifecycleService.Name())
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
@@ -629,10 +552,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule clients/android updated: fea0f3a7ba...fe128a6cd7
Submodule clients/apple updated: ffbf405b52...532c140f05
@@ -100,32 +100,11 @@ findVersion:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func publishTestflight(ctx context.Context) error {
|
func publishTestflight(ctx context.Context) error {
|
||||||
if len(os.Args) < 3 {
|
|
||||||
return E.New("platform required: ios, macos, or tvos")
|
|
||||||
}
|
|
||||||
var platform asc.Platform
|
|
||||||
switch os.Args[2] {
|
|
||||||
case "ios":
|
|
||||||
platform = asc.PlatformIOS
|
|
||||||
case "macos":
|
|
||||||
platform = asc.PlatformMACOS
|
|
||||||
case "tvos":
|
|
||||||
platform = asc.PlatformTVOS
|
|
||||||
default:
|
|
||||||
return E.New("unknown platform: ", os.Args[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
tagVersion, err := build_shared.ReadTagVersion()
|
tagVersion, err := build_shared.ReadTagVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tag := tagVersion.VersionString()
|
tag := tagVersion.VersionString()
|
||||||
|
|
||||||
releaseNotes := F.ToString("sing-box ", tagVersion.String())
|
|
||||||
if len(os.Args) >= 4 {
|
|
||||||
releaseNotes = strings.Join(os.Args[3:], " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
client := createClient(20 * time.Minute)
|
client := createClient(20 * time.Minute)
|
||||||
|
|
||||||
log.Info(tag, " list build IDs")
|
log.Info(tag, " list build IDs")
|
||||||
@@ -136,76 +115,97 @@ func publishTestflight(ctx context.Context) error {
|
|||||||
buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
|
buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
|
||||||
return it.ID
|
return it.ID
|
||||||
})
|
})
|
||||||
|
var platforms []asc.Platform
|
||||||
|
if len(os.Args) == 3 {
|
||||||
|
switch os.Args[2] {
|
||||||
|
case "ios":
|
||||||
|
platforms = []asc.Platform{asc.PlatformIOS}
|
||||||
|
case "macos":
|
||||||
|
platforms = []asc.Platform{asc.PlatformMACOS}
|
||||||
|
case "tvos":
|
||||||
|
platforms = []asc.Platform{asc.PlatformTVOS}
|
||||||
|
default:
|
||||||
|
return E.New("unknown platform: ", os.Args[2])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
platforms = []asc.Platform{
|
||||||
|
asc.PlatformIOS,
|
||||||
|
asc.PlatformMACOS,
|
||||||
|
asc.PlatformTVOS,
|
||||||
|
}
|
||||||
|
}
|
||||||
waitingForProcess := false
|
waitingForProcess := false
|
||||||
log.Info(string(platform), " list builds")
|
for _, platform := range platforms {
|
||||||
for {
|
log.Info(string(platform), " list builds")
|
||||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
for {
|
||||||
FilterApp: []string{appID},
|
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
FilterApp: []string{appID},
|
||||||
})
|
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||||
if err != nil {
|
})
|
||||||
return err
|
|
||||||
}
|
|
||||||
build := builds.Data[0]
|
|
||||||
log.Info(string(platform), " ", tag, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
|
|
||||||
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
|
|
||||||
log.Info(string(platform), " ", tag, " waiting for process")
|
|
||||||
time.Sleep(15 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if *build.Attributes.ProcessingState != "VALID" {
|
|
||||||
waitingForProcess = true
|
|
||||||
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
|
||||||
time.Sleep(15 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " list localizations")
|
|
||||||
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
|
|
||||||
return *it.Attributes.Locale == "en-US"
|
|
||||||
})
|
|
||||||
if localization.ID == "" {
|
|
||||||
log.Fatal(string(platform), " ", tag, " no en-US localization found")
|
|
||||||
}
|
|
||||||
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
|
||||||
log.Info(string(platform), " ", tag, " update localization")
|
|
||||||
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(releaseNotes))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
build := builds.Data[0]
|
||||||
log.Info(string(platform), " ", tag, " publish")
|
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
|
||||||
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
|
log.Info(string(platform), " ", tag, " waiting for process")
|
||||||
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
|
time.Sleep(15 * time.Second)
|
||||||
log.Info("waiting for process")
|
continue
|
||||||
time.Sleep(15 * time.Second)
|
}
|
||||||
continue
|
if *build.Attributes.ProcessingState != "VALID" {
|
||||||
} else if err != nil {
|
waitingForProcess = true
|
||||||
return err
|
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
||||||
}
|
time.Sleep(15 * time.Second)
|
||||||
log.Info(string(platform), " ", tag, " list submissions")
|
continue
|
||||||
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
|
}
|
||||||
FilterBuild: []string{build.ID},
|
log.Info(string(platform), " ", tag, " list localizations")
|
||||||
})
|
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(betaSubmissions.Data) == 0 {
|
|
||||||
log.Info(string(platform), " ", tag, " create submission")
|
|
||||||
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
|
return err
|
||||||
log.Error(err)
|
}
|
||||||
break
|
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
|
||||||
|
return *it.Attributes.Locale == "en-US"
|
||||||
|
})
|
||||||
|
if localization.ID == "" {
|
||||||
|
log.Fatal(string(platform), " ", tag, " no en-US localization found")
|
||||||
|
}
|
||||||
|
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
||||||
|
log.Info(string(platform), " ", tag, " update localization")
|
||||||
|
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(
|
||||||
|
F.ToString("sing-box ", tagVersion.String()),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
log.Info(string(platform), " ", tag, " publish")
|
||||||
|
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
|
||||||
|
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
|
||||||
|
log.Info("waiting for process")
|
||||||
|
time.Sleep(15 * time.Second)
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log.Info(string(platform), " ", tag, " list submissions")
|
||||||
|
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
|
||||||
|
FilterBuild: []string{build.ID},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(betaSubmissions.Data) == 0 {
|
||||||
|
log.Info(string(platform), " ", tag, " create submission")
|
||||||
|
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
|
||||||
|
log.Error(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
debugEnabled bool
|
debugEnabled bool
|
||||||
target string
|
target string
|
||||||
platform string
|
platform string
|
||||||
// withTailscale bool
|
withTailscale bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
||||||
flag.StringVar(&target, "target", "android", "target platform")
|
flag.StringVar(&target, "target", "android", "target platform")
|
||||||
flag.StringVar(&platform, "platform", "", "specify platform")
|
flag.StringVar(&platform, "platform", "", "specify platform")
|
||||||
// flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
|
flag.BoolVar(&withTailscale, "with-tailscale", false, "build tailscale for iOS and tvOS")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -48,7 +48,7 @@ var (
|
|||||||
debugFlags []string
|
debugFlags []string
|
||||||
sharedTags []string
|
sharedTags []string
|
||||||
darwinTags []string
|
darwinTags []string
|
||||||
// memcTags []string
|
memcTags []string
|
||||||
notMemcTags []string
|
notMemcTags []string
|
||||||
debugTags []string
|
debugTags []string
|
||||||
)
|
)
|
||||||
@@ -63,10 +63,9 @@ func init() {
|
|||||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0")
|
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0")
|
||||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
|
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0")
|
||||||
|
|
||||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0")
|
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0")
|
||||||
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
|
darwinTags = append(darwinTags, "with_dhcp")
|
||||||
// memcTags = append(memcTags, "with_tailscale")
|
memcTags = append(memcTags, "with_tailscale")
|
||||||
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
|
|
||||||
notMemcTags = append(notMemcTags, "with_low_memory")
|
notMemcTags = append(notMemcTags, "with_low_memory")
|
||||||
debugTags = append(debugTags, "debug")
|
debugTags = append(debugTags, "debug")
|
||||||
}
|
}
|
||||||
@@ -165,7 +164,7 @@ func buildAndroid() {
|
|||||||
|
|
||||||
// Build main variant (SDK 23)
|
// Build main variant (SDK 23)
|
||||||
mainTags := append([]string{}, sharedTags...)
|
mainTags := append([]string{}, sharedTags...)
|
||||||
// mainTags = append(mainTags, memcTags...)
|
mainTags = append(mainTags, memcTags...)
|
||||||
if debugEnabled {
|
if debugEnabled {
|
||||||
mainTags = append(mainTags, debugTags...)
|
mainTags = append(mainTags, debugTags...)
|
||||||
}
|
}
|
||||||
@@ -177,7 +176,7 @@ func buildAndroid() {
|
|||||||
|
|
||||||
// Build legacy variant (SDK 21, no naive outbound)
|
// Build legacy variant (SDK 21, no naive outbound)
|
||||||
legacyTags := filterTags(sharedTags, "with_naive_outbound")
|
legacyTags := filterTags(sharedTags, "with_naive_outbound")
|
||||||
// legacyTags = append(legacyTags, memcTags...)
|
legacyTags = append(legacyTags, memcTags...)
|
||||||
if debugEnabled {
|
if debugEnabled {
|
||||||
legacyTags = append(legacyTags, debugTags...)
|
legacyTags = append(legacyTags, debugTags...)
|
||||||
}
|
}
|
||||||
@@ -195,7 +194,7 @@ func buildApple() {
|
|||||||
} else if debugEnabled {
|
} else if debugEnabled {
|
||||||
bindTarget = "ios"
|
bindTarget = "ios"
|
||||||
} else {
|
} else {
|
||||||
bindTarget = "ios,iossimulator,tvos,tvossimulator,macos"
|
bindTarget = "ios,tvos,macos"
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
@@ -204,13 +203,10 @@ func buildApple() {
|
|||||||
"-target", bindTarget,
|
"-target", bindTarget,
|
||||||
"-libname=box",
|
"-libname=box",
|
||||||
"-tags-not-macos=with_low_memory",
|
"-tags-not-macos=with_low_memory",
|
||||||
"-iosversion=15.0",
|
|
||||||
"-macosversion=13.0",
|
|
||||||
"-tvosversion=17.0",
|
|
||||||
}
|
}
|
||||||
//if !withTailscale {
|
if !withTailscale {
|
||||||
// args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
|
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
|
||||||
//}
|
}
|
||||||
|
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
args = append(args, sharedFlags...)
|
args = append(args, sharedFlags...)
|
||||||
@@ -219,9 +215,9 @@ func buildApple() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tags := append(sharedTags, darwinTags...)
|
tags := append(sharedTags, darwinTags...)
|
||||||
//if withTailscale {
|
if withTailscale {
|
||||||
// tags = append(tags, memcTags...)
|
tags = append(tags, memcTags...)
|
||||||
//}
|
}
|
||||||
if debugEnabled {
|
if debugEnabled {
|
||||||
tags = append(tags, debugTags...)
|
tags = append(tags, debugTags...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,12 +71,12 @@ func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDLi
|
|||||||
indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
|
indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
|
||||||
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20
|
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20
|
||||||
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
|
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
|
||||||
version := strings.Trim(projectContent[versionStart:versionEnd], "\"")
|
version := projectContent[versionStart:versionEnd]
|
||||||
if version == newVersion {
|
if version == newVersion {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
updated = true
|
updated = true
|
||||||
projectContent = projectContent[:versionStart] + "\"" + newVersion + "\"" + projectContent[versionEnd:]
|
projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:]
|
||||||
}
|
}
|
||||||
return projectContent, updated
|
return projectContent, updated
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@@ -36,9 +35,21 @@ func updateMozillaIncludedRootCAs() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
geoIndex := slices.Index(header, "Geographic Focus")
|
geoIndex := slices.Index(header, "Geographic Focus")
|
||||||
|
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||||
certIndex := slices.Index(header, "PEM Info")
|
certIndex := slices.Index(header, "PEM Info")
|
||||||
|
|
||||||
pemBundle := strings.Builder{}
|
generated := strings.Builder{}
|
||||||
|
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||||
|
|
||||||
|
package certificate
|
||||||
|
|
||||||
|
import "crypto/x509"
|
||||||
|
|
||||||
|
var mozillaIncluded *x509.CertPool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mozillaIncluded = x509.NewCertPool()
|
||||||
|
`)
|
||||||
for {
|
for {
|
||||||
record, err := reader.Read()
|
record, err := reader.Read()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@@ -49,12 +60,18 @@ func updateMozillaIncludedRootCAs() error {
|
|||||||
if record[geoIndex] == "China" {
|
if record[geoIndex] == "China" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
generated.WriteString("\n // ")
|
||||||
|
generated.WriteString(record[nameIndex])
|
||||||
|
generated.WriteString("\n")
|
||||||
|
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||||
cert := record[certIndex]
|
cert := record[certIndex]
|
||||||
|
// Remove single quotes
|
||||||
cert = cert[1 : len(cert)-1]
|
cert = cert[1 : len(cert)-1]
|
||||||
pemBundle.WriteString(cert)
|
generated.WriteString(cert)
|
||||||
pemBundle.WriteString("\n")
|
generated.WriteString("`))\n")
|
||||||
}
|
}
|
||||||
return writeGeneratedCertificateBundle("mozilla", "mozillaIncluded", pemBundle.String())
|
generated.WriteString("}\n")
|
||||||
|
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchChinaFingerprints() (map[string]bool, error) {
|
func fetchChinaFingerprints() (map[string]bool, error) {
|
||||||
@@ -102,11 +119,23 @@ func updateChromeIncludedRootCAs() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
subjectIndex := slices.Index(header, "Subject")
|
||||||
statusIndex := slices.Index(header, "Google Chrome Status")
|
statusIndex := slices.Index(header, "Google Chrome Status")
|
||||||
certIndex := slices.Index(header, "X.509 Certificate (PEM)")
|
certIndex := slices.Index(header, "X.509 Certificate (PEM)")
|
||||||
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
|
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
|
||||||
|
|
||||||
pemBundle := strings.Builder{}
|
generated := strings.Builder{}
|
||||||
|
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||||
|
|
||||||
|
package certificate
|
||||||
|
|
||||||
|
import "crypto/x509"
|
||||||
|
|
||||||
|
var chromeIncluded *x509.CertPool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
chromeIncluded = x509.NewCertPool()
|
||||||
|
`)
|
||||||
for {
|
for {
|
||||||
record, err := reader.Read()
|
record, err := reader.Read()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@@ -120,39 +149,18 @@ func updateChromeIncludedRootCAs() error {
|
|||||||
if chinaFingerprints[record[fingerprintIndex]] {
|
if chinaFingerprints[record[fingerprintIndex]] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
generated.WriteString("\n // ")
|
||||||
|
generated.WriteString(record[subjectIndex])
|
||||||
|
generated.WriteString("\n")
|
||||||
|
generated.WriteString(" chromeIncluded.AppendCertsFromPEM([]byte(`")
|
||||||
cert := record[certIndex]
|
cert := record[certIndex]
|
||||||
|
// Remove single quotes if present
|
||||||
if len(cert) > 0 && cert[0] == '\'' {
|
if len(cert) > 0 && cert[0] == '\'' {
|
||||||
cert = cert[1 : len(cert)-1]
|
cert = cert[1 : len(cert)-1]
|
||||||
}
|
}
|
||||||
pemBundle.WriteString(cert)
|
generated.WriteString(cert)
|
||||||
pemBundle.WriteString("\n")
|
generated.WriteString("`))\n")
|
||||||
}
|
}
|
||||||
return writeGeneratedCertificateBundle("chrome", "chromeIncluded", pemBundle.String())
|
generated.WriteString("}\n")
|
||||||
}
|
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
|
||||||
|
|
||||||
func writeGeneratedCertificateBundle(name string, variableName string, pemBundle string) error {
|
|
||||||
goSource := `// Code generated by 'make update_certificates'. DO NOT EDIT.
|
|
||||||
|
|
||||||
package certificate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/x509"
|
|
||||||
_ "embed"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed ` + name + `.pem
|
|
||||||
var ` + variableName + `PEM string
|
|
||||||
|
|
||||||
var ` + variableName + ` *x509.CertPool
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
` + variableName + ` = x509.NewCertPool()
|
|
||||||
` + variableName + `.AppendCertsFromPEM([]byte(` + variableName + `PEM))
|
|
||||||
}
|
|
||||||
`
|
|
||||||
err := os.WriteFile(filepath.Join("common/certificate", name+".pem"), []byte(pemBundle), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(filepath.Join("common/certificate", name+".go"), []byte(goSource), 0o644)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,11 +82,6 @@ func compileRuleSet(sourcePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
|
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
|
||||||
if version == C.RuleSetVersion5 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
|
||||||
return len(rule.PackageNameRegex) > 0
|
|
||||||
}) {
|
|
||||||
version = C.RuleSetVersion4
|
|
||||||
}
|
|
||||||
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||||
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
||||||
len(rule.DefaultInterfaceAddress) > 0
|
len(rule.DefaultInterfaceAddress) > 0
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/networkquality"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
commandNetworkQualityFlagConfigURL string
|
|
||||||
commandNetworkQualityFlagSerial bool
|
|
||||||
commandNetworkQualityFlagMaxRuntime int
|
|
||||||
commandNetworkQualityFlagHTTP3 bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandNetworkQuality = &cobra.Command{
|
|
||||||
Use: "networkquality",
|
|
||||||
Short: "Run a network quality test",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := runNetworkQuality()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandNetworkQuality.Flags().StringVar(
|
|
||||||
&commandNetworkQualityFlagConfigURL,
|
|
||||||
"config-url", "",
|
|
||||||
"Network quality test config URL (default: Apple mensura)",
|
|
||||||
)
|
|
||||||
commandNetworkQuality.Flags().BoolVar(
|
|
||||||
&commandNetworkQualityFlagSerial,
|
|
||||||
"serial", false,
|
|
||||||
"Run download and upload tests sequentially instead of in parallel",
|
|
||||||
)
|
|
||||||
commandNetworkQuality.Flags().IntVar(
|
|
||||||
&commandNetworkQualityFlagMaxRuntime,
|
|
||||||
"max-runtime", int(networkquality.DefaultMaxRuntime/time.Second),
|
|
||||||
"Network quality maximum runtime in seconds",
|
|
||||||
)
|
|
||||||
commandNetworkQuality.Flags().BoolVar(
|
|
||||||
&commandNetworkQualityFlagHTTP3,
|
|
||||||
"http3", false,
|
|
||||||
"Use HTTP/3 (QUIC) for measurement traffic",
|
|
||||||
)
|
|
||||||
commandTools.AddCommand(commandNetworkQuality)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runNetworkQuality() error {
|
|
||||||
instance, err := createPreStartedClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer instance.Close()
|
|
||||||
|
|
||||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient := networkquality.NewHTTPClient(dialer)
|
|
||||||
defer httpClient.CloseIdleConnections()
|
|
||||||
|
|
||||||
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(dialer, commandNetworkQualityFlagHTTP3)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr, "==== NETWORK QUALITY TEST ====")
|
|
||||||
|
|
||||||
result, err := networkquality.Run(networkquality.Options{
|
|
||||||
ConfigURL: commandNetworkQualityFlagConfigURL,
|
|
||||||
HTTPClient: httpClient,
|
|
||||||
NewMeasurementClient: measurementClientFactory,
|
|
||||||
Serial: commandNetworkQualityFlagSerial,
|
|
||||||
MaxRuntime: time.Duration(commandNetworkQualityFlagMaxRuntime) * time.Second,
|
|
||||||
Context: globalCtx,
|
|
||||||
OnProgress: func(p networkquality.Progress) {
|
|
||||||
if !commandNetworkQualityFlagSerial && p.Phase != networkquality.PhaseIdle {
|
|
||||||
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d Upload: %s RPM: %d",
|
|
||||||
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM,
|
|
||||||
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch networkquality.Phase(p.Phase) {
|
|
||||||
case networkquality.PhaseIdle:
|
|
||||||
if p.IdleLatencyMs > 0 {
|
|
||||||
fmt.Fprintf(os.Stderr, "\rIdle Latency: %d ms", p.IdleLatencyMs)
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(os.Stderr, "\rMeasuring idle latency...")
|
|
||||||
}
|
|
||||||
case networkquality.PhaseDownload:
|
|
||||||
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d",
|
|
||||||
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM)
|
|
||||||
case networkquality.PhaseUpload:
|
|
||||||
fmt.Fprintf(os.Stderr, "\rUpload: %s RPM: %d",
|
|
||||||
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr)
|
|
||||||
fmt.Fprintln(os.Stderr, strings.Repeat("-", 40))
|
|
||||||
fmt.Fprintf(os.Stderr, "Idle Latency: %d ms\n", result.IdleLatencyMs)
|
|
||||||
fmt.Fprintf(os.Stderr, "Download Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.DownloadCapacity), result.DownloadCapacityAccuracy)
|
|
||||||
fmt.Fprintf(os.Stderr, "Upload Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.UploadCapacity), result.UploadCapacityAccuracy)
|
|
||||||
fmt.Fprintf(os.Stderr, "Download Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.DownloadRPM), result.DownloadRPMAccuracy)
|
|
||||||
fmt.Fprintf(os.Stderr, "Upload Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.UploadRPM), result.UploadRPMAccuracy)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/stun"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandSTUNFlagServer string
|
|
||||||
|
|
||||||
var commandSTUN = &cobra.Command{
|
|
||||||
Use: "stun",
|
|
||||||
Short: "Run a STUN test",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := runSTUN()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandSTUN.Flags().StringVarP(&commandSTUNFlagServer, "server", "s", stun.DefaultServer, "STUN server address")
|
|
||||||
commandTools.AddCommand(commandSTUN)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSTUN() error {
|
|
||||||
instance, err := createPreStartedClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer instance.Close()
|
|
||||||
|
|
||||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr, "==== STUN TEST ====")
|
|
||||||
|
|
||||||
result, err := stun.Run(stun.Options{
|
|
||||||
Server: commandSTUNFlagServer,
|
|
||||||
Dialer: dialer,
|
|
||||||
Context: globalCtx,
|
|
||||||
OnProgress: func(p stun.Progress) {
|
|
||||||
switch p.Phase {
|
|
||||||
case stun.PhaseBinding:
|
|
||||||
if p.ExternalAddr != "" {
|
|
||||||
fmt.Fprintf(os.Stderr, "\rExternal Address: %s (%d ms)", p.ExternalAddr, p.LatencyMs)
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(os.Stderr, "\rSending binding request...")
|
|
||||||
}
|
|
||||||
case stun.PhaseNATMapping:
|
|
||||||
fmt.Fprint(os.Stderr, "\rDetecting NAT mapping behavior...")
|
|
||||||
case stun.PhaseNATFiltering:
|
|
||||||
fmt.Fprint(os.Stderr, "\rDetecting NAT filtering behavior...")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr)
|
|
||||||
fmt.Fprintf(os.Stderr, "External Address: %s\n", result.ExternalAddr)
|
|
||||||
fmt.Fprintf(os.Stderr, "Latency: %d ms\n", result.LatencyMs)
|
|
||||||
if result.NATTypeSupported {
|
|
||||||
fmt.Fprintf(os.Stderr, "NAT Mapping: %s\n", result.NATMapping)
|
|
||||||
fmt.Fprintf(os.Stderr, "NAT Filtering: %s\n", result.NATFiltering)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintln(os.Stderr, "NAT Type Detection: not supported by server")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -22,10 +22,8 @@ var _ adapter.CertificateStore = (*Store)(nil)
|
|||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
access sync.RWMutex
|
access sync.RWMutex
|
||||||
store string
|
|
||||||
systemPool *x509.CertPool
|
systemPool *x509.CertPool
|
||||||
currentPool *x509.CertPool
|
currentPool *x509.CertPool
|
||||||
currentPEM []string
|
|
||||||
certificate string
|
certificate string
|
||||||
certificatePaths []string
|
certificatePaths []string
|
||||||
certificateDirectoryPaths []string
|
certificateDirectoryPaths []string
|
||||||
@@ -63,7 +61,6 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
|||||||
return nil, E.New("unknown certificate store: ", options.Store)
|
return nil, E.New("unknown certificate store: ", options.Store)
|
||||||
}
|
}
|
||||||
store := &Store{
|
store := &Store{
|
||||||
store: options.Store,
|
|
||||||
systemPool: systemPool,
|
systemPool: systemPool,
|
||||||
certificate: strings.Join(options.Certificate, "\n"),
|
certificate: strings.Join(options.Certificate, "\n"),
|
||||||
certificatePaths: options.CertificatePath,
|
certificatePaths: options.CertificatePath,
|
||||||
@@ -126,37 +123,19 @@ func (s *Store) Pool() *x509.CertPool {
|
|||||||
return s.currentPool
|
return s.currentPool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) StoreKind() string {
|
|
||||||
return s.store
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CurrentPEM() []string {
|
|
||||||
s.access.RLock()
|
|
||||||
defer s.access.RUnlock()
|
|
||||||
return append([]string(nil), s.currentPEM...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) update() error {
|
func (s *Store) update() error {
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
defer s.access.Unlock()
|
defer s.access.Unlock()
|
||||||
var currentPool *x509.CertPool
|
var currentPool *x509.CertPool
|
||||||
var currentPEM []string
|
|
||||||
if s.systemPool == nil {
|
if s.systemPool == nil {
|
||||||
currentPool = x509.NewCertPool()
|
currentPool = x509.NewCertPool()
|
||||||
} else {
|
} else {
|
||||||
currentPool = s.systemPool.Clone()
|
currentPool = s.systemPool.Clone()
|
||||||
}
|
}
|
||||||
switch s.store {
|
|
||||||
case C.CertificateStoreMozilla:
|
|
||||||
currentPEM = append(currentPEM, mozillaIncludedPEM)
|
|
||||||
case C.CertificateStoreChrome:
|
|
||||||
currentPEM = append(currentPEM, chromeIncludedPEM)
|
|
||||||
}
|
|
||||||
if s.certificate != "" {
|
if s.certificate != "" {
|
||||||
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||||
return E.New("invalid certificate PEM strings")
|
return E.New("invalid certificate PEM strings")
|
||||||
}
|
}
|
||||||
currentPEM = append(currentPEM, s.certificate)
|
|
||||||
}
|
}
|
||||||
for _, path := range s.certificatePaths {
|
for _, path := range s.certificatePaths {
|
||||||
pemContent, err := os.ReadFile(path)
|
pemContent, err := os.ReadFile(path)
|
||||||
@@ -166,7 +145,6 @@ func (s *Store) update() error {
|
|||||||
if !currentPool.AppendCertsFromPEM(pemContent) {
|
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||||
return E.New("invalid certificate PEM file: ", path)
|
return E.New("invalid certificate PEM file: ", path)
|
||||||
}
|
}
|
||||||
currentPEM = append(currentPEM, string(pemContent))
|
|
||||||
}
|
}
|
||||||
var firstErr error
|
var firstErr error
|
||||||
for _, directoryPath := range s.certificateDirectoryPaths {
|
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||||
@@ -179,8 +157,8 @@ func (s *Store) update() error {
|
|||||||
}
|
}
|
||||||
for _, directoryEntry := range directoryEntries {
|
for _, directoryEntry := range directoryEntries {
|
||||||
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||||
if err == nil && currentPool.AppendCertsFromPEM(pemContent) {
|
if err == nil {
|
||||||
currentPEM = append(currentPEM, string(pemContent))
|
currentPool.AppendCertsFromPEM(pemContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +166,6 @@ func (s *Store) update() error {
|
|||||||
return firstErr
|
return firstErr
|
||||||
}
|
}
|
||||||
s.currentPool = currentPool
|
s.currentPool = currentPool
|
||||||
s.currentPEM = currentPEM
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
54
common/conntrack/conn.go
Normal file
54
common/conntrack/conn.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package conntrack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
net.Conn
|
||||||
|
element *list.Element[io.Closer]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConn(conn net.Conn) (net.Conn, error) {
|
||||||
|
connAccess.Lock()
|
||||||
|
element := openConnection.PushBack(conn)
|
||||||
|
connAccess.Unlock()
|
||||||
|
if KillerEnabled {
|
||||||
|
err := KillerCheck()
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Conn{
|
||||||
|
Conn: conn,
|
||||||
|
element: element,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Close() error {
|
||||||
|
if c.element.Value != nil {
|
||||||
|
connAccess.Lock()
|
||||||
|
if c.element.Value != nil {
|
||||||
|
openConnection.Remove(c.element)
|
||||||
|
c.element.Value = nil
|
||||||
|
}
|
||||||
|
connAccess.Unlock()
|
||||||
|
}
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Upstream() any {
|
||||||
|
return c.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) ReaderReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) WriterReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
35
common/conntrack/killer.go
Normal file
35
common/conntrack/killer.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package conntrack
|
||||||
|
|
||||||
|
import (
|
||||||
|
runtimeDebug "runtime/debug"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
KillerEnabled bool
|
||||||
|
MemoryLimit uint64
|
||||||
|
killerLastCheck time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
func KillerCheck() error {
|
||||||
|
if !KillerEnabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
nowTime := time.Now()
|
||||||
|
if nowTime.Sub(killerLastCheck) < 3*time.Second {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
killerLastCheck = nowTime
|
||||||
|
if memory.Total() > MemoryLimit {
|
||||||
|
Close()
|
||||||
|
go func() {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
runtimeDebug.FreeOSMemory()
|
||||||
|
}()
|
||||||
|
return E.New("out of memory")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
55
common/conntrack/packet_conn.go
Normal file
55
common/conntrack/packet_conn.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package conntrack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PacketConn struct {
|
||||||
|
net.PacketConn
|
||||||
|
element *list.Element[io.Closer]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPacketConn(conn net.PacketConn) (net.PacketConn, error) {
|
||||||
|
connAccess.Lock()
|
||||||
|
element := openConnection.PushBack(conn)
|
||||||
|
connAccess.Unlock()
|
||||||
|
if KillerEnabled {
|
||||||
|
err := KillerCheck()
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &PacketConn{
|
||||||
|
PacketConn: conn,
|
||||||
|
element: element,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PacketConn) Close() error {
|
||||||
|
if c.element.Value != nil {
|
||||||
|
connAccess.Lock()
|
||||||
|
if c.element.Value != nil {
|
||||||
|
openConnection.Remove(c.element)
|
||||||
|
c.element.Value = nil
|
||||||
|
}
|
||||||
|
connAccess.Unlock()
|
||||||
|
}
|
||||||
|
return c.PacketConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PacketConn) Upstream() any {
|
||||||
|
return bufio.NewPacketConn(c.PacketConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PacketConn) ReaderReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PacketConn) WriterReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
47
common/conntrack/track.go
Normal file
47
common/conntrack/track.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package conntrack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
connAccess sync.RWMutex
|
||||||
|
openConnection list.List[io.Closer]
|
||||||
|
)
|
||||||
|
|
||||||
|
func Count() int {
|
||||||
|
if !Enabled {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return openConnection.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func List() []io.Closer {
|
||||||
|
if !Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
connAccess.RLock()
|
||||||
|
defer connAccess.RUnlock()
|
||||||
|
connList := make([]io.Closer, 0, openConnection.Len())
|
||||||
|
for element := openConnection.Front(); element != nil; element = element.Next() {
|
||||||
|
connList = append(connList, element.Value)
|
||||||
|
}
|
||||||
|
return connList
|
||||||
|
}
|
||||||
|
|
||||||
|
func Close() {
|
||||||
|
if !Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connAccess.Lock()
|
||||||
|
defer connAccess.Unlock()
|
||||||
|
for element := openConnection.Front(); element != nil; element = element.Next() {
|
||||||
|
common.Close(element.Value)
|
||||||
|
element.Value = nil
|
||||||
|
}
|
||||||
|
openConnection.Init()
|
||||||
|
}
|
||||||
5
common/conntrack/track_disable.go
Normal file
5
common/conntrack/track_disable.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build !with_conntrack
|
||||||
|
|
||||||
|
package conntrack
|
||||||
|
|
||||||
|
const Enabled = false
|
||||||
5
common/conntrack/track_enable.go
Normal file
5
common/conntrack/track_enable.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build with_conntrack
|
||||||
|
|
||||||
|
package conntrack
|
||||||
|
|
||||||
|
const Enabled = true
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/conntrack"
|
||||||
"github.com/sagernet/sing-box/common/listener"
|
"github.com/sagernet/sing-box/common/listener"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
@@ -36,7 +37,6 @@ type DefaultDialer struct {
|
|||||||
udpAddr4 string
|
udpAddr4 string
|
||||||
udpAddr6 string
|
udpAddr6 string
|
||||||
netns string
|
netns string
|
||||||
connectionManager adapter.ConnectionManager
|
|
||||||
networkManager adapter.NetworkManager
|
networkManager adapter.NetworkManager
|
||||||
networkStrategy *C.NetworkStrategy
|
networkStrategy *C.NetworkStrategy
|
||||||
defaultNetworkStrategy bool
|
defaultNetworkStrategy bool
|
||||||
@@ -47,7 +47,6 @@ type DefaultDialer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
||||||
connectionManager := service.FromContext[adapter.ConnectionManager](ctx)
|
|
||||||
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||||
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||||
|
|
||||||
@@ -90,7 +89,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
|
|
||||||
if networkManager != nil {
|
if networkManager != nil {
|
||||||
defaultOptions := networkManager.DefaultOptions()
|
defaultOptions := networkManager.DefaultOptions()
|
||||||
if defaultOptions.BindInterface != "" && !disableDefaultBind {
|
if defaultOptions.BindInterface != "" {
|
||||||
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
@@ -138,21 +137,12 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath))
|
dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath))
|
||||||
listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath))
|
listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath))
|
||||||
}
|
}
|
||||||
if options.BindAddressNoPort {
|
|
||||||
if !C.IsLinux {
|
|
||||||
return nil, E.New("`bind_address_no_port` is only supported on Linux")
|
|
||||||
}
|
|
||||||
dialer.Control = control.Append(dialer.Control, control.BindAddressNoPort())
|
|
||||||
}
|
|
||||||
if options.ConnectTimeout != 0 {
|
if options.ConnectTimeout != 0 {
|
||||||
dialer.Timeout = time.Duration(options.ConnectTimeout)
|
dialer.Timeout = time.Duration(options.ConnectTimeout)
|
||||||
} 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
|
||||||
@@ -161,11 +151,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
if keepInterval == 0 {
|
if keepInterval == 0 {
|
||||||
keepInterval = C.TCPKeepAliveInterval
|
keepInterval = C.TCPKeepAliveInterval
|
||||||
}
|
}
|
||||||
dialer.KeepAliveConfig = net.KeepAliveConfig{
|
dialer.KeepAlive = keepIdle
|
||||||
Enable: true,
|
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(keepIdle, keepInterval))
|
||||||
Idle: keepIdle,
|
|
||||||
Interval: keepInterval,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var udpFragment bool
|
var udpFragment bool
|
||||||
if options.UDPFragment != nil {
|
if options.UDPFragment != nil {
|
||||||
@@ -213,7 +200,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
udpAddr4: udpAddr4,
|
udpAddr4: udpAddr4,
|
||||||
udpAddr6: udpAddr6,
|
udpAddr6: udpAddr6,
|
||||||
netns: options.NetNs,
|
netns: options.NetNs,
|
||||||
connectionManager: connectionManager,
|
|
||||||
networkManager: networkManager,
|
networkManager: networkManager,
|
||||||
networkStrategy: networkStrategy,
|
networkStrategy: networkStrategy,
|
||||||
defaultNetworkStrategy: defaultNetworkStrategy,
|
defaultNetworkStrategy: defaultNetworkStrategy,
|
||||||
@@ -242,11 +228,11 @@ 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.IsDomain() {
|
} else if address.IsFqdn() {
|
||||||
return nil, E.New("domain not resolved")
|
return nil, E.New("domain not resolved")
|
||||||
}
|
}
|
||||||
if d.networkStrategy == nil {
|
if d.networkStrategy == nil {
|
||||||
return d.trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
|
return trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
|
||||||
switch N.NetworkName(network) {
|
switch N.NetworkName(network) {
|
||||||
case N.NetworkUDP:
|
case N.NetworkUDP:
|
||||||
if !address.IsIPv6() {
|
if !address.IsIPv6() {
|
||||||
@@ -311,12 +297,12 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
|||||||
if !fastFallback && !isPrimary {
|
if !fastFallback && !isPrimary {
|
||||||
d.networkLastFallback.Store(time.Now())
|
d.networkLastFallback.Store(time.Now())
|
||||||
}
|
}
|
||||||
return d.trackConn(conn, nil)
|
return trackConn(conn, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
if d.networkStrategy == nil {
|
if d.networkStrategy == nil {
|
||||||
return d.trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
|
return trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
|
||||||
if destination.IsIPv6() {
|
if destination.IsIPv6() {
|
||||||
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
|
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
|
||||||
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||||
@@ -332,9 +318,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.dialer4.Dialer
|
|
||||||
} else {
|
|
||||||
return d.dialer6.Dialer
|
return d.dialer6.Dialer
|
||||||
|
} else {
|
||||||
|
return d.dialer4.Dialer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,23 +354,23 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return d.trackPacketConn(packetConn, nil)
|
return trackPacketConn(packetConn, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) WireGuardControl() control.Func {
|
func (d *DefaultDialer) WireGuardControl() control.Func {
|
||||||
return d.udpListener.Control
|
return d.udpListener.Control
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) trackConn(conn net.Conn, err error) (net.Conn, error) {
|
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
||||||
if d.connectionManager == nil || err != nil {
|
if !conntrack.Enabled || err != nil {
|
||||||
return conn, err
|
return conn, err
|
||||||
}
|
}
|
||||||
return d.connectionManager.TrackConn(conn), nil
|
return conntrack.NewConn(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) trackPacketConn(conn net.PacketConn, err error) (net.PacketConn, error) {
|
func trackPacketConn(conn net.PacketConn, err error) (net.PacketConn, error) {
|
||||||
if d.connectionManager == nil || err != nil {
|
if !conntrack.Enabled || err != nil {
|
||||||
return conn, err
|
return conn, err
|
||||||
}
|
}
|
||||||
return d.connectionManager.TrackPacketConn(conn), nil
|
return conntrack.NewPacketConn(conn)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ type DirectDialer interface {
|
|||||||
type DetourDialer struct {
|
type DetourDialer struct {
|
||||||
outboundManager adapter.OutboundManager
|
outboundManager adapter.OutboundManager
|
||||||
detour string
|
detour string
|
||||||
defaultOutbound bool
|
|
||||||
legacyDNSDialer bool
|
legacyDNSDialer bool
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
initOnce sync.Once
|
initOnce sync.Once
|
||||||
@@ -34,13 +33,6 @@ func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultOutboundDetour(outboundManager adapter.OutboundManager) N.Dialer {
|
|
||||||
return &DetourDialer{
|
|
||||||
outboundManager: outboundManager,
|
|
||||||
defaultOutbound: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitializeDetour(dialer N.Dialer) error {
|
func InitializeDetour(dialer N.Dialer) error {
|
||||||
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
||||||
if !isDetour {
|
if !isDetour {
|
||||||
@@ -55,18 +47,12 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DetourDialer) init() {
|
func (d *DetourDialer) init() {
|
||||||
var dialer adapter.Outbound
|
dialer, loaded := d.outboundManager.Outbound(d.detour)
|
||||||
if d.detour != "" {
|
if !loaded {
|
||||||
var loaded bool
|
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||||
dialer, loaded = d.outboundManager.Outbound(d.detour)
|
return
|
||||||
if !loaded {
|
|
||||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dialer = d.outboundManager.Default()
|
|
||||||
}
|
}
|
||||||
if !d.defaultOutbound && !d.legacyDNSDialer {
|
if !d.legacyDNSDialer {
|
||||||
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
||||||
if directDialer.IsEmpty() {
|
if directDialer.IsEmpty() {
|
||||||
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ type Options struct {
|
|||||||
NewDialer bool
|
NewDialer bool
|
||||||
LegacyDNSDialer bool
|
LegacyDNSDialer bool
|
||||||
DirectOutbound bool
|
DirectOutbound bool
|
||||||
DefaultOutbound bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: merge with NewWithOptions
|
// TODO: merge with NewWithOptions
|
||||||
@@ -43,26 +42,19 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
|||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
hasDetour := dialOptions.Detour != "" || options.DefaultOutbound
|
|
||||||
if dialOptions.Detour != "" {
|
if dialOptions.Detour != "" {
|
||||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||||
if outboundManager == nil {
|
if outboundManager == nil {
|
||||||
return nil, E.New("missing outbound manager")
|
return nil, E.New("missing outbound manager")
|
||||||
}
|
}
|
||||||
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
||||||
} else if options.DefaultOutbound {
|
|
||||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
|
||||||
if outboundManager == nil {
|
|
||||||
return nil, E.New("missing outbound manager")
|
|
||||||
}
|
|
||||||
dialer = NewDefaultOutboundDetour(outboundManager)
|
|
||||||
} else {
|
} else {
|
||||||
dialer, err = NewDefault(options.Context, dialOptions)
|
dialer, err = NewDefault(options.Context, dialOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.RemoteIsDomain && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||||
var defaultOptions adapter.NetworkOptions
|
var defaultOptions adapter.NetworkOptions
|
||||||
@@ -95,12 +87,11 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
|||||||
}
|
}
|
||||||
server = dialOptions.DomainResolver.Server
|
server = dialOptions.DomainResolver.Server
|
||||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Strategy: strategy,
|
Strategy: strategy,
|
||||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||||
DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache,
|
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
|
||||||
}
|
}
|
||||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||||
} else if options.DirectResolver {
|
} else if options.DirectResolver {
|
||||||
@@ -154,7 +145,3 @@ type ParallelNetworkDialer interface {
|
|||||||
DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||||
ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
|
ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PacketDialerWithDestination interface {
|
|
||||||
ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.IsDomain() {
|
if !destination.IsFqdn() {
|
||||||
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.IsDomain() {
|
if !destination.IsFqdn() {
|
||||||
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.IsDomain() {
|
if !destination.IsFqdn() {
|
||||||
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.IsDomain() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
package geosite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/varbin"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Old implementation using varbin reflection-based serialization
|
|
||||||
|
|
||||||
func oldWriteString(writer varbin.Writer, value string) error {
|
|
||||||
//nolint:staticcheck
|
|
||||||
return varbin.Write(writer, binary.BigEndian, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func oldWriteItem(writer varbin.Writer, item Item) error {
|
|
||||||
//nolint:staticcheck
|
|
||||||
return varbin.Write(writer, binary.BigEndian, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
func oldReadString(reader varbin.Reader) (string, error) {
|
|
||||||
//nolint:staticcheck
|
|
||||||
return varbin.ReadValue[string](reader, binary.BigEndian)
|
|
||||||
}
|
|
||||||
|
|
||||||
func oldReadItem(reader varbin.Reader) (Item, error) {
|
|
||||||
//nolint:staticcheck
|
|
||||||
return varbin.ReadValue[Item](reader, binary.BigEndian)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStringCompat(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
}{
|
|
||||||
{"empty", ""},
|
|
||||||
{"single_char", "a"},
|
|
||||||
{"ascii", "example.com"},
|
|
||||||
{"utf8", "测试域名.中国"},
|
|
||||||
{"special_chars", "\x00\xff\n\t"},
|
|
||||||
{"127_bytes", strings.Repeat("x", 127)},
|
|
||||||
{"128_bytes", strings.Repeat("x", 128)},
|
|
||||||
{"16383_bytes", strings.Repeat("x", 16383)},
|
|
||||||
{"16384_bytes", strings.Repeat("x", 16384)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Old write
|
|
||||||
var oldBuf bytes.Buffer
|
|
||||||
err := oldWriteString(&oldBuf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// New write
|
|
||||||
var newBuf bytes.Buffer
|
|
||||||
err = writeString(&newBuf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Bytes must match
|
|
||||||
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
|
|
||||||
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
|
|
||||||
|
|
||||||
// New write -> old read
|
|
||||||
readBack, err := oldReadString(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.input, readBack)
|
|
||||||
|
|
||||||
// Old write -> new read
|
|
||||||
readBack2, err := readString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.input, readBack2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestItemCompat(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Note: varbin.Write has a bug where struct values (not pointers) don't write their fields
|
|
||||||
// because field.CanSet() returns false for non-addressable values.
|
|
||||||
// The old geosite code passed Item values to varbin.Write, which silently wrote nothing.
|
|
||||||
// The new code correctly writes Type + Value using manual serialization.
|
|
||||||
// This test verifies the new serialization format and round-trip correctness.
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
input Item
|
|
||||||
}{
|
|
||||||
{"domain_empty", Item{Type: RuleTypeDomain, Value: ""}},
|
|
||||||
{"domain_normal", Item{Type: RuleTypeDomain, Value: "example.com"}},
|
|
||||||
{"domain_suffix", Item{Type: RuleTypeDomainSuffix, Value: ".example.com"}},
|
|
||||||
{"domain_keyword", Item{Type: RuleTypeDomainKeyword, Value: "google"}},
|
|
||||||
{"domain_regex", Item{Type: RuleTypeDomainRegex, Value: `^.*\.example\.com$`}},
|
|
||||||
{"utf8_domain", Item{Type: RuleTypeDomain, Value: "测试.com"}},
|
|
||||||
{"long_domain", Item{Type: RuleTypeDomainSuffix, Value: strings.Repeat("a", 200) + ".com"}},
|
|
||||||
{"128_bytes_value", Item{Type: RuleTypeDomain, Value: strings.Repeat("x", 128)}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// New write
|
|
||||||
var newBuf bytes.Buffer
|
|
||||||
err := newBuf.WriteByte(byte(tc.input.Type))
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = writeString(&newBuf, tc.input.Value)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify format: Type (1 byte) + Value (uvarint len + bytes)
|
|
||||||
require.True(t, len(newBuf.Bytes()) >= 1, "output too short")
|
|
||||||
require.Equal(t, byte(tc.input.Type), newBuf.Bytes()[0], "type byte mismatch")
|
|
||||||
|
|
||||||
// New write -> old read (varbin can read correctly when given addressable target)
|
|
||||||
readBack, err := oldReadItem(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.input, readBack)
|
|
||||||
|
|
||||||
// New write -> new read
|
|
||||||
reader := bufio.NewReader(bytes.NewReader(newBuf.Bytes()))
|
|
||||||
typeByte, err := reader.ReadByte()
|
|
||||||
require.NoError(t, err)
|
|
||||||
value, err := readString(reader)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.input, Item{Type: ItemType(typeByte), Value: value})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGeositeWriteReadCompat(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
input map[string][]Item
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"empty_map",
|
|
||||||
map[string][]Item{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"single_code_empty_items",
|
|
||||||
map[string][]Item{"test": {}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"single_code_single_item",
|
|
||||||
map[string][]Item{"test": {{Type: RuleTypeDomain, Value: "a.com"}}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"single_code_multi_items",
|
|
||||||
map[string][]Item{
|
|
||||||
"test": {
|
|
||||||
{Type: RuleTypeDomain, Value: "a.com"},
|
|
||||||
{Type: RuleTypeDomainSuffix, Value: ".b.com"},
|
|
||||||
{Type: RuleTypeDomainKeyword, Value: "keyword"},
|
|
||||||
{Type: RuleTypeDomainRegex, Value: `^.*$`},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multi_code",
|
|
||||||
map[string][]Item{
|
|
||||||
"cn": {{Type: RuleTypeDomain, Value: "baidu.com"}, {Type: RuleTypeDomainSuffix, Value: ".cn"}},
|
|
||||||
"us": {{Type: RuleTypeDomain, Value: "google.com"}},
|
|
||||||
"jp": {{Type: RuleTypeDomainSuffix, Value: ".jp"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"utf8_values",
|
|
||||||
map[string][]Item{
|
|
||||||
"test": {
|
|
||||||
{Type: RuleTypeDomain, Value: "测试.中国"},
|
|
||||||
{Type: RuleTypeDomainSuffix, Value: ".テスト"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"large_items",
|
|
||||||
generateLargeItems(1000),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Write using new implementation
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err := Write(&buf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Read back and verify
|
|
||||||
reader, codes, err := NewReader(bytes.NewReader(buf.Bytes()))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify all codes exist
|
|
||||||
codeSet := make(map[string]bool)
|
|
||||||
for _, code := range codes {
|
|
||||||
codeSet[code] = true
|
|
||||||
}
|
|
||||||
for code := range tc.input {
|
|
||||||
require.True(t, codeSet[code], "missing code: %s", code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify items match
|
|
||||||
for code, expectedItems := range tc.input {
|
|
||||||
items, err := reader.Read(code)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, expectedItems, items, "items mismatch for code: %s", code)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateLargeItems(count int) map[string][]Item {
|
|
||||||
items := make([]Item, count)
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
items[i] = Item{
|
|
||||||
Type: ItemType(i % 4),
|
|
||||||
Value: strings.Repeat("x", i%200) + ".com",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map[string][]Item{"large": items}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/varbin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reader struct {
|
type Reader struct {
|
||||||
@@ -77,7 +78,7 @@ func (r *Reader) readMetadata() error {
|
|||||||
codeIndex uint64
|
codeIndex uint64
|
||||||
codeLength uint64
|
codeLength uint64
|
||||||
)
|
)
|
||||||
code, err = readString(reader)
|
code, err = varbin.ReadValue[string](reader, binary.BigEndian)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -111,16 +112,9 @@ func (r *Reader) Read(code string) ([]Item, error) {
|
|||||||
}
|
}
|
||||||
r.bufferedReader.Reset(r.reader)
|
r.bufferedReader.Reset(r.reader)
|
||||||
itemList := make([]Item, r.domainLength[code])
|
itemList := make([]Item, r.domainLength[code])
|
||||||
for i := range itemList {
|
err = varbin.Read(r.bufferedReader, binary.BigEndian, &itemList)
|
||||||
typeByte, err := r.bufferedReader.ReadByte()
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
itemList[i].Type = ItemType(typeByte)
|
|
||||||
itemList[i].Value, err = readString(r.bufferedReader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return itemList, nil
|
return itemList, nil
|
||||||
}
|
}
|
||||||
@@ -141,18 +135,3 @@ func (r *readCounter) Read(p []byte) (n int, err error) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func readString(reader io.ByteReader) (string, error) {
|
|
||||||
length, err := binary.ReadUvarint(reader)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
bytes := make([]byte, length)
|
|
||||||
for i := range bytes {
|
|
||||||
bytes[i], err = reader.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return string(bytes), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package geosite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/varbin"
|
"github.com/sagernet/sing/common/varbin"
|
||||||
@@ -19,11 +20,7 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
|
|||||||
for _, code := range keys {
|
for _, code := range keys {
|
||||||
index[code] = content.Len()
|
index[code] = content.Len()
|
||||||
for _, item := range domains[code] {
|
for _, item := range domains[code] {
|
||||||
err := content.WriteByte(byte(item.Type))
|
err := varbin.Write(content, binary.BigEndian, item)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = writeString(content, item.Value)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -41,7 +38,7 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, code := range keys {
|
for _, code := range keys {
|
||||||
err = writeString(writer, code)
|
err = varbin.Write(writer, binary.BigEndian, code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -62,12 +59,3 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeString(writer varbin.Writer, value string) error {
|
|
||||||
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = writer.Write([]byte(value))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,442 +0,0 @@
|
|||||||
//go:build darwin && cgo
|
|
||||||
|
|
||||||
package httpclient
|
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo CFLAGS: -x objective-c -fobjc-arc
|
|
||||||
#cgo LDFLAGS: -framework Foundation -framework Security
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include "apple_transport_darwin.h"
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/proxybridge"
|
|
||||||
boxTLS "github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
const applePinnedHashSize = sha256.Size
|
|
||||||
|
|
||||||
func verifyApplePinnedPublicKeySHA256(flatHashes []byte, leafCertificate []byte) error {
|
|
||||||
if len(flatHashes)%applePinnedHashSize != 0 {
|
|
||||||
return E.New("invalid pinned public key list")
|
|
||||||
}
|
|
||||||
knownHashes := make([][]byte, 0, len(flatHashes)/applePinnedHashSize)
|
|
||||||
for offset := 0; offset < len(flatHashes); offset += applePinnedHashSize {
|
|
||||||
knownHashes = append(knownHashes, append([]byte(nil), flatHashes[offset:offset+applePinnedHashSize]...))
|
|
||||||
}
|
|
||||||
return boxTLS.VerifyPublicKeySHA256(knownHashes, [][]byte{leafCertificate})
|
|
||||||
}
|
|
||||||
|
|
||||||
//export box_apple_http_verify_public_key_sha256
|
|
||||||
func box_apple_http_verify_public_key_sha256(knownHashValues *C.uint8_t, knownHashValuesLen C.size_t, leafCert *C.uint8_t, leafCertLen C.size_t) *C.char {
|
|
||||||
flatHashes := C.GoBytes(unsafe.Pointer(knownHashValues), C.int(knownHashValuesLen))
|
|
||||||
leafCertificate := C.GoBytes(unsafe.Pointer(leafCert), C.int(leafCertLen))
|
|
||||||
err := verifyApplePinnedPublicKeySHA256(flatHashes, leafCertificate)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return C.CString(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
type appleSessionConfig struct {
|
|
||||||
serverName string
|
|
||||||
minVersion uint16
|
|
||||||
maxVersion uint16
|
|
||||||
insecure bool
|
|
||||||
anchorPEM string
|
|
||||||
anchorOnly bool
|
|
||||||
pinnedPublicKeySHA256s []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type appleTransportShared struct {
|
|
||||||
logger logger.ContextLogger
|
|
||||||
bridge *proxybridge.Bridge
|
|
||||||
config appleSessionConfig
|
|
||||||
refs atomic.Int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type appleTransport struct {
|
|
||||||
shared *appleTransportShared
|
|
||||||
access sync.Mutex
|
|
||||||
session *C.box_apple_http_session_t
|
|
||||||
closed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type errorTransport struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDialer N.Dialer, options option.HTTPClientOptions) (adapter.HTTPTransport, error) {
|
|
||||||
sessionConfig, err := newAppleSessionConfig(ctx, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bridge, err := proxybridge.New(ctx, logger, "apple http proxy", rawDialer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
shared := &appleTransportShared{
|
|
||||||
logger: logger,
|
|
||||||
bridge: bridge,
|
|
||||||
config: sessionConfig,
|
|
||||||
}
|
|
||||||
shared.refs.Store(1)
|
|
||||||
session, err := shared.newSession()
|
|
||||||
if err != nil {
|
|
||||||
bridge.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &appleTransport{
|
|
||||||
shared: shared,
|
|
||||||
session: session,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAppleSessionConfig(ctx context.Context, options option.HTTPClientOptions) (appleSessionConfig, error) {
|
|
||||||
version := options.Version
|
|
||||||
if version == 0 {
|
|
||||||
version = 2
|
|
||||||
}
|
|
||||||
switch version {
|
|
||||||
case 2:
|
|
||||||
case 1:
|
|
||||||
return appleSessionConfig{}, E.New("HTTP/1.1 is unsupported in Apple HTTP engine")
|
|
||||||
case 3:
|
|
||||||
return appleSessionConfig{}, E.New("HTTP/3 is unsupported in Apple HTTP engine")
|
|
||||||
default:
|
|
||||||
return appleSessionConfig{}, E.New("unknown HTTP version: ", version)
|
|
||||||
}
|
|
||||||
if options.DisableVersionFallback {
|
|
||||||
return appleSessionConfig{}, E.New("disable_version_fallback is unsupported in Apple HTTP engine")
|
|
||||||
}
|
|
||||||
if options.HTTP2Options != (option.HTTP2Options{}) {
|
|
||||||
return appleSessionConfig{}, E.New("HTTP/2 options are unsupported in Apple HTTP engine")
|
|
||||||
}
|
|
||||||
if options.HTTP3Options != (option.QUICOptions{}) {
|
|
||||||
return appleSessionConfig{}, E.New("QUIC options are unsupported in Apple HTTP engine")
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
|
||||||
if tlsOptions.Engine != "" {
|
|
||||||
return appleSessionConfig{}, E.New("tls.engine is unsupported in Apple HTTP engine")
|
|
||||||
}
|
|
||||||
if len(tlsOptions.ALPN) > 0 {
|
|
||||||
return appleSessionConfig{}, E.New("tls.alpn is unsupported in Apple HTTP engine")
|
|
||||||
}
|
|
||||||
validated, err := boxTLS.ValidateAppleTLSOptions(ctx, tlsOptions, "Apple HTTP engine")
|
|
||||||
if err != nil {
|
|
||||||
return appleSessionConfig{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
config := appleSessionConfig{
|
|
||||||
serverName: tlsOptions.ServerName,
|
|
||||||
minVersion: validated.MinVersion,
|
|
||||||
maxVersion: validated.MaxVersion,
|
|
||||||
insecure: tlsOptions.Insecure || len(tlsOptions.CertificatePublicKeySHA256) > 0,
|
|
||||||
anchorPEM: validated.AnchorPEM,
|
|
||||||
anchorOnly: validated.AnchorOnly,
|
|
||||||
}
|
|
||||||
if len(tlsOptions.CertificatePublicKeySHA256) > 0 {
|
|
||||||
config.pinnedPublicKeySHA256s = make([]byte, 0, len(tlsOptions.CertificatePublicKeySHA256)*applePinnedHashSize)
|
|
||||||
for _, hashValue := range tlsOptions.CertificatePublicKeySHA256 {
|
|
||||||
if len(hashValue) != applePinnedHashSize {
|
|
||||||
return appleSessionConfig{}, E.New("invalid certificate_public_key_sha256 length: ", len(hashValue))
|
|
||||||
}
|
|
||||||
config.pinnedPublicKeySHA256s = append(config.pinnedPublicKeySHA256s, hashValue...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *appleTransportShared) retain() {
|
|
||||||
s.refs.Add(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *appleTransportShared) release() error {
|
|
||||||
if s.refs.Add(-1) == 0 {
|
|
||||||
return s.bridge.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *appleTransportShared) newSession() (*C.box_apple_http_session_t, error) {
|
|
||||||
cProxyHost := C.CString("127.0.0.1")
|
|
||||||
defer C.free(unsafe.Pointer(cProxyHost))
|
|
||||||
cProxyUsername := C.CString(s.bridge.Username())
|
|
||||||
defer C.free(unsafe.Pointer(cProxyUsername))
|
|
||||||
cProxyPassword := C.CString(s.bridge.Password())
|
|
||||||
defer C.free(unsafe.Pointer(cProxyPassword))
|
|
||||||
var cAnchorPEM *C.char
|
|
||||||
if s.config.anchorPEM != "" {
|
|
||||||
cAnchorPEM = C.CString(s.config.anchorPEM)
|
|
||||||
defer C.free(unsafe.Pointer(cAnchorPEM))
|
|
||||||
}
|
|
||||||
var pinnedPointer *C.uint8_t
|
|
||||||
if len(s.config.pinnedPublicKeySHA256s) > 0 {
|
|
||||||
pinnedPointer = (*C.uint8_t)(C.CBytes(s.config.pinnedPublicKeySHA256s))
|
|
||||||
defer C.free(unsafe.Pointer(pinnedPointer))
|
|
||||||
}
|
|
||||||
cConfig := C.box_apple_http_session_config_t{
|
|
||||||
proxy_host: cProxyHost,
|
|
||||||
proxy_port: C.int(s.bridge.Port()),
|
|
||||||
proxy_username: cProxyUsername,
|
|
||||||
proxy_password: cProxyPassword,
|
|
||||||
min_tls_version: C.uint16_t(s.config.minVersion),
|
|
||||||
max_tls_version: C.uint16_t(s.config.maxVersion),
|
|
||||||
insecure: C.bool(s.config.insecure),
|
|
||||||
anchor_pem: cAnchorPEM,
|
|
||||||
anchor_pem_len: C.size_t(len(s.config.anchorPEM)),
|
|
||||||
anchor_only: C.bool(s.config.anchorOnly),
|
|
||||||
pinned_public_key_sha256: pinnedPointer,
|
|
||||||
pinned_public_key_sha256_len: C.size_t(len(s.config.pinnedPublicKeySHA256s)),
|
|
||||||
}
|
|
||||||
var cErr *C.char
|
|
||||||
session := C.box_apple_http_session_create(&cConfig, &cErr)
|
|
||||||
if session != nil {
|
|
||||||
return session, nil
|
|
||||||
}
|
|
||||||
return nil, appleCStringError(cErr, "create Apple HTTP session")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *appleTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
if requestRequiresHTTP1(request) {
|
|
||||||
return nil, E.New("HTTP upgrade requests are unsupported in Apple HTTP engine")
|
|
||||||
}
|
|
||||||
if request.URL == nil {
|
|
||||||
return nil, E.New("missing request URL")
|
|
||||||
}
|
|
||||||
switch request.URL.Scheme {
|
|
||||||
case "http", "https":
|
|
||||||
default:
|
|
||||||
return nil, E.New("unsupported URL scheme: ", request.URL.Scheme)
|
|
||||||
}
|
|
||||||
if request.URL.Scheme == "https" && t.shared.config.serverName != "" && !strings.EqualFold(t.shared.config.serverName, request.URL.Hostname()) {
|
|
||||||
return nil, E.New("tls.server_name is unsupported in Apple HTTP engine unless it matches request host")
|
|
||||||
}
|
|
||||||
var body []byte
|
|
||||||
if request.Body != nil && request.Body != http.NoBody {
|
|
||||||
defer request.Body.Close()
|
|
||||||
var err error
|
|
||||||
body, err = io.ReadAll(request.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
headerKeys, headerValues := flattenRequestHeaders(request)
|
|
||||||
cMethod := C.CString(request.Method)
|
|
||||||
defer C.free(unsafe.Pointer(cMethod))
|
|
||||||
cURL := C.CString(request.URL.String())
|
|
||||||
defer C.free(unsafe.Pointer(cURL))
|
|
||||||
cHeaderKeys := make([]*C.char, len(headerKeys))
|
|
||||||
cHeaderValues := make([]*C.char, len(headerValues))
|
|
||||||
defer func() {
|
|
||||||
for _, ptr := range cHeaderKeys {
|
|
||||||
C.free(unsafe.Pointer(ptr))
|
|
||||||
}
|
|
||||||
for _, ptr := range cHeaderValues {
|
|
||||||
C.free(unsafe.Pointer(ptr))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for index, value := range headerKeys {
|
|
||||||
cHeaderKeys[index] = C.CString(value)
|
|
||||||
}
|
|
||||||
for index, value := range headerValues {
|
|
||||||
cHeaderValues[index] = C.CString(value)
|
|
||||||
}
|
|
||||||
var headerKeysPointer **C.char
|
|
||||||
var headerValuesPointer **C.char
|
|
||||||
if len(cHeaderKeys) > 0 {
|
|
||||||
pointerArraySize := C.size_t(len(cHeaderKeys)) * C.size_t(unsafe.Sizeof((*C.char)(nil)))
|
|
||||||
headerKeysPointer = (**C.char)(C.malloc(pointerArraySize))
|
|
||||||
defer C.free(unsafe.Pointer(headerKeysPointer))
|
|
||||||
headerValuesPointer = (**C.char)(C.malloc(pointerArraySize))
|
|
||||||
defer C.free(unsafe.Pointer(headerValuesPointer))
|
|
||||||
copy(unsafe.Slice(headerKeysPointer, len(cHeaderKeys)), cHeaderKeys)
|
|
||||||
copy(unsafe.Slice(headerValuesPointer, len(cHeaderValues)), cHeaderValues)
|
|
||||||
}
|
|
||||||
var bodyPointer *C.uint8_t
|
|
||||||
if len(body) > 0 {
|
|
||||||
bodyPointer = (*C.uint8_t)(C.CBytes(body))
|
|
||||||
defer C.free(unsafe.Pointer(bodyPointer))
|
|
||||||
}
|
|
||||||
cRequest := C.box_apple_http_request_t{
|
|
||||||
method: cMethod,
|
|
||||||
url: cURL,
|
|
||||||
header_keys: (**C.char)(headerKeysPointer),
|
|
||||||
header_values: (**C.char)(headerValuesPointer),
|
|
||||||
header_count: C.size_t(len(cHeaderKeys)),
|
|
||||||
body: bodyPointer,
|
|
||||||
body_len: C.size_t(len(body)),
|
|
||||||
}
|
|
||||||
var cErr *C.char
|
|
||||||
var task *C.box_apple_http_task_t
|
|
||||||
t.access.Lock()
|
|
||||||
if t.session == nil {
|
|
||||||
t.access.Unlock()
|
|
||||||
return nil, net.ErrClosed
|
|
||||||
}
|
|
||||||
// Keep the session attached until NSURLSession has created the task.
|
|
||||||
task = C.box_apple_http_session_send_async(t.session, &cRequest, &cErr)
|
|
||||||
t.access.Unlock()
|
|
||||||
if task == nil {
|
|
||||||
return nil, appleCStringError(cErr, "create Apple HTTP request")
|
|
||||||
}
|
|
||||||
cancelDone := make(chan struct{})
|
|
||||||
cancelExit := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(cancelExit)
|
|
||||||
select {
|
|
||||||
case <-request.Context().Done():
|
|
||||||
C.box_apple_http_task_cancel(task)
|
|
||||||
case <-cancelDone:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
cResponse := C.box_apple_http_task_wait(task, &cErr)
|
|
||||||
close(cancelDone)
|
|
||||||
<-cancelExit
|
|
||||||
C.box_apple_http_task_close(task)
|
|
||||||
if cResponse == nil {
|
|
||||||
err := appleCStringError(cErr, "Apple HTTP request failed")
|
|
||||||
if request.Context().Err() != nil {
|
|
||||||
return nil, request.Context().Err()
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer C.box_apple_http_response_free(cResponse)
|
|
||||||
return parseAppleHTTPResponse(request, cResponse), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *appleTransport) CloseIdleConnections() {
|
|
||||||
t.access.Lock()
|
|
||||||
if t.closed {
|
|
||||||
t.access.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.access.Unlock()
|
|
||||||
newSession, err := t.shared.newSession()
|
|
||||||
if err != nil {
|
|
||||||
t.shared.logger.Error(E.Cause(err, "reset Apple HTTP session"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.access.Lock()
|
|
||||||
if t.closed {
|
|
||||||
t.access.Unlock()
|
|
||||||
C.box_apple_http_session_close(newSession)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
oldSession := t.session
|
|
||||||
t.session = newSession
|
|
||||||
t.access.Unlock()
|
|
||||||
C.box_apple_http_session_retire(oldSession)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *appleTransport) Clone() adapter.HTTPTransport {
|
|
||||||
t.shared.retain()
|
|
||||||
session, err := t.shared.newSession()
|
|
||||||
if err != nil {
|
|
||||||
_ = t.shared.release()
|
|
||||||
return &errorTransport{err: err}
|
|
||||||
}
|
|
||||||
return &appleTransport{
|
|
||||||
shared: t.shared,
|
|
||||||
session: session,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *appleTransport) Close() error {
|
|
||||||
t.access.Lock()
|
|
||||||
if t.closed {
|
|
||||||
t.access.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
t.closed = true
|
|
||||||
session := t.session
|
|
||||||
t.session = nil
|
|
||||||
t.access.Unlock()
|
|
||||||
C.box_apple_http_session_close(session)
|
|
||||||
return t.shared.release()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *errorTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
return nil, t.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *errorTransport) CloseIdleConnections() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *errorTransport) Clone() adapter.HTTPTransport {
|
|
||||||
return &errorTransport{err: t.err}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *errorTransport) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func flattenRequestHeaders(request *http.Request) ([]string, []string) {
|
|
||||||
var (
|
|
||||||
keys []string
|
|
||||||
values []string
|
|
||||||
)
|
|
||||||
for key, headerValues := range request.Header {
|
|
||||||
for _, value := range headerValues {
|
|
||||||
keys = append(keys, key)
|
|
||||||
values = append(values, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if request.Host != "" {
|
|
||||||
keys = append(keys, "Host")
|
|
||||||
values = append(values, request.Host)
|
|
||||||
}
|
|
||||||
return keys, values
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAppleHTTPResponse(request *http.Request, response *C.box_apple_http_response_t) *http.Response {
|
|
||||||
headers := make(http.Header)
|
|
||||||
headerKeys := unsafe.Slice(response.header_keys, int(response.header_count))
|
|
||||||
headerValues := unsafe.Slice(response.header_values, int(response.header_count))
|
|
||||||
for index := range headerKeys {
|
|
||||||
headers.Add(C.GoString(headerKeys[index]), C.GoString(headerValues[index]))
|
|
||||||
}
|
|
||||||
body := bytes.NewReader(C.GoBytes(unsafe.Pointer(response.body), C.int(response.body_len)))
|
|
||||||
// NSURLSession's completion-handler API does not expose the negotiated protocol;
|
|
||||||
// callers that read Response.Proto will see HTTP/1.1 even when the wire was HTTP/2.
|
|
||||||
return &http.Response{
|
|
||||||
StatusCode: int(response.status_code),
|
|
||||||
Status: fmt.Sprintf("%d %s", int(response.status_code), http.StatusText(int(response.status_code))),
|
|
||||||
Proto: "HTTP/1.1",
|
|
||||||
ProtoMajor: 1,
|
|
||||||
ProtoMinor: 1,
|
|
||||||
Header: headers,
|
|
||||||
Body: io.NopCloser(body),
|
|
||||||
ContentLength: int64(body.Len()),
|
|
||||||
Request: request,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func appleCStringError(cErr *C.char, message string) error {
|
|
||||||
if cErr == nil {
|
|
||||||
return E.New(message)
|
|
||||||
}
|
|
||||||
defer C.free(unsafe.Pointer(cErr))
|
|
||||||
return E.New(message, ": ", C.GoString(cErr))
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
#include <stdbool.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
typedef struct box_apple_http_session box_apple_http_session_t;
|
|
||||||
typedef struct box_apple_http_task box_apple_http_task_t;
|
|
||||||
|
|
||||||
typedef struct box_apple_http_session_config {
|
|
||||||
const char *proxy_host;
|
|
||||||
int proxy_port;
|
|
||||||
const char *proxy_username;
|
|
||||||
const char *proxy_password;
|
|
||||||
uint16_t min_tls_version;
|
|
||||||
uint16_t max_tls_version;
|
|
||||||
bool insecure;
|
|
||||||
const char *anchor_pem;
|
|
||||||
size_t anchor_pem_len;
|
|
||||||
bool anchor_only;
|
|
||||||
const uint8_t *pinned_public_key_sha256;
|
|
||||||
size_t pinned_public_key_sha256_len;
|
|
||||||
} box_apple_http_session_config_t;
|
|
||||||
|
|
||||||
typedef struct box_apple_http_request {
|
|
||||||
const char *method;
|
|
||||||
const char *url;
|
|
||||||
const char **header_keys;
|
|
||||||
const char **header_values;
|
|
||||||
size_t header_count;
|
|
||||||
const uint8_t *body;
|
|
||||||
size_t body_len;
|
|
||||||
} box_apple_http_request_t;
|
|
||||||
|
|
||||||
typedef struct box_apple_http_response {
|
|
||||||
int status_code;
|
|
||||||
char **header_keys;
|
|
||||||
char **header_values;
|
|
||||||
size_t header_count;
|
|
||||||
uint8_t *body;
|
|
||||||
size_t body_len;
|
|
||||||
char *error;
|
|
||||||
} box_apple_http_response_t;
|
|
||||||
|
|
||||||
box_apple_http_session_t *box_apple_http_session_create(
|
|
||||||
const box_apple_http_session_config_t *config,
|
|
||||||
char **error_out
|
|
||||||
);
|
|
||||||
void box_apple_http_session_retire(box_apple_http_session_t *session);
|
|
||||||
void box_apple_http_session_close(box_apple_http_session_t *session);
|
|
||||||
|
|
||||||
box_apple_http_task_t *box_apple_http_session_send_async(
|
|
||||||
box_apple_http_session_t *session,
|
|
||||||
const box_apple_http_request_t *request,
|
|
||||||
char **error_out
|
|
||||||
);
|
|
||||||
box_apple_http_response_t *box_apple_http_task_wait(
|
|
||||||
box_apple_http_task_t *task,
|
|
||||||
char **error_out
|
|
||||||
);
|
|
||||||
void box_apple_http_task_cancel(box_apple_http_task_t *task);
|
|
||||||
void box_apple_http_task_close(box_apple_http_task_t *task);
|
|
||||||
|
|
||||||
void box_apple_http_response_free(box_apple_http_response_t *response);
|
|
||||||
|
|
||||||
char *box_apple_http_verify_public_key_sha256(
|
|
||||||
uint8_t *known_hash_values,
|
|
||||||
size_t known_hash_values_len,
|
|
||||||
uint8_t *leaf_cert,
|
|
||||||
size_t leaf_cert_len
|
|
||||||
);
|
|
||||||
@@ -1,386 +0,0 @@
|
|||||||
#import "apple_transport_darwin.h"
|
|
||||||
|
|
||||||
#import <CoreFoundation/CFStream.h>
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <Security/Security.h>
|
|
||||||
#import <dispatch/dispatch.h>
|
|
||||||
#import <stdlib.h>
|
|
||||||
#import <string.h>
|
|
||||||
|
|
||||||
typedef struct box_apple_http_session {
|
|
||||||
void *handle;
|
|
||||||
} box_apple_http_session_t;
|
|
||||||
|
|
||||||
typedef struct box_apple_http_task {
|
|
||||||
void *task;
|
|
||||||
void *done_semaphore;
|
|
||||||
box_apple_http_response_t *response;
|
|
||||||
char *error;
|
|
||||||
} box_apple_http_task_t;
|
|
||||||
|
|
||||||
static void box_set_error_string(char **error_out, NSString *message) {
|
|
||||||
if (error_out == NULL || *error_out != NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const char *utf8 = [message UTF8String];
|
|
||||||
*error_out = strdup(utf8 != NULL ? utf8 : "unknown error");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void box_set_error_from_nserror(char **error_out, NSError *error) {
|
|
||||||
if (error == nil) {
|
|
||||||
box_set_error_string(error_out, @"unknown error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
box_set_error_string(error_out, error.localizedDescription ?: error.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
static NSArray *box_parse_certificates_from_pem(const char *pem, size_t pem_len) {
|
|
||||||
if (pem == NULL || pem_len == 0) {
|
|
||||||
return @[];
|
|
||||||
}
|
|
||||||
NSString *content = [[NSString alloc] initWithBytes:pem length:pem_len encoding:NSUTF8StringEncoding];
|
|
||||||
if (content == nil) {
|
|
||||||
return @[];
|
|
||||||
}
|
|
||||||
NSString *beginMarker = @"-----BEGIN CERTIFICATE-----";
|
|
||||||
NSString *endMarker = @"-----END CERTIFICATE-----";
|
|
||||||
NSMutableArray *certificates = [NSMutableArray array];
|
|
||||||
NSUInteger searchFrom = 0;
|
|
||||||
while (searchFrom < content.length) {
|
|
||||||
NSRange beginRange = [content rangeOfString:beginMarker options:0 range:NSMakeRange(searchFrom, content.length - searchFrom)];
|
|
||||||
if (beginRange.location == NSNotFound) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
NSUInteger bodyStart = beginRange.location + beginRange.length;
|
|
||||||
NSRange endRange = [content rangeOfString:endMarker options:0 range:NSMakeRange(bodyStart, content.length - bodyStart)];
|
|
||||||
if (endRange.location == NSNotFound) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
NSString *base64Section = [content substringWithRange:NSMakeRange(bodyStart, endRange.location - bodyStart)];
|
|
||||||
NSArray<NSString *> *components = [base64Section componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
||||||
NSString *base64Content = [components componentsJoinedByString:@""];
|
|
||||||
NSData *der = [[NSData alloc] initWithBase64EncodedString:base64Content options:0];
|
|
||||||
if (der != nil) {
|
|
||||||
SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)der);
|
|
||||||
if (certificate != NULL) {
|
|
||||||
[certificates addObject:(__bridge id)certificate];
|
|
||||||
CFRelease(certificate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
searchFrom = endRange.location + endRange.length;
|
|
||||||
}
|
|
||||||
return certificates;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool box_evaluate_trust(SecTrustRef trustRef, NSArray *anchors, bool anchor_only) {
|
|
||||||
if (trustRef == NULL) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (anchors.count > 0 || anchor_only) {
|
|
||||||
CFMutableArrayRef anchorArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
|
|
||||||
for (id certificate in anchors) {
|
|
||||||
CFArrayAppendValue(anchorArray, (__bridge const void *)certificate);
|
|
||||||
}
|
|
||||||
SecTrustSetAnchorCertificates(trustRef, anchorArray);
|
|
||||||
SecTrustSetAnchorCertificatesOnly(trustRef, anchor_only);
|
|
||||||
CFRelease(anchorArray);
|
|
||||||
}
|
|
||||||
CFErrorRef error = NULL;
|
|
||||||
bool result = SecTrustEvaluateWithError(trustRef, &error);
|
|
||||||
if (error != NULL) {
|
|
||||||
CFRelease(error);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static box_apple_http_response_t *box_create_response(NSHTTPURLResponse *httpResponse, NSData *data) {
|
|
||||||
box_apple_http_response_t *response = calloc(1, sizeof(box_apple_http_response_t));
|
|
||||||
response->status_code = (int)httpResponse.statusCode;
|
|
||||||
NSDictionary *headers = httpResponse.allHeaderFields;
|
|
||||||
response->header_count = headers.count;
|
|
||||||
if (response->header_count > 0) {
|
|
||||||
response->header_keys = calloc(response->header_count, sizeof(char *));
|
|
||||||
response->header_values = calloc(response->header_count, sizeof(char *));
|
|
||||||
NSUInteger index = 0;
|
|
||||||
for (id key in headers) {
|
|
||||||
NSString *keyString = [[key description] copy];
|
|
||||||
NSString *valueString = [[headers[key] description] copy];
|
|
||||||
response->header_keys[index] = strdup(keyString.UTF8String ?: "");
|
|
||||||
response->header_values[index] = strdup(valueString.UTF8String ?: "");
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.length > 0) {
|
|
||||||
response->body_len = data.length;
|
|
||||||
response->body = malloc(data.length);
|
|
||||||
memcpy(response->body, data.bytes, data.length);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
@interface BoxAppleHTTPSessionDelegate : NSObject <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
|
|
||||||
@property(nonatomic, assign) BOOL insecure;
|
|
||||||
@property(nonatomic, assign) BOOL anchorOnly;
|
|
||||||
@property(nonatomic, strong) NSArray *anchors;
|
|
||||||
@property(nonatomic, strong) NSData *pinnedPublicKeyHashes;
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation BoxAppleHTTPSessionDelegate
|
|
||||||
|
|
||||||
- (void)URLSession:(NSURLSession *)session
|
|
||||||
task:(NSURLSessionTask *)task
|
|
||||||
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
|
|
||||||
newRequest:(NSURLRequest *)request
|
|
||||||
completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
|
|
||||||
completionHandler(nil);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)URLSession:(NSURLSession *)session
|
|
||||||
task:(NSURLSessionTask *)task
|
|
||||||
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
|
||||||
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
|
|
||||||
if (![challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
|
|
||||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SecTrustRef trustRef = challenge.protectionSpace.serverTrust;
|
|
||||||
if (trustRef == NULL) {
|
|
||||||
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BOOL needsCustomHandling = self.insecure || self.anchorOnly || self.anchors.count > 0 || self.pinnedPublicKeyHashes.length > 0;
|
|
||||||
if (!needsCustomHandling) {
|
|
||||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BOOL ok = YES;
|
|
||||||
if (!self.insecure) {
|
|
||||||
if (self.anchorOnly || self.anchors.count > 0) {
|
|
||||||
ok = box_evaluate_trust(trustRef, self.anchors, self.anchorOnly);
|
|
||||||
} else {
|
|
||||||
CFErrorRef error = NULL;
|
|
||||||
ok = SecTrustEvaluateWithError(trustRef, &error);
|
|
||||||
if (error != NULL) {
|
|
||||||
CFRelease(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ok && self.pinnedPublicKeyHashes.length > 0) {
|
|
||||||
CFArrayRef certificateChain = SecTrustCopyCertificateChain(trustRef);
|
|
||||||
SecCertificateRef leafCertificate = NULL;
|
|
||||||
if (certificateChain != NULL && CFArrayGetCount(certificateChain) > 0) {
|
|
||||||
leafCertificate = (SecCertificateRef)CFArrayGetValueAtIndex(certificateChain, 0);
|
|
||||||
}
|
|
||||||
if (leafCertificate == NULL) {
|
|
||||||
ok = NO;
|
|
||||||
} else {
|
|
||||||
NSData *leafData = CFBridgingRelease(SecCertificateCopyData(leafCertificate));
|
|
||||||
char *pinError = box_apple_http_verify_public_key_sha256(
|
|
||||||
(uint8_t *)self.pinnedPublicKeyHashes.bytes,
|
|
||||||
self.pinnedPublicKeyHashes.length,
|
|
||||||
(uint8_t *)leafData.bytes,
|
|
||||||
leafData.length
|
|
||||||
);
|
|
||||||
if (pinError != NULL) {
|
|
||||||
free(pinError);
|
|
||||||
ok = NO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (certificateChain != NULL) {
|
|
||||||
CFRelease(certificateChain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!ok) {
|
|
||||||
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:trustRef]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@interface BoxAppleHTTPSessionHandle : NSObject
|
|
||||||
@property(nonatomic, strong) NSURLSession *session;
|
|
||||||
@property(nonatomic, strong) BoxAppleHTTPSessionDelegate *delegate;
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation BoxAppleHTTPSessionHandle
|
|
||||||
@end
|
|
||||||
|
|
||||||
box_apple_http_session_t *box_apple_http_session_create(
|
|
||||||
const box_apple_http_session_config_t *config,
|
|
||||||
char **error_out
|
|
||||||
) {
|
|
||||||
@autoreleasepool {
|
|
||||||
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
|
||||||
sessionConfig.URLCache = nil;
|
|
||||||
sessionConfig.HTTPCookieStorage = nil;
|
|
||||||
sessionConfig.URLCredentialStorage = nil;
|
|
||||||
sessionConfig.HTTPShouldSetCookies = NO;
|
|
||||||
if (config != NULL && config->proxy_host != NULL && config->proxy_port > 0) {
|
|
||||||
NSMutableDictionary *proxyDictionary = [NSMutableDictionary dictionary];
|
|
||||||
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSProxyHost] = [NSString stringWithUTF8String:config->proxy_host];
|
|
||||||
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSProxyPort] = @(config->proxy_port);
|
|
||||||
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSVersion] = (__bridge NSString *)kCFStreamSocketSOCKSVersion5;
|
|
||||||
if (config->proxy_username != NULL) {
|
|
||||||
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSUser] = [NSString stringWithUTF8String:config->proxy_username];
|
|
||||||
}
|
|
||||||
if (config->proxy_password != NULL) {
|
|
||||||
proxyDictionary[(__bridge NSString *)kCFStreamPropertySOCKSPassword] = [NSString stringWithUTF8String:config->proxy_password];
|
|
||||||
}
|
|
||||||
sessionConfig.connectionProxyDictionary = proxyDictionary;
|
|
||||||
}
|
|
||||||
if (config != NULL && config->min_tls_version != 0) {
|
|
||||||
sessionConfig.TLSMinimumSupportedProtocolVersion = (tls_protocol_version_t)config->min_tls_version;
|
|
||||||
}
|
|
||||||
if (config != NULL && config->max_tls_version != 0) {
|
|
||||||
sessionConfig.TLSMaximumSupportedProtocolVersion = (tls_protocol_version_t)config->max_tls_version;
|
|
||||||
}
|
|
||||||
BoxAppleHTTPSessionDelegate *delegate = [[BoxAppleHTTPSessionDelegate alloc] init];
|
|
||||||
if (config != NULL) {
|
|
||||||
delegate.insecure = config->insecure;
|
|
||||||
delegate.anchorOnly = config->anchor_only;
|
|
||||||
delegate.anchors = box_parse_certificates_from_pem(config->anchor_pem, config->anchor_pem_len);
|
|
||||||
if (config->pinned_public_key_sha256 != NULL && config->pinned_public_key_sha256_len > 0) {
|
|
||||||
delegate.pinnedPublicKeyHashes = [NSData dataWithBytes:config->pinned_public_key_sha256 length:config->pinned_public_key_sha256_len];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:delegate delegateQueue:nil];
|
|
||||||
if (session == nil) {
|
|
||||||
box_set_error_string(error_out, @"create URLSession");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
BoxAppleHTTPSessionHandle *handle = [[BoxAppleHTTPSessionHandle alloc] init];
|
|
||||||
handle.session = session;
|
|
||||||
handle.delegate = delegate;
|
|
||||||
box_apple_http_session_t *sessionHandle = calloc(1, sizeof(box_apple_http_session_t));
|
|
||||||
sessionHandle->handle = (__bridge_retained void *)handle;
|
|
||||||
return sessionHandle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void box_apple_http_session_retire(box_apple_http_session_t *session) {
|
|
||||||
if (session == NULL || session->handle == NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BoxAppleHTTPSessionHandle *handle = (__bridge_transfer BoxAppleHTTPSessionHandle *)session->handle;
|
|
||||||
[handle.session finishTasksAndInvalidate];
|
|
||||||
free(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
void box_apple_http_session_close(box_apple_http_session_t *session) {
|
|
||||||
if (session == NULL || session->handle == NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BoxAppleHTTPSessionHandle *handle = (__bridge_transfer BoxAppleHTTPSessionHandle *)session->handle;
|
|
||||||
[handle.session invalidateAndCancel];
|
|
||||||
free(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
box_apple_http_task_t *box_apple_http_session_send_async(
|
|
||||||
box_apple_http_session_t *session,
|
|
||||||
const box_apple_http_request_t *request,
|
|
||||||
char **error_out
|
|
||||||
) {
|
|
||||||
@autoreleasepool {
|
|
||||||
if (session == NULL || session->handle == NULL || request == NULL || request->method == NULL || request->url == NULL) {
|
|
||||||
box_set_error_string(error_out, @"invalid apple HTTP request");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
BoxAppleHTTPSessionHandle *handle = (__bridge BoxAppleHTTPSessionHandle *)session->handle;
|
|
||||||
NSURL *requestURL = [NSURL URLWithString:[NSString stringWithUTF8String:request->url]];
|
|
||||||
if (requestURL == nil) {
|
|
||||||
box_set_error_string(error_out, @"invalid request URL");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:requestURL];
|
|
||||||
urlRequest.HTTPMethod = [NSString stringWithUTF8String:request->method];
|
|
||||||
for (size_t index = 0; index < request->header_count; index++) {
|
|
||||||
const char *key = request->header_keys[index];
|
|
||||||
const char *value = request->header_values[index];
|
|
||||||
if (key == NULL || value == NULL) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
[urlRequest addValue:[NSString stringWithUTF8String:value] forHTTPHeaderField:[NSString stringWithUTF8String:key]];
|
|
||||||
}
|
|
||||||
if (request->body != NULL && request->body_len > 0) {
|
|
||||||
urlRequest.HTTPBody = [NSData dataWithBytes:request->body length:request->body_len];
|
|
||||||
}
|
|
||||||
box_apple_http_task_t *task = calloc(1, sizeof(box_apple_http_task_t));
|
|
||||||
dispatch_semaphore_t doneSemaphore = dispatch_semaphore_create(0);
|
|
||||||
task->done_semaphore = (__bridge_retained void *)doneSemaphore;
|
|
||||||
NSURLSessionDataTask *dataTask = [handle.session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
||||||
if (error != nil) {
|
|
||||||
box_set_error_from_nserror(&task->error, error);
|
|
||||||
} else if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
|
|
||||||
box_set_error_string(&task->error, @"unexpected HTTP response type");
|
|
||||||
} else {
|
|
||||||
task->response = box_create_response((NSHTTPURLResponse *)response, data ?: [NSData data]);
|
|
||||||
}
|
|
||||||
dispatch_semaphore_signal((__bridge dispatch_semaphore_t)task->done_semaphore);
|
|
||||||
}];
|
|
||||||
if (dataTask == nil) {
|
|
||||||
box_set_error_string(error_out, @"create data task");
|
|
||||||
box_apple_http_task_close(task);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
task->task = (__bridge_retained void *)dataTask;
|
|
||||||
[dataTask resume];
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
box_apple_http_response_t *box_apple_http_task_wait(
|
|
||||||
box_apple_http_task_t *task,
|
|
||||||
char **error_out
|
|
||||||
) {
|
|
||||||
if (task == NULL || task->done_semaphore == NULL) {
|
|
||||||
box_set_error_string(error_out, @"invalid apple HTTP task");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
dispatch_semaphore_wait((__bridge dispatch_semaphore_t)task->done_semaphore, DISPATCH_TIME_FOREVER);
|
|
||||||
if (task->error != NULL) {
|
|
||||||
box_set_error_string(error_out, [NSString stringWithUTF8String:task->error]);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return task->response;
|
|
||||||
}
|
|
||||||
|
|
||||||
void box_apple_http_task_cancel(box_apple_http_task_t *task) {
|
|
||||||
if (task == NULL || task->task == NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
NSURLSessionTask *nsTask = (__bridge NSURLSessionTask *)task->task;
|
|
||||||
[nsTask cancel];
|
|
||||||
}
|
|
||||||
|
|
||||||
void box_apple_http_task_close(box_apple_http_task_t *task) {
|
|
||||||
if (task == NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (task->task != NULL) {
|
|
||||||
__unused NSURLSessionTask *nsTask = (__bridge_transfer NSURLSessionTask *)task->task;
|
|
||||||
task->task = NULL;
|
|
||||||
}
|
|
||||||
if (task->done_semaphore != NULL) {
|
|
||||||
__unused dispatch_semaphore_t doneSemaphore = (__bridge_transfer dispatch_semaphore_t)task->done_semaphore;
|
|
||||||
task->done_semaphore = NULL;
|
|
||||||
}
|
|
||||||
free(task->error);
|
|
||||||
free(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
void box_apple_http_response_free(box_apple_http_response_t *response) {
|
|
||||||
if (response == NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (size_t index = 0; index < response->header_count; index++) {
|
|
||||||
free(response->header_keys[index]);
|
|
||||||
free(response->header_values[index]);
|
|
||||||
}
|
|
||||||
free(response->header_keys);
|
|
||||||
free(response->header_values);
|
|
||||||
free(response->body);
|
|
||||||
free(response->error);
|
|
||||||
free(response);
|
|
||||||
}
|
|
||||||
@@ -1,876 +0,0 @@
|
|||||||
//go:build darwin && cgo
|
|
||||||
|
|
||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
stdtls "crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
boxTLS "github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing-box/route"
|
|
||||||
"github.com/sagernet/sing/common/json/badoption"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
const appleHTTPTestTimeout = 5 * time.Second
|
|
||||||
|
|
||||||
const appleHTTPRecoveryLoops = 5
|
|
||||||
|
|
||||||
type appleHTTPTestDialer struct {
|
|
||||||
dialer net.Dialer
|
|
||||||
listener net.ListenConfig
|
|
||||||
hostMap map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type appleHTTPObservedRequest struct {
|
|
||||||
method string
|
|
||||||
body string
|
|
||||||
host string
|
|
||||||
values []string
|
|
||||||
protoMajor int
|
|
||||||
}
|
|
||||||
|
|
||||||
type appleHTTPTestServer struct {
|
|
||||||
server *httptest.Server
|
|
||||||
baseURL string
|
|
||||||
dialHost string
|
|
||||||
certificate stdtls.Certificate
|
|
||||||
certificatePEM string
|
|
||||||
publicKeyHash []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewAppleSessionConfig(t *testing.T) {
|
|
||||||
serverCertificate, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
|
|
||||||
serverHash := certificatePublicKeySHA256(t, serverCertificate.Certificate[0])
|
|
||||||
otherHash := bytes.Repeat([]byte{0x7f}, applePinnedHashSize)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
options option.HTTPClientOptions
|
|
||||||
check func(t *testing.T, config appleSessionConfig)
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "success with certificate anchors",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
Version: 2,
|
|
||||||
DialerOptions: option.DialerOptions{
|
|
||||||
ConnectTimeout: badoption.Duration(2 * time.Second),
|
|
||||||
},
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
Enabled: true,
|
|
||||||
ServerName: "localhost",
|
|
||||||
MinVersion: "1.2",
|
|
||||||
MaxVersion: "1.3",
|
|
||||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
check: func(t *testing.T, config appleSessionConfig) {
|
|
||||||
t.Helper()
|
|
||||||
if config.serverName != "localhost" {
|
|
||||||
t.Fatalf("unexpected server name: %q", config.serverName)
|
|
||||||
}
|
|
||||||
if config.minVersion != stdtls.VersionTLS12 {
|
|
||||||
t.Fatalf("unexpected min version: %x", config.minVersion)
|
|
||||||
}
|
|
||||||
if config.maxVersion != stdtls.VersionTLS13 {
|
|
||||||
t.Fatalf("unexpected max version: %x", config.maxVersion)
|
|
||||||
}
|
|
||||||
if config.insecure {
|
|
||||||
t.Fatal("unexpected insecure flag")
|
|
||||||
}
|
|
||||||
if !config.anchorOnly {
|
|
||||||
t.Fatal("expected anchor_only")
|
|
||||||
}
|
|
||||||
if !strings.Contains(config.anchorPEM, "BEGIN CERTIFICATE") {
|
|
||||||
t.Fatalf("unexpected anchor pem: %q", config.anchorPEM)
|
|
||||||
}
|
|
||||||
if len(config.pinnedPublicKeySHA256s) != 0 {
|
|
||||||
t.Fatalf("unexpected pinned hashes: %d", len(config.pinnedPublicKeySHA256s))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success with flattened pins",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
Version: 2,
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
Enabled: true,
|
|
||||||
Insecure: true,
|
|
||||||
CertificatePublicKeySHA256: badoption.Listable[[]byte]{serverHash, otherHash},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
check: func(t *testing.T, config appleSessionConfig) {
|
|
||||||
t.Helper()
|
|
||||||
if !config.insecure {
|
|
||||||
t.Fatal("expected insecure flag")
|
|
||||||
}
|
|
||||||
if len(config.pinnedPublicKeySHA256s) != 2*applePinnedHashSize {
|
|
||||||
t.Fatalf("unexpected flattened pin length: %d", len(config.pinnedPublicKeySHA256s))
|
|
||||||
}
|
|
||||||
if !bytes.Equal(config.pinnedPublicKeySHA256s[:applePinnedHashSize], serverHash) {
|
|
||||||
t.Fatal("unexpected first pin")
|
|
||||||
}
|
|
||||||
if !bytes.Equal(config.pinnedPublicKeySHA256s[applePinnedHashSize:], otherHash) {
|
|
||||||
t.Fatal("unexpected second pin")
|
|
||||||
}
|
|
||||||
if config.anchorPEM != "" {
|
|
||||||
t.Fatalf("unexpected anchor pem: %q", config.anchorPEM)
|
|
||||||
}
|
|
||||||
if config.anchorOnly {
|
|
||||||
t.Fatal("unexpected anchor_only")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http11 unsupported",
|
|
||||||
options: option.HTTPClientOptions{Version: 1},
|
|
||||||
wantErr: "HTTP/1.1 is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http3 unsupported",
|
|
||||||
options: option.HTTPClientOptions{Version: 3},
|
|
||||||
wantErr: "HTTP/3 is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unknown version",
|
|
||||||
options: option.HTTPClientOptions{Version: 9},
|
|
||||||
wantErr: "unknown HTTP version: 9",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "disable version fallback unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
DisableVersionFallback: true,
|
|
||||||
},
|
|
||||||
wantErr: "disable_version_fallback is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "http2 options unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
HTTP2Options: option.HTTP2Options{
|
|
||||||
IdleTimeout: badoption.Duration(time.Second),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "HTTP/2 options are unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "quic options unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
HTTP3Options: option.QUICOptions{
|
|
||||||
InitialPacketSize: 1200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "QUIC options are unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tls engine unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{Engine: "go"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "tls.engine is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "disable sni unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{DisableSNI: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "disable_sni is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "alpn unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
ALPN: badoption.Listable[string]{"h2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "tls.alpn is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cipher suites unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
CipherSuites: badoption.Listable[string]{"TLS_AES_128_GCM_SHA256"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "cipher_suites is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "curve preferences unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
CurvePreferences: badoption.Listable[option.CurvePreference]{option.CurvePreference(option.X25519)},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "curve_preferences is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "client certificate unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
ClientCertificate: badoption.Listable[string]{"client-certificate"},
|
|
||||||
ClientKey: badoption.Listable[string]{"client-key"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "client certificate is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tls fragment unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{Fragment: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "tls fragment is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ktls unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{KernelTx: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "ktls is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ech unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
ECH: &option.OutboundECHOptions{Enabled: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "ech is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "utls unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
UTLS: &option.OutboundUTLSOptions{Enabled: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "utls is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "reality unsupported",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
Reality: &option.OutboundRealityOptions{Enabled: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "reality is unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "pin and certificate conflict",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
||||||
CertificatePublicKeySHA256: badoption.Listable[[]byte]{serverHash},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "certificate_public_key_sha256 is conflict with certificate or certificate_path",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid min version",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{MinVersion: "bogus"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "parse min_version",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid max version",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{MaxVersion: "bogus"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "parse max_version",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid pin length",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
CertificatePublicKeySHA256: badoption.Listable[[]byte]{{0x01, 0x02}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: "invalid certificate_public_key_sha256 length: 2",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
|
||||||
config, err := newAppleSessionConfig(context.Background(), testCase.options)
|
|
||||||
if testCase.wantErr != "" {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), testCase.wantErr) {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if testCase.check != nil {
|
|
||||||
testCase.check(t, config)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppleTransportVerifyPublicKeySHA256(t *testing.T) {
|
|
||||||
serverCertificate, _ := newAppleHTTPTestCertificate(t, "localhost")
|
|
||||||
goodHash := certificatePublicKeySHA256(t, serverCertificate.Certificate[0])
|
|
||||||
badHash := append([]byte(nil), goodHash...)
|
|
||||||
badHash[0] ^= 0xff
|
|
||||||
|
|
||||||
err := verifyApplePinnedPublicKeySHA256(goodHash, serverCertificate.Certificate[0])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected correct pin to succeed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = verifyApplePinnedPublicKeySHA256(badHash, serverCertificate.Certificate[0])
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected incorrect pin to fail")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "unrecognized remote public key") {
|
|
||||||
t.Fatalf("unexpected pin mismatch error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = verifyApplePinnedPublicKeySHA256(goodHash[:applePinnedHashSize-1], serverCertificate.Certificate[0])
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected malformed pin list to fail")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "invalid pinned public key list") {
|
|
||||||
t.Fatalf("unexpected malformed pin error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppleTransportRoundTripHTTPS(t *testing.T) {
|
|
||||||
requests := make(chan appleHTTPObservedRequest, 1)
|
|
||||||
server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
requests <- appleHTTPObservedRequest{
|
|
||||||
method: r.Method,
|
|
||||||
body: string(body),
|
|
||||||
host: r.Host,
|
|
||||||
values: append([]string(nil), r.Header.Values("X-Test")...),
|
|
||||||
protoMajor: r.ProtoMajor,
|
|
||||||
}
|
|
||||||
w.Header().Set("X-Reply", "apple")
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
_, _ = w.Write([]byte("response body"))
|
|
||||||
})
|
|
||||||
|
|
||||||
transport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{
|
|
||||||
Version: 2,
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: appleHTTPServerTLSOptions(server),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
request, err := http.NewRequest(http.MethodPost, server.URL("/roundtrip"), bytes.NewReader([]byte("request body")))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
request.Header.Add("X-Test", "one")
|
|
||||||
request.Header.Add("X-Test", "two")
|
|
||||||
request.Host = "custom.example"
|
|
||||||
|
|
||||||
response, err := transport.RoundTrip(request)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
responseBody := readResponseBody(t, response)
|
|
||||||
if response.StatusCode != http.StatusCreated {
|
|
||||||
t.Fatalf("unexpected status code: %d", response.StatusCode)
|
|
||||||
}
|
|
||||||
if response.Status != "201 Created" {
|
|
||||||
t.Fatalf("unexpected status: %q", response.Status)
|
|
||||||
}
|
|
||||||
if response.Header.Get("X-Reply") != "apple" {
|
|
||||||
t.Fatalf("unexpected response header: %q", response.Header.Get("X-Reply"))
|
|
||||||
}
|
|
||||||
if responseBody != "response body" {
|
|
||||||
t.Fatalf("unexpected response body: %q", responseBody)
|
|
||||||
}
|
|
||||||
if response.ContentLength != int64(len(responseBody)) {
|
|
||||||
t.Fatalf("unexpected content length: %d", response.ContentLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
observed := waitObservedRequest(t, requests)
|
|
||||||
if observed.method != http.MethodPost {
|
|
||||||
t.Fatalf("unexpected method: %q", observed.method)
|
|
||||||
}
|
|
||||||
if observed.body != "request body" {
|
|
||||||
t.Fatalf("unexpected request body: %q", observed.body)
|
|
||||||
}
|
|
||||||
if observed.host != "custom.example" {
|
|
||||||
t.Fatalf("unexpected host: %q", observed.host)
|
|
||||||
}
|
|
||||||
if observed.protoMajor != 2 {
|
|
||||||
t.Fatalf("expected HTTP/2 request, got HTTP/%d", observed.protoMajor)
|
|
||||||
}
|
|
||||||
var normalizedValues []string
|
|
||||||
for _, value := range observed.values {
|
|
||||||
for _, part := range strings.Split(value, ",") {
|
|
||||||
normalizedValues = append(normalizedValues, strings.TrimSpace(part))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slices.Sort(normalizedValues)
|
|
||||||
if !slices.Equal(normalizedValues, []string{"one", "two"}) {
|
|
||||||
t.Fatalf("unexpected header values: %#v", observed.values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppleTransportPinnedPublicKey(t *testing.T) {
|
|
||||||
server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = w.Write([]byte("pinned"))
|
|
||||||
})
|
|
||||||
|
|
||||||
goodTransport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{
|
|
||||||
Version: 2,
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
Enabled: true,
|
|
||||||
ServerName: "localhost",
|
|
||||||
Insecure: true,
|
|
||||||
CertificatePublicKeySHA256: badoption.Listable[[]byte]{server.publicKeyHash},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
response, err := goodTransport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/good"), nil))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected pinned request to succeed: %v", err)
|
|
||||||
}
|
|
||||||
response.Body.Close()
|
|
||||||
|
|
||||||
badHash := append([]byte(nil), server.publicKeyHash...)
|
|
||||||
badHash[0] ^= 0xff
|
|
||||||
badTransport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{
|
|
||||||
Version: 2,
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
Enabled: true,
|
|
||||||
ServerName: "localhost",
|
|
||||||
Insecure: true,
|
|
||||||
CertificatePublicKeySHA256: badoption.Listable[[]byte]{badHash},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
response, err = badTransport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/bad"), nil))
|
|
||||||
if err == nil {
|
|
||||||
response.Body.Close()
|
|
||||||
t.Fatal("expected incorrect pinned public key to fail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppleTransportGuardrails(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
options option.HTTPClientOptions
|
|
||||||
buildRequest func(t *testing.T) *http.Request
|
|
||||||
wantErrSubstr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "websocket upgrade rejected",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
Version: 2,
|
|
||||||
},
|
|
||||||
buildRequest: func(t *testing.T) *http.Request {
|
|
||||||
t.Helper()
|
|
||||||
request := newAppleHTTPRequest(t, http.MethodGet, "https://localhost/socket", nil)
|
|
||||||
request.Header.Set("Connection", "Upgrade")
|
|
||||||
request.Header.Set("Upgrade", "websocket")
|
|
||||||
return request
|
|
||||||
},
|
|
||||||
wantErrSubstr: "HTTP upgrade requests are unsupported in Apple HTTP engine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing url rejected",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
Version: 2,
|
|
||||||
},
|
|
||||||
buildRequest: func(t *testing.T) *http.Request {
|
|
||||||
t.Helper()
|
|
||||||
return &http.Request{Method: http.MethodGet}
|
|
||||||
},
|
|
||||||
wantErrSubstr: "missing request URL",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unsupported scheme rejected",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
Version: 2,
|
|
||||||
},
|
|
||||||
buildRequest: func(t *testing.T) *http.Request {
|
|
||||||
t.Helper()
|
|
||||||
return newAppleHTTPRequest(t, http.MethodGet, "ftp://localhost/file", nil)
|
|
||||||
},
|
|
||||||
wantErrSubstr: "unsupported URL scheme: ftp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "server name mismatch rejected",
|
|
||||||
options: option.HTTPClientOptions{
|
|
||||||
Version: 2,
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: &option.OutboundTLSOptions{
|
|
||||||
Enabled: true,
|
|
||||||
ServerName: "example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
buildRequest: func(t *testing.T) *http.Request {
|
|
||||||
t.Helper()
|
|
||||||
return newAppleHTTPRequest(t, http.MethodGet, "https://localhost/path", nil)
|
|
||||||
},
|
|
||||||
wantErrSubstr: "tls.server_name is unsupported in Apple HTTP engine unless it matches request host",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
|
||||||
transport := newAppleHTTPTestTransport(t, nil, testCase.options)
|
|
||||||
response, err := transport.RoundTrip(testCase.buildRequest(t))
|
|
||||||
if err == nil {
|
|
||||||
response.Body.Close()
|
|
||||||
t.Fatal("expected error")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), testCase.wantErrSubstr) {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppleTransportCancellationRecovery(t *testing.T) {
|
|
||||||
server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.URL.Path {
|
|
||||||
case "/block":
|
|
||||||
select {
|
|
||||||
case <-r.Context().Done():
|
|
||||||
return
|
|
||||||
case <-time.After(appleHTTPTestTimeout):
|
|
||||||
http.Error(w, "request was not canceled", http.StatusGatewayTimeout)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = w.Write([]byte("ok"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
transport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{
|
|
||||||
Version: 2,
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: appleHTTPServerTLSOptions(server),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
for index := 0; index < appleHTTPRecoveryLoops; index++ {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
||||||
request := newAppleHTTPRequestWithContext(t, ctx, http.MethodGet, server.URL("/block"), nil)
|
|
||||||
response, err := transport.RoundTrip(request)
|
|
||||||
cancel()
|
|
||||||
if err == nil {
|
|
||||||
response.Body.Close()
|
|
||||||
t.Fatalf("iteration %d: expected cancellation error", index)
|
|
||||||
}
|
|
||||||
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
|
|
||||||
t.Fatalf("iteration %d: unexpected cancellation error: %v", index, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err = transport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/ok"), nil))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("iteration %d: follow-up request failed: %v", index, err)
|
|
||||||
}
|
|
||||||
if body := readResponseBody(t, response); body != "ok" {
|
|
||||||
response.Body.Close()
|
|
||||||
t.Fatalf("iteration %d: unexpected follow-up body: %q", index, body)
|
|
||||||
}
|
|
||||||
response.Body.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppleTransportLifecycle(t *testing.T) {
|
|
||||||
server := startAppleHTTPTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = w.Write([]byte("ok"))
|
|
||||||
})
|
|
||||||
|
|
||||||
transport := newAppleHTTPTestTransport(t, server, option.HTTPClientOptions{
|
|
||||||
Version: 2,
|
|
||||||
OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
|
|
||||||
TLS: appleHTTPServerTLSOptions(server),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
clone := transport.Clone()
|
|
||||||
t.Cleanup(func() {
|
|
||||||
_ = clone.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
assertAppleHTTPSucceeds(t, transport, server.URL("/original"))
|
|
||||||
assertAppleHTTPSucceeds(t, clone, server.URL("/clone"))
|
|
||||||
|
|
||||||
transport.CloseIdleConnections()
|
|
||||||
assertAppleHTTPSucceeds(t, transport, server.URL("/reset"))
|
|
||||||
|
|
||||||
if err := transport.Close(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := transport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/closed"), nil))
|
|
||||||
if err == nil {
|
|
||||||
response.Body.Close()
|
|
||||||
t.Fatal("expected closed transport to fail")
|
|
||||||
}
|
|
||||||
if !errors.Is(err, net.ErrClosed) {
|
|
||||||
t.Fatalf("unexpected closed transport error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertAppleHTTPSucceeds(t, clone, server.URL("/clone-after-original-close"))
|
|
||||||
|
|
||||||
clone.CloseIdleConnections()
|
|
||||||
assertAppleHTTPSucceeds(t, clone, server.URL("/clone-reset"))
|
|
||||||
|
|
||||||
if err := clone.Close(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
response, err = clone.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, server.URL("/clone-closed"), nil))
|
|
||||||
if err == nil {
|
|
||||||
response.Body.Close()
|
|
||||||
t.Fatal("expected closed clone to fail")
|
|
||||||
}
|
|
||||||
if !errors.Is(err, net.ErrClosed) {
|
|
||||||
t.Fatalf("unexpected closed clone error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startAppleHTTPTestServer(t *testing.T, handler http.HandlerFunc) *appleHTTPTestServer {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
serverCertificate, serverCertificatePEM := newAppleHTTPTestCertificate(t, "localhost")
|
|
||||||
server := httptest.NewUnstartedServer(handler)
|
|
||||||
server.EnableHTTP2 = true
|
|
||||||
server.TLS = &stdtls.Config{
|
|
||||||
Certificates: []stdtls.Certificate{serverCertificate},
|
|
||||||
MinVersion: stdtls.VersionTLS12,
|
|
||||||
}
|
|
||||||
server.StartTLS()
|
|
||||||
t.Cleanup(server.Close)
|
|
||||||
|
|
||||||
parsedURL, err := url.Parse(server.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
baseURL := *parsedURL
|
|
||||||
baseURL.Host = net.JoinHostPort("localhost", parsedURL.Port())
|
|
||||||
|
|
||||||
return &appleHTTPTestServer{
|
|
||||||
server: server,
|
|
||||||
baseURL: baseURL.String(),
|
|
||||||
dialHost: parsedURL.Hostname(),
|
|
||||||
certificate: serverCertificate,
|
|
||||||
certificatePEM: serverCertificatePEM,
|
|
||||||
publicKeyHash: certificatePublicKeySHA256(t, serverCertificate.Certificate[0]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *appleHTTPTestServer) URL(path string) string {
|
|
||||||
if path == "" {
|
|
||||||
return s.baseURL
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(path, "/") {
|
|
||||||
return s.baseURL + path
|
|
||||||
}
|
|
||||||
return s.baseURL + "/" + path
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAppleHTTPTestTransport(t *testing.T, server *appleHTTPTestServer, options option.HTTPClientOptions) adapter.HTTPTransport {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
ctx := service.ContextWith[adapter.ConnectionManager](
|
|
||||||
context.Background(),
|
|
||||||
route.NewConnectionManager(log.NewNOPFactory().NewLogger("connection")),
|
|
||||||
)
|
|
||||||
dialer := &appleHTTPTestDialer{
|
|
||||||
hostMap: make(map[string]string),
|
|
||||||
}
|
|
||||||
if server != nil {
|
|
||||||
dialer.hostMap["localhost"] = server.dialHost
|
|
||||||
}
|
|
||||||
|
|
||||||
transport, err := newAppleTransport(ctx, log.NewNOPFactory().NewLogger("httpclient"), dialer, options)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
_ = transport.Close()
|
|
||||||
})
|
|
||||||
return transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *appleHTTPTestDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
||||||
host := destination.AddrString()
|
|
||||||
if destination.IsDomain() {
|
|
||||||
host = destination.Fqdn
|
|
||||||
if mappedHost, loaded := d.hostMap[host]; loaded {
|
|
||||||
host = mappedHost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return d.dialer.DialContext(ctx, network, net.JoinHostPort(host, strconv.Itoa(int(destination.Port))))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *appleHTTPTestDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
|
||||||
host := destination.AddrString()
|
|
||||||
if destination.IsDomain() {
|
|
||||||
host = destination.Fqdn
|
|
||||||
if mappedHost, loaded := d.hostMap[host]; loaded {
|
|
||||||
host = mappedHost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if host == "" {
|
|
||||||
host = "127.0.0.1"
|
|
||||||
}
|
|
||||||
return d.listener.ListenPacket(ctx, N.NetworkUDP, net.JoinHostPort(host, strconv.Itoa(int(destination.Port))))
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAppleHTTPTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
privateKeyPEM, certificatePEM, err := boxTLS.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
certificate, err := stdtls.X509KeyPair(certificatePEM, privateKeyPEM)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return certificate, string(certificatePEM)
|
|
||||||
}
|
|
||||||
|
|
||||||
func certificatePublicKeySHA256(t *testing.T, certificateDER []byte) []byte {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
certificate, err := x509.ParseCertificate(certificateDER)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
publicKeyDER, err := x509.MarshalPKIXPublicKey(certificate.PublicKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
hashValue := sha256.Sum256(publicKeyDER)
|
|
||||||
return append([]byte(nil), hashValue[:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func appleHTTPServerTLSOptions(server *appleHTTPTestServer) *option.OutboundTLSOptions {
|
|
||||||
return &option.OutboundTLSOptions{
|
|
||||||
Enabled: true,
|
|
||||||
ServerName: "localhost",
|
|
||||||
Certificate: badoption.Listable[string]{server.certificatePEM},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAppleHTTPRequest(t *testing.T, method string, rawURL string, body []byte) *http.Request {
|
|
||||||
t.Helper()
|
|
||||||
return newAppleHTTPRequestWithContext(t, context.Background(), method, rawURL, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAppleHTTPRequestWithContext(t *testing.T, ctx context.Context, method string, rawURL string, body []byte) *http.Request {
|
|
||||||
t.Helper()
|
|
||||||
request, err := http.NewRequestWithContext(ctx, method, rawURL, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return request
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitObservedRequest(t *testing.T, requests <-chan appleHTTPObservedRequest) appleHTTPObservedRequest {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case request := <-requests:
|
|
||||||
return request
|
|
||||||
case <-time.After(appleHTTPTestTimeout):
|
|
||||||
t.Fatal("timed out waiting for observed request")
|
|
||||||
return appleHTTPObservedRequest{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readResponseBody(t *testing.T, response *http.Response) string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return string(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertAppleHTTPSucceeds(t *testing.T, transport adapter.HTTPTransport, rawURL string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
response, err := transport.RoundTrip(newAppleHTTPRequest(t, http.MethodGet, rawURL, nil))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
if body := readResponseBody(t, response); body != "ok" {
|
|
||||||
t.Fatalf("unexpected response body: %q", body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
//go:build !darwin || !cgo
|
|
||||||
|
|
||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newAppleTransport(ctx context.Context, logger logger.ContextLogger, rawDialer N.Dialer, options option.HTTPClientOptions) (adapter.HTTPTransport, error) {
|
|
||||||
return nil, E.New("Apple HTTP engine is not available on non-Apple platforms")
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Transport struct {
|
|
||||||
transport adapter.HTTPTransport
|
|
||||||
dialer N.Dialer
|
|
||||||
headers http.Header
|
|
||||||
host string
|
|
||||||
tag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string, options option.HTTPClientOptions) (*Transport, error) {
|
|
||||||
rawDialer, err := dialer.NewWithOptions(dialer.Options{
|
|
||||||
Context: ctx,
|
|
||||||
Options: options.DialerOptions,
|
|
||||||
RemoteIsDomain: true,
|
|
||||||
DirectResolver: options.DirectResolver,
|
|
||||||
ResolverOnDetour: options.ResolveOnDetour,
|
|
||||||
NewDialer: options.ResolveOnDetour,
|
|
||||||
DefaultOutbound: options.DefaultOutbound,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch options.Engine {
|
|
||||||
case C.TLSEngineApple:
|
|
||||||
transport, transportErr := newAppleTransport(ctx, logger, rawDialer, options)
|
|
||||||
if transportErr != nil {
|
|
||||||
return nil, transportErr
|
|
||||||
}
|
|
||||||
headers := options.Headers.Build()
|
|
||||||
host := headers.Get("Host")
|
|
||||||
headers.Del("Host")
|
|
||||||
return &Transport{
|
|
||||||
transport: transport,
|
|
||||||
dialer: rawDialer,
|
|
||||||
headers: headers,
|
|
||||||
host: host,
|
|
||||||
tag: tag,
|
|
||||||
}, nil
|
|
||||||
case C.TLSEngineDefault, "go":
|
|
||||||
default:
|
|
||||||
return nil, E.New("unknown HTTP engine: ", options.Engine)
|
|
||||||
}
|
|
||||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
|
||||||
tlsOptions.Enabled = true
|
|
||||||
baseTLSConfig, err := tls.NewClientWithOptions(tls.ClientOptions{
|
|
||||||
Context: ctx,
|
|
||||||
Logger: logger,
|
|
||||||
Options: tlsOptions,
|
|
||||||
AllowEmptyServerName: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return NewTransportWithDialer(rawDialer, baseTLSConfig, tag, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTransportWithDialer(rawDialer N.Dialer, baseTLSConfig tls.Config, tag string, options option.HTTPClientOptions) (*Transport, error) {
|
|
||||||
transport, err := newTransport(rawDialer, baseTLSConfig, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
headers := options.Headers.Build()
|
|
||||||
host := headers.Get("Host")
|
|
||||||
headers.Del("Host")
|
|
||||||
return &Transport{
|
|
||||||
transport: transport,
|
|
||||||
dialer: rawDialer,
|
|
||||||
headers: headers,
|
|
||||||
host: host,
|
|
||||||
tag: tag,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTPClientOptions) (adapter.HTTPTransport, error) {
|
|
||||||
version := options.Version
|
|
||||||
if version == 0 {
|
|
||||||
version = 2
|
|
||||||
}
|
|
||||||
fallbackDelay := time.Duration(options.DialerOptions.FallbackDelay)
|
|
||||||
if fallbackDelay == 0 {
|
|
||||||
fallbackDelay = 300 * time.Millisecond
|
|
||||||
}
|
|
||||||
var transport adapter.HTTPTransport
|
|
||||||
var err error
|
|
||||||
switch version {
|
|
||||||
case 1:
|
|
||||||
transport = newHTTP1Transport(rawDialer, baseTLSConfig)
|
|
||||||
case 2:
|
|
||||||
if options.DisableVersionFallback {
|
|
||||||
transport, err = newHTTP2Transport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
|
||||||
} else {
|
|
||||||
transport, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
|
||||||
}
|
|
||||||
case 3:
|
|
||||||
if baseTLSConfig != nil {
|
|
||||||
_, err = baseTLSConfig.STDConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options.DisableVersionFallback {
|
|
||||||
transport, err = newHTTP3Transport(rawDialer, baseTLSConfig, options.HTTP3Options)
|
|
||||||
} else {
|
|
||||||
var h2Fallback adapter.HTTPTransport
|
|
||||||
h2Fallback, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
transport, err = newHTTP3FallbackTransport(rawDialer, baseTLSConfig, h2Fallback, options.HTTP3Options, fallbackDelay)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, E.New("unknown HTTP version: ", version)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return transport, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
if c.tag == "" && len(c.headers) == 0 && c.host == "" {
|
|
||||||
return c.transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
if c.tag != "" {
|
|
||||||
if transportTag, loaded := transportTagFromContext(request.Context()); loaded && transportTag == c.tag {
|
|
||||||
return nil, E.New("HTTP request loopback in transport[", c.tag, "]")
|
|
||||||
}
|
|
||||||
request = request.Clone(contextWithTransportTag(request.Context(), c.tag))
|
|
||||||
} else {
|
|
||||||
request = request.Clone(request.Context())
|
|
||||||
}
|
|
||||||
applyHeaders(request, c.headers, c.host)
|
|
||||||
return c.transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Transport) CloseIdleConnections() {
|
|
||||||
c.transport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Transport) Clone() adapter.HTTPTransport {
|
|
||||||
return &Transport{
|
|
||||||
transport: c.transport.Clone(),
|
|
||||||
dialer: c.dialer,
|
|
||||||
headers: c.headers.Clone(),
|
|
||||||
host: c.host,
|
|
||||||
tag: c.tag,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Transport) Close() error {
|
|
||||||
return c.transport.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitializeDetour eagerly resolves the detour dialer backing transport so that
|
|
||||||
// detour misconfigurations surface at startup instead of on the first request.
|
|
||||||
func InitializeDetour(transport adapter.HTTPTransport) error {
|
|
||||||
if shared, isShared := transport.(*sharedTransport); isShared {
|
|
||||||
transport = shared.HTTPTransport
|
|
||||||
}
|
|
||||||
inner, isInner := transport.(*Transport)
|
|
||||||
if !isInner {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return dialer.InitializeDetour(inner.dialer)
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type transportKey struct{}
|
|
||||||
|
|
||||||
func contextWithTransportTag(ctx context.Context, transportTag string) context.Context {
|
|
||||||
return context.WithValue(ctx, transportKey{}, transportTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func transportTagFromContext(ctx context.Context) (string, bool) {
|
|
||||||
value, loaded := ctx.Value(transportKey{}).(string)
|
|
||||||
return value, loaded
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
stdTLS "crypto/tls"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dialTLS(ctx context.Context, rawDialer N.Dialer, baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string, expectProto string) (net.Conn, error) {
|
|
||||||
if baseTLSConfig == nil {
|
|
||||||
return nil, E.New("TLS transport unavailable")
|
|
||||||
}
|
|
||||||
tlsConfig := baseTLSConfig.Clone()
|
|
||||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
|
||||||
tlsConfig.SetServerName(destination.AddrString())
|
|
||||||
}
|
|
||||||
tlsConfig.SetNextProtos(nextProtos)
|
|
||||||
conn, err := rawDialer.DialContext(ctx, N.NetworkTCP, destination)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsConn, err := tls.ClientHandshake(ctx, conn, tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if expectProto != "" && tlsConn.ConnectionState().NegotiatedProtocol != expectProto {
|
|
||||||
tlsConn.Close()
|
|
||||||
return nil, errHTTP2Fallback
|
|
||||||
}
|
|
||||||
return tlsConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyHeaders(request *http.Request, headers http.Header, host string) {
|
|
||||||
for header, values := range headers {
|
|
||||||
request.Header[header] = append([]string(nil), values...)
|
|
||||||
}
|
|
||||||
if host != "" {
|
|
||||||
request.Host = host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestRequiresHTTP1(request *http.Request) bool {
|
|
||||||
return strings.Contains(strings.ToLower(request.Header.Get("Connection")), "upgrade") &&
|
|
||||||
strings.EqualFold(request.Header.Get("Upgrade"), "websocket")
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestReplayable(request *http.Request) bool {
|
|
||||||
return request.Body == nil || request.Body == http.NoBody || request.GetBody != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneRequestForRetry(request *http.Request) *http.Request {
|
|
||||||
cloned := request.Clone(request.Context())
|
|
||||||
if request.Body != nil && request.Body != http.NoBody && request.GetBody != nil {
|
|
||||||
cloned.Body = mustGetBody(request)
|
|
||||||
}
|
|
||||||
return cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustGetBody(request *http.Request) io.ReadCloser {
|
|
||||||
body, err := request.GetBody()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildSTDTLSConfig(baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string) (*stdTLS.Config, error) {
|
|
||||||
if baseTLSConfig == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
tlsConfig := baseTLSConfig.Clone()
|
|
||||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
|
||||||
tlsConfig.SetServerName(destination.AddrString())
|
|
||||||
}
|
|
||||||
tlsConfig.SetNextProtos(nextProtos)
|
|
||||||
return tlsConfig.STDConfig()
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type http1Transport struct {
|
|
||||||
transport *http.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP1Transport(rawDialer N.Dialer, baseTLSConfig tls.Config) *http1Transport {
|
|
||||||
transport := &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return rawDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if baseTLSConfig != nil {
|
|
||||||
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{"http/1.1"}, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &http1Transport{transport: transport}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http1Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
return t.transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http1Transport) CloseIdleConnections() {
|
|
||||||
t.transport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http1Transport) Clone() adapter.HTTPTransport {
|
|
||||||
return &http1Transport{transport: t.transport.Clone()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http1Transport) Close() error {
|
|
||||||
t.CloseIdleConnections()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
stdTLS "crypto/tls"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CloneHTTP2Transport(transport *http2.Transport) *http2.Transport {
|
|
||||||
return &http2.Transport{
|
|
||||||
ReadIdleTimeout: transport.ReadIdleTimeout,
|
|
||||||
PingTimeout: transport.PingTimeout,
|
|
||||||
DialTLSContext: transport.DialTLSContext,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ConfigureHTTP2Transport(options option.HTTP2Options) (*http2.Transport, error) {
|
|
||||||
stdTransport := &http.Transport{
|
|
||||||
TLSClientConfig: &stdTLS.Config{},
|
|
||||||
HTTP2: &http.HTTP2Config{
|
|
||||||
MaxReceiveBufferPerStream: int(options.StreamReceiveWindow.Value()),
|
|
||||||
MaxReceiveBufferPerConnection: int(options.ConnectionReceiveWindow.Value()),
|
|
||||||
MaxConcurrentStreams: options.MaxConcurrentStreams,
|
|
||||||
SendPingTimeout: time.Duration(options.KeepAlivePeriod),
|
|
||||||
PingTimeout: time.Duration(options.IdleTimeout),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
h2Transport, err := http2.ConfigureTransports(stdTransport)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "configure HTTP/2 transport")
|
|
||||||
}
|
|
||||||
// ConfigureTransports binds ConnPool to the throwaway http.Transport; sever it so DialTLSContext is used directly.
|
|
||||||
h2Transport.ConnPool = nil
|
|
||||||
h2Transport.ReadIdleTimeout = time.Duration(options.KeepAlivePeriod)
|
|
||||||
h2Transport.PingTimeout = time.Duration(options.IdleTimeout)
|
|
||||||
return h2Transport, nil
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
stdTLS "crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errHTTP2Fallback = E.New("fallback to HTTP/1.1")
|
|
||||||
|
|
||||||
type http2FallbackTransport struct {
|
|
||||||
h2Transport *http2.Transport
|
|
||||||
h1Transport *http1Transport
|
|
||||||
h2Fallback *atomic.Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP2FallbackTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2FallbackTransport, error) {
|
|
||||||
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
|
|
||||||
var fallback atomic.Bool
|
|
||||||
h2Transport, err := ConfigureHTTP2Transport(options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
|
|
||||||
conn, dialErr := dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS, "http/1.1"}, http2.NextProtoTLS)
|
|
||||||
if dialErr != nil {
|
|
||||||
if errors.Is(dialErr, errHTTP2Fallback) {
|
|
||||||
fallback.Store(true)
|
|
||||||
}
|
|
||||||
return nil, dialErr
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
return &http2FallbackTransport{
|
|
||||||
h2Transport: h2Transport,
|
|
||||||
h1Transport: h1,
|
|
||||||
h2Fallback: &fallback,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
return t.roundTrip(request, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2FallbackTransport) roundTrip(request *http.Request, allowHTTP1Fallback bool) (*http.Response, error) {
|
|
||||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
|
||||||
return t.h1Transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
if t.h2Fallback.Load() {
|
|
||||||
if !allowHTTP1Fallback {
|
|
||||||
return nil, errHTTP2Fallback
|
|
||||||
}
|
|
||||||
return t.h1Transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
response, err := t.h2Transport.RoundTrip(request)
|
|
||||||
if err == nil {
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
if !errors.Is(err, errHTTP2Fallback) || !allowHTTP1Fallback {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return t.h1Transport.RoundTrip(cloneRequestForRetry(request))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2FallbackTransport) CloseIdleConnections() {
|
|
||||||
t.h1Transport.CloseIdleConnections()
|
|
||||||
t.h2Transport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2FallbackTransport) Clone() adapter.HTTPTransport {
|
|
||||||
return &http2FallbackTransport{
|
|
||||||
h2Transport: CloneHTTP2Transport(t.h2Transport),
|
|
||||||
h1Transport: t.h1Transport.Clone().(*http1Transport),
|
|
||||||
h2Fallback: t.h2Fallback,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2FallbackTransport) Close() error {
|
|
||||||
t.CloseIdleConnections()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
stdTLS "crypto/tls"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type http2Transport struct {
|
|
||||||
h2Transport *http2.Transport
|
|
||||||
h1Transport *http1Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP2Transport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2Transport, error) {
|
|
||||||
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
|
|
||||||
h2Transport, err := ConfigureHTTP2Transport(options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
|
|
||||||
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS}, http2.NextProtoTLS)
|
|
||||||
}
|
|
||||||
return &http2Transport{
|
|
||||||
h2Transport: h2Transport,
|
|
||||||
h1Transport: h1,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
|
||||||
return t.h1Transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
return t.h2Transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2Transport) CloseIdleConnections() {
|
|
||||||
t.h1Transport.CloseIdleConnections()
|
|
||||||
t.h2Transport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2Transport) Clone() adapter.HTTPTransport {
|
|
||||||
return &http2Transport{
|
|
||||||
h2Transport: CloneHTTP2Transport(t.h2Transport),
|
|
||||||
h1Transport: t.h1Transport.Clone().(*http1Transport),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2Transport) Close() error {
|
|
||||||
t.CloseIdleConnections()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
//go:build with_quic
|
|
||||||
|
|
||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
stdTLS "crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/quic-go"
|
|
||||||
"github.com/sagernet/quic-go/http3"
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type http3Transport struct {
|
|
||||||
h3Transport *http3.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
type http3FallbackTransport struct {
|
|
||||||
h3Transport *http3.Transport
|
|
||||||
h2Fallback adapter.HTTPTransport
|
|
||||||
fallbackDelay time.Duration
|
|
||||||
brokenAccess sync.Mutex
|
|
||||||
brokenUntil time.Time
|
|
||||||
brokenBackoff time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP3RoundTripper(
|
|
||||||
rawDialer N.Dialer,
|
|
||||||
baseTLSConfig tls.Config,
|
|
||||||
options option.QUICOptions,
|
|
||||||
) *http3.Transport {
|
|
||||||
var handshakeTimeout time.Duration
|
|
||||||
if baseTLSConfig != nil {
|
|
||||||
handshakeTimeout = baseTLSConfig.HandshakeTimeout()
|
|
||||||
}
|
|
||||||
quicConfig := &quic.Config{
|
|
||||||
InitialStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
|
||||||
MaxStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
|
||||||
InitialConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
|
||||||
MaxConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
|
||||||
KeepAlivePeriod: time.Duration(options.KeepAlivePeriod),
|
|
||||||
MaxIdleTimeout: time.Duration(options.IdleTimeout),
|
|
||||||
DisablePathMTUDiscovery: options.DisablePathMTUDiscovery,
|
|
||||||
}
|
|
||||||
if options.InitialPacketSize > 0 {
|
|
||||||
quicConfig.InitialPacketSize = uint16(options.InitialPacketSize)
|
|
||||||
}
|
|
||||||
if options.MaxConcurrentStreams > 0 {
|
|
||||||
quicConfig.MaxIncomingStreams = int64(options.MaxConcurrentStreams)
|
|
||||||
}
|
|
||||||
if handshakeTimeout > 0 {
|
|
||||||
quicConfig.HandshakeIdleTimeout = handshakeTimeout
|
|
||||||
}
|
|
||||||
h3Transport := &http3.Transport{
|
|
||||||
TLSClientConfig: &stdTLS.Config{},
|
|
||||||
QUICConfig: quicConfig,
|
|
||||||
Dial: func(ctx context.Context, addr string, tlsConfig *stdTLS.Config, quicConfig *quic.Config) (*quic.Conn, error) {
|
|
||||||
if handshakeTimeout > 0 && quicConfig.HandshakeIdleTimeout == 0 {
|
|
||||||
quicConfig = quicConfig.Clone()
|
|
||||||
quicConfig.HandshakeIdleTimeout = handshakeTimeout
|
|
||||||
}
|
|
||||||
if baseTLSConfig != nil {
|
|
||||||
var err error
|
|
||||||
tlsConfig, err = buildSTDTLSConfig(baseTLSConfig, M.ParseSocksaddr(addr), []string{http3.NextProtoH3})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tlsConfig = tlsConfig.Clone()
|
|
||||||
tlsConfig.NextProtos = []string{http3.NextProtoH3}
|
|
||||||
}
|
|
||||||
conn, err := rawDialer.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddr(addr))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
quicConn, err := quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsConfig, quicConfig)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return quicConn, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return h3Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP3Transport(
|
|
||||||
rawDialer N.Dialer,
|
|
||||||
baseTLSConfig tls.Config,
|
|
||||||
options option.QUICOptions,
|
|
||||||
) (adapter.HTTPTransport, error) {
|
|
||||||
return &http3Transport{
|
|
||||||
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP3FallbackTransport(
|
|
||||||
rawDialer N.Dialer,
|
|
||||||
baseTLSConfig tls.Config,
|
|
||||||
h2Fallback adapter.HTTPTransport,
|
|
||||||
options option.QUICOptions,
|
|
||||||
fallbackDelay time.Duration,
|
|
||||||
) (adapter.HTTPTransport, error) {
|
|
||||||
return &http3FallbackTransport{
|
|
||||||
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
|
|
||||||
h2Fallback: h2Fallback,
|
|
||||||
fallbackDelay: fallbackDelay,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
return t.h3Transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3Transport) CloseIdleConnections() {
|
|
||||||
t.h3Transport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3Transport) Close() error {
|
|
||||||
t.CloseIdleConnections()
|
|
||||||
return t.h3Transport.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3Transport) Clone() adapter.HTTPTransport {
|
|
||||||
return &http3Transport{
|
|
||||||
h3Transport: t.h3Transport,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
|
||||||
return t.h2Fallback.RoundTrip(request)
|
|
||||||
}
|
|
||||||
return t.roundTripHTTP3(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) roundTripHTTP3(request *http.Request) (*http.Response, error) {
|
|
||||||
if t.h3Broken() {
|
|
||||||
return t.h2FallbackRoundTrip(request)
|
|
||||||
}
|
|
||||||
response, err := t.h3Transport.RoundTripOpt(request, http3.RoundTripOpt{OnlyCachedConn: true})
|
|
||||||
if err == nil {
|
|
||||||
t.clearH3Broken()
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
if !errors.Is(err, http3.ErrNoCachedConn) {
|
|
||||||
t.markH3Broken()
|
|
||||||
return t.h2FallbackRoundTrip(cloneRequestForRetry(request))
|
|
||||||
}
|
|
||||||
if !requestReplayable(request) {
|
|
||||||
response, err = t.h3Transport.RoundTrip(request)
|
|
||||||
if err == nil {
|
|
||||||
t.clearH3Broken()
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
t.markH3Broken()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return t.roundTripHTTP3Race(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request) (*http.Response, error) {
|
|
||||||
ctx, cancel := context.WithCancel(request.Context())
|
|
||||||
defer cancel()
|
|
||||||
type result struct {
|
|
||||||
response *http.Response
|
|
||||||
err error
|
|
||||||
h3 bool
|
|
||||||
}
|
|
||||||
results := make(chan result, 2)
|
|
||||||
startRoundTrip := func(request *http.Request, useH3 bool) {
|
|
||||||
request = request.WithContext(ctx)
|
|
||||||
var (
|
|
||||||
response *http.Response
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if useH3 {
|
|
||||||
response, err = t.h3Transport.RoundTrip(request)
|
|
||||||
} else {
|
|
||||||
response, err = t.h2FallbackRoundTrip(request)
|
|
||||||
}
|
|
||||||
results <- result{response: response, err: err, h3: useH3}
|
|
||||||
}
|
|
||||||
goroutines := 1
|
|
||||||
received := 0
|
|
||||||
drainRemaining := func() {
|
|
||||||
cancel()
|
|
||||||
for range goroutines - received {
|
|
||||||
go func() {
|
|
||||||
loser := <-results
|
|
||||||
if loser.response != nil && loser.response.Body != nil {
|
|
||||||
loser.response.Body.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go startRoundTrip(cloneRequestForRetry(request), true)
|
|
||||||
timer := time.NewTimer(t.fallbackDelay)
|
|
||||||
defer timer.Stop()
|
|
||||||
var (
|
|
||||||
h3Err error
|
|
||||||
fallbackErr error
|
|
||||||
)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
if goroutines == 1 {
|
|
||||||
goroutines++
|
|
||||||
go startRoundTrip(cloneRequestForRetry(request), false)
|
|
||||||
}
|
|
||||||
case raceResult := <-results:
|
|
||||||
received++
|
|
||||||
if raceResult.err == nil {
|
|
||||||
if raceResult.h3 {
|
|
||||||
t.clearH3Broken()
|
|
||||||
}
|
|
||||||
drainRemaining()
|
|
||||||
return raceResult.response, nil
|
|
||||||
}
|
|
||||||
if raceResult.h3 {
|
|
||||||
t.markH3Broken()
|
|
||||||
h3Err = raceResult.err
|
|
||||||
if goroutines == 1 {
|
|
||||||
goroutines++
|
|
||||||
if !timer.Stop() {
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go startRoundTrip(cloneRequestForRetry(request), false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fallbackErr = raceResult.err
|
|
||||||
}
|
|
||||||
if received < goroutines {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
drainRemaining()
|
|
||||||
switch {
|
|
||||||
case h3Err != nil && fallbackErr != nil:
|
|
||||||
return nil, E.Errors(h3Err, fallbackErr)
|
|
||||||
case fallbackErr != nil:
|
|
||||||
return nil, fallbackErr
|
|
||||||
default:
|
|
||||||
return nil, h3Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) h2FallbackRoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
if fallback, isFallback := t.h2Fallback.(*http2FallbackTransport); isFallback {
|
|
||||||
return fallback.roundTrip(request, true)
|
|
||||||
}
|
|
||||||
return t.h2Fallback.RoundTrip(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) CloseIdleConnections() {
|
|
||||||
t.h3Transport.CloseIdleConnections()
|
|
||||||
t.h2Fallback.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) Close() error {
|
|
||||||
t.CloseIdleConnections()
|
|
||||||
return t.h3Transport.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) Clone() adapter.HTTPTransport {
|
|
||||||
return &http3FallbackTransport{
|
|
||||||
h3Transport: t.h3Transport,
|
|
||||||
h2Fallback: t.h2Fallback.Clone(),
|
|
||||||
fallbackDelay: t.fallbackDelay,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) h3Broken() bool {
|
|
||||||
t.brokenAccess.Lock()
|
|
||||||
defer t.brokenAccess.Unlock()
|
|
||||||
return !t.brokenUntil.IsZero() && time.Now().Before(t.brokenUntil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) clearH3Broken() {
|
|
||||||
t.brokenAccess.Lock()
|
|
||||||
t.brokenUntil = time.Time{}
|
|
||||||
t.brokenBackoff = 0
|
|
||||||
t.brokenAccess.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) markH3Broken() {
|
|
||||||
t.brokenAccess.Lock()
|
|
||||||
defer t.brokenAccess.Unlock()
|
|
||||||
if t.brokenBackoff == 0 {
|
|
||||||
t.brokenBackoff = 5 * time.Minute
|
|
||||||
} else {
|
|
||||||
t.brokenBackoff *= 2
|
|
||||||
if t.brokenBackoff > 48*time.Hour {
|
|
||||||
t.brokenBackoff = 48 * time.Hour
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.brokenUntil = time.Now().Add(t.brokenBackoff)
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
//go:build !with_quic
|
|
||||||
|
|
||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newHTTP3FallbackTransport(
|
|
||||||
rawDialer N.Dialer,
|
|
||||||
baseTLSConfig tls.Config,
|
|
||||||
h2Fallback adapter.HTTPTransport,
|
|
||||||
options option.QUICOptions,
|
|
||||||
fallbackDelay time.Duration,
|
|
||||||
) (adapter.HTTPTransport, error) {
|
|
||||||
return nil, E.New("HTTP/3 requires building with the with_quic tag")
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP3Transport(
|
|
||||||
rawDialer N.Dialer,
|
|
||||||
baseTLSConfig tls.Config,
|
|
||||||
options option.QUICOptions,
|
|
||||||
) (adapter.HTTPTransport, error) {
|
|
||||||
return nil, E.New("HTTP/3 requires building with the with_quic tag")
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ adapter.HTTPClientManager = (*Manager)(nil)
|
|
||||||
_ adapter.LifecycleService = (*Manager)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
ctx context.Context
|
|
||||||
logger log.ContextLogger
|
|
||||||
access sync.Mutex
|
|
||||||
defines map[string]option.HTTPClient
|
|
||||||
transports map[string]*Transport
|
|
||||||
defaultTag string
|
|
||||||
defaultTransport adapter.HTTPTransport
|
|
||||||
defaultTransportFallback func() (*Transport, error)
|
|
||||||
fallbackTransport *Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewManager(ctx context.Context, logger log.ContextLogger, clients []option.HTTPClient, defaultHTTPClient string) *Manager {
|
|
||||||
defines := make(map[string]option.HTTPClient, len(clients))
|
|
||||||
for _, client := range clients {
|
|
||||||
defines[client.Tag] = client
|
|
||||||
}
|
|
||||||
defaultTag := defaultHTTPClient
|
|
||||||
if defaultTag == "" && len(clients) > 0 {
|
|
||||||
defaultTag = clients[0].Tag
|
|
||||||
}
|
|
||||||
return &Manager{
|
|
||||||
ctx: ctx,
|
|
||||||
logger: logger,
|
|
||||||
defines: defines,
|
|
||||||
transports: make(map[string]*Transport),
|
|
||||||
defaultTag: defaultTag,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Initialize(defaultTransportFallback func() (*Transport, error)) {
|
|
||||||
m.defaultTransportFallback = defaultTransportFallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Name() string {
|
|
||||||
return "http-client"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
|
||||||
if stage != adapter.StartStateStart {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if m.defaultTag != "" {
|
|
||||||
transport, err := m.resolveShared(m.defaultTag)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "resolve default http client")
|
|
||||||
}
|
|
||||||
m.defaultTransport = transport
|
|
||||||
} else if m.defaultTransportFallback != nil {
|
|
||||||
transport, err := m.defaultTransportFallback()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "create default http client")
|
|
||||||
}
|
|
||||||
m.defaultTransport = transport
|
|
||||||
m.fallbackTransport = transport
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) DefaultTransport() adapter.HTTPTransport {
|
|
||||||
if m.defaultTransport == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &sharedTransport{m.defaultTransport}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) ResolveTransport(ctx context.Context, logger logger.ContextLogger, options option.HTTPClientOptions) (adapter.HTTPTransport, error) {
|
|
||||||
if options.Tag != "" {
|
|
||||||
if options.ResolveOnDetour {
|
|
||||||
define, loaded := m.defines[options.Tag]
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("http_client not found: ", options.Tag)
|
|
||||||
}
|
|
||||||
resolvedOptions := define.Options()
|
|
||||||
resolvedOptions.ResolveOnDetour = true
|
|
||||||
return NewTransport(ctx, logger, options.Tag, resolvedOptions)
|
|
||||||
}
|
|
||||||
transport, err := m.resolveShared(options.Tag)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &sharedTransport{transport}, nil
|
|
||||||
}
|
|
||||||
return NewTransport(ctx, logger, "", options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) resolveShared(tag string) (adapter.HTTPTransport, error) {
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
if transport, loaded := m.transports[tag]; loaded {
|
|
||||||
return transport, nil
|
|
||||||
}
|
|
||||||
define, loaded := m.defines[tag]
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("http_client not found: ", tag)
|
|
||||||
}
|
|
||||||
transport, err := NewTransport(m.ctx, m.logger, tag, define.Options())
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create shared http_client[", tag, "]")
|
|
||||||
}
|
|
||||||
m.transports[tag] = transport
|
|
||||||
return transport, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type sharedTransport struct {
|
|
||||||
adapter.HTTPTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *sharedTransport) CloseIdleConnections() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *sharedTransport) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) ResetNetwork() {
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
for _, transport := range m.transports {
|
|
||||||
transport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
if m.fallbackTransport != nil {
|
|
||||||
m.fallbackTransport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Close() error {
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
if m.transports == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
for _, transport := range m.transports {
|
|
||||||
err = E.Append(err, transport.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close http client")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if m.fallbackTransport != nil {
|
|
||||||
err = E.Append(err, m.fallbackTransport.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close default http client")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
m.transports = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"unsafe"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Conn) Read(b []byte) (int, error) {
|
func (c *Conn) Read(b []byte) (int, error) {
|
||||||
@@ -230,7 +229,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)((*[2]unsafe.Pointer)(unsafe.Pointer(&err))[1])))
|
err = c.rawConn.In.SetErrorLocked(c.sendAlert(uint8(err.(tls.AlertError))))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ 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,10 +37,7 @@ 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
|
||||||
@@ -102,6 +99,8 @@ func (l *Listener) loopTCPIn() {
|
|||||||
}
|
}
|
||||||
//nolint:staticcheck
|
//nolint:staticcheck
|
||||||
metadata.InboundDetour = l.listenOptions.Detour
|
metadata.InboundDetour = l.listenOptions.Detour
|
||||||
|
//nolint:staticcheck
|
||||||
|
metadata.InboundOptions = l.listenOptions.InboundOptions
|
||||||
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
|
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
|
||||||
metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
|
metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
|
||||||
ctx := log.ContextWithNewID(l.ctx)
|
ctx := log.ContextWithNewID(l.ctx)
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
package networkquality
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
sBufio "github.com/sagernet/sing/common/bufio"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FormatBitrate(bps int64) string {
|
|
||||||
switch {
|
|
||||||
case bps >= 1_000_000_000:
|
|
||||||
return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000)
|
|
||||||
case bps >= 1_000_000:
|
|
||||||
return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000)
|
|
||||||
case bps >= 1_000:
|
|
||||||
return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000)
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%d bps", bps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTPClient(dialer N.Dialer) *http.Client {
|
|
||||||
transport := &http.Transport{
|
|
||||||
ForceAttemptHTTP2: true,
|
|
||||||
TLSHandshakeTimeout: C.TCPTimeout,
|
|
||||||
}
|
|
||||||
if dialer != nil {
|
|
||||||
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
|
|
||||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &http.Client{Transport: transport}
|
|
||||||
}
|
|
||||||
|
|
||||||
func baseTransportFromClient(client *http.Client) (*http.Transport, error) {
|
|
||||||
if client == nil {
|
|
||||||
return nil, E.New("http client is nil")
|
|
||||||
}
|
|
||||||
if client.Transport == nil {
|
|
||||||
return http.DefaultTransport.(*http.Transport).Clone(), nil
|
|
||||||
}
|
|
||||||
transport, ok := client.Transport.(*http.Transport)
|
|
||||||
if !ok {
|
|
||||||
return nil, E.New("http client transport must be *http.Transport")
|
|
||||||
}
|
|
||||||
return transport.Clone(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMeasurementClient(
|
|
||||||
baseClient *http.Client,
|
|
||||||
connectEndpoint string,
|
|
||||||
singleConnection bool,
|
|
||||||
disableKeepAlives bool,
|
|
||||||
readCounters []N.CountFunc,
|
|
||||||
writeCounters []N.CountFunc,
|
|
||||||
) (*http.Client, error) {
|
|
||||||
transport, err := baseTransportFromClient(baseClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
transport.DisableCompression = true
|
|
||||||
transport.DisableKeepAlives = disableKeepAlives
|
|
||||||
if singleConnection {
|
|
||||||
transport.MaxConnsPerHost = 1
|
|
||||||
transport.MaxIdleConnsPerHost = 1
|
|
||||||
transport.MaxIdleConns = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
baseDialContext := transport.DialContext
|
|
||||||
if baseDialContext == nil {
|
|
||||||
dialer := &net.Dialer{}
|
|
||||||
baseDialContext = dialer.DialContext
|
|
||||||
}
|
|
||||||
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
|
|
||||||
dialAddr := addr
|
|
||||||
if connectEndpoint != "" {
|
|
||||||
dialAddr = rewriteDialAddress(addr, connectEndpoint)
|
|
||||||
}
|
|
||||||
conn, dialErr := baseDialContext(ctx, network, dialAddr)
|
|
||||||
if dialErr != nil {
|
|
||||||
return nil, dialErr
|
|
||||||
}
|
|
||||||
if len(readCounters) > 0 || len(writeCounters) > 0 {
|
|
||||||
return sBufio.NewCounterConn(conn, readCounters, writeCounters), nil
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
CheckRedirect: baseClient.CheckRedirect,
|
|
||||||
Jar: baseClient.Jar,
|
|
||||||
Timeout: baseClient.Timeout,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type MeasurementClientFactory func(
|
|
||||||
connectEndpoint string,
|
|
||||||
singleConnection bool,
|
|
||||||
disableKeepAlives bool,
|
|
||||||
readCounters []N.CountFunc,
|
|
||||||
writeCounters []N.CountFunc,
|
|
||||||
) (*http.Client, error)
|
|
||||||
|
|
||||||
func defaultMeasurementClientFactory(baseClient *http.Client) MeasurementClientFactory {
|
|
||||||
return func(connectEndpoint string, singleConnection, disableKeepAlives bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
|
|
||||||
return newMeasurementClient(baseClient, connectEndpoint, singleConnection, disableKeepAlives, readCounters, writeCounters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOptionalHTTP3Factory(dialer N.Dialer, useHTTP3 bool) (MeasurementClientFactory, error) {
|
|
||||||
if !useHTTP3 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return NewHTTP3MeasurementClientFactory(dialer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rewriteDialAddress(addr string, connectEndpoint string) string {
|
|
||||||
connectEndpoint = strings.TrimSpace(connectEndpoint)
|
|
||||||
host, port, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil {
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
endpointHost, endpointPort, err := net.SplitHostPort(connectEndpoint)
|
|
||||||
if err == nil {
|
|
||||||
host = endpointHost
|
|
||||||
if endpointPort != "" {
|
|
||||||
port = endpointPort
|
|
||||||
}
|
|
||||||
} else if connectEndpoint != "" {
|
|
||||||
host = connectEndpoint
|
|
||||||
}
|
|
||||||
return net.JoinHostPort(host, port)
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
//go:build with_quic
|
|
||||||
|
|
||||||
package networkquality
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/quic-go"
|
|
||||||
"github.com/sagernet/quic-go/http3"
|
|
||||||
sBufio "github.com/sagernet/sing/common/bufio"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
|
|
||||||
// singleConnection and disableKeepAlives are not applied:
|
|
||||||
// HTTP/3 multiplexes streams over a single QUIC connection by default.
|
|
||||||
return func(connectEndpoint string, _, _ bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
|
|
||||||
transport := &http3.Transport{
|
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
|
||||||
dialAddr := addr
|
|
||||||
if connectEndpoint != "" {
|
|
||||||
dialAddr = rewriteDialAddress(addr, connectEndpoint)
|
|
||||||
}
|
|
||||||
destination := M.ParseSocksaddr(dialAddr)
|
|
||||||
var udpConn net.Conn
|
|
||||||
var dialErr error
|
|
||||||
if dialer != nil {
|
|
||||||
udpConn, dialErr = dialer.DialContext(ctx, N.NetworkUDP, destination)
|
|
||||||
} else {
|
|
||||||
var netDialer net.Dialer
|
|
||||||
udpConn, dialErr = netDialer.DialContext(ctx, N.NetworkUDP, destination.String())
|
|
||||||
}
|
|
||||||
if dialErr != nil {
|
|
||||||
return nil, dialErr
|
|
||||||
}
|
|
||||||
wrappedConn := udpConn
|
|
||||||
if len(readCounters) > 0 || len(writeCounters) > 0 {
|
|
||||||
wrappedConn = sBufio.NewCounterConn(udpConn, readCounters, writeCounters)
|
|
||||||
}
|
|
||||||
packetConn := sBufio.NewUnbindPacketConn(wrappedConn)
|
|
||||||
quicConn, dialErr := quic.DialEarly(ctx, packetConn, udpConn.RemoteAddr(), tlsCfg, cfg)
|
|
||||||
if dialErr != nil {
|
|
||||||
udpConn.Close()
|
|
||||||
return nil, dialErr
|
|
||||||
}
|
|
||||||
return quicConn, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return &http.Client{Transport: transport}, nil
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//go:build !with_quic
|
|
||||||
|
|
||||||
package networkquality
|
|
||||||
|
|
||||||
import (
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
|
|
||||||
return nil, C.ErrQUICNotIncluded
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,6 @@ 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")
|
||||||
@@ -29,7 +28,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 && info.UserName == "" {
|
if info.UserId != -1 {
|
||||||
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,7 +6,6 @@ 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)
|
||||||
@@ -19,30 +18,22 @@ 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) {
|
||||||
family, protocol, err := socketDiagSettings(network, source)
|
_, uid, err := resolveSocketByNetlink(network, source, destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_, uid, err := querySocketDiagOnce(family, protocol, source)
|
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
|
||||||
if err != nil {
|
return &adapter.ConnectionOwner{
|
||||||
return nil, err
|
UserId: int32(uid),
|
||||||
|
AndroidPackageName: sharedPackage,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
appID := uid % 100000
|
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
|
||||||
var packageNames []string
|
return &adapter.ConnectionOwner{
|
||||||
if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded {
|
UserId: int32(uid),
|
||||||
packageNames = append(packageNames, sharedPackage)
|
AndroidPackageName: packageName,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
if packages, loaded := s.packageManager.PackagesByID(appID); loaded {
|
return &adapter.ConnectionOwner{UserId: int32(uid)}, nil
|
||||||
packageNames = append(packageNames, packages...)
|
|
||||||
}
|
|
||||||
packageNames = common.Uniq(packageNames)
|
|
||||||
return &adapter.ConnectionOwner{
|
|
||||||
UserId: int32(uid),
|
|
||||||
AndroidPackageNames: packageNames,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
//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)
|
||||||
@@ -20,12 +24,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) {
|
||||||
return FindDarwinConnectionOwner(network, source, destination)
|
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var structSize = func() int {
|
var structSize = func() int {
|
||||||
@@ -43,3 +47,107 @@ 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]))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,269 +0,0 @@
|
|||||||
//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,82 +4,33 @@ 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) {
|
||||||
searcher := &linuxSearcher{
|
return &linuxSearcher{config.Logger}, nil
|
||||||
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 := s.resolveSocketByNetlink(network, source, destination)
|
inode, uid, err := resolveSocketByNetlink(network, source, destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
processInfo := &adapter.ConnectionOwner{
|
processPath, err := resolveProcessNameByProcSearch(inode, uid)
|
||||||
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 processInfo, nil
|
return &adapter.ConnectionOwner{
|
||||||
}
|
UserId: int32(uid),
|
||||||
|
ProcessPath: processPath,
|
||||||
func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
}, nil
|
||||||
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,67 +3,43 @@
|
|||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common/buf"
|
||||||
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 (
|
||||||
sizeOfSocketDiagRequestData = 56
|
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
|
||||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData
|
socketDiagByFamily = 20
|
||||||
socketDiagResponseMinSize = 72
|
pathProc = "/proc"
|
||||||
socketDiagByFamily = 20
|
|
||||||
pathProc = "/proc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type socketDiagConn struct {
|
func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||||
access sync.Mutex
|
var family uint8
|
||||||
family uint8
|
var protocol 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
|
||||||
@@ -72,308 +48,151 @@ func socketDiagSettings(network string, source netip.AddrPort) (family, protocol
|
|||||||
default:
|
default:
|
||||||
return 0, 0, os.ErrInvalid
|
return 0, 0, os.ErrInvalid
|
||||||
}
|
}
|
||||||
switch {
|
|
||||||
case source.Addr().Is4():
|
if source.Addr().Is4() {
|
||||||
family = syscall.AF_INET
|
family = syscall.AF_INET
|
||||||
case source.Addr().Is6():
|
} else {
|
||||||
family = syscall.AF_INET6
|
family = syscall.AF_INET6
|
||||||
default:
|
|
||||||
return 0, 0, os.ErrInvalid
|
|
||||||
}
|
}
|
||||||
return family, protocol, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache {
|
req := packSocketDiagRequest(family, protocol, source)
|
||||||
cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32))
|
|
||||||
cache.SetLifetime(ttl)
|
|
||||||
return &uidProcessPathCache{cache: cache}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) {
|
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
|
||||||
if cached, ok := c.cache.Get(uid); ok {
|
|
||||||
if processPath, found := cached.entries[targetInode]; found {
|
|
||||||
return processPath, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
processPaths, err := buildProcessPathByUIDCache(uid)
|
|
||||||
if err != nil {
|
|
||||||
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 {
|
if err != nil {
|
||||||
return 0, 0, E.Cause(err, "dial netlink")
|
return 0, 0, E.Cause(err, "dial netlink")
|
||||||
}
|
}
|
||||||
defer syscall.Close(fd)
|
defer syscall.Close(socket)
|
||||||
return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *socketDiagConn) ensureOpenLocked() error {
|
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
|
||||||
if c.fd != -1 {
|
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
|
||||||
return nil
|
|
||||||
}
|
|
||||||
fd, err := openSocketDiag()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.fd = fd
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func openSocketDiag() (int, error) {
|
err = syscall.Connect(socket, &syscall.SockaddrNetlink{
|
||||||
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,
|
Family: syscall.AF_NETLINK,
|
||||||
|
Pad: 0,
|
||||||
Pid: 0,
|
Pid: 0,
|
||||||
Groups: 0,
|
Groups: 0,
|
||||||
}); err != nil {
|
})
|
||||||
syscall.Close(fd)
|
if err != nil {
|
||||||
return -1, err
|
return
|
||||||
}
|
}
|
||||||
return fd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *socketDiagConn) closeLocked() error {
|
_, err = syscall.Write(socket, req)
|
||||||
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 {
|
if err != nil {
|
||||||
return 0, 0, E.Cause(err, "write netlink request")
|
return 0, 0, E.Cause(err, "write netlink request")
|
||||||
}
|
}
|
||||||
buffer := make([]byte, 64<<10)
|
|
||||||
n, err := syscall.Read(fd, buffer)
|
buffer := buf.New()
|
||||||
|
defer buffer.Release()
|
||||||
|
|
||||||
|
n, err := syscall.Read(socket, buffer.FreeBytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, E.Cause(err, "read netlink response")
|
return 0, 0, E.Cause(err, "read netlink response")
|
||||||
}
|
}
|
||||||
messages, err := syscall.ParseNetlinkMessage(buffer[:n])
|
|
||||||
|
buffer.Truncate(n)
|
||||||
|
|
||||||
|
messages, err := syscall.ParseNetlinkMessage(buffer.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, E.Cause(err, "parse netlink message")
|
return 0, 0, E.Cause(err, "parse netlink message")
|
||||||
|
} else if len(messages) == 0 {
|
||||||
|
return 0, 0, E.New("unexcepted netlink response")
|
||||||
}
|
}
|
||||||
return unpackSocketDiagMessages(messages)
|
|
||||||
|
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 unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) {
|
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte {
|
||||||
for _, message := range messages {
|
s := make([]byte, 16)
|
||||||
switch message.Header.Type {
|
copy(s, source.Addr().AsSlice())
|
||||||
case syscall.NLMSG_DONE:
|
|
||||||
continue
|
buf := make([]byte, sizeOfSocketDiagRequest)
|
||||||
case syscall.NLMSG_ERROR:
|
|
||||||
err = unpackSocketDiagError(&message)
|
nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
|
||||||
if err != nil {
|
nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
|
||||||
return 0, 0, err
|
nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
|
||||||
}
|
nativeEndian.PutUint32(buf[8:12], 0)
|
||||||
case socketDiagByFamily:
|
nativeEndian.PutUint32(buf[12:16], 0)
|
||||||
inode, uid = unpackSocketDiagResponse(&message)
|
|
||||||
if inode != 0 || uid != 0 {
|
buf[16] = family
|
||||||
return inode, uid, nil
|
buf[17] = protocol
|
||||||
}
|
buf[18] = 0
|
||||||
}
|
buf[19] = 0
|
||||||
}
|
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
|
||||||
return 0, 0, ErrNotFound
|
|
||||||
|
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 unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
||||||
if len(msg.Data) < socketDiagResponseMinSize {
|
if len(msg.Data) < 72 {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
uid = binary.NativeEndian.Uint32(msg.Data[64:68])
|
|
||||||
inode = binary.NativeEndian.Uint32(msg.Data[68:72])
|
data := msg.Data
|
||||||
return inode, uid
|
|
||||||
|
uid = nativeEndian.Uint32(data[64:68])
|
||||||
|
inode = nativeEndian.Uint32(data[68:72])
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func unpackSocketDiagError(msg *syscall.NetlinkMessage) error {
|
func resolveProcessNameByProcSearch(inode, uid uint32) (string, 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)
|
files, err := os.ReadDir(pathProc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := make([]byte, syscall.PathMax)
|
buffer := make([]byte, syscall.PathMax)
|
||||||
processPaths := make(map[uint32]string)
|
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
|
||||||
for _, file := range files {
|
|
||||||
if !file.IsDir() || !isPid(file.Name()) {
|
for _, f := range files {
|
||||||
|
if !f.IsDir() || !isPid(f.Name()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
info, err := file.Info()
|
|
||||||
|
info, err := f.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isIgnorableProcError(err) {
|
return "", 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())
|
|
||||||
fdPath := filepath.Join(processPath, "fd")
|
processPath := path.Join(pathProc, f.Name())
|
||||||
exePath, err := os.Readlink(filepath.Join(processPath, "exe"))
|
fdPath := path.Join(processPath, "fd")
|
||||||
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(filepath.Join(fdPath, fd.Name()), buffer)
|
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
inode, ok := parseSocketInode(buffer[:n])
|
|
||||||
if !ok {
|
if bytes.Equal(buffer[:n], socket) {
|
||||||
continue
|
return os.Readlink(path.Join(processPath, "exe"))
|
||||||
}
|
|
||||||
if _, loaded := processPaths[inode]; !loaded {
|
|
||||||
processPaths[inode] = exePath
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return processPaths, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isIgnorableProcError(err error) bool {
|
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
|
||||||
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 {
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
//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,10 +28,6 @@ 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 {
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
package proxybridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
std_bufio "bufio"
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/auth"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/protocol/socks"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Bridge struct {
|
|
||||||
ctx context.Context
|
|
||||||
logger logger.ContextLogger
|
|
||||||
tag string
|
|
||||||
dialer N.Dialer
|
|
||||||
connection adapter.ConnectionManager
|
|
||||||
tcpListener *net.TCPListener
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
authenticator *auth.Authenticator
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(ctx context.Context, logger logger.ContextLogger, tag string, dialer N.Dialer) (*Bridge, error) {
|
|
||||||
username := randomHex(16)
|
|
||||||
password := randomHex(16)
|
|
||||||
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bridge := &Bridge{
|
|
||||||
ctx: ctx,
|
|
||||||
logger: logger,
|
|
||||||
tag: tag,
|
|
||||||
dialer: dialer,
|
|
||||||
connection: service.FromContext[adapter.ConnectionManager](ctx),
|
|
||||||
tcpListener: tcpListener,
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
authenticator: auth.NewAuthenticator([]auth.User{{Username: username, Password: password}}),
|
|
||||||
}
|
|
||||||
go bridge.acceptLoop()
|
|
||||||
return bridge, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomHex(size int) string {
|
|
||||||
raw := make([]byte, size)
|
|
||||||
rand.Read(raw)
|
|
||||||
return hex.EncodeToString(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) Port() uint16 {
|
|
||||||
return M.SocksaddrFromNet(b.tcpListener.Addr()).Port
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) Username() string {
|
|
||||||
return b.username
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) Password() string {
|
|
||||||
return b.password
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) Close() error {
|
|
||||||
return common.Close(b.tcpListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) acceptLoop() {
|
|
||||||
for {
|
|
||||||
tcpConn, err := b.tcpListener.AcceptTCP()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx := log.ContextWithNewID(b.ctx)
|
|
||||||
go func() {
|
|
||||||
hErr := socks.HandleConnectionEx(ctx, tcpConn, std_bufio.NewReader(tcpConn), b.authenticator, b, nil, 0, M.SocksaddrFromNet(tcpConn.RemoteAddr()), nil)
|
|
||||||
if hErr == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if E.IsClosedOrCanceled(hErr) {
|
|
||||||
b.logger.DebugContext(ctx, E.Cause(hErr, b.tag, " connection closed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b.logger.ErrorContext(ctx, E.Cause(hErr, b.tag))
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
metadata.Source = source
|
|
||||||
metadata.Destination = destination
|
|
||||||
metadata.Network = N.NetworkTCP
|
|
||||||
b.logger.InfoContext(ctx, b.tag, " connection to ", metadata.Destination)
|
|
||||||
b.connection.NewConnection(ctx, b.dialer, conn, metadata, onClose)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bridge) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
metadata.Source = source
|
|
||||||
metadata.Destination = destination
|
|
||||||
metadata.Network = N.NetworkUDP
|
|
||||||
b.logger.InfoContext(ctx, b.tag, " packet connection to ", metadata.Destination)
|
|
||||||
b.connection.NewPacketConnection(ctx, b.dialer, conn, metadata, onClose)
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
@@ -46,7 +45,6 @@ const (
|
|||||||
ruleItemNetworkIsConstrained
|
ruleItemNetworkIsConstrained
|
||||||
ruleItemNetworkInterfaceAddress
|
ruleItemNetworkInterfaceAddress
|
||||||
ruleItemDefaultInterfaceAddress
|
ruleItemDefaultInterfaceAddress
|
||||||
ruleItemPackageNameRegex
|
|
||||||
ruleItemFinal uint8 = 0xFF
|
ruleItemFinal uint8 = 0xFF
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -216,8 +214,6 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
|
|||||||
rule.ProcessPathRegex, err = readRuleItemString(reader)
|
rule.ProcessPathRegex, err = readRuleItemString(reader)
|
||||||
case ruleItemPackageName:
|
case ruleItemPackageName:
|
||||||
rule.PackageName, err = readRuleItemString(reader)
|
rule.PackageName, err = readRuleItemString(reader)
|
||||||
case ruleItemPackageNameRegex:
|
|
||||||
rule.PackageNameRegex, err = readRuleItemString(reader)
|
|
||||||
case ruleItemWIFISSID:
|
case ruleItemWIFISSID:
|
||||||
rule.WIFISSID, err = readRuleItemString(reader)
|
rule.WIFISSID, err = readRuleItemString(reader)
|
||||||
case ruleItemWIFIBSSID:
|
case ruleItemWIFIBSSID:
|
||||||
@@ -397,15 +393,6 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(rule.PackageNameRegex) > 0 {
|
|
||||||
if generateVersion < C.RuleSetVersion5 {
|
|
||||||
return E.New("`package_name_regex` rule item is only supported in version 5 or later")
|
|
||||||
}
|
|
||||||
err = writeRuleItemString(writer, ruleItemPackageNameRegex, rule.PackageNameRegex)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.NetworkType) > 0 {
|
if len(rule.NetworkType) > 0 {
|
||||||
if generateVersion < C.RuleSetVersion3 {
|
if generateVersion < C.RuleSetVersion3 {
|
||||||
return E.New("`network_type` rule item is only supported in version 3 or later")
|
return E.New("`network_type` rule item is only supported in version 3 or later")
|
||||||
@@ -518,24 +505,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readRuleItemString(reader varbin.Reader) ([]string, error) {
|
func readRuleItemString(reader varbin.Reader) ([]string, error) {
|
||||||
length, err := binary.ReadUvarint(reader)
|
return varbin.ReadValue[[]string](reader, binary.BigEndian)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := make([]string, length)
|
|
||||||
for i := range result {
|
|
||||||
strLen, err := binary.ReadUvarint(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buf := make([]byte, strLen)
|
|
||||||
_, err = io.ReadFull(reader, buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result[i] = string(buf)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error {
|
func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error {
|
||||||
@@ -543,34 +513,11 @@ func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = varbin.WriteUvarint(writer, uint64(len(value)))
|
return varbin.Write(writer, binary.BigEndian, value)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, s := range value {
|
|
||||||
_, err = varbin.WriteUvarint(writer, uint64(len(s)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = writer.Write([]byte(s))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readRuleItemUint8[E ~uint8](reader varbin.Reader) ([]E, error) {
|
func readRuleItemUint8[E ~uint8](reader varbin.Reader) ([]E, error) {
|
||||||
length, err := binary.ReadUvarint(reader)
|
return varbin.ReadValue[[]E](reader, binary.BigEndian)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := make([]E, length)
|
|
||||||
_, err = io.ReadFull(reader, *(*[]byte)(unsafe.Pointer(&result)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []E) error {
|
func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []E) error {
|
||||||
@@ -578,25 +525,11 @@ func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = varbin.WriteUvarint(writer, uint64(len(value)))
|
return varbin.Write(writer, binary.BigEndian, value)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value)))
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) {
|
func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) {
|
||||||
length, err := binary.ReadUvarint(reader)
|
return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := make([]uint16, length)
|
|
||||||
err = binary.Read(reader, binary.BigEndian, result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error {
|
func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error {
|
||||||
@@ -604,11 +537,7 @@ func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = varbin.WriteUvarint(writer, uint64(len(value)))
|
return varbin.Write(writer, binary.BigEndian, value)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return binary.Write(writer, binary.BigEndian, value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error {
|
func writeRuleItemCIDR(writer varbin.Writer, itemType uint8, value []string) error {
|
||||||
|
|||||||
@@ -1,494 +0,0 @@
|
|||||||
package srs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
"github.com/sagernet/sing/common/varbin"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"go4.org/netipx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Old implementations using varbin reflection-based serialization
|
|
||||||
|
|
||||||
func oldWriteStringSlice(writer varbin.Writer, value []string) error {
|
|
||||||
//nolint:staticcheck
|
|
||||||
return varbin.Write(writer, binary.BigEndian, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func oldReadStringSlice(reader varbin.Reader) ([]string, error) {
|
|
||||||
//nolint:staticcheck
|
|
||||||
return varbin.ReadValue[[]string](reader, binary.BigEndian)
|
|
||||||
}
|
|
||||||
|
|
||||||
func oldWriteUint8Slice[E ~uint8](writer varbin.Writer, value []E) error {
|
|
||||||
//nolint:staticcheck
|
|
||||||
return varbin.Write(writer, binary.BigEndian, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func oldReadUint8Slice[E ~uint8](reader varbin.Reader) ([]E, error) {
|
|
||||||
//nolint:staticcheck
|
|
||||||
return varbin.ReadValue[[]E](reader, binary.BigEndian)
|
|
||||||
}
|
|
||||||
|
|
||||||
func oldWriteUint16Slice(writer varbin.Writer, value []uint16) error {
|
|
||||||
//nolint:staticcheck
|
|
||||||
return varbin.Write(writer, binary.BigEndian, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func oldReadUint16Slice(reader varbin.Reader) ([]uint16, error) {
|
|
||||||
//nolint:staticcheck
|
|
||||||
return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
|
|
||||||
}
|
|
||||||
|
|
||||||
func oldWritePrefix(writer varbin.Writer, prefix netip.Prefix) error {
|
|
||||||
//nolint:staticcheck
|
|
||||||
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
|
|
||||||
}
|
|
||||||
|
|
||||||
type oldIPRangeData struct {
|
|
||||||
From []byte
|
|
||||||
To []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: The old writeIPSet had a bug where varbin.Write(writer, binary.BigEndian, data)
|
|
||||||
// with a struct VALUE (not pointer) silently wrote nothing because field.CanSet() returned false.
|
|
||||||
// This caused IP range data to be missing from the output.
|
|
||||||
// The new implementation correctly writes all range data.
|
|
||||||
//
|
|
||||||
// The old readIPSet used varbin.Read with a pre-allocated slice, which worked because
|
|
||||||
// slice elements are addressable and CanSet() returns true for them.
|
|
||||||
//
|
|
||||||
// For compatibility testing, we verify:
|
|
||||||
// 1. New write produces correct output with range data
|
|
||||||
// 2. New read can parse the new format correctly
|
|
||||||
// 3. Round-trip works correctly
|
|
||||||
|
|
||||||
func oldReadIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
|
|
||||||
version, err := reader.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if version != 1 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var length uint64
|
|
||||||
err = binary.Read(reader, binary.BigEndian, &length)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ranges := make([]oldIPRangeData, length)
|
|
||||||
//nolint:staticcheck
|
|
||||||
err = varbin.Read(reader, binary.BigEndian, &ranges)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mySet := &myIPSet{
|
|
||||||
rr: make([]myIPRange, len(ranges)),
|
|
||||||
}
|
|
||||||
for i, rangeData := range ranges {
|
|
||||||
mySet.rr[i].from = M.AddrFromIP(rangeData.From)
|
|
||||||
mySet.rr[i].to = M.AddrFromIP(rangeData.To)
|
|
||||||
}
|
|
||||||
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// New write functions (without itemType prefix for testing)
|
|
||||||
|
|
||||||
func newWriteStringSlice(writer varbin.Writer, value []string) error {
|
|
||||||
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, s := range value {
|
|
||||||
_, err = varbin.WriteUvarint(writer, uint64(len(s)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = writer.Write([]byte(s))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWriteUint8Slice[E ~uint8](writer varbin.Writer, value []E) error {
|
|
||||||
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value)))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWriteUint16Slice(writer varbin.Writer, value []uint16) error {
|
|
||||||
_, err := varbin.WriteUvarint(writer, uint64(len(value)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return binary.Write(writer, binary.BigEndian, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newWritePrefix(writer varbin.Writer, prefix netip.Prefix) error {
|
|
||||||
addrSlice := prefix.Addr().AsSlice()
|
|
||||||
_, err := varbin.WriteUvarint(writer, uint64(len(addrSlice)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = writer.Write(addrSlice)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return writer.WriteByte(uint8(prefix.Bits()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests
|
|
||||||
|
|
||||||
func TestStringSliceCompat(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
input []string
|
|
||||||
}{
|
|
||||||
{"nil", nil},
|
|
||||||
{"empty", []string{}},
|
|
||||||
{"single_empty", []string{""}},
|
|
||||||
{"single", []string{"test"}},
|
|
||||||
{"multi", []string{"a", "b", "c"}},
|
|
||||||
{"with_empty", []string{"a", "", "c"}},
|
|
||||||
{"utf8", []string{"测试", "テスト", "тест"}},
|
|
||||||
{"long_string", []string{strings.Repeat("x", 128)}},
|
|
||||||
{"many_elements", generateStrings(128)},
|
|
||||||
{"many_elements_256", generateStrings(256)},
|
|
||||||
{"127_byte_string", []string{strings.Repeat("x", 127)}},
|
|
||||||
{"128_byte_string", []string{strings.Repeat("x", 128)}},
|
|
||||||
{"mixed_lengths", []string{"a", strings.Repeat("b", 100), "", strings.Repeat("c", 200)}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Old write
|
|
||||||
var oldBuf bytes.Buffer
|
|
||||||
err := oldWriteStringSlice(&oldBuf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// New write
|
|
||||||
var newBuf bytes.Buffer
|
|
||||||
err = newWriteStringSlice(&newBuf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Bytes must match
|
|
||||||
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
|
|
||||||
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
|
|
||||||
|
|
||||||
// New write -> old read
|
|
||||||
readBack, err := oldReadStringSlice(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
requireStringSliceEqual(t, tc.input, readBack)
|
|
||||||
|
|
||||||
// Old write -> new read
|
|
||||||
readBack2, err := readRuleItemString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
requireStringSliceEqual(t, tc.input, readBack2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUint8SliceCompat(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
input []uint8
|
|
||||||
}{
|
|
||||||
{"nil", nil},
|
|
||||||
{"empty", []uint8{}},
|
|
||||||
{"single_zero", []uint8{0}},
|
|
||||||
{"single_max", []uint8{255}},
|
|
||||||
{"multi", []uint8{0, 1, 127, 128, 255}},
|
|
||||||
{"boundary", []uint8{0x00, 0x7f, 0x80, 0xff}},
|
|
||||||
{"sequential", generateUint8Slice(256)},
|
|
||||||
{"127_elements", generateUint8Slice(127)},
|
|
||||||
{"128_elements", generateUint8Slice(128)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Old write
|
|
||||||
var oldBuf bytes.Buffer
|
|
||||||
err := oldWriteUint8Slice(&oldBuf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// New write
|
|
||||||
var newBuf bytes.Buffer
|
|
||||||
err = newWriteUint8Slice(&newBuf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Bytes must match
|
|
||||||
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
|
|
||||||
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
|
|
||||||
|
|
||||||
// New write -> old read
|
|
||||||
readBack, err := oldReadUint8Slice[uint8](bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
requireUint8SliceEqual(t, tc.input, readBack)
|
|
||||||
|
|
||||||
// Old write -> new read
|
|
||||||
readBack2, err := readRuleItemUint8[uint8](bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
requireUint8SliceEqual(t, tc.input, readBack2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUint16SliceCompat(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
input []uint16
|
|
||||||
}{
|
|
||||||
{"nil", nil},
|
|
||||||
{"empty", []uint16{}},
|
|
||||||
{"single_zero", []uint16{0}},
|
|
||||||
{"single_max", []uint16{65535}},
|
|
||||||
{"multi", []uint16{0, 255, 256, 32767, 32768, 65535}},
|
|
||||||
{"ports", []uint16{80, 443, 8080, 8443}},
|
|
||||||
{"127_elements", generateUint16Slice(127)},
|
|
||||||
{"128_elements", generateUint16Slice(128)},
|
|
||||||
{"256_elements", generateUint16Slice(256)},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Old write
|
|
||||||
var oldBuf bytes.Buffer
|
|
||||||
err := oldWriteUint16Slice(&oldBuf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// New write
|
|
||||||
var newBuf bytes.Buffer
|
|
||||||
err = newWriteUint16Slice(&newBuf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Bytes must match
|
|
||||||
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
|
|
||||||
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
|
|
||||||
|
|
||||||
// New write -> old read
|
|
||||||
readBack, err := oldReadUint16Slice(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
requireUint16SliceEqual(t, tc.input, readBack)
|
|
||||||
|
|
||||||
// Old write -> new read
|
|
||||||
readBack2, err := readRuleItemUint16(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
requireUint16SliceEqual(t, tc.input, readBack2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrefixCompat(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
input netip.Prefix
|
|
||||||
}{
|
|
||||||
{"ipv4_0", netip.MustParsePrefix("0.0.0.0/0")},
|
|
||||||
{"ipv4_8", netip.MustParsePrefix("10.0.0.0/8")},
|
|
||||||
{"ipv4_16", netip.MustParsePrefix("192.168.0.0/16")},
|
|
||||||
{"ipv4_24", netip.MustParsePrefix("192.168.1.0/24")},
|
|
||||||
{"ipv4_32", netip.MustParsePrefix("1.2.3.4/32")},
|
|
||||||
{"ipv6_0", netip.MustParsePrefix("::/0")},
|
|
||||||
{"ipv6_64", netip.MustParsePrefix("2001:db8::/64")},
|
|
||||||
{"ipv6_128", netip.MustParsePrefix("::1/128")},
|
|
||||||
{"ipv6_full", netip.MustParsePrefix("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128")},
|
|
||||||
{"ipv4_private", netip.MustParsePrefix("172.16.0.0/12")},
|
|
||||||
{"ipv6_link_local", netip.MustParsePrefix("fe80::/10")},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Old write
|
|
||||||
var oldBuf bytes.Buffer
|
|
||||||
err := oldWritePrefix(&oldBuf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// New write
|
|
||||||
var newBuf bytes.Buffer
|
|
||||||
err = newWritePrefix(&newBuf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Bytes must match
|
|
||||||
require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
|
|
||||||
"mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
|
|
||||||
|
|
||||||
// New write -> new read (no old read for prefix)
|
|
||||||
readBack, err := readPrefix(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.input, readBack)
|
|
||||||
|
|
||||||
// Old write -> new read
|
|
||||||
readBack2, err := readPrefix(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.input, readBack2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIPSetCompat(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Note: The old writeIPSet was buggy (varbin.Write with struct values wrote nothing).
|
|
||||||
// This test verifies the new implementation writes correct data and round-trips correctly.
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
input *netipx.IPSet
|
|
||||||
}{
|
|
||||||
{"single_ipv4", buildIPSet("1.2.3.4")},
|
|
||||||
{"ipv4_range", buildIPSet("192.168.0.0/16")},
|
|
||||||
{"multi_ipv4", buildIPSet("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")},
|
|
||||||
{"single_ipv6", buildIPSet("::1")},
|
|
||||||
{"ipv6_range", buildIPSet("2001:db8::/32")},
|
|
||||||
{"mixed", buildIPSet("10.0.0.0/8", "::1", "2001:db8::/32")},
|
|
||||||
{"large", buildLargeIPSet(100)},
|
|
||||||
{"adjacent_ranges", buildIPSet("192.168.0.0/24", "192.168.1.0/24", "192.168.2.0/24")},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// New write
|
|
||||||
var newBuf bytes.Buffer
|
|
||||||
err := writeIPSet(&newBuf, tc.input)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify format starts with version byte (1) + uint64 count
|
|
||||||
require.True(t, len(newBuf.Bytes()) >= 9, "output too short")
|
|
||||||
require.Equal(t, byte(1), newBuf.Bytes()[0], "version byte mismatch")
|
|
||||||
|
|
||||||
// New write -> old read (varbin.Read with pre-allocated slice works correctly)
|
|
||||||
readBack, err := oldReadIPSet(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
requireIPSetEqual(t, tc.input, readBack)
|
|
||||||
|
|
||||||
// New write -> new read
|
|
||||||
readBack2, err := readIPSet(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
|
|
||||||
require.NoError(t, err)
|
|
||||||
requireIPSetEqual(t, tc.input, readBack2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
|
|
||||||
func generateStrings(count int) []string {
|
|
||||||
result := make([]string, count)
|
|
||||||
for i := range result {
|
|
||||||
result[i] = strings.Repeat("x", i%50)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateUint8Slice(count int) []uint8 {
|
|
||||||
result := make([]uint8, count)
|
|
||||||
for i := range result {
|
|
||||||
result[i] = uint8(i % 256)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateUint16Slice(count int) []uint16 {
|
|
||||||
result := make([]uint16, count)
|
|
||||||
for i := range result {
|
|
||||||
result[i] = uint16(i * 257)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildIPSet(cidrs ...string) *netipx.IPSet {
|
|
||||||
var builder netipx.IPSetBuilder
|
|
||||||
for _, cidr := range cidrs {
|
|
||||||
prefix, err := netip.ParsePrefix(cidr)
|
|
||||||
if err != nil {
|
|
||||||
addr, err := netip.ParseAddr(cidr)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
builder.Add(addr)
|
|
||||||
} else {
|
|
||||||
builder.AddPrefix(prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set, _ := builder.IPSet()
|
|
||||||
return set
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildLargeIPSet(count int) *netipx.IPSet {
|
|
||||||
var builder netipx.IPSetBuilder
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
prefix := netip.PrefixFrom(netip.AddrFrom4([4]byte{10, byte(i / 256), byte(i % 256), 0}), 24)
|
|
||||||
builder.AddPrefix(prefix)
|
|
||||||
}
|
|
||||||
set, _ := builder.IPSet()
|
|
||||||
return set
|
|
||||||
}
|
|
||||||
|
|
||||||
func requireStringSliceEqual(t *testing.T, expected, actual []string) {
|
|
||||||
t.Helper()
|
|
||||||
if len(expected) == 0 && len(actual) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.Equal(t, expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func requireUint8SliceEqual(t *testing.T, expected, actual []uint8) {
|
|
||||||
t.Helper()
|
|
||||||
if len(expected) == 0 && len(actual) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.Equal(t, expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func requireUint16SliceEqual(t *testing.T, expected, actual []uint16) {
|
|
||||||
t.Helper()
|
|
||||||
if len(expected) == 0 && len(actual) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.Equal(t, expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func requireIPSetEqual(t *testing.T, expected, actual *netipx.IPSet) {
|
|
||||||
t.Helper()
|
|
||||||
expectedRanges := expected.Ranges()
|
|
||||||
actualRanges := actual.Ranges()
|
|
||||||
require.Equal(t, len(expectedRanges), len(actualRanges), "range count mismatch")
|
|
||||||
for i := range expectedRanges {
|
|
||||||
require.Equal(t, expectedRanges[i].From(), actualRanges[i].From(), "range[%d].from mismatch", i)
|
|
||||||
require.Equal(t, expectedRanges[i].To(), actualRanges[i].To(), "range[%d].to mismatch", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package srs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
@@ -10,16 +9,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
|
func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
|
||||||
addrLen, err := binary.ReadUvarint(reader)
|
addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return netip.Prefix{}, err
|
return netip.Prefix{}, err
|
||||||
}
|
}
|
||||||
addrSlice := make([]byte, addrLen)
|
prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian)
|
||||||
_, err = io.ReadFull(reader, addrSlice)
|
|
||||||
if err != nil {
|
|
||||||
return netip.Prefix{}, err
|
|
||||||
}
|
|
||||||
prefixBits, err := reader.ReadByte()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return netip.Prefix{}, err
|
return netip.Prefix{}, err
|
||||||
}
|
}
|
||||||
@@ -27,16 +21,11 @@ func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writePrefix(writer varbin.Writer, prefix netip.Prefix) error {
|
func writePrefix(writer varbin.Writer, prefix netip.Prefix) error {
|
||||||
addrSlice := prefix.Addr().AsSlice()
|
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
|
||||||
_, err := varbin.WriteUvarint(writer, uint64(len(addrSlice)))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = writer.Write(addrSlice)
|
err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = writer.WriteByte(uint8(prefix.Bits()))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package srs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/common/varbin"
|
"github.com/sagernet/sing/common/varbin"
|
||||||
|
|
||||||
@@ -22,6 +22,11 @@ type myIPRange struct {
|
|||||||
to netip.Addr
|
to netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type myIPRangeData struct {
|
||||||
|
From []byte
|
||||||
|
To []byte
|
||||||
|
}
|
||||||
|
|
||||||
func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
|
func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
|
||||||
version, err := reader.ReadByte()
|
version, err := reader.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -36,30 +41,17 @@ func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mySet := &myIPSet{
|
ranges := make([]myIPRangeData, length)
|
||||||
rr: make([]myIPRange, length),
|
err = varbin.Read(reader, binary.BigEndian, &ranges)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
for i := range mySet.rr {
|
mySet := &myIPSet{
|
||||||
fromLen, err := binary.ReadUvarint(reader)
|
rr: make([]myIPRange, len(ranges)),
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
for i, rangeData := range ranges {
|
||||||
}
|
mySet.rr[i].from = M.AddrFromIP(rangeData.From)
|
||||||
fromBytes := make([]byte, fromLen)
|
mySet.rr[i].to = M.AddrFromIP(rangeData.To)
|
||||||
_, err = io.ReadFull(reader, fromBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
toLen, err := binary.ReadUvarint(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
toBytes := make([]byte, toLen)
|
|
||||||
_, err = io.ReadFull(reader, toBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mySet.rr[i].from = M.AddrFromIP(fromBytes)
|
|
||||||
mySet.rr[i].to = M.AddrFromIP(toBytes)
|
|
||||||
}
|
}
|
||||||
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
|
return (*netipx.IPSet)(unsafe.Pointer(mySet)), nil
|
||||||
}
|
}
|
||||||
@@ -69,27 +61,18 @@ func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
mySet := (*myIPSet)(unsafe.Pointer(set))
|
dataList := common.Map((*myIPSet)(unsafe.Pointer(set)).rr, func(rr myIPRange) myIPRangeData {
|
||||||
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr)))
|
return myIPRangeData{
|
||||||
|
From: rr.from.AsSlice(),
|
||||||
|
To: rr.to.AsSlice(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
err = binary.Write(writer, binary.BigEndian, uint64(len(dataList)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, rr := range mySet.rr {
|
for _, data := range dataList {
|
||||||
fromBytes := rr.from.AsSlice()
|
err = varbin.Write(writer, binary.BigEndian, data)
|
||||||
_, err = varbin.WriteUvarint(writer, uint64(len(fromBytes)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = writer.Write(fromBytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
toBytes := rr.to.AsSlice()
|
|
||||||
_, err = varbin.WriteUvarint(writer, uint64(len(toBytes)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = writer.Write(toBytes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,612 +0,0 @@
|
|||||||
package stun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
"github.com/sagernet/sing/common/bufio/deadline"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultServer = "stun.voipgate.com:3478"
|
|
||||||
|
|
||||||
magicCookie = 0x2112A442
|
|
||||||
headerSize = 20
|
|
||||||
|
|
||||||
bindingRequest = 0x0001
|
|
||||||
bindingSuccessResponse = 0x0101
|
|
||||||
bindingErrorResponse = 0x0111
|
|
||||||
|
|
||||||
attrMappedAddress = 0x0001
|
|
||||||
attrChangeRequest = 0x0003
|
|
||||||
attrErrorCode = 0x0009
|
|
||||||
attrXORMappedAddress = 0x0020
|
|
||||||
attrOtherAddress = 0x802c
|
|
||||||
|
|
||||||
familyIPv4 = 0x01
|
|
||||||
familyIPv6 = 0x02
|
|
||||||
|
|
||||||
changeIP = 0x04
|
|
||||||
changePort = 0x02
|
|
||||||
|
|
||||||
defaultRTO = 500 * time.Millisecond
|
|
||||||
minRTO = 250 * time.Millisecond
|
|
||||||
maxRetransmit = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
type Phase int32
|
|
||||||
|
|
||||||
const (
|
|
||||||
PhaseBinding Phase = iota
|
|
||||||
PhaseNATMapping
|
|
||||||
PhaseNATFiltering
|
|
||||||
PhaseDone
|
|
||||||
)
|
|
||||||
|
|
||||||
type NATMapping int32
|
|
||||||
|
|
||||||
const (
|
|
||||||
NATMappingUnknown NATMapping = iota
|
|
||||||
_ // reserved
|
|
||||||
NATMappingEndpointIndependent
|
|
||||||
NATMappingAddressDependent
|
|
||||||
NATMappingAddressAndPortDependent
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m NATMapping) String() string {
|
|
||||||
switch m {
|
|
||||||
case NATMappingEndpointIndependent:
|
|
||||||
return "Endpoint Independent"
|
|
||||||
case NATMappingAddressDependent:
|
|
||||||
return "Address Dependent"
|
|
||||||
case NATMappingAddressAndPortDependent:
|
|
||||||
return "Address and Port Dependent"
|
|
||||||
default:
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type NATFiltering int32
|
|
||||||
|
|
||||||
const (
|
|
||||||
NATFilteringUnknown NATFiltering = iota
|
|
||||||
NATFilteringEndpointIndependent
|
|
||||||
NATFilteringAddressDependent
|
|
||||||
NATFilteringAddressAndPortDependent
|
|
||||||
)
|
|
||||||
|
|
||||||
func (f NATFiltering) String() string {
|
|
||||||
switch f {
|
|
||||||
case NATFilteringEndpointIndependent:
|
|
||||||
return "Endpoint Independent"
|
|
||||||
case NATFilteringAddressDependent:
|
|
||||||
return "Address Dependent"
|
|
||||||
case NATFilteringAddressAndPortDependent:
|
|
||||||
return "Address and Port Dependent"
|
|
||||||
default:
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransactionID [12]byte
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
Server string
|
|
||||||
Dialer N.Dialer
|
|
||||||
Context context.Context
|
|
||||||
OnProgress func(Progress)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Progress struct {
|
|
||||||
Phase Phase
|
|
||||||
ExternalAddr string
|
|
||||||
LatencyMs int32
|
|
||||||
NATMapping NATMapping
|
|
||||||
NATFiltering NATFiltering
|
|
||||||
}
|
|
||||||
|
|
||||||
type Result struct {
|
|
||||||
ExternalAddr string
|
|
||||||
LatencyMs int32
|
|
||||||
NATMapping NATMapping
|
|
||||||
NATFiltering NATFiltering
|
|
||||||
NATTypeSupported bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type parsedResponse struct {
|
|
||||||
xorMappedAddr netip.AddrPort
|
|
||||||
mappedAddr netip.AddrPort
|
|
||||||
otherAddr netip.AddrPort
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *parsedResponse) externalAddr() (netip.AddrPort, bool) {
|
|
||||||
if r.xorMappedAddr.IsValid() {
|
|
||||||
return r.xorMappedAddr, true
|
|
||||||
}
|
|
||||||
if r.mappedAddr.IsValid() {
|
|
||||||
return r.mappedAddr, true
|
|
||||||
}
|
|
||||||
return netip.AddrPort{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
type stunAttribute struct {
|
|
||||||
typ uint16
|
|
||||||
value []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTransactionID() TransactionID {
|
|
||||||
var id TransactionID
|
|
||||||
_, _ = rand.Read(id[:])
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildBindingRequest(txID TransactionID, attrs ...stunAttribute) []byte {
|
|
||||||
attrLen := 0
|
|
||||||
for _, attr := range attrs {
|
|
||||||
attrLen += 4 + len(attr.value) + paddingLen(len(attr.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, headerSize+attrLen)
|
|
||||||
binary.BigEndian.PutUint16(buf[0:2], bindingRequest)
|
|
||||||
binary.BigEndian.PutUint16(buf[2:4], uint16(attrLen))
|
|
||||||
binary.BigEndian.PutUint32(buf[4:8], magicCookie)
|
|
||||||
copy(buf[8:20], txID[:])
|
|
||||||
|
|
||||||
offset := headerSize
|
|
||||||
for _, attr := range attrs {
|
|
||||||
binary.BigEndian.PutUint16(buf[offset:offset+2], attr.typ)
|
|
||||||
binary.BigEndian.PutUint16(buf[offset+2:offset+4], uint16(len(attr.value)))
|
|
||||||
copy(buf[offset+4:offset+4+len(attr.value)], attr.value)
|
|
||||||
offset += 4 + len(attr.value) + paddingLen(len(attr.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
func changeRequestAttr(flags byte) stunAttribute {
|
|
||||||
return stunAttribute{
|
|
||||||
typ: attrChangeRequest,
|
|
||||||
value: []byte{0, 0, 0, flags},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseResponse(data []byte, expectedTxID TransactionID) (*parsedResponse, error) {
|
|
||||||
if len(data) < headerSize {
|
|
||||||
return nil, E.New("response too short")
|
|
||||||
}
|
|
||||||
|
|
||||||
msgType := binary.BigEndian.Uint16(data[0:2])
|
|
||||||
if msgType&0xC000 != 0 {
|
|
||||||
return nil, E.New("invalid STUN message: top 2 bits not zero")
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie := binary.BigEndian.Uint32(data[4:8])
|
|
||||||
if cookie != magicCookie {
|
|
||||||
return nil, E.New("invalid magic cookie")
|
|
||||||
}
|
|
||||||
|
|
||||||
var txID TransactionID
|
|
||||||
copy(txID[:], data[8:20])
|
|
||||||
if txID != expectedTxID {
|
|
||||||
return nil, E.New("transaction ID mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
msgLen := int(binary.BigEndian.Uint16(data[2:4]))
|
|
||||||
if msgLen > len(data)-headerSize {
|
|
||||||
return nil, E.New("message length exceeds data")
|
|
||||||
}
|
|
||||||
|
|
||||||
attrData := data[headerSize : headerSize+msgLen]
|
|
||||||
|
|
||||||
if msgType == bindingErrorResponse {
|
|
||||||
return nil, parseErrorResponse(attrData)
|
|
||||||
}
|
|
||||||
if msgType != bindingSuccessResponse {
|
|
||||||
return nil, E.New("unexpected message type: ", fmt.Sprintf("0x%04x", msgType))
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &parsedResponse{}
|
|
||||||
offset := 0
|
|
||||||
for offset+4 <= len(attrData) {
|
|
||||||
attrType := binary.BigEndian.Uint16(attrData[offset : offset+2])
|
|
||||||
attrLen := int(binary.BigEndian.Uint16(attrData[offset+2 : offset+4]))
|
|
||||||
if offset+4+attrLen > len(attrData) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
attrValue := attrData[offset+4 : offset+4+attrLen]
|
|
||||||
|
|
||||||
switch attrType {
|
|
||||||
case attrXORMappedAddress:
|
|
||||||
addr, err := parseXORMappedAddress(attrValue, txID)
|
|
||||||
if err == nil {
|
|
||||||
resp.xorMappedAddr = addr
|
|
||||||
}
|
|
||||||
case attrMappedAddress:
|
|
||||||
addr, err := parseMappedAddress(attrValue)
|
|
||||||
if err == nil {
|
|
||||||
resp.mappedAddr = addr
|
|
||||||
}
|
|
||||||
case attrOtherAddress:
|
|
||||||
addr, err := parseMappedAddress(attrValue)
|
|
||||||
if err == nil {
|
|
||||||
resp.otherAddr = addr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += 4 + attrLen + paddingLen(attrLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseErrorResponse(data []byte) error {
|
|
||||||
offset := 0
|
|
||||||
for offset+4 <= len(data) {
|
|
||||||
attrType := binary.BigEndian.Uint16(data[offset : offset+2])
|
|
||||||
attrLen := int(binary.BigEndian.Uint16(data[offset+2 : offset+4]))
|
|
||||||
if offset+4+attrLen > len(data) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if attrType == attrErrorCode && attrLen >= 4 {
|
|
||||||
attrValue := data[offset+4 : offset+4+attrLen]
|
|
||||||
class := int(attrValue[2] & 0x07)
|
|
||||||
number := int(attrValue[3])
|
|
||||||
code := class*100 + number
|
|
||||||
if attrLen > 4 {
|
|
||||||
return E.New("STUN error ", code, ": ", string(attrValue[4:]))
|
|
||||||
}
|
|
||||||
return E.New("STUN error ", code)
|
|
||||||
}
|
|
||||||
offset += 4 + attrLen + paddingLen(attrLen)
|
|
||||||
}
|
|
||||||
return E.New("STUN error response")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseXORMappedAddress(data []byte, txID TransactionID) (netip.AddrPort, error) {
|
|
||||||
if len(data) < 4 {
|
|
||||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS too short")
|
|
||||||
}
|
|
||||||
|
|
||||||
family := data[1]
|
|
||||||
xPort := binary.BigEndian.Uint16(data[2:4])
|
|
||||||
port := xPort ^ uint16(magicCookie>>16)
|
|
||||||
|
|
||||||
switch family {
|
|
||||||
case familyIPv4:
|
|
||||||
if len(data) < 8 {
|
|
||||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv4 too short")
|
|
||||||
}
|
|
||||||
var ip [4]byte
|
|
||||||
binary.BigEndian.PutUint32(ip[:], binary.BigEndian.Uint32(data[4:8])^magicCookie)
|
|
||||||
return netip.AddrPortFrom(netip.AddrFrom4(ip), port), nil
|
|
||||||
case familyIPv6:
|
|
||||||
if len(data) < 20 {
|
|
||||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv6 too short")
|
|
||||||
}
|
|
||||||
var ip [16]byte
|
|
||||||
var xorKey [16]byte
|
|
||||||
binary.BigEndian.PutUint32(xorKey[0:4], magicCookie)
|
|
||||||
copy(xorKey[4:16], txID[:])
|
|
||||||
for i := range 16 {
|
|
||||||
ip[i] = data[4+i] ^ xorKey[i]
|
|
||||||
}
|
|
||||||
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
|
||||||
default:
|
|
||||||
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseMappedAddress(data []byte) (netip.AddrPort, error) {
|
|
||||||
if len(data) < 4 {
|
|
||||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS too short")
|
|
||||||
}
|
|
||||||
|
|
||||||
family := data[1]
|
|
||||||
port := binary.BigEndian.Uint16(data[2:4])
|
|
||||||
|
|
||||||
switch family {
|
|
||||||
case familyIPv4:
|
|
||||||
if len(data) < 8 {
|
|
||||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv4 too short")
|
|
||||||
}
|
|
||||||
return netip.AddrPortFrom(
|
|
||||||
netip.AddrFrom4([4]byte{data[4], data[5], data[6], data[7]}), port,
|
|
||||||
), nil
|
|
||||||
case familyIPv6:
|
|
||||||
if len(data) < 20 {
|
|
||||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv6 too short")
|
|
||||||
}
|
|
||||||
var ip [16]byte
|
|
||||||
copy(ip[:], data[4:20])
|
|
||||||
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
|
||||||
default:
|
|
||||||
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func roundTrip(conn net.PacketConn, addr net.Addr, txID TransactionID, attrs []stunAttribute, rto time.Duration) (*parsedResponse, time.Duration, error) {
|
|
||||||
request := buildBindingRequest(txID, attrs...)
|
|
||||||
currentRTO := rto
|
|
||||||
retransmitCount := 0
|
|
||||||
|
|
||||||
sendTime := time.Now()
|
|
||||||
_, err := conn.WriteTo(request, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, E.Cause(err, "send STUN request")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
for {
|
|
||||||
err = conn.SetReadDeadline(sendTime.Add(currentRTO))
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, E.Cause(err, "set read deadline")
|
|
||||||
}
|
|
||||||
|
|
||||||
n, _, readErr := conn.ReadFrom(buf)
|
|
||||||
if readErr != nil {
|
|
||||||
if E.IsTimeout(readErr) && retransmitCount < maxRetransmit {
|
|
||||||
retransmitCount++
|
|
||||||
currentRTO *= 2
|
|
||||||
sendTime = time.Now()
|
|
||||||
_, err = conn.WriteTo(request, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, E.Cause(err, "retransmit STUN request")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, 0, E.Cause(readErr, "read STUN response")
|
|
||||||
}
|
|
||||||
|
|
||||||
if n < headerSize || buf[0]&0xC0 != 0 ||
|
|
||||||
binary.BigEndian.Uint32(buf[4:8]) != magicCookie {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var receivedTxID TransactionID
|
|
||||||
copy(receivedTxID[:], buf[8:20])
|
|
||||||
if receivedTxID != txID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
latency := time.Since(sendTime)
|
|
||||||
|
|
||||||
resp, parseErr := parseResponse(buf[:n], txID)
|
|
||||||
if parseErr != nil {
|
|
||||||
return nil, 0, parseErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, latency, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Run(options Options) (*Result, error) {
|
|
||||||
ctx := options.Context
|
|
||||||
if ctx == nil {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
|
|
||||||
server := options.Server
|
|
||||||
if server == "" {
|
|
||||||
server = DefaultServer
|
|
||||||
}
|
|
||||||
serverSocksaddr := M.ParseSocksaddr(server)
|
|
||||||
if serverSocksaddr.Port == 0 {
|
|
||||||
serverSocksaddr.Port = 3478
|
|
||||||
}
|
|
||||||
|
|
||||||
reportProgress := options.OnProgress
|
|
||||||
if reportProgress == nil {
|
|
||||||
reportProgress = func(Progress) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
packetConn net.PacketConn
|
|
||||||
serverAddr net.Addr
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if options.Dialer != nil {
|
|
||||||
packetConn, err = options.Dialer.ListenPacket(ctx, serverSocksaddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create UDP socket")
|
|
||||||
}
|
|
||||||
serverAddr = serverSocksaddr
|
|
||||||
} else {
|
|
||||||
serverUDPAddr, resolveErr := net.ResolveUDPAddr("udp", serverSocksaddr.String())
|
|
||||||
if resolveErr != nil {
|
|
||||||
return nil, E.Cause(resolveErr, "resolve STUN server")
|
|
||||||
}
|
|
||||||
packetConn, err = net.ListenPacket("udp", "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create UDP socket")
|
|
||||||
}
|
|
||||||
serverAddr = serverUDPAddr
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = packetConn.Close()
|
|
||||||
}()
|
|
||||||
if deadline.NeedAdditionalReadDeadline(packetConn) {
|
|
||||||
packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn))
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
rto := defaultRTO
|
|
||||||
|
|
||||||
// Phase 1: Binding
|
|
||||||
reportProgress(Progress{Phase: PhaseBinding})
|
|
||||||
|
|
||||||
txID := newTransactionID()
|
|
||||||
resp, latency, err := roundTrip(packetConn, serverAddr, txID, nil, rto)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "binding request")
|
|
||||||
}
|
|
||||||
|
|
||||||
rto = max(minRTO, 3*latency)
|
|
||||||
|
|
||||||
externalAddr, ok := resp.externalAddr()
|
|
||||||
if !ok {
|
|
||||||
return nil, E.New("no mapped address in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &Result{
|
|
||||||
ExternalAddr: externalAddr.String(),
|
|
||||||
LatencyMs: int32(latency.Milliseconds()),
|
|
||||||
}
|
|
||||||
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseBinding,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
})
|
|
||||||
|
|
||||||
otherAddr := resp.otherAddr
|
|
||||||
if !otherAddr.IsValid() {
|
|
||||||
result.NATTypeSupported = false
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseDone,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
})
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
result.NATTypeSupported = true
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return result, nil
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: NAT Mapping Detection (RFC 5780 Section 4.3)
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseNATMapping,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
})
|
|
||||||
|
|
||||||
result.NATMapping = detectNATMapping(
|
|
||||||
packetConn, serverSocksaddr.Port, externalAddr, otherAddr, rto,
|
|
||||||
)
|
|
||||||
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseNATMapping,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
NATMapping: result.NATMapping,
|
|
||||||
})
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return result, nil
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 3: NAT Filtering Detection (RFC 5780 Section 4.4)
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseNATFiltering,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
NATMapping: result.NATMapping,
|
|
||||||
})
|
|
||||||
|
|
||||||
result.NATFiltering = detectNATFiltering(packetConn, serverAddr, rto)
|
|
||||||
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseDone,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
NATMapping: result.NATMapping,
|
|
||||||
NATFiltering: result.NATFiltering,
|
|
||||||
})
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectNATMapping(
|
|
||||||
conn net.PacketConn,
|
|
||||||
serverPort uint16,
|
|
||||||
externalAddr netip.AddrPort,
|
|
||||||
otherAddr netip.AddrPort,
|
|
||||||
rto time.Duration,
|
|
||||||
) NATMapping {
|
|
||||||
// Mapping Test II: Send to other_ip:server_port
|
|
||||||
testIIAddr := net.UDPAddrFromAddrPort(
|
|
||||||
netip.AddrPortFrom(otherAddr.Addr(), serverPort),
|
|
||||||
)
|
|
||||||
txID2 := newTransactionID()
|
|
||||||
resp2, _, err := roundTrip(conn, testIIAddr, txID2, nil, rto)
|
|
||||||
if err != nil {
|
|
||||||
return NATMappingUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
externalAddr2, ok := resp2.externalAddr()
|
|
||||||
if !ok {
|
|
||||||
return NATMappingUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
if externalAddr == externalAddr2 {
|
|
||||||
return NATMappingEndpointIndependent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mapping Test III: Send to other_ip:other_port
|
|
||||||
testIIIAddr := net.UDPAddrFromAddrPort(otherAddr)
|
|
||||||
txID3 := newTransactionID()
|
|
||||||
resp3, _, err := roundTrip(conn, testIIIAddr, txID3, nil, rto)
|
|
||||||
if err != nil {
|
|
||||||
return NATMappingUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
externalAddr3, ok := resp3.externalAddr()
|
|
||||||
if !ok {
|
|
||||||
return NATMappingUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
if externalAddr2 == externalAddr3 {
|
|
||||||
return NATMappingAddressDependent
|
|
||||||
}
|
|
||||||
return NATMappingAddressAndPortDependent
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectNATFiltering(
|
|
||||||
conn net.PacketConn,
|
|
||||||
serverAddr net.Addr,
|
|
||||||
rto time.Duration,
|
|
||||||
) NATFiltering {
|
|
||||||
// Filtering Test II: Request response from different IP and port
|
|
||||||
txID := newTransactionID()
|
|
||||||
_, _, err := roundTrip(conn, serverAddr, txID,
|
|
||||||
[]stunAttribute{changeRequestAttr(changeIP | changePort)}, rto)
|
|
||||||
if err == nil {
|
|
||||||
return NATFilteringEndpointIndependent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtering Test III: Request response from different port only
|
|
||||||
txID = newTransactionID()
|
|
||||||
_, _, err = roundTrip(conn, serverAddr, txID,
|
|
||||||
[]stunAttribute{changeRequestAttr(changePort)}, rto)
|
|
||||||
if err == nil {
|
|
||||||
return NATFilteringAddressDependent
|
|
||||||
}
|
|
||||||
|
|
||||||
return NATFilteringAddressAndPortDependent
|
|
||||||
}
|
|
||||||
|
|
||||||
func paddingLen(n int) int {
|
|
||||||
if n%4 == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return 4 - n%4
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/libdns/acmedns"
|
|
||||||
"github.com/libdns/alidns"
|
"github.com/libdns/alidns"
|
||||||
"github.com/libdns/cloudflare"
|
"github.com/libdns/cloudflare"
|
||||||
"github.com/mholt/acmez/v3/acme"
|
"github.com/mholt/acmez/v3/acme"
|
||||||
@@ -38,6 +37,37 @@ func (w *acmeWrapper) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type acmeLogWriter struct {
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *acmeLogWriter) Write(p []byte) (n int, err error) {
|
||||||
|
logLine := strings.ReplaceAll(string(p), " ", ": ")
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(logLine, "error: "):
|
||||||
|
w.logger.Error(logLine[7:])
|
||||||
|
case strings.HasPrefix(logLine, "warn: "):
|
||||||
|
w.logger.Warn(logLine[6:])
|
||||||
|
case strings.HasPrefix(logLine, "info: "):
|
||||||
|
w.logger.Info(logLine[6:])
|
||||||
|
case strings.HasPrefix(logLine, "debug: "):
|
||||||
|
w.logger.Debug(logLine[7:])
|
||||||
|
default:
|
||||||
|
w.logger.Debug(logLine)
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *acmeLogWriter) Sync() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encoderConfig() zapcore.EncoderConfig {
|
||||||
|
config := zap.NewProductionEncoderConfig()
|
||||||
|
config.TimeKey = zapcore.OmitKey
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
|
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
|
||||||
var acmeServer string
|
var acmeServer string
|
||||||
switch options.Provider {
|
switch options.Provider {
|
||||||
@@ -60,8 +90,8 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
|
|||||||
storage = certmagic.Default.Storage
|
storage = certmagic.Default.Storage
|
||||||
}
|
}
|
||||||
zapLogger := zap.New(zapcore.NewCore(
|
zapLogger := zap.New(zapcore.NewCore(
|
||||||
zapcore.NewConsoleEncoder(ACMEEncoderConfig()),
|
zapcore.NewConsoleEncoder(encoderConfig()),
|
||||||
&ACMELogWriter{Logger: logger},
|
&acmeLogWriter{logger: logger},
|
||||||
zap.DebugLevel,
|
zap.DebugLevel,
|
||||||
))
|
))
|
||||||
config := &certmagic.Config{
|
config := &certmagic.Config{
|
||||||
@@ -96,13 +126,6 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
|
|||||||
APIToken: dnsOptions.CloudflareOptions.APIToken,
|
APIToken: dnsOptions.CloudflareOptions.APIToken,
|
||||||
ZoneToken: dnsOptions.CloudflareOptions.ZoneToken,
|
ZoneToken: dnsOptions.CloudflareOptions.ZoneToken,
|
||||||
}
|
}
|
||||||
case C.DNSProviderACMEDNS:
|
|
||||||
solver.DNSProvider = &acmedns.Provider{
|
|
||||||
Username: dnsOptions.ACMEDNSOptions.Username,
|
|
||||||
Password: dnsOptions.ACMEDNSOptions.Password,
|
|
||||||
Subdomain: dnsOptions.ACMEDNSOptions.Subdomain,
|
|
||||||
ServerURL: dnsOptions.ACMEDNSOptions.ServerURL,
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider)
|
return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider)
|
||||||
}
|
}
|
||||||
@@ -127,7 +150,7 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
|
|||||||
} else {
|
} else {
|
||||||
tlsConfig = &tls.Config{
|
tlsConfig = &tls.Config{
|
||||||
GetCertificate: config.GetCertificate,
|
GetCertificate: config.GetCertificate,
|
||||||
NextProtos: []string{C.ACMETLS1Protocol},
|
NextProtos: []string{ACMETLS1Protocol},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
|
return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user