mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-12 01:57:18 +10:00
Compare commits
102 Commits
renovate/g
...
v1.13.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c142414999 | ||
|
|
b126c1e5bf | ||
|
|
daed4a5f2d | ||
|
|
d553a761a9 | ||
|
|
642719b3b6 | ||
|
|
c325c4ab51 | ||
|
|
5842fde994 | ||
|
|
f45291fa79 | ||
|
|
7f56a3ae8f | ||
|
|
d5bb1a30dd | ||
|
|
4fbf0c4bd6 | ||
|
|
762b567693 | ||
|
|
1a511ae711 | ||
|
|
dcf5fde029 | ||
|
|
04949914f1 | ||
|
|
fbc0fe43ad | ||
|
|
857319ef3d | ||
|
|
b48ddee8b1 | ||
|
|
8e3d70d197 | ||
|
|
21468cda93 | ||
|
|
2ff67c9ece | ||
|
|
6ff279ba49 | ||
|
|
5d15a241e3 | ||
|
|
e01a5c96a5 | ||
|
|
9d40074cfa | ||
|
|
4796bce0c2 | ||
|
|
fe632ef3ff | ||
|
|
b15fecf992 | ||
|
|
774269a2bf | ||
|
|
c995273244 | ||
|
|
a80208e7b1 | ||
|
|
60580df446 | ||
|
|
d28305be5f | ||
|
|
886a45a204 | ||
|
|
401d195944 | ||
|
|
d7489c0653 | ||
|
|
1d6d829dee | ||
|
|
69555fc6ea | ||
|
|
655e534ea8 | ||
|
|
5d86f63906 | ||
|
|
2162dab4a1 | ||
|
|
51fd4b7e1a | ||
|
|
ac51a74dbe | ||
|
|
d8a15ecddf | ||
|
|
dda91d50bd | ||
|
|
b3aec2997b | ||
|
|
f3ae84442a | ||
|
|
8d373fb004 | ||
|
|
db3c3a6621 | ||
|
|
eaaaddb61e | ||
|
|
e97784d05b | ||
|
|
7a0d211224 | ||
|
|
5264802927 | ||
|
|
69896f8b80 | ||
|
|
0e3552da6f | ||
|
|
0ed29288a0 | ||
|
|
774fed206b | ||
|
|
71e1eb1e7f | ||
|
|
5112488d39 | ||
|
|
f228bd5b49 | ||
|
|
6424476ace | ||
|
|
51dc7c4f88 | ||
|
|
92c072d849 | ||
|
|
ac7f0c9a3d | ||
|
|
31dcdc4f14 | ||
|
|
3a267e1557 | ||
|
|
73c5575866 | ||
|
|
f00854dd72 | ||
|
|
c04d1414dd | ||
|
|
ec4e411fa7 | ||
|
|
cc3a4c7dec | ||
|
|
20b0e4c3a0 | ||
|
|
5aabc42823 | ||
|
|
f0771fb623 | ||
|
|
67c79ebac8 | ||
|
|
ef185fed09 | ||
|
|
f8cdc40d62 | ||
|
|
3bcf072750 | ||
|
|
3ef15fa8aa | ||
|
|
159418324a | ||
|
|
0c30128c44 | ||
|
|
354330cd80 | ||
|
|
b991ae1c91 | ||
|
|
53fef79ab3 | ||
|
|
dfe2e083d5 | ||
|
|
ce408b2989 | ||
|
|
395a1aadfa | ||
|
|
905573e386 | ||
|
|
5814753603 | ||
|
|
8cef967847 | ||
|
|
af532b8d05 | ||
|
|
5585855367 | ||
|
|
2344b35b0f | ||
|
|
3a9ae0f91f | ||
|
|
4fdbf786e1 | ||
|
|
1e3ea29602 | ||
|
|
ac5cb50368 | ||
|
|
722f9ee971 | ||
|
|
3df72d04e9 | ||
|
|
4f93a515d6 | ||
|
|
a3c9ceca60 | ||
|
|
a57bdc82e8 |
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
|
||||
2
.github/CRONET_GO_VERSION
vendored
2
.github/CRONET_GO_VERSION
vendored
@@ -1 +1 @@
|
||||
ea7cd33752aed62603775af3df946c1b83f4b0b3
|
||||
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"
|
||||
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
@@ -6,7 +6,7 @@
|
||||
":disableRateLimiting"
|
||||
],
|
||||
"baseBranches": [
|
||||
"unstable"
|
||||
"dev-next"
|
||||
],
|
||||
"golang": {
|
||||
"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
|
||||
|
||||
set -euo pipefail
|
||||
VERSION="1.25.5"
|
||||
|
||||
VERSION="1.25.8"
|
||||
PATCH_COMMITS=(
|
||||
"466f6c7a29bc098b0d4c987b803c779222894a11"
|
||||
"1bdabae205052afe1dadb2ad6f1ba612cdbc532a"
|
||||
"a90777dcf692dd2168577853ba743b4338721b06"
|
||||
"f6bddda4e8ff58a957462a1a09562924d5f3d05c"
|
||||
"bed309eff415bcb3c77dd4bc3277b682b89a388d"
|
||||
"34b899c2fb39b092db4fa67c4417e41dc046be4b"
|
||||
)
|
||||
CURL_ARGS=(
|
||||
-fL
|
||||
--silent
|
||||
--show-error
|
||||
)
|
||||
|
||||
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
|
||||
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
|
||||
fi
|
||||
|
||||
mkdir -p "$HOME/go"
|
||||
cd "$HOME/go"
|
||||
mkdir -p $HOME/go
|
||||
cd $HOME/go
|
||||
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
|
||||
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
|
||||
mv go go_win7
|
||||
cd go_win7
|
||||
|
||||
# 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
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
||||
# revert:
|
||||
@@ -37,10 +18,10 @@ cd go_win7
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
# 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
|
||||
curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1
|
||||
done
|
||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||
|
||||
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@$(git -C $PROJECTS/cronet-go rev-parse origin/go)
|
||||
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"
|
||||
198
.github/workflows/build.yml
vendored
198
.github/workflows/build.yml
vendored
@@ -25,9 +25,8 @@ on:
|
||||
- publish-android
|
||||
push:
|
||||
branches:
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
- main-next
|
||||
- dev-next
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
||||
@@ -41,13 +40,13 @@ jobs:
|
||||
version: ${{ steps.outputs.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.26.0
|
||||
go-version: ^1.25.5
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -72,41 +71,33 @@ jobs:
|
||||
include:
|
||||
- { os: linux, arch: amd64, variant: purego, 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: 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", 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, 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: 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: 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: "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: "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: mipsle, gomips: hardfloat, openwrt: "mipsel_24kc_24kf" }
|
||||
- { os: linux, arch: mipsle, gomips: softfloat }
|
||||
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
|
||||
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
|
||||
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
|
||||
- { os: linux, arch: mips64le, gomips: hardfloat }
|
||||
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
|
||||
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
|
||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
- { os: linux, arch: riscv64 }
|
||||
- { os: linux, arch: loong64 }
|
||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
|
||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
||||
|
||||
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
|
||||
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
|
||||
@@ -117,14 +108,19 @@ jobs:
|
||||
- { os: android, arch: "386", ndk: "i686-linux-android23" }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
if: ${{ ! matrix.legacy_win7 }}
|
||||
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.26.0
|
||||
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
|
||||
if: matrix.legacy_win7
|
||||
id: cache-go-for-windows7
|
||||
@@ -132,11 +128,9 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/go/go_win7
|
||||
key: go_win7_1258
|
||||
key: go_win7_1255
|
||||
- name: Setup Go for Windows 7
|
||||
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |-
|
||||
.github/setup_go_for_windows7.sh
|
||||
- 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 checkout FETCH_HEAD
|
||||
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
|
||||
if: matrix.naive
|
||||
id: cache-chromium-toolchain
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/cronet-go/naiveproxy/src/third_party/llvm-build/
|
||||
~/cronet-go/naiveproxy/src/gn/out/
|
||||
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
|
||||
~/cronet-go/naiveproxy/src/out/sysroot-build/
|
||||
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
|
||||
~/cronet-go/naiveproxy/src/out/sysroot-build
|
||||
key: chromium-toolchain-${{ matrix.arch }}-${{ matrix.variant }}-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
||||
- name: Download Chromium toolchain
|
||||
if: matrix.naive
|
||||
@@ -205,10 +190,9 @@ jobs:
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
|
||||
if [[ "${{ matrix.naive }}" == "true" ]]; then
|
||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
|
||||
else
|
||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
||||
TAGS="${TAGS},with_naive_outbound"
|
||||
fi
|
||||
if [[ "${{ matrix.variant }}" == "purego" ]]; then
|
||||
TAGS="${TAGS},with_purego"
|
||||
@@ -216,16 +200,13 @@ jobs:
|
||||
TAGS="${TAGS},with_musl"
|
||||
fi
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Set shared ldflags
|
||||
run: |
|
||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
|
||||
- name: Build (purego)
|
||||
if: matrix.variant == 'purego'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
@@ -247,7 +228,7 @@ jobs:
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
@@ -255,8 +236,6 @@ jobs:
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GO386: ${{ matrix.go386 }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
GOMIPS64: ${{ matrix.gomips }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build (musl)
|
||||
if: matrix.variant == 'musl'
|
||||
@@ -264,7 +243,7 @@ jobs:
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
@@ -272,8 +251,6 @@ jobs:
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GO386: ${{ matrix.go386 }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
GOMIPS64: ${{ matrix.gomips }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build (non-variant)
|
||||
if: matrix.os != 'android' && matrix.variant == ''
|
||||
@@ -281,7 +258,7 @@ jobs:
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
@@ -301,7 +278,7 @@ jobs:
|
||||
export CXX="${CC}++"
|
||||
mkdir -p dist
|
||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags "-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
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
@@ -375,7 +352,7 @@ jobs:
|
||||
sudo gem install fpm
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libarchive-tools
|
||||
cp .fpm_pacman .fpm
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t pacman \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
||||
@@ -396,30 +373,6 @@ jobs:
|
||||
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
|
||||
done
|
||||
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
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
@@ -455,36 +408,22 @@ jobs:
|
||||
include:
|
||||
- { arch: amd64 }
|
||||
- { arch: arm64 }
|
||||
- { arch: amd64, legacy_osx: true, legacy_name: "macos-10.13" }
|
||||
- { arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
if: ${{ ! matrix.legacy_osx }}
|
||||
if: ${{ ! matrix.legacy_go124 }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.3
|
||||
- name: Cache Go for macOS 10.13
|
||||
if: matrix.legacy_osx
|
||||
id: cache-go-for-macos1013
|
||||
uses: actions/cache@v4
|
||||
- name: Setup Go 1.24
|
||||
if: matrix.legacy_go124
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/go_osx
|
||||
key: go_osx_1258
|
||||
- name: Setup Go for macOS 10.13
|
||||
if: matrix.legacy_osx && steps.cache-go-for-macos1013.outputs.cache-hit != 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |-
|
||||
.github/setup_go_for_macos1013.sh
|
||||
- name: Setup Go for macOS 10.13
|
||||
if: matrix.legacy_osx
|
||||
run: |-
|
||||
echo "PATH=$HOME/go/go_osx/bin:$PATH" >> $GITHUB_ENV
|
||||
echo "GOROOT=$HOME/go/go_osx" >> $GITHUB_ENV
|
||||
go-version: ~1.24.6
|
||||
- name: Set tag
|
||||
run: |-
|
||||
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
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then
|
||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
|
||||
else
|
||||
TAGS=$(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'
|
||||
if [[ "${{ matrix.legacy_go124 }}" != "true" ]]; then
|
||||
TAGS="${TAGS},with_naive_outbound"
|
||||
fi
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Set shared ldflags
|
||||
run: |
|
||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
GOOS: darwin
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.legacy_osx && '10.13' || '' }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set name
|
||||
run: |-
|
||||
@@ -551,7 +485,7 @@ jobs:
|
||||
- { arch: arm64, naive: true }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
@@ -565,11 +499,9 @@ jobs:
|
||||
- name: Build
|
||||
if: matrix.naive
|
||||
run: |
|
||||
$TAGS = Get-Content release/DEFAULT_BUILD_TAGS_WINDOWS
|
||||
$LDFLAGS_SHARED = Get-Content release/LDFLAGS
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box.exe -tags "$TAGS" `
|
||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' $LDFLAGS_SHARED -s -w -buildid=" `
|
||||
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 "-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
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
@@ -579,11 +511,9 @@ jobs:
|
||||
- name: Build
|
||||
if: ${{ !matrix.naive }}
|
||||
run: |
|
||||
$TAGS = Get-Content release/DEFAULT_BUILD_TAGS_OTHERS
|
||||
$LDFLAGS_SHARED = Get-Content release/LDFLAGS
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box.exe -tags "$TAGS" `
|
||||
-ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' $LDFLAGS_SHARED -s -w -buildid=" `
|
||||
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 "-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
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
@@ -628,20 +558,20 @@ jobs:
|
||||
path: "dist"
|
||||
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
|
||||
needs:
|
||||
- calculate_version
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.26.0
|
||||
go-version: ^1.25.5
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -664,12 +594,12 @@ jobs:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
- 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: |-
|
||||
cd clients/android
|
||||
git checkout main
|
||||
- name: Checkout dev branch
|
||||
if: github.ref == 'refs/heads/testing'
|
||||
if: github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
cd clients/android
|
||||
git checkout dev
|
||||
@@ -718,20 +648,20 @@ jobs:
|
||||
path: 'dist'
|
||||
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
|
||||
needs:
|
||||
- calculate_version
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.26.0
|
||||
go-version: ^1.25.5
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -754,12 +684,12 @@ jobs:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
- 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: |-
|
||||
cd clients/android
|
||||
git checkout main
|
||||
- name: Checkout dev branch
|
||||
if: github.ref == 'refs/heads/testing'
|
||||
if: github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
cd clients/android
|
||||
git checkout dev
|
||||
@@ -822,7 +752,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: matrix.if
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
@@ -830,7 +760,7 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.26.0
|
||||
go-version: ^1.25.5
|
||||
- name: Set tag
|
||||
if: matrix.if
|
||||
run: |-
|
||||
@@ -838,12 +768,12 @@ jobs:
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||
- 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: |-
|
||||
cd clients/apple
|
||||
git checkout main
|
||||
- name: Checkout dev branch
|
||||
if: matrix.if && github.ref == 'refs/heads/testing'
|
||||
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
cd clients/apple
|
||||
git checkout dev
|
||||
@@ -929,7 +859,7 @@ jobs:
|
||||
-authenticationKeyID $ASC_KEY_ID \
|
||||
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||
- 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: |-
|
||||
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
|
||||
- name: Build image
|
||||
@@ -976,7 +906,7 @@ jobs:
|
||||
- build_apple
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Cache ghr
|
||||
|
||||
64
.github/workflows/docker.yml
vendored
64
.github/workflows/docker.yml
vendored
@@ -3,8 +3,8 @@ name: Publish Docker Images
|
||||
on:
|
||||
#push:
|
||||
# branches:
|
||||
# - stable
|
||||
# - testing
|
||||
# - main-next
|
||||
# - dev-next
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
@@ -19,7 +19,6 @@ env:
|
||||
jobs:
|
||||
build_binary:
|
||||
name: Build binary
|
||||
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@@ -30,12 +29,10 @@ jobs:
|
||||
- { arch: arm64, naive: true, docker_platform: "linux/arm64" }
|
||||
- { arch: "386", naive: true, docker_platform: "linux/386" }
|
||||
- { 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
|
||||
- { arch: arm, goarm: "6", docker_platform: "linux/arm/v6" }
|
||||
- { arch: ppc64le, docker_platform: "linux/ppc64le" }
|
||||
- { arch: riscv64, docker_platform: "linux/riscv64" }
|
||||
- { arch: s390x, docker_platform: "linux/s390x" }
|
||||
steps:
|
||||
- name: Get commit to build
|
||||
@@ -49,14 +46,14 @@ jobs:
|
||||
echo "ref=$ref"
|
||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.26.0
|
||||
go-version: ^1.25.4
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
@@ -67,23 +64,14 @@ jobs:
|
||||
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go checkout FETCH_HEAD
|
||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||
- name: Regenerate Debian keyring
|
||||
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
|
||||
if: matrix.naive
|
||||
id: cache-chromium-toolchain
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/cronet-go/naiveproxy/src/third_party/llvm-build/
|
||||
~/cronet-go/naiveproxy/src/gn/out/
|
||||
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
|
||||
~/cronet-go/naiveproxy/src/out/sysroot-build/
|
||||
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
|
||||
~/cronet-go/naiveproxy/src/out/sysroot-build
|
||||
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
||||
- name: Download Chromium toolchain
|
||||
if: matrix.naive
|
||||
@@ -105,34 +93,29 @@ jobs:
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
|
||||
if [[ "${{ matrix.naive }}" == "true" ]]; then
|
||||
TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl"
|
||||
else
|
||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
||||
TAGS="${TAGS},with_naive_outbound,with_musl"
|
||||
fi
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Set shared ldflags
|
||||
run: |
|
||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
|
||||
- name: Build (naive)
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
- name: Build (non-naive)
|
||||
if: ${{ ! matrix.naive }}
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
@@ -165,17 +148,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- { platform: "linux/amd64" }
|
||||
- { platform: "linux/arm/v6" }
|
||||
- { platform: "linux/arm/v7" }
|
||||
- { platform: "linux/arm64" }
|
||||
- { platform: "linux/386" }
|
||||
# mipsle: no base Docker image available for this platform
|
||||
- { platform: "linux/ppc64le" }
|
||||
- { platform: "linux/riscv64" }
|
||||
- { platform: "linux/s390x" }
|
||||
- { platform: "linux/loong64", base_image: "ghcr.io/loong64/alpine:edge" }
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
- linux/arm64
|
||||
- linux/386
|
||||
- linux/ppc64le
|
||||
- linux/riscv64
|
||||
- linux/s390x
|
||||
steps:
|
||||
- name: Get commit to build
|
||||
id: ref
|
||||
@@ -188,7 +169,7 @@ jobs:
|
||||
echo "ref=$ref"
|
||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
@@ -228,8 +209,6 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
context: .
|
||||
file: Dockerfile.binary
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ matrix.base_image || 'alpine' }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
- name: Export digest
|
||||
@@ -245,7 +224,6 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
merge:
|
||||
if: github.event_name != 'push'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_docker
|
||||
|
||||
16
.github/workflows/lint.yml
vendored
16
.github/workflows/lint.yml
vendored
@@ -3,20 +3,18 @@ name: Lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- oldstable
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
- stable-next
|
||||
- main-next
|
||||
- dev-next
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/lint.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- oldstable
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
- stable-next
|
||||
- main-next
|
||||
- dev-next
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -24,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
|
||||
48
.github/workflows/linux.yml
vendored
48
.github/workflows/linux.yml
vendored
@@ -3,8 +3,8 @@ name: Build Linux Packages
|
||||
on:
|
||||
#push:
|
||||
# branches:
|
||||
# - stable
|
||||
# - testing
|
||||
# - main-next
|
||||
# - dev-next
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
@@ -23,19 +23,18 @@ on:
|
||||
jobs:
|
||||
calculate_version:
|
||||
name: Calculate version
|
||||
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.outputs.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.26.0
|
||||
go-version: ^1.25.5
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -62,23 +61,23 @@ jobs:
|
||||
- { 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: 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)
|
||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
|
||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
|
||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.26.0
|
||||
go-version: ^1.25.5
|
||||
- name: Clone cronet-go
|
||||
if: matrix.naive
|
||||
run: |
|
||||
@@ -89,23 +88,14 @@ jobs:
|
||||
git -C ~/cronet-go fetch --depth=1 origin "$CRONET_GO_VERSION"
|
||||
git -C ~/cronet-go checkout FETCH_HEAD
|
||||
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||
- name: Regenerate Debian keyring
|
||||
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
|
||||
if: matrix.naive
|
||||
id: cache-chromium-toolchain
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/cronet-go/naiveproxy/src/third_party/llvm-build/
|
||||
~/cronet-go/naiveproxy/src/gn/out/
|
||||
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
|
||||
~/cronet-go/naiveproxy/src/out/sysroot-build/
|
||||
~/cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts
|
||||
~/cronet-go/naiveproxy/src/out/sysroot-build
|
||||
key: chromium-toolchain-${{ matrix.arch }}-musl-${{ hashFiles('.github/CRONET_GO_VERSION') }}
|
||||
- name: Download Chromium toolchain
|
||||
if: matrix.naive
|
||||
@@ -126,30 +116,24 @@ jobs:
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,badlinkname,tfogo_checklinkname0'
|
||||
if [[ "${{ matrix.naive }}" == "true" ]]; then
|
||||
TAGS="$(cat release/DEFAULT_BUILD_TAGS),with_musl"
|
||||
else
|
||||
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
|
||||
TAGS="${TAGS},with_naive_outbound,with_musl"
|
||||
fi
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Set shared ldflags
|
||||
run: |
|
||||
echo "LDFLAGS_SHARED=$(cat release/LDFLAGS)" >> "${GITHUB_ENV}"
|
||||
- name: Build (naive)
|
||||
if: matrix.naive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
GOMIPS64: ${{ matrix.gomips }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build (non-naive)
|
||||
if: ${{ ! matrix.naive }}
|
||||
@@ -157,7 +141,7 @@ jobs:
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
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
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
@@ -236,7 +220,7 @@ jobs:
|
||||
- build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set tag
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,9 +12,6 @@
|
||||
/*.jar
|
||||
/*.aar
|
||||
/*.xcframework/
|
||||
/experimental/libbox/*.aar
|
||||
/experimental/libbox/*.xcframework/
|
||||
/experimental/libbox/*.nupkg
|
||||
.DS_Store
|
||||
/config.d/
|
||||
/venv/
|
||||
|
||||
@@ -9,11 +9,6 @@ run:
|
||||
- with_utls
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_ccm
|
||||
- with_ocm
|
||||
- badlinkname
|
||||
- tfogo_checklinkname0
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
|
||||
@@ -12,11 +12,10 @@ RUN set -ex \
|
||||
&& apk add git build-base \
|
||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||
&& export TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) \
|
||||
&& export LDFLAGS_SHARED=$(cat release/LDFLAGS) \
|
||||
&& go build -v -trimpath -tags "$TAGS" \
|
||||
&& go build -v -trimpath -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" \
|
||||
-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
|
||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
ARG BASE_IMAGE=alpine
|
||||
FROM ${BASE_IMAGE}
|
||||
FROM alpine
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
RUN set -ex \
|
||||
&& if command -v apk > /dev/null; then \
|
||||
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
|
||||
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
||||
COPY sing-box-${TARGETARCH}${TARGETVARIANT} /usr/local/bin/sing-box
|
||||
ENTRYPOINT ["sing-box"]
|
||||
|
||||
43
Makefile
43
Makefile
@@ -1,18 +1,15 @@
|
||||
NAME = sing-box
|
||||
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)
|
||||
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)
|
||||
|
||||
LDFLAGS_SHARED = $(shell cat release/LDFLAGS)
|
||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' $(LDFLAGS_SHARED) -s -w -buildid="
|
||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -X 'internal/godebug.defaultGODEBUG=multipathtcp=0' -s -w -buildid= -checklinkname=0"
|
||||
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
||||
MAIN = ./cmd/sing-box
|
||||
PREFIX ?= $(shell go env GOPATH)
|
||||
SING_FFI ?= sing-ffi
|
||||
LIBBOX_FFI_CONFIG ?= ./experimental/libbox/ffi.json
|
||||
|
||||
.PHONY: test release docs build
|
||||
|
||||
@@ -92,12 +89,12 @@ update_android_version:
|
||||
go run ./cmd/internal/update_android_version
|
||||
|
||||
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:
|
||||
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/otherLegacy/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/other/release/*-universal.apk dist/release_android
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
|
||||
rm -rf dist/release_android
|
||||
|
||||
@@ -209,12 +206,12 @@ update_apple_version:
|
||||
update_macos_version:
|
||||
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
go run -v ./cmd/internal/app_store_connect prepare_app_store
|
||||
@@ -237,21 +234,22 @@ test_stdio:
|
||||
lib_android:
|
||||
go run ./cmd/internal/build_libbox -target android
|
||||
|
||||
lib_android_debug:
|
||||
go run ./cmd/internal/build_libbox -target android -debug
|
||||
|
||||
lib_apple:
|
||||
go run ./cmd/internal/build_libbox -target apple
|
||||
|
||||
lib_windows:
|
||||
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type csharp
|
||||
lib_ios:
|
||||
go run ./cmd/internal/build_libbox -target apple -platform ios -debug
|
||||
|
||||
lib_android_new:
|
||||
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type android
|
||||
|
||||
lib_apple_new:
|
||||
$(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple
|
||||
lib:
|
||||
go run ./cmd/internal/build_libbox -target android
|
||||
go run ./cmd/internal/build_libbox -target ios
|
||||
|
||||
lib_install:
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.12
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.11
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.11
|
||||
|
||||
docs:
|
||||
venv/bin/mkdocs serve
|
||||
@@ -260,8 +258,8 @@ publish_docs:
|
||||
venv/bin/mkdocs gh-deploy -m "Update" --force --ignore-version --no-history
|
||||
|
||||
docs_install:
|
||||
python3 -m venv venv
|
||||
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.7.2" mkdocs-static-i18n=="1.2.*"
|
||||
python -m venv venv
|
||||
source ./venv/bin/activate && pip install --force-reinstall mkdocs-material=="9.*" mkdocs-static-i18n=="1.2.*"
|
||||
|
||||
clean:
|
||||
rm -rf bin dist sing-box
|
||||
@@ -271,6 +269,3 @@ update:
|
||||
git fetch
|
||||
git reset FETCH_HEAD --hard
|
||||
git clean -fdx
|
||||
|
||||
%:
|
||||
@:
|
||||
|
||||
@@ -9,10 +9,6 @@ import (
|
||||
|
||||
type ConnectionManager interface {
|
||||
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)
|
||||
NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
@@ -69,11 +68,7 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.Content)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = buffer.Write(s.Content)
|
||||
err = varbin.Write(&buffer, binary.BigEndian, s.Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -81,11 +76,7 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(&buffer, uint64(len(s.LastEtag)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = buffer.WriteString(s.LastEtag)
|
||||
err = varbin.Write(&buffer, binary.BigEndian, s.LastEtag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -99,12 +90,7 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contentLength, err := binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Content = make([]byte, contentLength)
|
||||
_, err = io.ReadFull(reader, s.Content)
|
||||
err = varbin.Read(reader, binary.BigEndian, &s.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -114,16 +100,10 @@ func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
||||
return err
|
||||
}
|
||||
s.LastUpdated = time.Unix(lastUpdated, 0)
|
||||
etagLength, err := binary.ReadUvarint(reader)
|
||||
err = varbin.Read(reader, binary.BigEndian, &s.LastEtag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
etagBytes := make([]byte, etagLength)
|
||||
_, err = io.ReadFull(reader, etagBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.LastEtag = string(etagBytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
@@ -63,10 +62,13 @@ type InboundContext struct {
|
||||
// cache
|
||||
|
||||
// Deprecated: implement in rule action
|
||||
InboundDetour string
|
||||
LastInbound string
|
||||
OriginDestination M.Socksaddr
|
||||
RouteOriginalDestination M.Socksaddr
|
||||
InboundDetour string
|
||||
LastInbound string
|
||||
OriginDestination M.Socksaddr
|
||||
RouteOriginalDestination M.Socksaddr
|
||||
// Deprecated: to be removed
|
||||
//nolint:staticcheck
|
||||
InboundOptions option.InboundOptions
|
||||
UDPDisableDomainUnmapping bool
|
||||
UDPConnect bool
|
||||
UDPTimeout time.Duration
|
||||
@@ -83,8 +85,6 @@ type InboundContext struct {
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
SourceMACAddress net.HardwareAddr
|
||||
SourceHostname string
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
|
||||
|
||||
@@ -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
|
||||
SendNotification(notification *Notification) error
|
||||
|
||||
UsePlatformNeighborResolver() bool
|
||||
StartNeighborMonitor(listener NeighborUpdateListener) error
|
||||
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
||||
}
|
||||
|
||||
type FindConnectionOwnerRequest struct {
|
||||
|
||||
@@ -26,8 +26,6 @@ type Router interface {
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
Rules() []Rule
|
||||
NeedFindProcess() bool
|
||||
NeedFindNeighbor() bool
|
||||
NeighborResolver() NeighborResolver
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
5
box.go
5
box.go
@@ -125,10 +125,7 @@ func New(options Options) (*Box, error) {
|
||||
|
||||
ctx = pause.WithDefaultManager(ctx)
|
||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||
err := applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
|
||||
var needCacheFile bool
|
||||
var needClashAPI bool
|
||||
var needV2RayAPI bool
|
||||
|
||||
Submodule clients/android updated: 0d31ac467f...863cd1ce4f
Submodule clients/apple updated: 22dcf646ce...532c140f05
@@ -100,32 +100,11 @@ findVersion:
|
||||
}
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
|
||||
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 {
|
||||
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
|
||||
log.Info(string(platform), " list builds")
|
||||
for {
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
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))
|
||||
for _, platform := range platforms {
|
||||
log.Info(string(platform), " list builds")
|
||||
for {
|
||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
||||
FilterApp: []string{appID},
|
||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
||||
})
|
||||
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
|
||||
}
|
||||
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)
|
||||
build := builds.Data[0]
|
||||
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 {
|
||||
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
|
||||
log.Error(err)
|
||||
break
|
||||
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(
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -17,17 +17,17 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
debugEnabled bool
|
||||
target string
|
||||
platform string
|
||||
// withTailscale bool
|
||||
debugEnabled bool
|
||||
target string
|
||||
platform string
|
||||
withTailscale bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
||||
flag.StringVar(&target, "target", "android", "target 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() {
|
||||
@@ -48,7 +48,7 @@ var (
|
||||
debugFlags []string
|
||||
sharedTags []string
|
||||
darwinTags []string
|
||||
// memcTags []string
|
||||
memcTags []string
|
||||
notMemcTags []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")
|
||||
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")
|
||||
darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace")
|
||||
// memcTags = append(memcTags, "with_tailscale")
|
||||
sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird")
|
||||
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")
|
||||
memcTags = append(memcTags, "with_tailscale")
|
||||
notMemcTags = append(notMemcTags, "with_low_memory")
|
||||
debugTags = append(debugTags, "debug")
|
||||
}
|
||||
@@ -165,7 +164,7 @@ func buildAndroid() {
|
||||
|
||||
// Build main variant (SDK 23)
|
||||
mainTags := append([]string{}, sharedTags...)
|
||||
// mainTags = append(mainTags, memcTags...)
|
||||
mainTags = append(mainTags, memcTags...)
|
||||
if debugEnabled {
|
||||
mainTags = append(mainTags, debugTags...)
|
||||
}
|
||||
@@ -177,7 +176,7 @@ func buildAndroid() {
|
||||
|
||||
// Build legacy variant (SDK 21, no naive outbound)
|
||||
legacyTags := filterTags(sharedTags, "with_naive_outbound")
|
||||
// legacyTags = append(legacyTags, memcTags...)
|
||||
legacyTags = append(legacyTags, memcTags...)
|
||||
if debugEnabled {
|
||||
legacyTags = append(legacyTags, debugTags...)
|
||||
}
|
||||
@@ -205,9 +204,9 @@ func buildApple() {
|
||||
"-libname=box",
|
||||
"-tags-not-macos=with_low_memory",
|
||||
}
|
||||
//if !withTailscale {
|
||||
// args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
|
||||
//}
|
||||
if !withTailscale {
|
||||
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
args = append(args, sharedFlags...)
|
||||
@@ -216,9 +215,9 @@ func buildApple() {
|
||||
}
|
||||
|
||||
tags := append(sharedTags, darwinTags...)
|
||||
//if withTailscale {
|
||||
// tags = append(tags, memcTags...)
|
||||
//}
|
||||
if withTailscale {
|
||||
tags = append(tags, memcTags...)
|
||||
}
|
||||
if debugEnabled {
|
||||
tags = append(tags, debugTags...)
|
||||
}
|
||||
|
||||
@@ -71,12 +71,12 @@ func findAndReplace(objectsMap map[string]any, projectContent string, bundleIDLi
|
||||
indexEnd := indexStart + strings.Index(projectContent[indexStart:], "}")
|
||||
versionStart := indexStart + strings.Index(projectContent[indexStart:indexEnd], "MARKETING_VERSION = ") + 20
|
||||
versionEnd := versionStart + strings.Index(projectContent[versionStart:indexEnd], ";")
|
||||
version := strings.Trim(projectContent[versionStart:versionEnd], "\"")
|
||||
version := projectContent[versionStart:versionEnd]
|
||||
if version == newVersion {
|
||||
continue
|
||||
}
|
||||
updated = true
|
||||
projectContent = projectContent[:versionStart] + "\"" + newVersion + "\"" + projectContent[versionEnd:]
|
||||
projectContent = projectContent[:versionStart] + newVersion + projectContent[versionEnd:]
|
||||
}
|
||||
return projectContent, updated
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/conntrack"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -36,7 +37,6 @@ type DefaultDialer struct {
|
||||
udpAddr4 string
|
||||
udpAddr6 string
|
||||
netns string
|
||||
connectionManager adapter.ConnectionManager
|
||||
networkManager adapter.NetworkManager
|
||||
networkStrategy *C.NetworkStrategy
|
||||
defaultNetworkStrategy bool
|
||||
@@ -47,7 +47,6 @@ type DefaultDialer struct {
|
||||
}
|
||||
|
||||
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
||||
connectionManager := service.FromContext[adapter.ConnectionManager](ctx)
|
||||
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||
|
||||
@@ -90,7 +89,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
|
||||
if networkManager != nil {
|
||||
defaultOptions := networkManager.DefaultOptions()
|
||||
if defaultOptions.BindInterface != "" && !disableDefaultBind {
|
||||
if defaultOptions.BindInterface != "" {
|
||||
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
@@ -158,11 +157,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
if keepInterval == 0 {
|
||||
keepInterval = C.TCPKeepAliveInterval
|
||||
}
|
||||
dialer.KeepAliveConfig = net.KeepAliveConfig{
|
||||
Enable: true,
|
||||
Idle: keepIdle,
|
||||
Interval: keepInterval,
|
||||
}
|
||||
dialer.KeepAlive = keepIdle
|
||||
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(keepIdle, keepInterval))
|
||||
}
|
||||
var udpFragment bool
|
||||
if options.UDPFragment != nil {
|
||||
@@ -210,7 +206,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
udpAddr4: udpAddr4,
|
||||
udpAddr6: udpAddr6,
|
||||
netns: options.NetNs,
|
||||
connectionManager: connectionManager,
|
||||
networkManager: networkManager,
|
||||
networkStrategy: networkStrategy,
|
||||
defaultNetworkStrategy: defaultNetworkStrategy,
|
||||
@@ -243,7 +238,7 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
|
||||
return nil, E.New("domain not resolved")
|
||||
}
|
||||
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) {
|
||||
case N.NetworkUDP:
|
||||
if !address.IsIPv6() {
|
||||
@@ -308,12 +303,12 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
||||
if !fastFallback && !isPrimary {
|
||||
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) {
|
||||
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() {
|
||||
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
|
||||
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||
@@ -365,23 +360,23 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return d.trackPacketConn(packetConn, nil)
|
||||
return trackPacketConn(packetConn, nil)
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) WireGuardControl() control.Func {
|
||||
return d.udpListener.Control
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) trackConn(conn net.Conn, err error) (net.Conn, error) {
|
||||
if d.connectionManager == nil || err != nil {
|
||||
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
||||
if !conntrack.Enabled || err != nil {
|
||||
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) {
|
||||
if d.connectionManager == nil || err != nil {
|
||||
func trackPacketConn(conn net.PacketConn, err error) (net.PacketConn, error) {
|
||||
if !conntrack.Enabled || err != nil {
|
||||
return conn, err
|
||||
}
|
||||
return d.connectionManager.TrackPacketConn(conn), nil
|
||||
return conntrack.NewPacketConn(conn)
|
||||
}
|
||||
|
||||
@@ -145,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)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
)
|
||||
|
||||
type Reader struct {
|
||||
@@ -77,7 +78,7 @@ func (r *Reader) readMetadata() error {
|
||||
codeIndex uint64
|
||||
codeLength uint64
|
||||
)
|
||||
code, err = readString(reader)
|
||||
code, err = varbin.ReadValue[string](reader, binary.BigEndian)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -111,16 +112,9 @@ func (r *Reader) Read(code string) ([]Item, error) {
|
||||
}
|
||||
r.bufferedReader.Reset(r.reader)
|
||||
itemList := make([]Item, r.domainLength[code])
|
||||
for i := range itemList {
|
||||
typeByte, err := r.bufferedReader.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
itemList[i].Type = ItemType(typeByte)
|
||||
itemList[i].Value, err = readString(r.bufferedReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = varbin.Read(r.bufferedReader, binary.BigEndian, &itemList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return itemList, nil
|
||||
}
|
||||
@@ -141,18 +135,3 @@ func (r *readCounter) Read(p []byte) (n int, err error) {
|
||||
}
|
||||
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 (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"sort"
|
||||
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
@@ -19,11 +20,7 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
|
||||
for _, code := range keys {
|
||||
index[code] = content.Len()
|
||||
for _, item := range domains[code] {
|
||||
err := content.WriteByte(byte(item.Type))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = writeString(content, item.Value)
|
||||
err := varbin.Write(content, binary.BigEndian, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -41,7 +38,7 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
|
||||
}
|
||||
|
||||
for _, code := range keys {
|
||||
err = writeString(writer, code)
|
||||
err = varbin.Write(writer, binary.BigEndian, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -62,12 +59,3 @@ func Write(writer varbin.Writer, domains map[string][]Item) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -151,7 +151,6 @@ func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "get current netns")
|
||||
}
|
||||
defer currentNs.Close()
|
||||
defer netns.Set(currentNs)
|
||||
var targetNs netns.NsHandle
|
||||
if strings.HasPrefix(nameOrPath, "/") {
|
||||
|
||||
@@ -99,6 +99,8 @@ func (l *Listener) loopTCPIn() {
|
||||
}
|
||||
//nolint:staticcheck
|
||||
metadata.InboundDetour = l.listenOptions.Detour
|
||||
//nolint:staticcheck
|
||||
metadata.InboundOptions = l.listenOptions.InboundOptions
|
||||
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
|
||||
metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
|
||||
ctx := log.ContextWithNewID(l.ctx)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net/netip"
|
||||
"unsafe"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -506,24 +505,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
||||
}
|
||||
|
||||
func readRuleItemString(reader varbin.Reader) ([]string, error) {
|
||||
length, err := binary.ReadUvarint(reader)
|
||||
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
|
||||
return varbin.ReadValue[[]string](reader, binary.BigEndian)
|
||||
}
|
||||
|
||||
func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) error {
|
||||
@@ -531,34 +513,11 @@ func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) e
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, 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
|
||||
return varbin.Write(writer, binary.BigEndian, value)
|
||||
}
|
||||
|
||||
func readRuleItemUint8[E ~uint8](reader varbin.Reader) ([]E, error) {
|
||||
length, err := binary.ReadUvarint(reader)
|
||||
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
|
||||
return varbin.ReadValue[[]E](reader, binary.BigEndian)
|
||||
}
|
||||
|
||||
func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []E) error {
|
||||
@@ -566,25 +525,11 @@ func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(writer, uint64(len(value)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(*(*[]byte)(unsafe.Pointer(&value)))
|
||||
return err
|
||||
return varbin.Write(writer, binary.BigEndian, value)
|
||||
}
|
||||
|
||||
func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) {
|
||||
length, err := binary.ReadUvarint(reader)
|
||||
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
|
||||
return varbin.ReadValue[[]uint16](reader, binary.BigEndian)
|
||||
}
|
||||
|
||||
func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) error {
|
||||
@@ -592,11 +537,7 @@ func writeRuleItemUint16(writer varbin.Writer, itemType uint8, value []uint16) e
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(writer, uint64(len(value)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return binary.Write(writer, binary.BigEndian, value)
|
||||
return varbin.Write(writer, binary.BigEndian, value)
|
||||
}
|
||||
|
||||
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 (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net/netip"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
@@ -10,16 +9,11 @@ import (
|
||||
)
|
||||
|
||||
func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
|
||||
addrLen, err := binary.ReadUvarint(reader)
|
||||
addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, err
|
||||
}
|
||||
addrSlice := make([]byte, addrLen)
|
||||
_, err = io.ReadFull(reader, addrSlice)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, err
|
||||
}
|
||||
prefixBits, err := reader.ReadByte()
|
||||
prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian)
|
||||
if err != nil {
|
||||
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 {
|
||||
addrSlice := prefix.Addr().AsSlice()
|
||||
_, err := varbin.WriteUvarint(writer, uint64(len(addrSlice)))
|
||||
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(addrSlice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = writer.WriteByte(uint8(prefix.Bits()))
|
||||
err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package srs
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
|
||||
@@ -22,6 +22,11 @@ type myIPRange struct {
|
||||
to netip.Addr
|
||||
}
|
||||
|
||||
type myIPRangeData struct {
|
||||
From []byte
|
||||
To []byte
|
||||
}
|
||||
|
||||
func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
|
||||
version, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
@@ -36,30 +41,17 @@ func readIPSet(reader varbin.Reader) (*netipx.IPSet, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mySet := &myIPSet{
|
||||
rr: make([]myIPRange, length),
|
||||
ranges := make([]myIPRangeData, length)
|
||||
err = varbin.Read(reader, binary.BigEndian, &ranges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range mySet.rr {
|
||||
fromLen, err := binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fromBytes := make([]byte, fromLen)
|
||||
_, 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)
|
||||
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
|
||||
}
|
||||
@@ -69,27 +61,18 @@ func writeIPSet(writer varbin.Writer, set *netipx.IPSet) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mySet := (*myIPSet)(unsafe.Pointer(set))
|
||||
err = binary.Write(writer, binary.BigEndian, uint64(len(mySet.rr)))
|
||||
dataList := common.Map((*myIPSet)(unsafe.Pointer(set)).rr, func(rr myIPRange) myIPRangeData {
|
||||
return myIPRangeData{
|
||||
From: rr.from.AsSlice(),
|
||||
To: rr.to.AsSlice(),
|
||||
}
|
||||
})
|
||||
err = binary.Write(writer, binary.BigEndian, uint64(len(dataList)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rr := range mySet.rr {
|
||||
fromBytes := rr.from.AsSlice()
|
||||
_, 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)
|
||||
for _, data := range dataList {
|
||||
err = varbin.Write(writer, binary.BigEndian, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/libdns/acmedns"
|
||||
"github.com/libdns/alidns"
|
||||
"github.com/libdns/cloudflare"
|
||||
"github.com/mholt/acmez/v3/acme"
|
||||
@@ -127,13 +126,6 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
|
||||
APIToken: dnsOptions.CloudflareOptions.APIToken,
|
||||
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:
|
||||
return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
@@ -37,7 +38,7 @@ func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, op
|
||||
}
|
||||
//nolint:staticcheck
|
||||
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
|
||||
return nil, E.New("legacy ECH options are deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0")
|
||||
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
|
||||
}
|
||||
if len(echConfig) > 0 {
|
||||
block, rest := pem.Decode(echConfig)
|
||||
@@ -76,7 +77,7 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
|
||||
tlsConfig.EncryptedClientHelloKeys = echKeys
|
||||
//nolint:staticcheck
|
||||
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
|
||||
return E.New("legacy ECH options are deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0")
|
||||
deprecated.Report(ctx, deprecated.OptionLegacyECHOptions)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -33,5 +33,4 @@ const (
|
||||
const (
|
||||
DNSProviderAliDNS = "alidns"
|
||||
DNSProviderCloudflare = "cloudflare"
|
||||
DNSProviderACMEDNS = "acmedns"
|
||||
)
|
||||
|
||||
@@ -30,7 +30,6 @@ const (
|
||||
TypeSSMAPI = "ssm-api"
|
||||
TypeCCM = "ccm"
|
||||
TypeOCM = "ocm"
|
||||
TypeOOMKiller = "oom-killer"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -86,8 +85,6 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "Hysteria2"
|
||||
case TypeAnyTLS:
|
||||
return "AnyTLS"
|
||||
case TypeTailscale:
|
||||
return "Tailscale"
|
||||
case TypeSelector:
|
||||
return "Selector"
|
||||
case TypeURLTest:
|
||||
|
||||
@@ -7,12 +7,9 @@ import (
|
||||
"github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/include"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/service"
|
||||
@@ -23,7 +20,6 @@ type Instance struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
instance *box.Box
|
||||
connectionManager adapter.ConnectionManager
|
||||
clashServer adapter.ClashServer
|
||||
cacheFile adapter.CacheFile
|
||||
pauseManager pause.Manager
|
||||
@@ -87,15 +83,6 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.oomKiller && C.IsIos {
|
||||
if !common.Any(options.Services, func(it option.Service) bool {
|
||||
return it.Type == C.TypeOOMKiller
|
||||
}) {
|
||||
options.Services = append(options.Services, option.Service{
|
||||
Type: C.TypeOOMKiller,
|
||||
})
|
||||
}
|
||||
}
|
||||
urlTestHistoryStorage := urltest.NewHistoryStorage()
|
||||
ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
|
||||
i := &Instance{
|
||||
@@ -113,11 +100,9 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
|
||||
return nil, err
|
||||
}
|
||||
i.instance = boxInstance
|
||||
i.connectionManager = service.FromContext[adapter.ConnectionManager](ctx)
|
||||
i.clashServer = service.FromContext[adapter.ClashServer](ctx)
|
||||
i.pauseManager = service.FromContext[pause.Manager](ctx)
|
||||
i.cacheFile = service.FromContext[adapter.CacheFile](ctx)
|
||||
log.SetStdLogger(boxInstance.LogFactory().Logger())
|
||||
return i, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/conntrack"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
"github.com/sagernet/sing-box/experimental/clashapi"
|
||||
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
|
||||
@@ -35,7 +36,6 @@ type StartedService struct {
|
||||
handler PlatformHandler
|
||||
debug bool
|
||||
logMaxLines int
|
||||
oomKiller bool
|
||||
// workingDirectory string
|
||||
// tempDirectory string
|
||||
// userID int
|
||||
@@ -56,9 +56,6 @@ type StartedService struct {
|
||||
urlTestHistoryStorage *urltest.HistoryStorage
|
||||
clashModeSubscriber *observable.Subscriber[struct{}]
|
||||
clashModeObserver *observable.Observer[struct{}]
|
||||
|
||||
connectionEventSubscriber *observable.Subscriber[trafficontrol.ConnectionEvent]
|
||||
connectionEventObserver *observable.Observer[trafficontrol.ConnectionEvent]
|
||||
}
|
||||
|
||||
type ServiceOptions struct {
|
||||
@@ -67,7 +64,6 @@ type ServiceOptions struct {
|
||||
Handler PlatformHandler
|
||||
Debug bool
|
||||
LogMaxLines int
|
||||
OOMKiller bool
|
||||
// WorkingDirectory string
|
||||
// TempDirectory string
|
||||
// UserID int
|
||||
@@ -82,25 +78,22 @@ func NewStartedService(options ServiceOptions) *StartedService {
|
||||
handler: options.Handler,
|
||||
debug: options.Debug,
|
||||
logMaxLines: options.LogMaxLines,
|
||||
oomKiller: options.OOMKiller,
|
||||
// workingDirectory: options.WorkingDirectory,
|
||||
// tempDirectory: options.TempDirectory,
|
||||
// userID: options.UserID,
|
||||
// groupID: options.GroupID,
|
||||
// systemProxyEnabled: options.SystemProxyEnabled,
|
||||
serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE},
|
||||
serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4),
|
||||
logSubscriber: observable.NewSubscriber[*log.Entry](128),
|
||||
urlTestSubscriber: observable.NewSubscriber[struct{}](1),
|
||||
urlTestHistoryStorage: urltest.NewHistoryStorage(),
|
||||
clashModeSubscriber: observable.NewSubscriber[struct{}](1),
|
||||
connectionEventSubscriber: observable.NewSubscriber[trafficontrol.ConnectionEvent](256),
|
||||
serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE},
|
||||
serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4),
|
||||
logSubscriber: observable.NewSubscriber[*log.Entry](128),
|
||||
urlTestSubscriber: observable.NewSubscriber[struct{}](1),
|
||||
urlTestHistoryStorage: urltest.NewHistoryStorage(),
|
||||
clashModeSubscriber: observable.NewSubscriber[struct{}](1),
|
||||
}
|
||||
s.serviceStatusObserver = observable.NewObserver(s.serviceStatusSubscriber, 2)
|
||||
s.logObserver = observable.NewObserver(s.logSubscriber, 64)
|
||||
s.urlTestObserver = observable.NewObserver(s.urlTestSubscriber, 1)
|
||||
s.clashModeObserver = observable.NewObserver(s.clashModeSubscriber, 1)
|
||||
s.connectionEventObserver = observable.NewObserver(s.connectionEventSubscriber, 64)
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -190,7 +183,6 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
|
||||
instance.urlTestHistoryStorage.SetHook(s.urlTestSubscriber)
|
||||
if instance.clashServer != nil {
|
||||
instance.clashServer.SetModeUpdateHook(s.clashModeSubscriber)
|
||||
instance.clashServer.(*clashapi.Server).TrafficManager().SetEventHook(s.connectionEventSubscriber)
|
||||
}
|
||||
s.serviceAccess.Unlock()
|
||||
err = instance.Start()
|
||||
@@ -209,14 +201,6 @@ func (s *StartedService) StartOrReloadService(profileContent string, options *Ov
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StartedService) Close() {
|
||||
s.serviceStatusSubscriber.Close()
|
||||
s.logSubscriber.Close()
|
||||
s.urlTestSubscriber.Close()
|
||||
s.clashModeSubscriber.Close()
|
||||
s.connectionEventSubscriber.Close()
|
||||
}
|
||||
|
||||
func (s *StartedService) CloseService() error {
|
||||
s.serviceAccess.Lock()
|
||||
switch s.serviceStatus.Status {
|
||||
@@ -409,14 +393,12 @@ func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server
|
||||
|
||||
func (s *StartedService) readStatus() *Status {
|
||||
var status Status
|
||||
status.Memory = memory.Total()
|
||||
status.Memory = memory.Inuse()
|
||||
status.Goroutines = int32(runtime.NumGoroutine())
|
||||
status.ConnectionsOut = int32(conntrack.Count())
|
||||
s.serviceAccess.RLock()
|
||||
nowService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
if nowService != nil && nowService.connectionManager != nil {
|
||||
status.ConnectionsOut = int32(nowService.connectionManager.Count())
|
||||
}
|
||||
if nowService != nil {
|
||||
if clashServer := nowService.clashServer; clashServer != nil {
|
||||
status.TrafficAvailable = true
|
||||
@@ -684,7 +666,7 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error {
|
||||
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[Connections]) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -692,260 +674,69 @@ func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsReque
|
||||
s.serviceAccess.RLock()
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
|
||||
if boxService.clashServer == nil {
|
||||
return E.New("clash server not available")
|
||||
}
|
||||
|
||||
trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager()
|
||||
|
||||
subscription, done, err := s.connectionEventObserver.Subscribe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.connectionEventObserver.UnSubscribe(subscription)
|
||||
|
||||
connectionSnapshots := make(map[uuid.UUID]connectionSnapshot)
|
||||
initialEvents := s.buildInitialConnectionState(trafficManager, connectionSnapshots)
|
||||
err = server.Send(&ConnectionEvents{
|
||||
Events: initialEvents,
|
||||
Reset_: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interval := time.Duration(request.Interval)
|
||||
if interval <= 0 {
|
||||
interval = time.Second
|
||||
}
|
||||
ticker := time.NewTicker(interval)
|
||||
ticker := time.NewTicker(time.Duration(request.Interval))
|
||||
defer ticker.Stop()
|
||||
|
||||
trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager()
|
||||
var (
|
||||
connections = make(map[uuid.UUID]*Connection)
|
||||
outConnections []*Connection
|
||||
)
|
||||
for {
|
||||
outConnections = outConnections[:0]
|
||||
for _, connection := range trafficManager.Connections() {
|
||||
outConnections = append(outConnections, newConnection(connections, connection, false))
|
||||
}
|
||||
for _, connection := range trafficManager.ClosedConnections() {
|
||||
outConnections = append(outConnections, newConnection(connections, connection, true))
|
||||
}
|
||||
err := server.Send(&Connections{Connections: outConnections})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return s.ctx.Err()
|
||||
case <-server.Context().Done():
|
||||
return server.Context().Err()
|
||||
case <-done:
|
||||
return nil
|
||||
|
||||
case event := <-subscription:
|
||||
var pendingEvents []*ConnectionEvent
|
||||
if protoEvent := s.applyConnectionEvent(event, connectionSnapshots); protoEvent != nil {
|
||||
pendingEvents = append(pendingEvents, protoEvent)
|
||||
}
|
||||
drain:
|
||||
for {
|
||||
select {
|
||||
case event = <-subscription:
|
||||
if protoEvent := s.applyConnectionEvent(event, connectionSnapshots); protoEvent != nil {
|
||||
pendingEvents = append(pendingEvents, protoEvent)
|
||||
}
|
||||
default:
|
||||
break drain
|
||||
}
|
||||
}
|
||||
if len(pendingEvents) > 0 {
|
||||
err = server.Send(&ConnectionEvents{Events: pendingEvents})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
protoEvents := s.buildTrafficUpdates(trafficManager, connectionSnapshots)
|
||||
if len(protoEvents) == 0 {
|
||||
continue
|
||||
}
|
||||
err = server.Send(&ConnectionEvents{Events: protoEvents})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type connectionSnapshot struct {
|
||||
uplink int64
|
||||
downlink int64
|
||||
hadTraffic bool
|
||||
}
|
||||
|
||||
func (s *StartedService) buildInitialConnectionState(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent {
|
||||
var events []*ConnectionEvent
|
||||
|
||||
for _, metadata := range manager.Connections() {
|
||||
events = append(events, &ConnectionEvent{
|
||||
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
|
||||
Id: metadata.ID.String(),
|
||||
Connection: buildConnectionProto(metadata),
|
||||
})
|
||||
snapshots[metadata.ID] = connectionSnapshot{
|
||||
uplink: metadata.Upload.Load(),
|
||||
downlink: metadata.Download.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
for _, metadata := range manager.ClosedConnections() {
|
||||
conn := buildConnectionProto(metadata)
|
||||
conn.ClosedAt = metadata.ClosedAt.UnixMilli()
|
||||
events = append(events, &ConnectionEvent{
|
||||
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
|
||||
Id: metadata.ID.String(),
|
||||
Connection: conn,
|
||||
})
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
func (s *StartedService) applyConnectionEvent(event trafficontrol.ConnectionEvent, snapshots map[uuid.UUID]connectionSnapshot) *ConnectionEvent {
|
||||
switch event.Type {
|
||||
case trafficontrol.ConnectionEventNew:
|
||||
if _, exists := snapshots[event.ID]; exists {
|
||||
return nil
|
||||
}
|
||||
snapshots[event.ID] = connectionSnapshot{
|
||||
uplink: event.Metadata.Upload.Load(),
|
||||
downlink: event.Metadata.Download.Load(),
|
||||
}
|
||||
return &ConnectionEvent{
|
||||
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
|
||||
Id: event.ID.String(),
|
||||
Connection: buildConnectionProto(event.Metadata),
|
||||
}
|
||||
case trafficontrol.ConnectionEventClosed:
|
||||
delete(snapshots, event.ID)
|
||||
protoEvent := &ConnectionEvent{
|
||||
Type: ConnectionEventType_CONNECTION_EVENT_CLOSED,
|
||||
Id: event.ID.String(),
|
||||
}
|
||||
closedAt := event.ClosedAt
|
||||
if closedAt.IsZero() && !event.Metadata.ClosedAt.IsZero() {
|
||||
closedAt = event.Metadata.ClosedAt
|
||||
}
|
||||
if closedAt.IsZero() {
|
||||
closedAt = time.Now()
|
||||
}
|
||||
protoEvent.ClosedAt = closedAt.UnixMilli()
|
||||
if event.Metadata.ID != uuid.Nil {
|
||||
conn := buildConnectionProto(event.Metadata)
|
||||
conn.ClosedAt = protoEvent.ClosedAt
|
||||
protoEvent.Connection = conn
|
||||
}
|
||||
return protoEvent
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartedService) buildTrafficUpdates(manager *trafficontrol.Manager, snapshots map[uuid.UUID]connectionSnapshot) []*ConnectionEvent {
|
||||
activeConnections := manager.Connections()
|
||||
activeIndex := make(map[uuid.UUID]*trafficontrol.TrackerMetadata, len(activeConnections))
|
||||
var events []*ConnectionEvent
|
||||
|
||||
for _, metadata := range activeConnections {
|
||||
activeIndex[metadata.ID] = metadata
|
||||
currentUpload := metadata.Upload.Load()
|
||||
currentDownload := metadata.Download.Load()
|
||||
snapshot, exists := snapshots[metadata.ID]
|
||||
if !exists {
|
||||
snapshots[metadata.ID] = connectionSnapshot{
|
||||
uplink: currentUpload,
|
||||
downlink: currentDownload,
|
||||
func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) *Connection {
|
||||
if oldConnection, loaded := connections[metadata.ID]; loaded {
|
||||
if isClosed {
|
||||
if oldConnection.ClosedAt == 0 {
|
||||
oldConnection.Uplink = 0
|
||||
oldConnection.Downlink = 0
|
||||
oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli()
|
||||
}
|
||||
events = append(events, &ConnectionEvent{
|
||||
Type: ConnectionEventType_CONNECTION_EVENT_NEW,
|
||||
Id: metadata.ID.String(),
|
||||
Connection: buildConnectionProto(metadata),
|
||||
})
|
||||
continue
|
||||
}
|
||||
uplinkDelta := currentUpload - snapshot.uplink
|
||||
downlinkDelta := currentDownload - snapshot.downlink
|
||||
if uplinkDelta < 0 || downlinkDelta < 0 {
|
||||
if snapshot.hadTraffic {
|
||||
events = append(events, &ConnectionEvent{
|
||||
Type: ConnectionEventType_CONNECTION_EVENT_UPDATE,
|
||||
Id: metadata.ID.String(),
|
||||
UplinkDelta: 0,
|
||||
DownlinkDelta: 0,
|
||||
})
|
||||
}
|
||||
snapshot.uplink = currentUpload
|
||||
snapshot.downlink = currentDownload
|
||||
snapshot.hadTraffic = false
|
||||
snapshots[metadata.ID] = snapshot
|
||||
continue
|
||||
}
|
||||
if uplinkDelta > 0 || downlinkDelta > 0 {
|
||||
snapshot.uplink = currentUpload
|
||||
snapshot.downlink = currentDownload
|
||||
snapshot.hadTraffic = true
|
||||
snapshots[metadata.ID] = snapshot
|
||||
events = append(events, &ConnectionEvent{
|
||||
Type: ConnectionEventType_CONNECTION_EVENT_UPDATE,
|
||||
Id: metadata.ID.String(),
|
||||
UplinkDelta: uplinkDelta,
|
||||
DownlinkDelta: downlinkDelta,
|
||||
})
|
||||
continue
|
||||
}
|
||||
if snapshot.hadTraffic {
|
||||
snapshot.uplink = currentUpload
|
||||
snapshot.downlink = currentDownload
|
||||
snapshot.hadTraffic = false
|
||||
snapshots[metadata.ID] = snapshot
|
||||
events = append(events, &ConnectionEvent{
|
||||
Type: ConnectionEventType_CONNECTION_EVENT_UPDATE,
|
||||
Id: metadata.ID.String(),
|
||||
UplinkDelta: 0,
|
||||
DownlinkDelta: 0,
|
||||
})
|
||||
return oldConnection
|
||||
}
|
||||
lastUplink := oldConnection.UplinkTotal
|
||||
lastDownlink := oldConnection.DownlinkTotal
|
||||
uplinkTotal := metadata.Upload.Load()
|
||||
downlinkTotal := metadata.Download.Load()
|
||||
oldConnection.Uplink = uplinkTotal - lastUplink
|
||||
oldConnection.Downlink = downlinkTotal - lastDownlink
|
||||
oldConnection.UplinkTotal = uplinkTotal
|
||||
oldConnection.DownlinkTotal = downlinkTotal
|
||||
return oldConnection
|
||||
}
|
||||
|
||||
var closedIndex map[uuid.UUID]*trafficontrol.TrackerMetadata
|
||||
for id := range snapshots {
|
||||
if _, exists := activeIndex[id]; exists {
|
||||
continue
|
||||
}
|
||||
if closedIndex == nil {
|
||||
closedIndex = make(map[uuid.UUID]*trafficontrol.TrackerMetadata)
|
||||
for _, metadata := range manager.ClosedConnections() {
|
||||
closedIndex[metadata.ID] = metadata
|
||||
}
|
||||
}
|
||||
closedAt := time.Now()
|
||||
var conn *Connection
|
||||
if metadata, ok := closedIndex[id]; ok {
|
||||
if !metadata.ClosedAt.IsZero() {
|
||||
closedAt = metadata.ClosedAt
|
||||
}
|
||||
conn = buildConnectionProto(metadata)
|
||||
conn.ClosedAt = closedAt.UnixMilli()
|
||||
}
|
||||
events = append(events, &ConnectionEvent{
|
||||
Type: ConnectionEventType_CONNECTION_EVENT_CLOSED,
|
||||
Id: id.String(),
|
||||
ClosedAt: closedAt.UnixMilli(),
|
||||
Connection: conn,
|
||||
})
|
||||
delete(snapshots, id)
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
|
||||
var rule string
|
||||
if metadata.Rule != nil {
|
||||
rule = metadata.Rule.String()
|
||||
}
|
||||
uplinkTotal := metadata.Upload.Load()
|
||||
downlinkTotal := metadata.Download.Load()
|
||||
uplink := uplinkTotal
|
||||
downlink := downlinkTotal
|
||||
var closedAt int64
|
||||
if !metadata.ClosedAt.IsZero() {
|
||||
closedAt = metadata.ClosedAt.UnixMilli()
|
||||
uplink = 0
|
||||
downlink = 0
|
||||
}
|
||||
var processInfo *ProcessInfo
|
||||
if metadata.Metadata.ProcessInfo != nil {
|
||||
processInfo = &ProcessInfo{
|
||||
@@ -956,7 +747,7 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
|
||||
PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName,
|
||||
}
|
||||
}
|
||||
return &Connection{
|
||||
connection := &Connection{
|
||||
Id: metadata.ID.String(),
|
||||
Inbound: metadata.Metadata.Inbound,
|
||||
InboundType: metadata.Metadata.InboundType,
|
||||
@@ -969,6 +760,9 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
|
||||
User: metadata.Metadata.User,
|
||||
FromOutbound: metadata.Metadata.Outbound,
|
||||
CreatedAt: metadata.CreatedAt.UnixMilli(),
|
||||
ClosedAt: closedAt,
|
||||
Uplink: uplink,
|
||||
Downlink: downlink,
|
||||
UplinkTotal: uplinkTotal,
|
||||
DownlinkTotal: downlinkTotal,
|
||||
Rule: rule,
|
||||
@@ -977,6 +771,8 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
|
||||
ChainList: metadata.Chain,
|
||||
ProcessInfo: processInfo,
|
||||
}
|
||||
connections[metadata.ID] = connection
|
||||
return connection
|
||||
}
|
||||
|
||||
func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConnectionRequest) (*emptypb.Empty, error) {
|
||||
@@ -997,12 +793,7 @@ func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConn
|
||||
}
|
||||
|
||||
func (s *StartedService) CloseAllConnections(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
s.serviceAccess.RLock()
|
||||
nowService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
if nowService != nil && nowService.connectionManager != nil {
|
||||
nowService.connectionManager.CloseAll()
|
||||
}
|
||||
conntrack.Close()
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -78,55 +78,104 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
type ConnectionEventType int32
|
||||
type ConnectionFilter int32
|
||||
|
||||
const (
|
||||
ConnectionEventType_CONNECTION_EVENT_NEW ConnectionEventType = 0
|
||||
ConnectionEventType_CONNECTION_EVENT_UPDATE ConnectionEventType = 1
|
||||
ConnectionEventType_CONNECTION_EVENT_CLOSED ConnectionEventType = 2
|
||||
ConnectionFilter_ALL ConnectionFilter = 0
|
||||
ConnectionFilter_ACTIVE ConnectionFilter = 1
|
||||
ConnectionFilter_CLOSED ConnectionFilter = 2
|
||||
)
|
||||
|
||||
// Enum value maps for ConnectionEventType.
|
||||
// Enum value maps for ConnectionFilter.
|
||||
var (
|
||||
ConnectionEventType_name = map[int32]string{
|
||||
0: "CONNECTION_EVENT_NEW",
|
||||
1: "CONNECTION_EVENT_UPDATE",
|
||||
2: "CONNECTION_EVENT_CLOSED",
|
||||
ConnectionFilter_name = map[int32]string{
|
||||
0: "ALL",
|
||||
1: "ACTIVE",
|
||||
2: "CLOSED",
|
||||
}
|
||||
ConnectionEventType_value = map[string]int32{
|
||||
"CONNECTION_EVENT_NEW": 0,
|
||||
"CONNECTION_EVENT_UPDATE": 1,
|
||||
"CONNECTION_EVENT_CLOSED": 2,
|
||||
ConnectionFilter_value = map[string]int32{
|
||||
"ALL": 0,
|
||||
"ACTIVE": 1,
|
||||
"CLOSED": 2,
|
||||
}
|
||||
)
|
||||
|
||||
func (x ConnectionEventType) Enum() *ConnectionEventType {
|
||||
p := new(ConnectionEventType)
|
||||
func (x ConnectionFilter) Enum() *ConnectionFilter {
|
||||
p := new(ConnectionFilter)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x ConnectionEventType) String() string {
|
||||
func (x ConnectionFilter) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (ConnectionEventType) Descriptor() protoreflect.EnumDescriptor {
|
||||
func (ConnectionFilter) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_daemon_started_service_proto_enumTypes[1].Descriptor()
|
||||
}
|
||||
|
||||
func (ConnectionEventType) Type() protoreflect.EnumType {
|
||||
func (ConnectionFilter) Type() protoreflect.EnumType {
|
||||
return &file_daemon_started_service_proto_enumTypes[1]
|
||||
}
|
||||
|
||||
func (x ConnectionEventType) Number() protoreflect.EnumNumber {
|
||||
func (x ConnectionFilter) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionEventType.Descriptor instead.
|
||||
func (ConnectionEventType) EnumDescriptor() ([]byte, []int) {
|
||||
// Deprecated: Use ConnectionFilter.Descriptor instead.
|
||||
func (ConnectionFilter) EnumDescriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
type ConnectionSortBy int32
|
||||
|
||||
const (
|
||||
ConnectionSortBy_DATE ConnectionSortBy = 0
|
||||
ConnectionSortBy_TRAFFIC ConnectionSortBy = 1
|
||||
ConnectionSortBy_TOTAL_TRAFFIC ConnectionSortBy = 2
|
||||
)
|
||||
|
||||
// Enum value maps for ConnectionSortBy.
|
||||
var (
|
||||
ConnectionSortBy_name = map[int32]string{
|
||||
0: "DATE",
|
||||
1: "TRAFFIC",
|
||||
2: "TOTAL_TRAFFIC",
|
||||
}
|
||||
ConnectionSortBy_value = map[string]int32{
|
||||
"DATE": 0,
|
||||
"TRAFFIC": 1,
|
||||
"TOTAL_TRAFFIC": 2,
|
||||
}
|
||||
)
|
||||
|
||||
func (x ConnectionSortBy) Enum() *ConnectionSortBy {
|
||||
p := new(ConnectionSortBy)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x ConnectionSortBy) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (ConnectionSortBy) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_daemon_started_service_proto_enumTypes[2].Descriptor()
|
||||
}
|
||||
|
||||
func (ConnectionSortBy) Type() protoreflect.EnumType {
|
||||
return &file_daemon_started_service_proto_enumTypes[2]
|
||||
}
|
||||
|
||||
func (x ConnectionSortBy) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionSortBy.Descriptor instead.
|
||||
func (ConnectionSortBy) EnumDescriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
type ServiceStatus_Type int32
|
||||
|
||||
const (
|
||||
@@ -166,11 +215,11 @@ func (x ServiceStatus_Type) String() string {
|
||||
}
|
||||
|
||||
func (ServiceStatus_Type) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_daemon_started_service_proto_enumTypes[2].Descriptor()
|
||||
return file_daemon_started_service_proto_enumTypes[3].Descriptor()
|
||||
}
|
||||
|
||||
func (ServiceStatus_Type) Type() protoreflect.EnumType {
|
||||
return &file_daemon_started_service_proto_enumTypes[2]
|
||||
return &file_daemon_started_service_proto_enumTypes[3]
|
||||
}
|
||||
|
||||
func (x ServiceStatus_Type) Number() protoreflect.EnumNumber {
|
||||
@@ -1065,6 +1114,8 @@ func (x *SetSystemProxyEnabledRequest) GetEnabled() bool {
|
||||
type SubscribeConnectionsRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Interval int64 `protobuf:"varint,1,opt,name=interval,proto3" json:"interval,omitempty"`
|
||||
Filter ConnectionFilter `protobuf:"varint,2,opt,name=filter,proto3,enum=daemon.ConnectionFilter" json:"filter,omitempty"`
|
||||
SortBy ConnectionSortBy `protobuf:"varint,3,opt,name=sortBy,proto3,enum=daemon.ConnectionSortBy" json:"sortBy,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -1106,32 +1157,41 @@ func (x *SubscribeConnectionsRequest) GetInterval() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
type ConnectionEvent struct {
|
||||
func (x *SubscribeConnectionsRequest) GetFilter() ConnectionFilter {
|
||||
if x != nil {
|
||||
return x.Filter
|
||||
}
|
||||
return ConnectionFilter_ALL
|
||||
}
|
||||
|
||||
func (x *SubscribeConnectionsRequest) GetSortBy() ConnectionSortBy {
|
||||
if x != nil {
|
||||
return x.SortBy
|
||||
}
|
||||
return ConnectionSortBy_DATE
|
||||
}
|
||||
|
||||
type Connections struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type ConnectionEventType `protobuf:"varint,1,opt,name=type,proto3,enum=daemon.ConnectionEventType" json:"type,omitempty"`
|
||||
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Connection *Connection `protobuf:"bytes,3,opt,name=connection,proto3" json:"connection,omitempty"`
|
||||
UplinkDelta int64 `protobuf:"varint,4,opt,name=uplinkDelta,proto3" json:"uplinkDelta,omitempty"`
|
||||
DownlinkDelta int64 `protobuf:"varint,5,opt,name=downlinkDelta,proto3" json:"downlinkDelta,omitempty"`
|
||||
ClosedAt int64 `protobuf:"varint,6,opt,name=closedAt,proto3" json:"closedAt,omitempty"`
|
||||
Connections []*Connection `protobuf:"bytes,1,rep,name=connections,proto3" json:"connections,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ConnectionEvent) Reset() {
|
||||
*x = ConnectionEvent{}
|
||||
func (x *Connections) Reset() {
|
||||
*x = Connections{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[17]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ConnectionEvent) String() string {
|
||||
func (x *Connections) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ConnectionEvent) ProtoMessage() {}
|
||||
func (*Connections) ProtoMessage() {}
|
||||
|
||||
func (x *ConnectionEvent) ProtoReflect() protoreflect.Message {
|
||||
func (x *Connections) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[17]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
@@ -1143,105 +1203,18 @@ func (x *ConnectionEvent) ProtoReflect() protoreflect.Message {
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionEvent.ProtoReflect.Descriptor instead.
|
||||
func (*ConnectionEvent) Descriptor() ([]byte, []int) {
|
||||
// Deprecated: Use Connections.ProtoReflect.Descriptor instead.
|
||||
func (*Connections) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{17}
|
||||
}
|
||||
|
||||
func (x *ConnectionEvent) GetType() ConnectionEventType {
|
||||
func (x *Connections) GetConnections() []*Connection {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ConnectionEventType_CONNECTION_EVENT_NEW
|
||||
}
|
||||
|
||||
func (x *ConnectionEvent) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ConnectionEvent) GetConnection() *Connection {
|
||||
if x != nil {
|
||||
return x.Connection
|
||||
return x.Connections
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ConnectionEvent) GetUplinkDelta() int64 {
|
||||
if x != nil {
|
||||
return x.UplinkDelta
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ConnectionEvent) GetDownlinkDelta() int64 {
|
||||
if x != nil {
|
||||
return x.DownlinkDelta
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ConnectionEvent) GetClosedAt() int64 {
|
||||
if x != nil {
|
||||
return x.ClosedAt
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ConnectionEvents struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Events []*ConnectionEvent `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"`
|
||||
Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ConnectionEvents) Reset() {
|
||||
*x = ConnectionEvents{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[18]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ConnectionEvents) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ConnectionEvents) ProtoMessage() {}
|
||||
|
||||
func (x *ConnectionEvents) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[18]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionEvents.ProtoReflect.Descriptor instead.
|
||||
func (*ConnectionEvents) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{18}
|
||||
}
|
||||
|
||||
func (x *ConnectionEvents) GetEvents() []*ConnectionEvent {
|
||||
if x != nil {
|
||||
return x.Events
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ConnectionEvents) GetReset_() bool {
|
||||
if x != nil {
|
||||
return x.Reset_
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
@@ -1272,7 +1245,7 @@ type Connection struct {
|
||||
|
||||
func (x *Connection) Reset() {
|
||||
*x = Connection{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[19]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[18]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1284,7 +1257,7 @@ func (x *Connection) String() string {
|
||||
func (*Connection) ProtoMessage() {}
|
||||
|
||||
func (x *Connection) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[19]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[18]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1297,7 +1270,7 @@ func (x *Connection) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Connection.ProtoReflect.Descriptor instead.
|
||||
func (*Connection) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{19}
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{18}
|
||||
}
|
||||
|
||||
func (x *Connection) GetId() string {
|
||||
@@ -1467,7 +1440,7 @@ type ProcessInfo struct {
|
||||
|
||||
func (x *ProcessInfo) Reset() {
|
||||
*x = ProcessInfo{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[20]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[19]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1479,7 +1452,7 @@ func (x *ProcessInfo) String() string {
|
||||
func (*ProcessInfo) ProtoMessage() {}
|
||||
|
||||
func (x *ProcessInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[20]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[19]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1492,7 +1465,7 @@ func (x *ProcessInfo) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ProcessInfo.ProtoReflect.Descriptor instead.
|
||||
func (*ProcessInfo) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{20}
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{19}
|
||||
}
|
||||
|
||||
func (x *ProcessInfo) GetProcessId() uint32 {
|
||||
@@ -1539,7 +1512,7 @@ type CloseConnectionRequest struct {
|
||||
|
||||
func (x *CloseConnectionRequest) Reset() {
|
||||
*x = CloseConnectionRequest{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[21]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[20]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1551,7 +1524,7 @@ func (x *CloseConnectionRequest) String() string {
|
||||
func (*CloseConnectionRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[21]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[20]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1564,7 +1537,7 @@ func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CloseConnectionRequest) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{21}
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{20}
|
||||
}
|
||||
|
||||
func (x *CloseConnectionRequest) GetId() string {
|
||||
@@ -1583,7 +1556,7 @@ type DeprecatedWarnings struct {
|
||||
|
||||
func (x *DeprecatedWarnings) Reset() {
|
||||
*x = DeprecatedWarnings{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[22]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[21]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1595,7 +1568,7 @@ func (x *DeprecatedWarnings) String() string {
|
||||
func (*DeprecatedWarnings) ProtoMessage() {}
|
||||
|
||||
func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[22]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[21]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1608,7 +1581,7 @@ func (x *DeprecatedWarnings) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use DeprecatedWarnings.ProtoReflect.Descriptor instead.
|
||||
func (*DeprecatedWarnings) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{22}
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{21}
|
||||
}
|
||||
|
||||
func (x *DeprecatedWarnings) GetWarnings() []*DeprecatedWarning {
|
||||
@@ -1629,7 +1602,7 @@ type DeprecatedWarning struct {
|
||||
|
||||
func (x *DeprecatedWarning) Reset() {
|
||||
*x = DeprecatedWarning{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[23]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[22]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1641,7 +1614,7 @@ func (x *DeprecatedWarning) String() string {
|
||||
func (*DeprecatedWarning) ProtoMessage() {}
|
||||
|
||||
func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[23]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[22]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1654,7 +1627,7 @@ func (x *DeprecatedWarning) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use DeprecatedWarning.ProtoReflect.Descriptor instead.
|
||||
func (*DeprecatedWarning) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{23}
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{22}
|
||||
}
|
||||
|
||||
func (x *DeprecatedWarning) GetMessage() string {
|
||||
@@ -1687,7 +1660,7 @@ type StartedAt struct {
|
||||
|
||||
func (x *StartedAt) Reset() {
|
||||
*x = StartedAt{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[24]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[23]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1699,7 +1672,7 @@ func (x *StartedAt) String() string {
|
||||
func (*StartedAt) ProtoMessage() {}
|
||||
|
||||
func (x *StartedAt) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[24]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[23]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1712,7 +1685,7 @@ func (x *StartedAt) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use StartedAt.ProtoReflect.Descriptor instead.
|
||||
func (*StartedAt) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{24}
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{23}
|
||||
}
|
||||
|
||||
func (x *StartedAt) GetStartedAt() int64 {
|
||||
@@ -1732,7 +1705,7 @@ type Log_Message struct {
|
||||
|
||||
func (x *Log_Message) Reset() {
|
||||
*x = Log_Message{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[25]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[24]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1744,7 +1717,7 @@ func (x *Log_Message) String() string {
|
||||
func (*Log_Message) ProtoMessage() {}
|
||||
|
||||
func (x *Log_Message) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[25]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[24]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1845,21 +1818,13 @@ const file_daemon_started_service_proto_rawDesc = "" +
|
||||
"\tavailable\x18\x01 \x01(\bR\tavailable\x12\x18\n" +
|
||||
"\aenabled\x18\x02 \x01(\bR\aenabled\"8\n" +
|
||||
"\x1cSetSystemProxyEnabledRequest\x12\x18\n" +
|
||||
"\aenabled\x18\x01 \x01(\bR\aenabled\"9\n" +
|
||||
"\aenabled\x18\x01 \x01(\bR\aenabled\"\x9d\x01\n" +
|
||||
"\x1bSubscribeConnectionsRequest\x12\x1a\n" +
|
||||
"\binterval\x18\x01 \x01(\x03R\binterval\"\xea\x01\n" +
|
||||
"\x0fConnectionEvent\x12/\n" +
|
||||
"\x04type\x18\x01 \x01(\x0e2\x1b.daemon.ConnectionEventTypeR\x04type\x12\x0e\n" +
|
||||
"\x02id\x18\x02 \x01(\tR\x02id\x122\n" +
|
||||
"\n" +
|
||||
"connection\x18\x03 \x01(\v2\x12.daemon.ConnectionR\n" +
|
||||
"connection\x12 \n" +
|
||||
"\vuplinkDelta\x18\x04 \x01(\x03R\vuplinkDelta\x12$\n" +
|
||||
"\rdownlinkDelta\x18\x05 \x01(\x03R\rdownlinkDelta\x12\x1a\n" +
|
||||
"\bclosedAt\x18\x06 \x01(\x03R\bclosedAt\"Y\n" +
|
||||
"\x10ConnectionEvents\x12/\n" +
|
||||
"\x06events\x18\x01 \x03(\v2\x17.daemon.ConnectionEventR\x06events\x12\x14\n" +
|
||||
"\x05reset\x18\x02 \x01(\bR\x05reset\"\x95\x05\n" +
|
||||
"\binterval\x18\x01 \x01(\x03R\binterval\x120\n" +
|
||||
"\x06filter\x18\x02 \x01(\x0e2\x18.daemon.ConnectionFilterR\x06filter\x120\n" +
|
||||
"\x06sortBy\x18\x03 \x01(\x0e2\x18.daemon.ConnectionSortByR\x06sortBy\"C\n" +
|
||||
"\vConnections\x124\n" +
|
||||
"\vconnections\x18\x01 \x03(\v2\x12.daemon.ConnectionR\vconnections\"\x95\x05\n" +
|
||||
"\n" +
|
||||
"Connection\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" +
|
||||
@@ -1908,11 +1873,17 @@ const file_daemon_started_service_proto_rawDesc = "" +
|
||||
"\x04WARN\x10\x03\x12\b\n" +
|
||||
"\x04INFO\x10\x04\x12\t\n" +
|
||||
"\x05DEBUG\x10\x05\x12\t\n" +
|
||||
"\x05TRACE\x10\x06*i\n" +
|
||||
"\x13ConnectionEventType\x12\x18\n" +
|
||||
"\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" +
|
||||
"\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" +
|
||||
"\x17CONNECTION_EVENT_CLOSED\x10\x022\xe5\v\n" +
|
||||
"\x05TRACE\x10\x06*3\n" +
|
||||
"\x10ConnectionFilter\x12\a\n" +
|
||||
"\x03ALL\x10\x00\x12\n" +
|
||||
"\n" +
|
||||
"\x06ACTIVE\x10\x01\x12\n" +
|
||||
"\n" +
|
||||
"\x06CLOSED\x10\x02*<\n" +
|
||||
"\x10ConnectionSortBy\x12\b\n" +
|
||||
"\x04DATE\x10\x00\x12\v\n" +
|
||||
"\aTRAFFIC\x10\x01\x12\x11\n" +
|
||||
"\rTOTAL_TRAFFIC\x10\x022\xe0\v\n" +
|
||||
"\x0eStartedService\x12=\n" +
|
||||
"\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" +
|
||||
"\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" +
|
||||
@@ -1929,8 +1900,8 @@ const file_daemon_started_service_proto_rawDesc = "" +
|
||||
"\x0eSelectOutbound\x12\x1d.daemon.SelectOutboundRequest\x1a\x16.google.protobuf.Empty\"\x00\x12I\n" +
|
||||
"\x0eSetGroupExpand\x12\x1d.daemon.SetGroupExpandRequest\x1a\x16.google.protobuf.Empty\"\x00\x12K\n" +
|
||||
"\x14GetSystemProxyStatus\x12\x16.google.protobuf.Empty\x1a\x19.daemon.SystemProxyStatus\"\x00\x12W\n" +
|
||||
"\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12Y\n" +
|
||||
"\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x18.daemon.ConnectionEvents\"\x000\x01\x12K\n" +
|
||||
"\x15SetSystemProxyEnabled\x12$.daemon.SetSystemProxyEnabledRequest\x1a\x16.google.protobuf.Empty\"\x00\x12T\n" +
|
||||
"\x14SubscribeConnections\x12#.daemon.SubscribeConnectionsRequest\x1a\x13.daemon.Connections\"\x000\x01\x12K\n" +
|
||||
"\x0fCloseConnection\x12\x1e.daemon.CloseConnectionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" +
|
||||
"\x13CloseAllConnections\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12M\n" +
|
||||
"\x15GetDeprecatedWarnings\x12\x16.google.protobuf.Empty\x1a\x1a.daemon.DeprecatedWarnings\"\x00\x12;\n" +
|
||||
@@ -1949,31 +1920,31 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var (
|
||||
file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
|
||||
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
|
||||
file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
|
||||
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
|
||||
file_daemon_started_service_proto_goTypes = []any{
|
||||
(LogLevel)(0), // 0: daemon.LogLevel
|
||||
(ConnectionEventType)(0), // 1: daemon.ConnectionEventType
|
||||
(ServiceStatus_Type)(0), // 2: daemon.ServiceStatus.Type
|
||||
(*ServiceStatus)(nil), // 3: daemon.ServiceStatus
|
||||
(*ReloadServiceRequest)(nil), // 4: daemon.ReloadServiceRequest
|
||||
(*SubscribeStatusRequest)(nil), // 5: daemon.SubscribeStatusRequest
|
||||
(*Log)(nil), // 6: daemon.Log
|
||||
(*DefaultLogLevel)(nil), // 7: daemon.DefaultLogLevel
|
||||
(*Status)(nil), // 8: daemon.Status
|
||||
(*Groups)(nil), // 9: daemon.Groups
|
||||
(*Group)(nil), // 10: daemon.Group
|
||||
(*GroupItem)(nil), // 11: daemon.GroupItem
|
||||
(*URLTestRequest)(nil), // 12: daemon.URLTestRequest
|
||||
(*SelectOutboundRequest)(nil), // 13: daemon.SelectOutboundRequest
|
||||
(*SetGroupExpandRequest)(nil), // 14: daemon.SetGroupExpandRequest
|
||||
(*ClashMode)(nil), // 15: daemon.ClashMode
|
||||
(*ClashModeStatus)(nil), // 16: daemon.ClashModeStatus
|
||||
(*SystemProxyStatus)(nil), // 17: daemon.SystemProxyStatus
|
||||
(*SetSystemProxyEnabledRequest)(nil), // 18: daemon.SetSystemProxyEnabledRequest
|
||||
(*SubscribeConnectionsRequest)(nil), // 19: daemon.SubscribeConnectionsRequest
|
||||
(*ConnectionEvent)(nil), // 20: daemon.ConnectionEvent
|
||||
(*ConnectionEvents)(nil), // 21: daemon.ConnectionEvents
|
||||
(ConnectionFilter)(0), // 1: daemon.ConnectionFilter
|
||||
(ConnectionSortBy)(0), // 2: daemon.ConnectionSortBy
|
||||
(ServiceStatus_Type)(0), // 3: daemon.ServiceStatus.Type
|
||||
(*ServiceStatus)(nil), // 4: daemon.ServiceStatus
|
||||
(*ReloadServiceRequest)(nil), // 5: daemon.ReloadServiceRequest
|
||||
(*SubscribeStatusRequest)(nil), // 6: daemon.SubscribeStatusRequest
|
||||
(*Log)(nil), // 7: daemon.Log
|
||||
(*DefaultLogLevel)(nil), // 8: daemon.DefaultLogLevel
|
||||
(*Status)(nil), // 9: daemon.Status
|
||||
(*Groups)(nil), // 10: daemon.Groups
|
||||
(*Group)(nil), // 11: daemon.Group
|
||||
(*GroupItem)(nil), // 12: daemon.GroupItem
|
||||
(*URLTestRequest)(nil), // 13: daemon.URLTestRequest
|
||||
(*SelectOutboundRequest)(nil), // 14: daemon.SelectOutboundRequest
|
||||
(*SetGroupExpandRequest)(nil), // 15: daemon.SetGroupExpandRequest
|
||||
(*ClashMode)(nil), // 16: daemon.ClashMode
|
||||
(*ClashModeStatus)(nil), // 17: daemon.ClashModeStatus
|
||||
(*SystemProxyStatus)(nil), // 18: daemon.SystemProxyStatus
|
||||
(*SetSystemProxyEnabledRequest)(nil), // 19: daemon.SetSystemProxyEnabledRequest
|
||||
(*SubscribeConnectionsRequest)(nil), // 20: daemon.SubscribeConnectionsRequest
|
||||
(*Connections)(nil), // 21: daemon.Connections
|
||||
(*Connection)(nil), // 22: daemon.Connection
|
||||
(*ProcessInfo)(nil), // 23: daemon.ProcessInfo
|
||||
(*CloseConnectionRequest)(nil), // 24: daemon.CloseConnectionRequest
|
||||
@@ -1986,14 +1957,14 @@ var (
|
||||
)
|
||||
|
||||
var file_daemon_started_service_proto_depIdxs = []int32{
|
||||
2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type
|
||||
3, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type
|
||||
28, // 1: daemon.Log.messages:type_name -> daemon.Log.Message
|
||||
0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel
|
||||
10, // 3: daemon.Groups.group:type_name -> daemon.Group
|
||||
11, // 4: daemon.Group.items:type_name -> daemon.GroupItem
|
||||
1, // 5: daemon.ConnectionEvent.type:type_name -> daemon.ConnectionEventType
|
||||
22, // 6: daemon.ConnectionEvent.connection:type_name -> daemon.Connection
|
||||
20, // 7: daemon.ConnectionEvents.events:type_name -> daemon.ConnectionEvent
|
||||
11, // 3: daemon.Groups.group:type_name -> daemon.Group
|
||||
12, // 4: daemon.Group.items:type_name -> daemon.GroupItem
|
||||
1, // 5: daemon.SubscribeConnectionsRequest.filter:type_name -> daemon.ConnectionFilter
|
||||
2, // 6: daemon.SubscribeConnectionsRequest.sortBy:type_name -> daemon.ConnectionSortBy
|
||||
22, // 7: daemon.Connections.connections:type_name -> daemon.Connection
|
||||
23, // 8: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo
|
||||
26, // 9: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning
|
||||
0, // 10: daemon.Log.Message.level:type_name -> daemon.LogLevel
|
||||
@@ -2003,38 +1974,38 @@ var file_daemon_started_service_proto_depIdxs = []int32{
|
||||
29, // 14: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
|
||||
29, // 15: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
|
||||
29, // 16: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty
|
||||
5, // 17: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
|
||||
6, // 17: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
|
||||
29, // 18: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
|
||||
29, // 19: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
|
||||
29, // 20: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
|
||||
15, // 21: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
|
||||
12, // 22: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
|
||||
13, // 23: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
|
||||
14, // 24: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
|
||||
16, // 21: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
|
||||
13, // 22: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
|
||||
14, // 23: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
|
||||
15, // 24: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
|
||||
29, // 25: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
|
||||
18, // 26: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
|
||||
19, // 27: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
|
||||
19, // 26: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
|
||||
20, // 27: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
|
||||
24, // 28: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
|
||||
29, // 29: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
|
||||
29, // 30: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
|
||||
29, // 31: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty
|
||||
29, // 32: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
|
||||
29, // 33: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
|
||||
3, // 34: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
|
||||
6, // 35: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
|
||||
7, // 36: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
|
||||
4, // 34: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
|
||||
7, // 35: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
|
||||
8, // 36: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
|
||||
29, // 37: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty
|
||||
8, // 38: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
|
||||
9, // 39: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
|
||||
16, // 40: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
|
||||
15, // 41: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
|
||||
9, // 38: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
|
||||
10, // 39: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
|
||||
17, // 40: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
|
||||
16, // 41: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
|
||||
29, // 42: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
|
||||
29, // 43: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
|
||||
29, // 44: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
|
||||
29, // 45: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
|
||||
17, // 46: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
|
||||
18, // 46: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
|
||||
29, // 47: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
|
||||
21, // 48: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents
|
||||
21, // 48: daemon.StartedService.SubscribeConnections:output_type -> daemon.Connections
|
||||
29, // 49: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
|
||||
29, // 50: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
|
||||
25, // 51: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
|
||||
@@ -2056,8 +2027,8 @@ func file_daemon_started_service_proto_init() {
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)),
|
||||
NumEnums: 3,
|
||||
NumMessages: 26,
|
||||
NumEnums: 4,
|
||||
NumMessages: 25,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ service StartedService {
|
||||
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
|
||||
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
|
||||
|
||||
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
|
||||
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream Connections) {}
|
||||
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
|
||||
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
|
||||
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
|
||||
@@ -143,26 +143,24 @@ message SetSystemProxyEnabledRequest {
|
||||
|
||||
message SubscribeConnectionsRequest {
|
||||
int64 interval = 1;
|
||||
ConnectionFilter filter = 2;
|
||||
ConnectionSortBy sortBy = 3;
|
||||
}
|
||||
|
||||
enum ConnectionEventType {
|
||||
CONNECTION_EVENT_NEW = 0;
|
||||
CONNECTION_EVENT_UPDATE = 1;
|
||||
CONNECTION_EVENT_CLOSED = 2;
|
||||
enum ConnectionFilter {
|
||||
ALL = 0;
|
||||
ACTIVE = 1;
|
||||
CLOSED = 2;
|
||||
}
|
||||
|
||||
message ConnectionEvent {
|
||||
ConnectionEventType type = 1;
|
||||
string id = 2;
|
||||
Connection connection = 3;
|
||||
int64 uplinkDelta = 4;
|
||||
int64 downlinkDelta = 5;
|
||||
int64 closedAt = 6;
|
||||
enum ConnectionSortBy {
|
||||
DATE = 0;
|
||||
TRAFFIC = 1;
|
||||
TOTAL_TRAFFIC = 2;
|
||||
}
|
||||
|
||||
message ConnectionEvents {
|
||||
repeated ConnectionEvent events = 1;
|
||||
bool reset = 2;
|
||||
message Connections {
|
||||
repeated Connection connections = 1;
|
||||
}
|
||||
|
||||
message Connection {
|
||||
|
||||
@@ -58,7 +58,7 @@ type StartedServiceClient interface {
|
||||
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
|
||||
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error)
|
||||
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Connections], error)
|
||||
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
|
||||
@@ -278,13 +278,13 @@ func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *Se
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
|
||||
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Connections], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[SubscribeConnectionsRequest, ConnectionEvents]{ClientStream: stream}
|
||||
x := &grpc.GenericClientStream[SubscribeConnectionsRequest, Connections]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -295,7 +295,7 @@ func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *Sub
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_SubscribeConnectionsClient = grpc.ServerStreamingClient[ConnectionEvents]
|
||||
type StartedService_SubscribeConnectionsClient = grpc.ServerStreamingClient[Connections]
|
||||
|
||||
func (c *startedServiceClient) CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
@@ -357,7 +357,7 @@ type StartedServiceServer interface {
|
||||
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
|
||||
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
|
||||
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
|
||||
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
|
||||
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[Connections]) error
|
||||
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
|
||||
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
|
||||
@@ -373,87 +373,87 @@ type StartedServiceServer interface {
|
||||
type UnimplementedStartedServiceServer struct{}
|
||||
|
||||
func (UnimplementedStartedServiceServer) StopService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method StopService not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method StopService not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) ReloadService(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ReloadService not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReloadService not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeServiceStatus(*emptypb.Empty, grpc.ServerStreamingServer[ServiceStatus]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeServiceStatus not implemented")
|
||||
return status.Errorf(codes.Unimplemented, "method SubscribeServiceStatus not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeLog(*emptypb.Empty, grpc.ServerStreamingServer[Log]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeLog not implemented")
|
||||
return status.Errorf(codes.Unimplemented, "method SubscribeLog not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) GetDefaultLogLevel(context.Context, *emptypb.Empty) (*DefaultLogLevel, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetDefaultLogLevel not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetDefaultLogLevel not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) ClearLogs(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ClearLogs not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ClearLogs not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeStatus(*SubscribeStatusRequest, grpc.ServerStreamingServer[Status]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeStatus not implemented")
|
||||
return status.Errorf(codes.Unimplemented, "method SubscribeStatus not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeGroups(*emptypb.Empty, grpc.ServerStreamingServer[Groups]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeGroups not implemented")
|
||||
return status.Errorf(codes.Unimplemented, "method SubscribeGroups not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) GetClashModeStatus(context.Context, *emptypb.Empty) (*ClashModeStatus, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetClashModeStatus not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetClashModeStatus not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeClashMode(*emptypb.Empty, grpc.ServerStreamingServer[ClashMode]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeClashMode not implemented")
|
||||
return status.Errorf(codes.Unimplemented, "method SubscribeClashMode not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SetClashMode(context.Context, *ClashMode) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetClashMode not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetClashMode not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) URLTest(context.Context, *URLTestRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method URLTest not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method URLTest not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SelectOutbound(context.Context, *SelectOutboundRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SelectOutbound not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SelectOutbound not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetGroupExpand not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetGroupExpand not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetSystemProxyStatus not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetSystemProxyStatus not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
|
||||
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[Connections]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method SubscribeConnections not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CloseConnection not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CloseConnection not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CloseAllConnections not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CloseAllConnections not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetDeprecatedWarnings not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetDeprecatedWarnings not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetStartedAt not implemented")
|
||||
}
|
||||
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
|
||||
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
|
||||
@@ -466,7 +466,7 @@ type UnsafeStartedServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterStartedServiceServer(s grpc.ServiceRegistrar, srv StartedServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedStartedServiceServer was
|
||||
// If the following call pancis, it indicates UnimplementedStartedServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@@ -734,11 +734,11 @@ func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.S
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, ConnectionEvents]{ServerStream: stream})
|
||||
return srv.(StartedServiceServer).SubscribeConnections(m, &grpc.GenericServerStream[SubscribeConnectionsRequest, Connections]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_SubscribeConnectionsServer = grpc.ServerStreamingServer[ConnectionEvents]
|
||||
type StartedService_SubscribeConnectionsServer = grpc.ServerStreamingServer[Connections]
|
||||
|
||||
func _StartedService_CloseConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CloseConnectionRequest)
|
||||
|
||||
8
debug.go
8
debug.go
@@ -3,11 +3,11 @@ package box
|
||||
import (
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/sagernet/sing-box/common/conntrack"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func applyDebugOptions(options option.DebugOptions) error {
|
||||
func applyDebugOptions(options option.DebugOptions) {
|
||||
applyDebugListenOption(options)
|
||||
if options.GCPercent != nil {
|
||||
debug.SetGCPercent(*options.GCPercent)
|
||||
@@ -26,9 +26,9 @@ func applyDebugOptions(options option.DebugOptions) error {
|
||||
}
|
||||
if options.MemoryLimit.Value() != 0 {
|
||||
debug.SetMemoryLimit(int64(float64(options.MemoryLimit.Value()) / 1.5))
|
||||
conntrack.MemoryLimit = options.MemoryLimit.Value()
|
||||
}
|
||||
if options.OOMKiller != nil {
|
||||
return E.New("legacy oom_killer in debug options is removed, use oom-killer service instead")
|
||||
conntrack.KillerEnabled = *options.OOMKiller
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -144,11 +144,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
if c.cache != nil {
|
||||
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
|
||||
if loaded {
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
<-cond
|
||||
} else {
|
||||
defer func() {
|
||||
c.cacheLock.Delete(question)
|
||||
@@ -158,11 +154,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
} else if c.transportCache != nil {
|
||||
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
|
||||
if loaded {
|
||||
select {
|
||||
case <-cond:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
<-cond
|
||||
} else {
|
||||
defer func() {
|
||||
c.transportCacheLock.Delete(question)
|
||||
@@ -240,10 +232,8 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
if responseChecker != nil {
|
||||
var rejected bool
|
||||
// TODO: add accept_any rule and support to check response instead of addresses
|
||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||
if response.Rcode != dns.RcodeSuccess || len(response.Answer) == 0 {
|
||||
rejected = true
|
||||
} else if len(response.Answer) == 0 {
|
||||
rejected = !responseChecker(nil)
|
||||
} else {
|
||||
rejected = !responseChecker(MessageToAddresses(response))
|
||||
}
|
||||
@@ -324,20 +314,16 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
||||
} else {
|
||||
strategy = options.Strategy
|
||||
}
|
||||
lookupOptions := options
|
||||
if options.LookupStrategy != C.DomainStrategyAsIS {
|
||||
lookupOptions.Strategy = strategy
|
||||
}
|
||||
if strategy == C.DomainStrategyIPv4Only {
|
||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker)
|
||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
|
||||
} else if strategy == C.DomainStrategyIPv6Only {
|
||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker)
|
||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
|
||||
}
|
||||
var response4 []netip.Addr
|
||||
var response6 []netip.Addr
|
||||
var group task.Group
|
||||
group.Append("exchange4", func(ctx context.Context) error {
|
||||
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker)
|
||||
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -345,7 +331,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
||||
return nil
|
||||
})
|
||||
group.Append("exchange6", func(ctx context.Context) error {
|
||||
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker)
|
||||
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -195,16 +195,7 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
|
||||
}
|
||||
}
|
||||
}
|
||||
transport := r.transport.Default()
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = legacyTransport.LegacyStrategy()
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
}
|
||||
}
|
||||
return transport, nil, -1
|
||||
return r.transport.Default(), nil, -1
|
||||
}
|
||||
|
||||
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
|
||||
@@ -281,7 +272,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
return action.Response(message), nil
|
||||
}
|
||||
}
|
||||
responseCheck := addressLimitResponseCheck(rule, metadata)
|
||||
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||
if rule != nil && rule.WithAddressLimit() {
|
||||
responseCheck = func(responseAddrs []netip.Addr) bool {
|
||||
metadata.DestinationAddresses = responseAddrs
|
||||
return rule.MatchAddressLimit(metadata)
|
||||
}
|
||||
}
|
||||
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
@@ -354,7 +351,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
transport := options.Transport
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = legacyTransport.LegacyStrategy()
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
@@ -380,11 +377,9 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
case *R.RuleActionReject:
|
||||
return nil, &R.RejectedError{Cause: action.Error(ctx)}
|
||||
case *R.RuleActionPredefined:
|
||||
responseAddrs = nil
|
||||
if action.Rcode != mDNS.RcodeSuccess {
|
||||
err = RcodeError(action.Rcode)
|
||||
} else {
|
||||
err = nil
|
||||
for _, answer := range action.Answer {
|
||||
switch record := answer.(type) {
|
||||
case *mDNS.A:
|
||||
@@ -397,7 +392,13 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
goto response
|
||||
}
|
||||
}
|
||||
responseCheck := addressLimitResponseCheck(rule, metadata)
|
||||
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||
if rule != nil && rule.WithAddressLimit() {
|
||||
responseCheck = func(responseAddrs []netip.Addr) bool {
|
||||
metadata.DestinationAddresses = responseAddrs
|
||||
return rule.MatchAddressLimit(metadata)
|
||||
}
|
||||
}
|
||||
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
@@ -425,18 +426,6 @@ func isAddressQuery(message *mDNS.Msg) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(responseAddrs []netip.Addr) bool {
|
||||
if rule == nil || !rule.WithAddressLimit() {
|
||||
return nil
|
||||
}
|
||||
responseMetadata := *metadata
|
||||
return func(responseAddrs []netip.Addr) bool {
|
||||
checkMetadata := responseMetadata
|
||||
checkMetadata.DestinationAddresses = responseAddrs
|
||||
return rule.MatchAddressLimit(&checkMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) ClearCache() {
|
||||
r.client.ClearCache()
|
||||
if r.platformInterface != nil {
|
||||
|
||||
@@ -4,9 +4,6 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConnectorCallbacks[T any] struct {
|
||||
@@ -19,11 +16,10 @@ type Connector[T any] struct {
|
||||
dial func(ctx context.Context) (T, error)
|
||||
callbacks ConnectorCallbacks[T]
|
||||
|
||||
access sync.Mutex
|
||||
connection T
|
||||
hasConnection bool
|
||||
connectionCancel context.CancelFunc
|
||||
connecting chan struct{}
|
||||
access sync.Mutex
|
||||
connection T
|
||||
hasConnection bool
|
||||
connecting chan struct{}
|
||||
|
||||
closeCtx context.Context
|
||||
closed bool
|
||||
@@ -51,10 +47,6 @@ func NewSingleflightConnector(closeCtx context.Context, dial func(context.Contex
|
||||
})
|
||||
}
|
||||
|
||||
type contextKeyConnecting struct{}
|
||||
|
||||
var errRecursiveConnectorDial = E.New("recursive connector dial")
|
||||
|
||||
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
|
||||
var zero T
|
||||
for {
|
||||
@@ -72,14 +64,6 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
|
||||
}
|
||||
|
||||
c.hasConnection = false
|
||||
if c.connectionCancel != nil {
|
||||
c.connectionCancel()
|
||||
c.connectionCancel = nil
|
||||
}
|
||||
if isRecursiveConnectorDial(ctx, c) {
|
||||
c.access.Unlock()
|
||||
return zero, errRecursiveConnectorDial
|
||||
}
|
||||
|
||||
if c.connecting != nil {
|
||||
connecting := c.connecting
|
||||
@@ -95,16 +79,10 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
c.access.Unlock()
|
||||
return zero, err
|
||||
}
|
||||
|
||||
c.connecting = make(chan struct{})
|
||||
c.access.Unlock()
|
||||
|
||||
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
|
||||
connection, cancel, err := c.dialWithCancellation(dialContext)
|
||||
connection, err := c.dialWithCancellation(ctx)
|
||||
|
||||
c.access.Lock()
|
||||
close(c.connecting)
|
||||
@@ -116,21 +94,13 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
|
||||
}
|
||||
|
||||
if c.closed {
|
||||
cancel()
|
||||
c.callbacks.Close(connection)
|
||||
c.access.Unlock()
|
||||
return zero, ErrTransportClosed
|
||||
}
|
||||
if err = ctx.Err(); err != nil {
|
||||
cancel()
|
||||
c.callbacks.Close(connection)
|
||||
c.access.Unlock()
|
||||
return zero, err
|
||||
}
|
||||
|
||||
c.connection = connection
|
||||
c.hasConnection = true
|
||||
c.connectionCancel = cancel
|
||||
result := c.connection
|
||||
c.access.Unlock()
|
||||
|
||||
@@ -138,63 +108,19 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T]) bool {
|
||||
dialConnector, loaded := ctx.Value(contextKeyConnecting{}).(*Connector[T])
|
||||
return loaded && dialConnector == connector
|
||||
}
|
||||
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, error) {
|
||||
dialCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) {
|
||||
var zero T
|
||||
if err := ctx.Err(); err != nil {
|
||||
return zero, nil, err
|
||||
}
|
||||
connCtx, cancel := context.WithCancel(c.closeCtx)
|
||||
|
||||
var (
|
||||
stateAccess sync.Mutex
|
||||
dialComplete bool
|
||||
)
|
||||
stopCancel := context.AfterFunc(ctx, func() {
|
||||
stateAccess.Lock()
|
||||
if !dialComplete {
|
||||
go func() {
|
||||
select {
|
||||
case <-c.closeCtx.Done():
|
||||
cancel()
|
||||
case <-dialCtx.Done():
|
||||
}
|
||||
stateAccess.Unlock()
|
||||
})
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
stateAccess.Lock()
|
||||
dialComplete = true
|
||||
stateAccess.Unlock()
|
||||
stopCancel()
|
||||
cancel()
|
||||
return zero, nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
}()
|
||||
|
||||
connection, err := c.dial(valueContext{connCtx, ctx})
|
||||
stateAccess.Lock()
|
||||
dialComplete = true
|
||||
stateAccess.Unlock()
|
||||
stopCancel()
|
||||
if err != nil {
|
||||
cancel()
|
||||
return zero, nil, err
|
||||
}
|
||||
return connection, cancel, nil
|
||||
}
|
||||
|
||||
type valueContext struct {
|
||||
context.Context
|
||||
parent context.Context
|
||||
}
|
||||
|
||||
func (v valueContext) Value(key any) any {
|
||||
return v.parent.Value(key)
|
||||
}
|
||||
|
||||
func (v valueContext) Deadline() (time.Time, bool) {
|
||||
return v.parent.Deadline()
|
||||
return c.dial(dialCtx)
|
||||
}
|
||||
|
||||
func (c *Connector[T]) Close() error {
|
||||
@@ -206,10 +132,6 @@ func (c *Connector[T]) Close() error {
|
||||
}
|
||||
c.closed = true
|
||||
|
||||
if c.connectionCancel != nil {
|
||||
c.connectionCancel()
|
||||
c.connectionCancel = nil
|
||||
}
|
||||
if c.hasConnection {
|
||||
c.callbacks.Close(c.connection)
|
||||
c.hasConnection = false
|
||||
@@ -222,10 +144,6 @@ func (c *Connector[T]) Reset() {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
|
||||
if c.connectionCancel != nil {
|
||||
c.connectionCancel()
|
||||
c.connectionCancel = nil
|
||||
}
|
||||
if c.hasConnection {
|
||||
c.callbacks.Reset(c.connection)
|
||||
c.hasConnection = false
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testConnectorConnection struct{}
|
||||
|
||||
func TestConnectorRecursiveGetFailsFast(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
dialCount atomic.Int32
|
||||
closeCount atomic.Int32
|
||||
connector *Connector[*testConnectorConnection]
|
||||
)
|
||||
|
||||
dial := func(ctx context.Context) (*testConnectorConnection, error) {
|
||||
dialCount.Add(1)
|
||||
_, err := connector.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &testConnectorConnection{}, nil
|
||||
}
|
||||
|
||||
connector = NewConnector(context.Background(), dial, ConnectorCallbacks[*testConnectorConnection]{
|
||||
IsClosed: func(connection *testConnectorConnection) bool {
|
||||
return false
|
||||
},
|
||||
Close: func(connection *testConnectorConnection) {
|
||||
closeCount.Add(1)
|
||||
},
|
||||
Reset: func(connection *testConnectorConnection) {
|
||||
closeCount.Add(1)
|
||||
},
|
||||
})
|
||||
|
||||
_, err := connector.Get(context.Background())
|
||||
require.ErrorIs(t, err, errRecursiveConnectorDial)
|
||||
require.EqualValues(t, 1, dialCount.Load())
|
||||
require.EqualValues(t, 0, closeCount.Load())
|
||||
}
|
||||
|
||||
func TestConnectorRecursiveGetAcrossConnectorsAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
outerDialCount atomic.Int32
|
||||
innerDialCount atomic.Int32
|
||||
outerConnector *Connector[*testConnectorConnection]
|
||||
innerConnector *Connector[*testConnectorConnection]
|
||||
)
|
||||
|
||||
innerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
||||
innerDialCount.Add(1)
|
||||
return &testConnectorConnection{}, nil
|
||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
||||
IsClosed: func(connection *testConnectorConnection) bool {
|
||||
return false
|
||||
},
|
||||
Close: func(connection *testConnectorConnection) {},
|
||||
Reset: func(connection *testConnectorConnection) {},
|
||||
})
|
||||
|
||||
outerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
||||
outerDialCount.Add(1)
|
||||
_, err := innerConnector.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &testConnectorConnection{}, nil
|
||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
||||
IsClosed: func(connection *testConnectorConnection) bool {
|
||||
return false
|
||||
},
|
||||
Close: func(connection *testConnectorConnection) {},
|
||||
Reset: func(connection *testConnectorConnection) {},
|
||||
})
|
||||
|
||||
_, err := outerConnector.Get(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 1, outerDialCount.Load())
|
||||
require.EqualValues(t, 1, innerDialCount.Load())
|
||||
}
|
||||
|
||||
func TestConnectorDialContextPreservesValueAndDeadline(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type contextKey struct{}
|
||||
|
||||
var (
|
||||
dialValue any
|
||||
dialDeadline time.Time
|
||||
dialHasDeadline bool
|
||||
)
|
||||
|
||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
||||
dialValue = ctx.Value(contextKey{})
|
||||
dialDeadline, dialHasDeadline = ctx.Deadline()
|
||||
return &testConnectorConnection{}, nil
|
||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
||||
IsClosed: func(connection *testConnectorConnection) bool {
|
||||
return false
|
||||
},
|
||||
Close: func(connection *testConnectorConnection) {},
|
||||
Reset: func(connection *testConnectorConnection) {},
|
||||
})
|
||||
|
||||
deadline := time.Now().Add(time.Minute)
|
||||
requestContext, cancel := context.WithDeadline(context.WithValue(context.Background(), contextKey{}, "test-value"), deadline)
|
||||
defer cancel()
|
||||
|
||||
_, err := connector.Get(requestContext)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test-value", dialValue)
|
||||
require.True(t, dialHasDeadline)
|
||||
require.WithinDuration(t, deadline, dialDeadline, time.Second)
|
||||
}
|
||||
|
||||
func TestConnectorDialSkipsCanceledRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var dialCount atomic.Int32
|
||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
||||
dialCount.Add(1)
|
||||
return &testConnectorConnection{}, nil
|
||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
||||
IsClosed: func(connection *testConnectorConnection) bool {
|
||||
return false
|
||||
},
|
||||
Close: func(connection *testConnectorConnection) {},
|
||||
Reset: func(connection *testConnectorConnection) {},
|
||||
})
|
||||
|
||||
requestContext, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
_, err := connector.Get(requestContext)
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
require.EqualValues(t, 0, dialCount.Load())
|
||||
}
|
||||
|
||||
func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
dialCount atomic.Int32
|
||||
closeCount atomic.Int32
|
||||
)
|
||||
dialStarted := make(chan struct{}, 1)
|
||||
releaseDial := make(chan struct{})
|
||||
|
||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
||||
dialCount.Add(1)
|
||||
select {
|
||||
case dialStarted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
<-releaseDial
|
||||
return &testConnectorConnection{}, nil
|
||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
||||
IsClosed: func(connection *testConnectorConnection) bool {
|
||||
return false
|
||||
},
|
||||
Close: func(connection *testConnectorConnection) {
|
||||
closeCount.Add(1)
|
||||
},
|
||||
Reset: func(connection *testConnectorConnection) {},
|
||||
})
|
||||
|
||||
requestContext, cancel := context.WithCancel(context.Background())
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := connector.Get(requestContext)
|
||||
result <- err
|
||||
}()
|
||||
|
||||
<-dialStarted
|
||||
cancel()
|
||||
close(releaseDial)
|
||||
|
||||
err := <-result
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
require.EqualValues(t, 1, dialCount.Load())
|
||||
require.EqualValues(t, 1, closeCount.Load())
|
||||
|
||||
_, err = connector.Get(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 2, dialCount.Load())
|
||||
}
|
||||
|
||||
func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var dialContext context.Context
|
||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
||||
dialContext = ctx
|
||||
return &testConnectorConnection{}, nil
|
||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
||||
IsClosed: func(connection *testConnectorConnection) bool {
|
||||
return false
|
||||
},
|
||||
Close: func(connection *testConnectorConnection) {},
|
||||
Reset: func(connection *testConnectorConnection) {},
|
||||
})
|
||||
|
||||
requestContext, cancel := context.WithCancel(context.Background())
|
||||
_, err := connector.Get(requestContext)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, dialContext)
|
||||
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-dialContext.Done():
|
||||
t.Fatal("dial context canceled by request context after successful dial")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
|
||||
err = connector.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestConnectorDialContextCanceledOnClose(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var dialContext context.Context
|
||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
||||
dialContext = ctx
|
||||
return &testConnectorConnection{}, nil
|
||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
||||
IsClosed: func(connection *testConnectorConnection) bool {
|
||||
return false
|
||||
},
|
||||
Close: func(connection *testConnectorConnection) {},
|
||||
Reset: func(connection *testConnectorConnection) {},
|
||||
})
|
||||
|
||||
_, err := connector.Get(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, dialContext)
|
||||
|
||||
select {
|
||||
case <-dialContext.Done():
|
||||
t.Fatal("dial context canceled before connector close")
|
||||
default:
|
||||
}
|
||||
|
||||
err = connector.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case <-dialContext.Done():
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("dial context not canceled after connector close")
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,6 @@ type Store struct {
|
||||
logger logger.Logger
|
||||
inet4Range netip.Prefix
|
||||
inet6Range netip.Prefix
|
||||
inet4Last netip.Addr
|
||||
inet6Last netip.Addr
|
||||
storage adapter.FakeIPStorage
|
||||
|
||||
addressAccess sync.Mutex
|
||||
@@ -28,35 +26,12 @@ type Store struct {
|
||||
}
|
||||
|
||||
func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store {
|
||||
store := &Store{
|
||||
return &Store{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
inet4Range: inet4Range,
|
||||
inet6Range: inet6Range,
|
||||
}
|
||||
if inet4Range.IsValid() {
|
||||
store.inet4Last = broadcastAddress(inet4Range)
|
||||
}
|
||||
if inet6Range.IsValid() {
|
||||
store.inet6Last = broadcastAddress(inet6Range)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func broadcastAddress(prefix netip.Prefix) netip.Addr {
|
||||
addr := prefix.Addr()
|
||||
raw := addr.As16()
|
||||
bits := prefix.Bits()
|
||||
if addr.Is4() {
|
||||
bits += 96
|
||||
}
|
||||
for i := bits; i < 128; i++ {
|
||||
raw[i/8] |= 1 << (7 - i%8)
|
||||
}
|
||||
if addr.Is4() {
|
||||
return netip.AddrFrom4([4]byte(raw[12:]))
|
||||
}
|
||||
return netip.AddrFrom16(raw)
|
||||
}
|
||||
|
||||
func (s *Store) Start() error {
|
||||
@@ -74,10 +49,10 @@ func (s *Store) Start() error {
|
||||
s.inet6Current = metadata.Inet6Current
|
||||
} else {
|
||||
if s.inet4Range.IsValid() {
|
||||
s.inet4Current = s.inet4Range.Addr().Next()
|
||||
s.inet4Current = s.inet4Range.Addr().Next().Next()
|
||||
}
|
||||
if s.inet6Range.IsValid() {
|
||||
s.inet6Current = s.inet6Range.Addr().Next()
|
||||
s.inet6Current = s.inet6Range.Addr().Next().Next()
|
||||
}
|
||||
_ = storage.FakeIPReset()
|
||||
}
|
||||
@@ -123,7 +98,7 @@ func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
|
||||
return netip.Addr{}, E.New("missing IPv4 fakeip address range")
|
||||
}
|
||||
nextAddress := s.inet4Current.Next()
|
||||
if nextAddress == s.inet4Last || !s.inet4Range.Contains(nextAddress) {
|
||||
if !s.inet4Range.Contains(nextAddress) {
|
||||
nextAddress = s.inet4Range.Addr().Next().Next()
|
||||
}
|
||||
s.inet4Current = nextAddress
|
||||
@@ -133,7 +108,7 @@ func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
|
||||
return netip.Addr{}, E.New("missing IPv6 fakeip address range")
|
||||
}
|
||||
nextAddress := s.inet6Current.Next()
|
||||
if nextAddress == s.inet6Last || !s.inet6Range.Contains(nextAddress) {
|
||||
if !s.inet6Range.Contains(nextAddress) {
|
||||
nextAddress = s.inet6Range.Addr().Next().Next()
|
||||
}
|
||||
s.inet6Current = nextAddress
|
||||
|
||||
@@ -81,7 +81,10 @@ func (t *Transport) Reset() {
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
if t.resolved != nil {
|
||||
return t.resolved.Exchange(ctx, message)
|
||||
resolverObject := t.resolved.Object()
|
||||
if resolverObject != nil {
|
||||
return t.resolved.Exchange(resolverObject, ctx, message)
|
||||
}
|
||||
}
|
||||
question := message.Question[0]
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
|
||||
@@ -9,5 +9,6 @@ import (
|
||||
type ResolvedResolver interface {
|
||||
Start() error
|
||||
Close() error
|
||||
Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
|
||||
Object() any
|
||||
Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
|
||||
}
|
||||
|
||||
@@ -4,26 +4,19 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"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/dns"
|
||||
dnsTransport "github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/service/resolved"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
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/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
@@ -56,23 +49,13 @@ type DBusResolvedResolver struct {
|
||||
interfaceMonitor tun.DefaultInterfaceMonitor
|
||||
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
|
||||
systemBus *dbus.Conn
|
||||
savedServerSet atomic.Pointer[resolvedServerSet]
|
||||
resoledObject atomic.Pointer[ResolvedObject]
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
type resolvedServerSet struct {
|
||||
servers []resolvedServer
|
||||
}
|
||||
|
||||
type resolvedServer struct {
|
||||
primaryTransport adapter.DNSTransport
|
||||
fallbackTransport adapter.DNSTransport
|
||||
}
|
||||
|
||||
type resolvedServerSpecification struct {
|
||||
address netip.Addr
|
||||
port uint16
|
||||
serverName string
|
||||
type ResolvedObject struct {
|
||||
dbus.BusObject
|
||||
InterfaceIndex int32
|
||||
}
|
||||
|
||||
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
|
||||
@@ -99,31 +82,17 @@ func (t *DBusResolvedResolver) Start() error {
|
||||
"org.freedesktop.DBus",
|
||||
"NameOwnerChanged",
|
||||
dbus.WithMatchSender("org.freedesktop.DBus"),
|
||||
dbus.WithMatchArg(0, "org.freedesktop.resolve1"),
|
||||
).Err
|
||||
if err != nil {
|
||||
return E.Cause(err, "configure resolved restart listener")
|
||||
}
|
||||
err = t.systemBus.BusObject().AddMatchSignal(
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"PropertiesChanged",
|
||||
dbus.WithMatchSender("org.freedesktop.resolve1"),
|
||||
dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"),
|
||||
).Err
|
||||
if err != nil {
|
||||
return E.Cause(err, "configure resolved properties listener")
|
||||
return E.Cause(err, "configure resolved restart listener")
|
||||
}
|
||||
go t.loopUpdateStatus()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) Close() error {
|
||||
var closeErr error
|
||||
t.closeOnce.Do(func() {
|
||||
serverSet := t.savedServerSet.Swap(nil)
|
||||
if serverSet != nil {
|
||||
closeErr = serverSet.Close()
|
||||
}
|
||||
if t.interfaceCallback != nil {
|
||||
t.interfaceMonitor.UnregisterCallback(t.interfaceCallback)
|
||||
}
|
||||
@@ -131,97 +100,99 @@ func (t *DBusResolvedResolver) Close() error {
|
||||
_ = t.systemBus.Close()
|
||||
}
|
||||
})
|
||||
return closeErr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
serverSet := t.savedServerSet.Load()
|
||||
if serverSet == nil {
|
||||
var err error
|
||||
serverSet, err = t.checkResolved(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
previousServerSet := t.savedServerSet.Swap(serverSet)
|
||||
if previousServerSet != nil {
|
||||
_ = previousServerSet.Close()
|
||||
func (t *DBusResolvedResolver) Object() any {
|
||||
return common.PtrOrNil(t.resoledObject.Load())
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
question := message.Question[0]
|
||||
resolvedObject := object.(*ResolvedObject)
|
||||
call := resolvedObject.CallWithContext(
|
||||
ctx,
|
||||
"org.freedesktop.resolve1.Manager.ResolveRecord",
|
||||
0,
|
||||
resolvedObject.InterfaceIndex,
|
||||
question.Name,
|
||||
question.Qclass,
|
||||
question.Qtype,
|
||||
uint64(0),
|
||||
)
|
||||
if call.Err != nil {
|
||||
var dbusError dbus.Error
|
||||
if errors.As(call.Err, &dbusError) && dbusError.Name == "org.freedesktop.resolve1.NoNameServers" {
|
||||
t.updateStatus()
|
||||
}
|
||||
return nil, E.Cause(call.Err, " resolve record via resolved")
|
||||
}
|
||||
response, err := t.exchangeServerSet(ctx, message, serverSet)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
t.updateStatus()
|
||||
refreshedServerSet := t.savedServerSet.Load()
|
||||
if refreshedServerSet == nil || refreshedServerSet == serverSet {
|
||||
var (
|
||||
records []resolved.ResourceRecord
|
||||
outflags uint64
|
||||
)
|
||||
err := call.Store(&records, &outflags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.exchangeServerSet(ctx, message, refreshedServerSet)
|
||||
response := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionDesired: true,
|
||||
RecursionAvailable: true,
|
||||
Rcode: mDNS.RcodeSuccess,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
}
|
||||
for _, record := range records {
|
||||
var rr mDNS.RR
|
||||
rr, _, err = mDNS.UnpackRR(record.Data, 0)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack resource record")
|
||||
}
|
||||
response.Answer = append(response.Answer, rr)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) loopUpdateStatus() {
|
||||
signalChan := make(chan *dbus.Signal, 1)
|
||||
t.systemBus.Signal(signalChan)
|
||||
for signal := range signalChan {
|
||||
switch signal.Name {
|
||||
case "org.freedesktop.DBus.NameOwnerChanged":
|
||||
if len(signal.Body) != 3 {
|
||||
continue
|
||||
}
|
||||
newOwner, loaded := signal.Body[2].(string)
|
||||
if !loaded || newOwner == "" {
|
||||
continue
|
||||
}
|
||||
t.updateStatus()
|
||||
case "org.freedesktop.DBus.Properties.PropertiesChanged":
|
||||
if !shouldUpdateResolvedServerSet(signal) {
|
||||
var restarted bool
|
||||
if signal.Name == "org.freedesktop.DBus.NameOwnerChanged" {
|
||||
if len(signal.Body) != 3 || signal.Body[2].(string) == "" {
|
||||
continue
|
||||
} else {
|
||||
restarted = true
|
||||
}
|
||||
}
|
||||
if restarted {
|
||||
t.updateStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) updateStatus() {
|
||||
serverSet, err := t.checkResolved(context.Background())
|
||||
oldServerSet := t.savedServerSet.Swap(serverSet)
|
||||
if oldServerSet != nil {
|
||||
_ = oldServerSet.Close()
|
||||
}
|
||||
dbusObject, err := t.checkResolved(context.Background())
|
||||
oldValue := t.resoledObject.Swap(dbusObject)
|
||||
if err != nil {
|
||||
var dbusErr dbus.Error
|
||||
if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" {
|
||||
if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwnerCould" {
|
||||
t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable"))
|
||||
}
|
||||
if oldServerSet != nil {
|
||||
if oldValue != nil {
|
||||
t.logger.Debug("systemd-resolved service is gone")
|
||||
}
|
||||
return
|
||||
} else if oldServerSet == nil {
|
||||
} else if oldValue == nil {
|
||||
t.logger.Debug("using systemd-resolved service as resolver")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet) (*mDNS.Msg, error) {
|
||||
if serverSet == nil || len(serverSet.servers) == 0 {
|
||||
return nil, E.New("link has no DNS servers configured")
|
||||
}
|
||||
var lastError error
|
||||
for _, server := range serverSet.servers {
|
||||
response, err := server.primaryTransport.Exchange(ctx, message)
|
||||
if err != nil && server.fallbackTransport != nil {
|
||||
response, err = server.fallbackTransport.Exchange(ctx, message)
|
||||
}
|
||||
if err != nil {
|
||||
lastError = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
return nil, lastError
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) {
|
||||
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObject, error) {
|
||||
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
|
||||
err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err
|
||||
if err != nil {
|
||||
@@ -249,19 +220,16 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ
|
||||
if linkObject == nil {
|
||||
return nil, E.New("missing link object for default interface")
|
||||
}
|
||||
dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject)
|
||||
dnsProp, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
linkDNSEx, err := loadResolvedLinkDNSEx(linkObject)
|
||||
var linkDNS []resolved.LinkDNS
|
||||
err = dnsProp.Store(&linkDNS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
linkDNS, err := loadResolvedLinkDNS(linkObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(linkDNSEx) == 0 && len(linkDNS) == 0 {
|
||||
if len(linkDNS) == 0 {
|
||||
for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() {
|
||||
if inbound.Type() == C.TypeTun {
|
||||
return nil, E.New("No appropriate name servers or networks for name found")
|
||||
@@ -269,233 +237,12 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ
|
||||
}
|
||||
return nil, E.New("link has no DNS servers configured")
|
||||
}
|
||||
serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{
|
||||
BindInterface: defaultInterface.Name,
|
||||
UDPFragmentDefault: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var serverSpecifications []resolvedServerSpecification
|
||||
if len(linkDNSEx) > 0 {
|
||||
for _, entry := range linkDNSEx {
|
||||
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, entry.Port, entry.Name)
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
serverSpecifications = append(serverSpecifications, serverSpecification)
|
||||
}
|
||||
} else {
|
||||
for _, entry := range linkDNS {
|
||||
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, 0, "")
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
serverSpecifications = append(serverSpecifications, serverSpecification)
|
||||
}
|
||||
}
|
||||
if len(serverSpecifications) == 0 {
|
||||
return nil, E.New("no valid DNS servers on link")
|
||||
}
|
||||
serverSet := &resolvedServerSet{
|
||||
servers: make([]resolvedServer, 0, len(serverSpecifications)),
|
||||
}
|
||||
for _, serverSpecification := range serverSpecifications {
|
||||
server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification)
|
||||
if createErr != nil {
|
||||
_ = serverSet.Close()
|
||||
return nil, createErr
|
||||
}
|
||||
serverSet.servers = append(serverSet.servers, server)
|
||||
}
|
||||
return serverSet, nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) {
|
||||
if dnsOverTLSMode == "yes" {
|
||||
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
|
||||
if err != nil {
|
||||
return resolvedServer{}, err
|
||||
}
|
||||
return resolvedServer{
|
||||
primaryTransport: primaryTransport,
|
||||
}, nil
|
||||
}
|
||||
if dnsOverTLSMode == "opportunistic" {
|
||||
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
|
||||
if err != nil {
|
||||
return resolvedServer{}, err
|
||||
}
|
||||
fallbackTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
|
||||
if err != nil {
|
||||
_ = primaryTransport.Close()
|
||||
return resolvedServer{}, err
|
||||
}
|
||||
return resolvedServer{
|
||||
primaryTransport: primaryTransport,
|
||||
fallbackTransport: fallbackTransport,
|
||||
}, nil
|
||||
}
|
||||
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
|
||||
if err != nil {
|
||||
return resolvedServer{}, err
|
||||
}
|
||||
return resolvedServer{
|
||||
primaryTransport: primaryTransport,
|
||||
return &ResolvedObject{
|
||||
BusObject: dbusObject,
|
||||
InterfaceIndex: int32(defaultInterface.Index),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) {
|
||||
serverAddress := M.SocksaddrFrom(serverSpecification.address, resolvedServerPort(serverSpecification.port, useTLS))
|
||||
if useTLS {
|
||||
tlsAddress := serverSpecification.address
|
||||
if tlsAddress.Zone() != "" {
|
||||
tlsAddress = tlsAddress.WithZone("")
|
||||
}
|
||||
serverName := serverSpecification.serverName
|
||||
if serverName == "" {
|
||||
serverName = tlsAddress.String()
|
||||
}
|
||||
tlsConfig, err := tls.NewClient(t.ctx, t.logger, tlsAddress.String(), option.OutboundTLSOptions{
|
||||
Enabled: true,
|
||||
ServerName: serverName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverTransport := dnsTransport.NewTLSRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeTLS, "", nil), serverDialer, serverAddress, tlsConfig)
|
||||
err = serverTransport.Start(adapter.StartStateStart)
|
||||
if err != nil {
|
||||
_ = serverTransport.Close()
|
||||
return nil, err
|
||||
}
|
||||
return serverTransport, nil
|
||||
}
|
||||
serverTransport := dnsTransport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), serverDialer, serverAddress)
|
||||
err := serverTransport.Start(adapter.StartStateStart)
|
||||
if err != nil {
|
||||
_ = serverTransport.Close()
|
||||
return nil, err
|
||||
}
|
||||
return serverTransport, nil
|
||||
}
|
||||
|
||||
func (s *resolvedServerSet) Close() error {
|
||||
var errors []error
|
||||
for _, server := range s.servers {
|
||||
errors = append(errors, server.primaryTransport.Close())
|
||||
if server.fallbackTransport != nil {
|
||||
errors = append(errors, server.fallbackTransport.Close())
|
||||
}
|
||||
}
|
||||
return E.Errors(errors...)
|
||||
}
|
||||
|
||||
func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, port uint16, serverName string) (resolvedServerSpecification, bool) {
|
||||
address, loaded := netip.AddrFromSlice(rawAddress)
|
||||
if !loaded {
|
||||
return resolvedServerSpecification{}, false
|
||||
}
|
||||
if address.Is6() && address.IsLinkLocalUnicast() && address.Zone() == "" {
|
||||
address = address.WithZone(interfaceName)
|
||||
}
|
||||
return resolvedServerSpecification{
|
||||
address: address,
|
||||
port: port,
|
||||
serverName: serverName,
|
||||
}, true
|
||||
}
|
||||
|
||||
func resolvedServerPort(port uint16, useTLS bool) uint16 {
|
||||
if port > 0 {
|
||||
return port
|
||||
}
|
||||
if useTLS {
|
||||
return 853
|
||||
}
|
||||
return 53
|
||||
}
|
||||
|
||||
func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) {
|
||||
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
|
||||
if err != nil {
|
||||
if isResolvedUnknownPropertyError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var linkDNS []resolved.LinkDNS
|
||||
err = dnsProperty.Store(&linkDNS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return linkDNS, nil
|
||||
}
|
||||
|
||||
func loadResolvedLinkDNSEx(linkObject dbus.BusObject) ([]resolved.LinkDNSEx, error) {
|
||||
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSEx")
|
||||
if err != nil {
|
||||
if isResolvedUnknownPropertyError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var linkDNSEx []resolved.LinkDNSEx
|
||||
err = dnsProperty.Store(&linkDNSEx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return linkDNSEx, nil
|
||||
}
|
||||
|
||||
func loadResolvedLinkDNSOverTLS(linkObject dbus.BusObject) (string, error) {
|
||||
dnsOverTLSProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSOverTLS")
|
||||
if err != nil {
|
||||
if isResolvedUnknownPropertyError(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
var dnsOverTLSMode string
|
||||
err = dnsOverTLSProperty.Store(&dnsOverTLSMode)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dnsOverTLSMode, nil
|
||||
}
|
||||
|
||||
func isResolvedUnknownPropertyError(err error) bool {
|
||||
var dbusError dbus.Error
|
||||
return errors.As(err, &dbusError) && dbusError.Name == "org.freedesktop.DBus.Error.UnknownProperty"
|
||||
}
|
||||
|
||||
func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool {
|
||||
if len(signal.Body) != 3 {
|
||||
return true
|
||||
}
|
||||
changedProperties, loaded := signal.Body[1].(map[string]dbus.Variant)
|
||||
if !loaded {
|
||||
return true
|
||||
}
|
||||
for propertyName := range changedProperties {
|
||||
switch propertyName {
|
||||
case "DNS", "DNSEx", "DNSOverTLS":
|
||||
return true
|
||||
}
|
||||
}
|
||||
invalidatedProperties, loaded := signal.Body[2].([]string)
|
||||
if !loaded {
|
||||
return true
|
||||
}
|
||||
for _, propertyName := range invalidatedProperties {
|
||||
switch propertyName {
|
||||
case "DNS", "DNSEx", "DNSOverTLS":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) {
|
||||
t.updateStatus()
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
@@ -64,9 +63,6 @@ func dnsReadConfig(ctx context.Context, _ string) *dnsConfig {
|
||||
continue
|
||||
}
|
||||
dnsServerAddr = netip.AddrFrom16(sockaddr.Addr)
|
||||
if sockaddr.ZoneId != 0 {
|
||||
dnsServerAddr = dnsServerAddr.WithZone(strconv.FormatInt(int64(sockaddr.ZoneId), 10))
|
||||
}
|
||||
default:
|
||||
// Unexpected type.
|
||||
continue
|
||||
|
||||
@@ -2,351 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.14.0-alpha.2
|
||||
|
||||
* Add OpenWrt and Alpine APK packages to release **1**
|
||||
* Backport to macOS 10.13 High Sierra **2**
|
||||
* OCM service: Add WebSocket support for Responses API **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
|
||||
|
||||
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
|
||||
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
|
||||
|
||||
**2**:
|
||||
|
||||
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
|
||||
macOS 10.13 High Sierra, built using Go 1.25 with patches
|
||||
from [SagerNet/go](https://github.com/SagerNet/go).
|
||||
|
||||
**3**:
|
||||
|
||||
See [OCM](/configuration/service/ocm).
|
||||
|
||||
#### 1.13.3-beta.1
|
||||
|
||||
* Add OpenWrt and Alpine APK packages to release **1**
|
||||
* Backport to macOS 10.13 High Sierra **2**
|
||||
* OCM service: Add WebSocket support for Responses API **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
|
||||
|
||||
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
|
||||
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
|
||||
|
||||
**2**:
|
||||
|
||||
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
|
||||
macOS 10.13 High Sierra, built using Go 1.25 with patches
|
||||
from [SagerNet/go](https://github.com/SagerNet/go).
|
||||
|
||||
**3**:
|
||||
|
||||
See [OCM](/configuration/service/ocm).
|
||||
|
||||
#### 1.14.0-alpha.1
|
||||
|
||||
* Add `source_mac_address` and `source_hostname` rule items **1**
|
||||
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
|
||||
* Update NaiveProxy to 145.0.7632.159 **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
|
||||
Supported on Linux, macOS, or in graphical clients on Android and macOS.
|
||||
|
||||
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
|
||||
|
||||
**2**:
|
||||
|
||||
Limit or exclude devices from TUN routing by MAC address.
|
||||
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||
|
||||
See [TUN](/configuration/inbound/tun/#include_mac_address).
|
||||
|
||||
**3**:
|
||||
|
||||
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
|
||||
|
||||
#### 1.13.2
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.1
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.14
|
||||
|
||||
* Backport fixes
|
||||
|
||||
#### 1.13.0
|
||||
|
||||
Important changes since 1.12:
|
||||
|
||||
* Add NaiveProxy outbound **1**
|
||||
* Add pre-match support for `auto_redirect` **2**
|
||||
* Improve `auto_redirect` **3**
|
||||
* Add Chrome Root Store certificate option **4**
|
||||
* Add new options for ACME DNS-01 challenge providers **5**
|
||||
* Add Wi-Fi state support for Linux and Windows **6**
|
||||
* Add curve preferences, pinned public key SHA256, mTLS and ECH `query_server_name` for TLS options **7**
|
||||
* Add kTLS support **8**
|
||||
* Add ICMP echo (ping) proxy support **9**
|
||||
* Add `interface_address`, `network_interface_address` and `default_interface_address` rule items **10**
|
||||
* Add `preferred_by` route rule item **11**
|
||||
* Improve `local` DNS server **12**
|
||||
* Add `disable_tcp_keep_alive`, `tcp_keep_alive` and `tcp_keep_alive_interval` options for listen and dial fields **13**
|
||||
* Add `bind_address_no_port` option for dial fields **14**
|
||||
* Add system interface, relay server and advertise tags options for Tailscale endpoint **15**
|
||||
* Add Claude Code Multiplexer service **16**
|
||||
* Add OpenAI Codex Multiplexer service **17**
|
||||
* Apple/Android: Refactor GUI
|
||||
* Apple/Android: Add support for sharing configurations via [QRS](https://github.com/qifi-dev/qrs)
|
||||
* Android: Add support for resisting VPN detection via Xposed
|
||||
* Drop support for go1.23 **18**
|
||||
* Drop support for Android 5.0 **19**
|
||||
* Update uTLS to v1.8.2 **20**
|
||||
* Update quic-go to v0.59.0
|
||||
* Update gVisor to v20250811
|
||||
* Update Tailscale to v1.92.4
|
||||
|
||||
**1**:
|
||||
|
||||
NaiveProxy outbound now supports QUIC, ECH, UDP over TCP, and configurable QUIC congestion control.
|
||||
|
||||
Only available on Apple platforms, Android, Windows and some Linux architectures.
|
||||
Each Windows release includes `libcronet.dll` —
|
||||
ensure this file is in the same directory as `sing-box.exe` or in a directory listed in `PATH`.
|
||||
|
||||
See [NaiveProxy outbound](/configuration/outbound/naive/).
|
||||
|
||||
**2**:
|
||||
|
||||
`auto_redirect` now allows you to bypass sing-box for connections based on routing rules.
|
||||
|
||||
A new rule action `bypass` is introduced to support this feature. When matched during pre-match, the connection will bypass sing-box and connect directly.
|
||||
|
||||
This feature requires Linux with `auto_redirect` enabled.
|
||||
|
||||
See [Pre-match](/configuration/shared/pre-match/) and [Rule Action](/configuration/route/rule_action/#bypass).
|
||||
|
||||
**3**:
|
||||
|
||||
`auto_redirect` now rejects MPTCP connections by default to fix compatibility issues.
|
||||
You can change it to bypass sing-box via the new `exclude_mptcp` option.
|
||||
|
||||
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
|
||||
ensuring traffic is routed to the sing-box table when no route is found in system tables.
|
||||
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
|
||||
|
||||
See [TUN](/configuration/inbound/tun/#exclude_mptcp).
|
||||
|
||||
**4**:
|
||||
|
||||
Adds `chrome` as a new certificate store option alongside `mozilla`.
|
||||
Both stores filter out China-based CA certificates.
|
||||
|
||||
See [Certificate](/configuration/certificate/#store).
|
||||
|
||||
**5**:
|
||||
|
||||
See [DNS-01 Challenge](/configuration/shared/dns01_challenge/).
|
||||
|
||||
**6**:
|
||||
|
||||
sing-box can now monitor Wi-Fi state on Linux and Windows to enable routing rules based on `wifi_ssid` and `wifi_bssid`.
|
||||
|
||||
See [Wi-Fi State](/configuration/shared/wifi-state/).
|
||||
|
||||
**7**:
|
||||
|
||||
See [TLS](/configuration/shared/tls/).
|
||||
|
||||
**8**:
|
||||
|
||||
Adds `kernel_tx` and `kernel_rx` options for TLS inbound.
|
||||
Enables kernel-level TLS offloading via `splice(2)` on Linux 5.1+ with TLS 1.3.
|
||||
|
||||
See [TLS](/configuration/shared/tls/).
|
||||
|
||||
**9**:
|
||||
|
||||
sing-box can now proxy ICMP echo (ping) requests.
|
||||
A new `icmp` network type is available for route rules.
|
||||
Supported from TUN, WireGuard and Tailscale inbounds to Direct, WireGuard and Tailscale outbounds.
|
||||
The `reject` action can also reply to ICMP echo requests.
|
||||
|
||||
**10**:
|
||||
|
||||
New rule items for matching based on interface IP addresses, available in route rules, DNS rules and rule-sets.
|
||||
|
||||
**11**:
|
||||
|
||||
Matches outbounds' preferred routes.
|
||||
For Tailscale: MagicDNS domains and peers' allowed IPs. For WireGuard: peers' allowed IPs.
|
||||
|
||||
**12**:
|
||||
|
||||
The `local` DNS server now uses platform-native resolution:
|
||||
`getaddrinfo`/libresolv on Apple platforms, systemd-resolved DBus on Linux.
|
||||
A new `prefer_go` option is available to opt out.
|
||||
|
||||
See [Local DNS](/configuration/dns/server/local/).
|
||||
|
||||
**13**:
|
||||
|
||||
The default TCP keep-alive initial period has been updated from 10 minutes to 5 minutes.
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/#tcp_keep_alive).
|
||||
|
||||
**14**:
|
||||
|
||||
Adds the Linux socket option `IP_BIND_ADDRESS_NO_PORT` support when explicitly binding to a source address.
|
||||
|
||||
This allows reusing the same source port for multiple connections, improving scalability for high-concurrency proxy scenarios.
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/#bind_address_no_port).
|
||||
|
||||
**15**:
|
||||
|
||||
Tailscale endpoint can now create a system TUN interface to handle traffic directly.
|
||||
New `relay_server_port` and `relay_server_static_endpoints` options for incoming relay connections.
|
||||
New `advertise_tags` option for ACL tag advertisement.
|
||||
|
||||
See [Tailscale endpoint](/configuration/endpoint/tailscale/).
|
||||
|
||||
**16**:
|
||||
|
||||
CCM (Claude Code Multiplexer) service allows you to access your local Claude Code subscription remotely through custom tokens, eliminating the need for OAuth authentication on remote clients.
|
||||
|
||||
See [CCM](/configuration/service/ccm).
|
||||
|
||||
**17**:
|
||||
|
||||
See [OCM](/configuration/service/ocm).
|
||||
|
||||
**18**:
|
||||
|
||||
Due to maintenance difficulties, sing-box 1.13.0 requires at least Go 1.24 to compile.
|
||||
|
||||
**19**:
|
||||
|
||||
Due to maintenance difficulties, sing-box 1.13.0 will be the last version to support Android 5.0,
|
||||
and only through a separate legacy build (with `-legacy-android-5` suffix).
|
||||
|
||||
For standalone binaries, the minimum Android version has been raised to Android 6.0,
|
||||
since Termux requires Android 7.0 or later.
|
||||
|
||||
**20**:
|
||||
|
||||
This update fixes missing padding extension for Chrome 120+ fingerprints.
|
||||
|
||||
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
|
||||
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
|
||||
use NaiveProxy instead for TLS fingerprint resistance.
|
||||
|
||||
#### 1.12.23
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.0-rc.5
|
||||
|
||||
* Add `mipsle`, `mips64le`, `riscv64` and `loong64` support for NaiveProxy outbound
|
||||
|
||||
#### 1.12.22
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.0-rc.3
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.21
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.0-rc.2
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.20
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.0-rc.1
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.19
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.0-beta.8
|
||||
|
||||
* Add fallback routing rule for `auto_redirect` **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
|
||||
ensuring traffic is routed to the sing-box table when no route is found in system tables.
|
||||
|
||||
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
|
||||
|
||||
#### 1.12.18
|
||||
|
||||
* Add fallback routing rule for `auto_redirect` **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
|
||||
ensuring traffic is routed to the sing-box table when no route is found in system tables.
|
||||
|
||||
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
|
||||
|
||||
#### 1.13.0-beta.6
|
||||
|
||||
* Update uTLS to v1.8.2 **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
This update fixes missing padding extension for Chrome 120+ fingerprints.
|
||||
|
||||
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
|
||||
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
|
||||
use NaiveProxy instead for TLS fingerprint resistance.
|
||||
|
||||
#### 1.12.17
|
||||
|
||||
* Update uTLS to v1.8.2 **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
This update fixes missing padding extension for Chrome 120+ fingerprints.
|
||||
|
||||
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
|
||||
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
|
||||
use NaiveProxy instead for TLS fingerprint resistance.
|
||||
|
||||
#### 1.13.0-beta.5
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.16
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.0-beta.4
|
||||
|
||||
* Apple/Android: Add support for sharing configurations via [QRS](https://github.com/qifi-dev/qrs)
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -154,12 +149,6 @@ icon: material/alert-decagram
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@@ -419,26 +408,6 @@ Matches network interface (same values as `network_type`) address.
|
||||
|
||||
Match default interface address.
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
Match source device MAC address.
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
Match source device hostname from DHCP leases.
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -154,12 +149,6 @@ icon: material/alert-decagram
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@@ -418,26 +407,6 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
匹配默认接口地址。
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
匹配源设备 MAC 地址。
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
匹配源设备从 DHCP 租约获取的主机名。
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
|
||||
@@ -8,8 +8,7 @@ icon: material/new-box
|
||||
:material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints)
|
||||
:material-plus: [system_interface](#system_interface)
|
||||
:material-plus: [system_interface_name](#system_interface_name)
|
||||
:material-plus: [system_interface_mtu](#system_interface_mtu)
|
||||
:material-plus: [advertise_tags](#advertise_tags)
|
||||
:material-plus: [system_interface_mtu](#system_interface_mtu)
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -29,7 +28,6 @@ icon: material/new-box
|
||||
"exit_node_allow_lan_access": false,
|
||||
"advertise_routes": [],
|
||||
"advertise_exit_node": false,
|
||||
"advertise_tags": [],
|
||||
"relay_server_port": 0,
|
||||
"relay_server_static_endpoints": [],
|
||||
"system_interface": false,
|
||||
@@ -104,14 +102,6 @@ Example: `["192.168.1.1/24"]`
|
||||
|
||||
Indicates whether the node should advertise itself as an exit node.
|
||||
|
||||
#### advertise_tags
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
Tags to advertise for this node, for ACL enforcement purposes.
|
||||
|
||||
Example: `["tag:server"]`
|
||||
|
||||
#### relay_server_port
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
@@ -8,8 +8,7 @@ icon: material/new-box
|
||||
:material-plus: [relay_server_static_endpoints](#relay_server_static_endpoints)
|
||||
:material-plus: [system_interface](#system_interface)
|
||||
:material-plus: [system_interface_name](#system_interface_name)
|
||||
:material-plus: [system_interface_mtu](#system_interface_mtu)
|
||||
:material-plus: [advertise_tags](#advertise_tags)
|
||||
:material-plus: [system_interface_mtu](#system_interface_mtu)
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
@@ -29,7 +28,6 @@ icon: material/new-box
|
||||
"exit_node_allow_lan_access": false,
|
||||
"advertise_routes": [],
|
||||
"advertise_exit_node": false,
|
||||
"advertise_tags": [],
|
||||
"relay_server_port": 0,
|
||||
"relay_server_static_endpoints": [],
|
||||
"system_interface": false,
|
||||
@@ -103,14 +101,6 @@ icon: material/new-box
|
||||
|
||||
指示节点是否应将自己通告为出口节点。
|
||||
|
||||
#### advertise_tags
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
为此节点通告的标签,用于 ACL 执行。
|
||||
|
||||
示例:`["tag:server"]`
|
||||
|
||||
#### relay_server_port
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
@@ -2,21 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [include_mac_address](#include_mac_address)
|
||||
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.3"
|
||||
|
||||
:material-alert: [strict_route](#strict_route)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
|
||||
:material-plus: [auto_redirect_nfqueue](#auto_redirect_nfqueue)
|
||||
:material-plus: [exclude_mptcp](#exclude_mptcp)
|
||||
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
||||
:material-plus: [exclude_mptcp](#exclude_mptcp)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
@@ -81,7 +71,6 @@ icon: material/new-box
|
||||
"auto_redirect_output_mark": "0x2024",
|
||||
"auto_redirect_reset_mark": "0x2025",
|
||||
"auto_redirect_nfqueue": 100,
|
||||
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
||||
"exclude_mptcp": false,
|
||||
"loopback_address": [
|
||||
"10.7.0.1"
|
||||
@@ -134,12 +123,6 @@ icon: material/new-box
|
||||
"exclude_package": [
|
||||
"com.android.captiveportallogin"
|
||||
],
|
||||
"include_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"exclude_mac_address": [
|
||||
"66:77:88:99:aa:bb"
|
||||
],
|
||||
"platform": {
|
||||
"http_proxy": {
|
||||
"enabled": false,
|
||||
@@ -320,17 +303,6 @@ NFQueue number used by `auto_redirect` pre-matching.
|
||||
|
||||
`100` is used by default.
|
||||
|
||||
#### auto_redirect_iproute2_fallback_rule_index
|
||||
|
||||
!!! question "Since sing-box 1.12.18"
|
||||
|
||||
Linux iproute2 fallback rule index generated by `auto_redirect`.
|
||||
|
||||
This rule is checked after system default rules (32766: main, 32767: default),
|
||||
routing traffic to the sing-box table only when no route is found in system tables.
|
||||
|
||||
`32768` is used by default.
|
||||
|
||||
#### exclude_mptcp
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
@@ -363,9 +335,6 @@ Enforce strict routing rules when `auto_route` is enabled:
|
||||
|
||||
* Let unsupported network unreachable
|
||||
* For legacy reasons, when neither `strict_route` nor `auto_redirect` are enabled, all ICMP traffic will not go through TUN.
|
||||
* When `auto_redirect` is enabled, `strict_route` also affects `SO_BINDTODEVICE` traffic:
|
||||
* Enabled: `SO_BINDTODEVICE` traffic is redirected through sing-box.
|
||||
* Disabled: `SO_BINDTODEVICE` traffic bypasses sing-box.
|
||||
|
||||
*In Windows*:
|
||||
|
||||
@@ -566,30 +535,6 @@ Limit android packages in route.
|
||||
|
||||
Exclude android packages in route.
|
||||
|
||||
#### include_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||
|
||||
Limit MAC addresses in route. Not limited by default.
|
||||
|
||||
Conflict with `exclude_mac_address`.
|
||||
|
||||
#### exclude_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||
|
||||
Exclude MAC addresses in route.
|
||||
|
||||
Conflict with `include_mac_address`.
|
||||
|
||||
#### platform
|
||||
|
||||
Platform-specific settings, provided by client applications.
|
||||
|
||||
@@ -2,21 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [include_mac_address](#include_mac_address)
|
||||
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
||||
|
||||
!!! quote "sing-box 1.13.3 中的更改"
|
||||
|
||||
:material-alert: [strict_route](#strict_route)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
|
||||
:material-plus: [auto_redirect_nfqueue](#auto_redirect_nfqueue)
|
||||
:material-plus: [exclude_mptcp](#exclude_mptcp)
|
||||
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
||||
:material-plus: [exclude_mptcp](#exclude_mptcp)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
@@ -81,7 +71,6 @@ icon: material/new-box
|
||||
"auto_redirect_output_mark": "0x2024",
|
||||
"auto_redirect_reset_mark": "0x2025",
|
||||
"auto_redirect_nfqueue": 100,
|
||||
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
||||
"exclude_mptcp": false,
|
||||
"loopback_address": [
|
||||
"10.7.0.1"
|
||||
@@ -135,12 +124,6 @@ icon: material/new-box
|
||||
"exclude_package": [
|
||||
"com.android.captiveportallogin"
|
||||
],
|
||||
"include_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"exclude_mac_address": [
|
||||
"66:77:88:99:aa:bb"
|
||||
],
|
||||
"platform": {
|
||||
"http_proxy": {
|
||||
"enabled": false,
|
||||
@@ -319,24 +302,13 @@ tun 接口的 IPv6 前缀。
|
||||
|
||||
默认使用 `100`。
|
||||
|
||||
#### auto_redirect_iproute2_fallback_rule_index
|
||||
|
||||
!!! question "自 sing-box 1.12.18 起"
|
||||
|
||||
`auto_redirect` 生成的 iproute2 回退规则索引。
|
||||
|
||||
此规则在系统默认规则(32766: main,32767: default)之后检查,
|
||||
仅当系统路由表中未找到路由时才将流量路由到 sing-box 路由表。
|
||||
|
||||
默认使用 `32768`。
|
||||
|
||||
#### exclude_mptcp
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。
|
||||
仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。
|
||||
|
||||
由于协议限制,MPTCP 无法被透明代理。
|
||||
|
||||
@@ -362,9 +334,6 @@ tun 接口的 IPv6 前缀。
|
||||
|
||||
* 使不支持的网络不可达。
|
||||
* 出于历史遗留原因,当未启用 `strict_route` 或 `auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。
|
||||
* 当启用 `auto_redirect` 时,`strict_route` 也影响 `SO_BINDTODEVICE` 流量:
|
||||
* 启用:`SO_BINDTODEVICE` 流量被重定向通过 sing-box。
|
||||
* 禁用:`SO_BINDTODEVICE` 流量绕过 sing-box。
|
||||
|
||||
*在 Windows 中*:
|
||||
|
||||
@@ -554,30 +523,6 @@ TCP/IP 栈。
|
||||
|
||||
排除路由的 Android 应用包名。
|
||||
|
||||
#### include_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux,且需要 `auto_route` 和 `auto_redirect` 已启用。
|
||||
|
||||
限制被路由的 MAC 地址。默认不限制。
|
||||
|
||||
与 `exclude_mac_address` 冲突。
|
||||
|
||||
#### exclude_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux,且需要 `auto_route` 和 `auto_redirect` 已启用。
|
||||
|
||||
排除路由的 MAC 地址。
|
||||
|
||||
与 `include_mac_address` 冲突。
|
||||
|
||||
#### platform
|
||||
|
||||
平台特定的设置,由客户端应用提供。
|
||||
|
||||
@@ -34,12 +34,10 @@ icon: material/new-box
|
||||
|
||||
| Build Variant | Platforms | Description |
|
||||
|---------------|-----------|-------------|
|
||||
| (no suffix) | Linux amd64/arm64 | purego build, `libcronet.so` included |
|
||||
| `-glibc` | Linux 386/amd64/arm/arm64/mipsle/mips64le/riscv64/loong64 | CGO build, dynamically linked with glibc, requires glibc >= 2.31 (loong64: >= 2.36) |
|
||||
| `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO build, statically linked with musl |
|
||||
| (no suffix) | Windows amd64/arm64 | purego build, `libcronet.dll` included |
|
||||
|
||||
For Linux, choose the glibc or musl variant based on your distribution's libc type.
|
||||
| (default) | Linux amd64/arm64 | purego build with `libcronet.so` included |
|
||||
| `-glibc` | Linux 386/amd64/arm/arm64 | CGO build dynamically linked with glibc, requires glibc >= 2.31 |
|
||||
| `-musl` | Linux 386/amd64/arm/arm64 | CGO build statically linked with musl, no system requirements |
|
||||
| (default) | Windows amd64/arm64 | purego build with `libcronet.dll` included |
|
||||
|
||||
**Runtime Requirements:**
|
||||
|
||||
|
||||
@@ -32,14 +32,12 @@ icon: material/new-box
|
||||
|
||||
**官方发布版本区别:**
|
||||
|
||||
| 构建变体 | 平台 | 说明 |
|
||||
|---|---|---|
|
||||
| (无后缀) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` |
|
||||
| `-glibc` | Linux 386/amd64/arm/arm64/mipsle/mips64le/riscv64/loong64 | CGO 构建,动态链接 glibc,要求 glibc >= 2.31(loong64: >= 2.36) |
|
||||
| `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO 构建,静态链接 musl |
|
||||
| (无后缀) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` |
|
||||
|
||||
对于 Linux,请根据发行版的 libc 类型选择 glibc 或 musl 变体。
|
||||
| 构建变体 | 平台 | 说明 |
|
||||
|-----------|------------------------|------------------------------------------|
|
||||
| (默认) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` |
|
||||
| `-glibc` | Linux 386/amd64/arm/arm64 | CGO 构建,动态链接 glibc,要求 glibc >= 2.31 |
|
||||
| `-musl` | Linux 386/amd64/arm/arm64 | CGO 构建,静态链接 musl,无系统要求 |
|
||||
| (默认) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` |
|
||||
|
||||
**运行时要求:**
|
||||
|
||||
|
||||
@@ -4,11 +4,6 @@ icon: material/alert-decagram
|
||||
|
||||
# Route
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [default_domain_resolver](#default_domain_resolver)
|
||||
@@ -40,9 +35,6 @@ icon: material/alert-decagram
|
||||
"override_android_vpn": false,
|
||||
"default_interface": "",
|
||||
"default_mark": 0,
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_domain_resolver": "", // or {}
|
||||
"default_network_strategy": "",
|
||||
"default_network_type": [],
|
||||
@@ -115,38 +107,6 @@ Set routing mark by default.
|
||||
|
||||
Takes no effect if `outbound.routing_mark` is set.
|
||||
|
||||
#### find_process
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, Windows, and macOS.
|
||||
|
||||
Enable process search for logging when no `process_name`, `process_path`, `package_name`, `user` or `user_id` rules exist.
|
||||
|
||||
#### find_neighbor
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux and macOS.
|
||||
|
||||
Enable neighbor resolution for logging when no `source_mac_address` or `source_hostname` rules exist.
|
||||
|
||||
See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
#### dhcp_lease_files
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux and macOS.
|
||||
|
||||
Custom DHCP lease file paths for hostname and MAC address resolution.
|
||||
|
||||
Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty.
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -4,11 +4,6 @@ icon: material/alert-decagram
|
||||
|
||||
# 路由
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [default_domain_resolver](#default_domain_resolver)
|
||||
@@ -42,9 +37,6 @@ icon: material/alert-decagram
|
||||
"override_android_vpn": false,
|
||||
"default_interface": "",
|
||||
"default_mark": 0,
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_network_strategy": "",
|
||||
"default_fallback_delay": ""
|
||||
}
|
||||
@@ -114,38 +106,6 @@ icon: material/alert-decagram
|
||||
|
||||
如果设置了 `outbound.routing_mark` 设置,则不生效。
|
||||
|
||||
#### find_process
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、Windows 和 macOS。
|
||||
|
||||
在没有 `process_name`、`process_path`、`package_name`、`user` 或 `user_id` 规则时启用进程搜索以输出日志。
|
||||
|
||||
#### find_neighbor
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux 和 macOS。
|
||||
|
||||
在没有 `source_mac_address` 或 `source_hostname` 规则时启用邻居解析以输出日志。
|
||||
|
||||
参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
#### dhcp_lease_files
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux 和 macOS。
|
||||
|
||||
用于主机名和 MAC 地址解析的自定义 DHCP 租约文件路径。
|
||||
|
||||
为空时自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -164,12 +159,6 @@ icon: material/new-box
|
||||
"tailscale",
|
||||
"wireguard"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
@@ -460,26 +449,6 @@ Match specified outbounds' preferred routes.
|
||||
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
|
||||
| `wireguard` | Match peers's allowed IPs |
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
Match source device MAC address.
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
Match source device hostname from DHCP leases.
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -161,12 +156,6 @@ icon: material/new-box
|
||||
"tailscale",
|
||||
"wireguard"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
@@ -457,26 +446,6 @@ icon: material/new-box
|
||||
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
|
||||
| `wireguard` | 匹配对端的 allowed IPs |
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
匹配源设备 MAC 地址。
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
匹配源设备从 DHCP 租约获取的主机名。
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
@@ -10,11 +10,6 @@ CCM (Claude Code Multiplexer) service is a multiplexing service that allows you
|
||||
|
||||
It handles OAuth authentication with Claude's API on your local machine while allowing remote Claude Code to authenticate using Auth Tokens via the `ANTHROPIC_AUTH_TOKEN` environment variable.
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [credentials](#credentials)
|
||||
:material-alert: [users](#users)
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
@@ -24,7 +19,6 @@ It handles OAuth authentication with Claude's API on your local machine while al
|
||||
... // Listen Fields
|
||||
|
||||
"credential_path": "",
|
||||
"credentials": [],
|
||||
"usages_path": "",
|
||||
"users": [],
|
||||
"headers": {},
|
||||
@@ -51,77 +45,6 @@ On macOS, credentials are read from the system keychain first, then fall back to
|
||||
|
||||
Refreshed tokens are automatically written back to the same location.
|
||||
|
||||
When `credential_path` points to a file, the service can start before the file exists. The credential becomes available automatically after the file is created or updated, and becomes unavailable immediately if the file is later removed or becomes invalid.
|
||||
|
||||
On macOS without an explicit `credential_path`, keychain changes are not watched. Automatic reload only applies to the credential file path.
|
||||
|
||||
Conflict with `credentials`.
|
||||
|
||||
#### credentials
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
List of credential configurations for multi-credential mode.
|
||||
|
||||
When set, top-level `credential_path`, `usages_path`, and `detour` are forbidden. Each user must specify a `credential` tag.
|
||||
|
||||
Each credential has a `type` field (`default`, `balancer`, or `fallback`) and a required `tag` field.
|
||||
|
||||
##### Default Credential
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "a",
|
||||
"credential_path": "/path/to/.credentials.json",
|
||||
"usages_path": "/path/to/usages.json",
|
||||
"detour": "",
|
||||
"reserve_5h": 20,
|
||||
"reserve_weekly": 20
|
||||
}
|
||||
```
|
||||
|
||||
A single OAuth credential file. The `type` field can be omitted (defaults to `default`). The service can start before the file exists, and reloads file updates automatically.
|
||||
|
||||
- `credential_path`: Path to the credentials file. Same defaults as top-level `credential_path`.
|
||||
- `usages_path`: Optional usage tracking file for this credential.
|
||||
- `detour`: Outbound tag for connecting to the Claude API with this credential.
|
||||
- `reserve_5h`: Reserve threshold (1-99) for 5-hour window. Credential pauses at (100-N)% utilization.
|
||||
- `reserve_weekly`: Reserve threshold (1-99) for weekly window. Credential pauses at (100-N)% utilization.
|
||||
|
||||
##### Balancer Credential
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "pool",
|
||||
"type": "balancer",
|
||||
"strategy": "",
|
||||
"credentials": ["a", "b"],
|
||||
"poll_interval": "60s"
|
||||
}
|
||||
```
|
||||
|
||||
Assigns sessions to default credentials based on the selected strategy. Sessions are sticky until the assigned credential hits a rate limit.
|
||||
|
||||
- `strategy`: Selection strategy. One of `least_used` `round_robin` `random`. `least_used` will be used by default.
|
||||
- `credentials`: ==Required== List of default credential tags.
|
||||
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
|
||||
|
||||
##### Fallback Credential
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "backup",
|
||||
"type": "fallback",
|
||||
"credentials": ["a", "b"],
|
||||
"poll_interval": "30s"
|
||||
}
|
||||
```
|
||||
|
||||
Uses credentials in order. Falls through to the next when the current one is exhausted.
|
||||
|
||||
- `credentials`: ==Required== Ordered list of default credential tags.
|
||||
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
|
||||
|
||||
#### usages_path
|
||||
|
||||
Path to the file for storing aggregated API usage statistics.
|
||||
@@ -137,29 +60,13 @@ Statistics are organized by model, context window (200k standard vs 1M premium),
|
||||
|
||||
The statistics file is automatically saved every minute and upon service shutdown.
|
||||
|
||||
Conflict with `credentials`. In multi-credential mode, use `usages_path` on individual default credentials.
|
||||
|
||||
#### users
|
||||
|
||||
List of authorized users for token authentication.
|
||||
|
||||
If empty, no authentication is required.
|
||||
|
||||
Object format:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "",
|
||||
"token": "",
|
||||
"credential": ""
|
||||
}
|
||||
```
|
||||
|
||||
Object fields:
|
||||
|
||||
- `name`: Username identifier for tracking purposes.
|
||||
- `token`: Bearer token for authentication. Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value.
|
||||
- `credential`: Credential tag to use for this user. ==Required== when `credentials` is set.
|
||||
Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value.
|
||||
|
||||
#### headers
|
||||
|
||||
@@ -171,93 +78,29 @@ These headers will override any existing headers with the same name.
|
||||
|
||||
Outbound tag for connecting to the Claude API.
|
||||
|
||||
Conflict with `credentials`. In multi-credential mode, use `detour` on individual default credentials.
|
||||
|
||||
#### tls
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
|
||||
### Example
|
||||
|
||||
#### Server
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"type": "ccm",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8080,
|
||||
"usages_path": "./claude-usages.json",
|
||||
"users": [
|
||||
{
|
||||
"name": "alice",
|
||||
"token": "ak-ccm-hello-world"
|
||||
},
|
||||
{
|
||||
"name": "bob",
|
||||
"token": "ak-ccm-hello-bob"
|
||||
}
|
||||
]
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 8080
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Client
|
||||
Connect to the CCM service:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_BASE_URL="http://127.0.0.1:8080"
|
||||
export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world"
|
||||
export ANTHROPIC_AUTH_TOKEN="sk-ant-ccm-auth-token-not-required-in-this-context"
|
||||
|
||||
claude
|
||||
```
|
||||
|
||||
### Example with Multiple Credentials
|
||||
|
||||
#### Server
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"type": "ccm",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8080,
|
||||
"credentials": [
|
||||
{
|
||||
"tag": "a",
|
||||
"credential_path": "/home/user/.claude-a/.credentials.json",
|
||||
"usages_path": "/data/usages-a.json",
|
||||
"reserve_5h": 20,
|
||||
"reserve_weekly": 20
|
||||
},
|
||||
{
|
||||
"tag": "b",
|
||||
"credential_path": "/home/user/.claude-b/.credentials.json",
|
||||
"reserve_5h": 10,
|
||||
"reserve_weekly": 10
|
||||
},
|
||||
{
|
||||
"tag": "pool",
|
||||
"type": "balancer",
|
||||
"poll_interval": "60s",
|
||||
"credentials": ["a", "b"]
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"name": "alice",
|
||||
"token": "ak-ccm-hello-world",
|
||||
"credential": "pool"
|
||||
},
|
||||
{
|
||||
"name": "bob",
|
||||
"token": "ak-ccm-hello-bob",
|
||||
"credential": "a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -10,11 +10,6 @@ CCM(Claude Code 多路复用器)服务是一个多路复用服务,允许
|
||||
|
||||
它在本地机器上处理与 Claude API 的 OAuth 身份验证,同时允许远程 Claude Code 通过 `ANTHROPIC_AUTH_TOKEN` 环境变量使用认证令牌进行身份验证。
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [credentials](#credentials)
|
||||
:material-alert: [users](#users)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@@ -24,7 +19,6 @@ CCM(Claude Code 多路复用器)服务是一个多路复用服务,允许
|
||||
... // 监听字段
|
||||
|
||||
"credential_path": "",
|
||||
"credentials": [],
|
||||
"usages_path": "",
|
||||
"users": [],
|
||||
"headers": {},
|
||||
@@ -51,77 +45,6 @@ Claude Code OAuth 凭据文件的路径。
|
||||
|
||||
刷新的令牌会自动写回相同位置。
|
||||
|
||||
当 `credential_path` 指向文件时,即使文件尚不存在,服务也可以启动。文件被创建或更新后,凭据会自动变为可用;如果文件之后被删除或变为无效,该凭据会立即变为不可用。
|
||||
|
||||
在 macOS 上如果未显式设置 `credential_path`,不会监听钥匙串变化。自动重载只作用于凭据文件路径。
|
||||
|
||||
与 `credentials` 冲突。
|
||||
|
||||
#### credentials
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
多凭据模式的凭据配置列表。
|
||||
|
||||
设置后,顶层 `credential_path`、`usages_path` 和 `detour` 被禁止。每个用户必须指定 `credential` 标签。
|
||||
|
||||
每个凭据有一个 `type` 字段(`default`、`balancer` 或 `fallback`)和一个必填的 `tag` 字段。
|
||||
|
||||
##### 默认凭据
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "a",
|
||||
"credential_path": "/path/to/.credentials.json",
|
||||
"usages_path": "/path/to/usages.json",
|
||||
"detour": "",
|
||||
"reserve_5h": 20,
|
||||
"reserve_weekly": 20
|
||||
}
|
||||
```
|
||||
|
||||
单个 OAuth 凭据文件。`type` 字段可以省略(默认为 `default`)。即使文件尚不存在,服务也可以启动,并会自动重载文件更新。
|
||||
|
||||
- `credential_path`:凭据文件的路径。默认值与顶层 `credential_path` 相同。
|
||||
- `usages_path`:此凭据的可选使用跟踪文件。
|
||||
- `detour`:此凭据用于连接 Claude API 的出站标签。
|
||||
- `reserve_5h`:5 小时窗口的保留阈值(1-99)。凭据在利用率达到 (100-N)% 时暂停。
|
||||
- `reserve_weekly`:每周窗口的保留阈值(1-99)。凭据在利用率达到 (100-N)% 时暂停。
|
||||
|
||||
##### 均衡凭据
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "pool",
|
||||
"type": "balancer",
|
||||
"strategy": "",
|
||||
"credentials": ["a", "b"],
|
||||
"poll_interval": "60s"
|
||||
}
|
||||
```
|
||||
|
||||
根据选择的策略将会话分配给默认凭据。会话保持粘性,直到分配的凭据触发速率限制。
|
||||
|
||||
- `strategy`:选择策略。可选值:`least_used` `round_robin` `random`。默认使用 `least_used`。
|
||||
- `credentials`:==必填== 默认凭据标签列表。
|
||||
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`。
|
||||
|
||||
##### 回退凭据
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "backup",
|
||||
"type": "fallback",
|
||||
"credentials": ["a", "b"],
|
||||
"poll_interval": "30s"
|
||||
}
|
||||
```
|
||||
|
||||
按顺序使用凭据。当前凭据耗尽后切换到下一个。
|
||||
|
||||
- `credentials`:==必填== 有序的默认凭据标签列表。
|
||||
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`。
|
||||
|
||||
#### usages_path
|
||||
|
||||
用于存储聚合 API 使用统计信息的文件路径。
|
||||
@@ -137,29 +60,13 @@ Claude Code OAuth 凭据文件的路径。
|
||||
|
||||
统计文件每分钟自动保存一次,并在服务关闭时保存。
|
||||
|
||||
与 `credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `usages_path`。
|
||||
|
||||
#### users
|
||||
|
||||
用于令牌身份验证的授权用户列表。
|
||||
|
||||
如果为空,则不需要身份验证。
|
||||
|
||||
对象格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "",
|
||||
"token": "",
|
||||
"credential": ""
|
||||
}
|
||||
```
|
||||
|
||||
对象字段:
|
||||
|
||||
- `name`:用于跟踪的用户名标识符。
|
||||
- `token`:用于身份验证的 Bearer 令牌。Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。
|
||||
- `credential`:此用户使用的凭据标签。设置 `credentials` 时==必填==。
|
||||
Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。
|
||||
|
||||
#### headers
|
||||
|
||||
@@ -171,93 +78,29 @@ Claude Code OAuth 凭据文件的路径。
|
||||
|
||||
用于连接 Claude API 的出站标签。
|
||||
|
||||
与 `credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `detour`。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
### 示例
|
||||
|
||||
#### 服务端
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"type": "ccm",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8080,
|
||||
"usages_path": "./claude-usages.json",
|
||||
"users": [
|
||||
{
|
||||
"name": "alice",
|
||||
"token": "ak-ccm-hello-world"
|
||||
},
|
||||
{
|
||||
"name": "bob",
|
||||
"token": "ak-ccm-hello-bob"
|
||||
}
|
||||
]
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 8080
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 客户端
|
||||
连接到 CCM 服务:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_BASE_URL="http://127.0.0.1:8080"
|
||||
export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world"
|
||||
export ANTHROPIC_AUTH_TOKEN="sk-ant-ccm-auth-token-not-required-in-this-context"
|
||||
|
||||
claude
|
||||
```
|
||||
|
||||
### 多凭据示例
|
||||
|
||||
#### 服务端
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"type": "ccm",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8080,
|
||||
"credentials": [
|
||||
{
|
||||
"tag": "a",
|
||||
"credential_path": "/home/user/.claude-a/.credentials.json",
|
||||
"usages_path": "/data/usages-a.json",
|
||||
"reserve_5h": 20,
|
||||
"reserve_weekly": 20
|
||||
},
|
||||
{
|
||||
"tag": "b",
|
||||
"credential_path": "/home/user/.claude-b/.credentials.json",
|
||||
"reserve_5h": 10,
|
||||
"reserve_weekly": 10
|
||||
},
|
||||
{
|
||||
"tag": "pool",
|
||||
"type": "balancer",
|
||||
"poll_interval": "60s",
|
||||
"credentials": ["a", "b"]
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"name": "alice",
|
||||
"token": "ak-ccm-hello-world",
|
||||
"credential": "pool"
|
||||
},
|
||||
{
|
||||
"name": "bob",
|
||||
"token": "ak-ccm-hello-bob",
|
||||
"credential": "a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -10,11 +10,6 @@ OCM (OpenAI Codex Multiplexer) service is a multiplexing service that allows you
|
||||
|
||||
It handles OAuth authentication with OpenAI's API on your local machine while allowing remote clients to authenticate using custom tokens.
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [credentials](#credentials)
|
||||
:material-alert: [users](#users)
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
@@ -24,7 +19,6 @@ It handles OAuth authentication with OpenAI's API on your local machine while al
|
||||
... // Listen Fields
|
||||
|
||||
"credential_path": "",
|
||||
"credentials": [],
|
||||
"usages_path": "",
|
||||
"users": [],
|
||||
"headers": {},
|
||||
@@ -43,81 +37,10 @@ See [Listen Fields](/configuration/shared/listen/) for details.
|
||||
|
||||
Path to the OpenAI OAuth credentials file.
|
||||
|
||||
If not specified, defaults to:
|
||||
- `$CODEX_HOME/auth.json` if `CODEX_HOME` environment variable is set
|
||||
- `~/.codex/auth.json` otherwise
|
||||
If not specified, defaults to `~/.codex/auth.json`.
|
||||
|
||||
Refreshed tokens are automatically written back to the same location.
|
||||
|
||||
When `credential_path` points to a file, the service can start before the file exists. The credential becomes available automatically after the file is created or updated, and becomes unavailable immediately if the file is later removed or becomes invalid.
|
||||
|
||||
Conflict with `credentials`.
|
||||
|
||||
#### credentials
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
List of credential configurations for multi-credential mode.
|
||||
|
||||
When set, top-level `credential_path`, `usages_path`, and `detour` are forbidden. Each user must specify a `credential` tag.
|
||||
|
||||
Each credential has a `type` field (`default`, `balancer`, or `fallback`) and a required `tag` field.
|
||||
|
||||
##### Default Credential
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "a",
|
||||
"credential_path": "/path/to/auth.json",
|
||||
"usages_path": "/path/to/usages.json",
|
||||
"detour": "",
|
||||
"reserve_5h": 20,
|
||||
"reserve_weekly": 20
|
||||
}
|
||||
```
|
||||
|
||||
A single OAuth credential file. The `type` field can be omitted (defaults to `default`). The service can start before the file exists, and reloads file updates automatically.
|
||||
|
||||
- `credential_path`: Path to the credentials file. Same defaults as top-level `credential_path`.
|
||||
- `usages_path`: Optional usage tracking file for this credential.
|
||||
- `detour`: Outbound tag for connecting to the OpenAI API with this credential.
|
||||
- `reserve_5h`: Reserve threshold (1-99) for primary rate limit window. Credential pauses at (100-N)% utilization.
|
||||
- `reserve_weekly`: Reserve threshold (1-99) for secondary (weekly) rate limit window. Credential pauses at (100-N)% utilization.
|
||||
|
||||
##### Balancer Credential
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "pool",
|
||||
"type": "balancer",
|
||||
"strategy": "",
|
||||
"credentials": ["a", "b"],
|
||||
"poll_interval": "60s"
|
||||
}
|
||||
```
|
||||
|
||||
Assigns sessions to default credentials based on the selected strategy. Sessions are sticky until the assigned credential hits a rate limit.
|
||||
|
||||
- `strategy`: Selection strategy. One of `least_used` `round_robin` `random`. `least_used` will be used by default.
|
||||
- `credentials`: ==Required== List of default credential tags.
|
||||
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
|
||||
|
||||
##### Fallback Credential
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "backup",
|
||||
"type": "fallback",
|
||||
"credentials": ["a", "b"],
|
||||
"poll_interval": "30s"
|
||||
}
|
||||
```
|
||||
|
||||
Uses credentials in order. Falls through to the next when the current one is exhausted.
|
||||
|
||||
- `credentials`: ==Required== Ordered list of default credential tags.
|
||||
- `poll_interval`: How often to poll upstream usage API. Default `60s`.
|
||||
|
||||
#### usages_path
|
||||
|
||||
Path to the file for storing aggregated API usage statistics.
|
||||
@@ -133,8 +56,6 @@ Statistics are organized by model and optionally by user when authentication is
|
||||
|
||||
The statistics file is automatically saved every minute and upon service shutdown.
|
||||
|
||||
Conflict with `credentials`. In multi-credential mode, use `usages_path` on individual default credentials.
|
||||
|
||||
#### users
|
||||
|
||||
List of authorized users for token authentication.
|
||||
@@ -146,8 +67,7 @@ Object format:
|
||||
```json
|
||||
{
|
||||
"name": "",
|
||||
"token": "",
|
||||
"credential": ""
|
||||
"token": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -155,7 +75,6 @@ Object fields:
|
||||
|
||||
- `name`: Username identifier for tracking purposes.
|
||||
- `token`: Bearer token for authentication. Clients authenticate by setting the `Authorization: Bearer <token>` header.
|
||||
- `credential`: Credential tag to use for this user. ==Required== when `credentials` is set.
|
||||
|
||||
#### headers
|
||||
|
||||
@@ -167,8 +86,6 @@ These headers will override any existing headers with the same name.
|
||||
|
||||
Outbound tag for connecting to the OpenAI API.
|
||||
|
||||
Conflict with `credentials`. In multi-credential mode, use `detour` on individual default credentials.
|
||||
|
||||
#### tls
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
@@ -194,23 +111,17 @@ TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
||||
Add to `~/.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
# profile = "ocm" # set as default profile
|
||||
|
||||
[model_providers.ocm]
|
||||
name = "OCM Proxy"
|
||||
base_url = "http://127.0.0.1:8080/v1"
|
||||
supports_websockets = true
|
||||
|
||||
[profiles.ocm]
|
||||
model_provider = "ocm"
|
||||
# model = "gpt-5.4" # if the latest model is not yet publicly released
|
||||
# model_reasoning_effort = "xhigh"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = false
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
codex --profile ocm
|
||||
codex --model-provider ocm
|
||||
```
|
||||
|
||||
### Example with Authentication
|
||||
@@ -228,11 +139,11 @@ codex --profile ocm
|
||||
"users": [
|
||||
{
|
||||
"name": "alice",
|
||||
"token": "sk-ocm-hello-world"
|
||||
"token": "sk-alice-secret-token"
|
||||
},
|
||||
{
|
||||
"name": "bob",
|
||||
"token": "sk-ocm-hello-bob"
|
||||
"token": "sk-bob-secret-token"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -245,71 +156,16 @@ codex --profile ocm
|
||||
Add to `~/.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
# profile = "ocm" # set as default profile
|
||||
|
||||
[model_providers.ocm]
|
||||
name = "OCM Proxy"
|
||||
base_url = "http://127.0.0.1:8080/v1"
|
||||
supports_websockets = true
|
||||
experimental_bearer_token = "sk-ocm-hello-world"
|
||||
|
||||
[profiles.ocm]
|
||||
model_provider = "ocm"
|
||||
# model = "gpt-5.4" # if the latest model is not yet publicly released
|
||||
# model_reasoning_effort = "xhigh"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = false
|
||||
experimental_bearer_token = "sk-alice-secret-token"
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
codex --profile ocm
|
||||
```
|
||||
|
||||
### Example with Multiple Credentials
|
||||
|
||||
#### Server
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"type": "ocm",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8080,
|
||||
"credentials": [
|
||||
{
|
||||
"tag": "a",
|
||||
"credential_path": "/home/user/.codex-a/auth.json",
|
||||
"usages_path": "/data/usages-a.json",
|
||||
"reserve_5h": 20,
|
||||
"reserve_weekly": 20
|
||||
},
|
||||
{
|
||||
"tag": "b",
|
||||
"credential_path": "/home/user/.codex-b/auth.json",
|
||||
"reserve_5h": 10,
|
||||
"reserve_weekly": 10
|
||||
},
|
||||
{
|
||||
"tag": "pool",
|
||||
"type": "balancer",
|
||||
"poll_interval": "60s",
|
||||
"credentials": ["a", "b"]
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"name": "alice",
|
||||
"token": "sk-ocm-hello-world",
|
||||
"credential": "pool"
|
||||
},
|
||||
{
|
||||
"name": "bob",
|
||||
"token": "sk-ocm-hello-bob",
|
||||
"credential": "a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
codex --model-provider ocm
|
||||
```
|
||||
|
||||
@@ -10,11 +10,6 @@ OCM(OpenAI Codex 多路复用器)服务是一个多路复用服务,允许
|
||||
|
||||
它在本地机器上处理与 OpenAI API 的 OAuth 身份验证,同时允许远程客户端使用自定义令牌进行身份验证。
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [credentials](#credentials)
|
||||
:material-alert: [users](#users)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@@ -24,7 +19,6 @@ OCM(OpenAI Codex 多路复用器)服务是一个多路复用服务,允许
|
||||
... // 监听字段
|
||||
|
||||
"credential_path": "",
|
||||
"credentials": [],
|
||||
"usages_path": "",
|
||||
"users": [],
|
||||
"headers": {},
|
||||
@@ -43,81 +37,10 @@ OCM(OpenAI Codex 多路复用器)服务是一个多路复用服务,允许
|
||||
|
||||
OpenAI OAuth 凭据文件的路径。
|
||||
|
||||
如果未指定,默认值为:
|
||||
- 如果设置了 `CODEX_HOME` 环境变量,则使用 `$CODEX_HOME/auth.json`
|
||||
- 否则使用 `~/.codex/auth.json`
|
||||
如果未指定,默认值为 `~/.codex/auth.json`。
|
||||
|
||||
刷新的令牌会自动写回相同位置。
|
||||
|
||||
当 `credential_path` 指向文件时,即使文件尚不存在,服务也可以启动。文件被创建或更新后,凭据会自动变为可用;如果文件之后被删除或变为无效,该凭据会立即变为不可用。
|
||||
|
||||
与 `credentials` 冲突。
|
||||
|
||||
#### credentials
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
多凭据模式的凭据配置列表。
|
||||
|
||||
设置后,顶层 `credential_path`、`usages_path` 和 `detour` 被禁止。每个用户必须指定 `credential` 标签。
|
||||
|
||||
每个凭据有一个 `type` 字段(`default`、`balancer` 或 `fallback`)和一个必填的 `tag` 字段。
|
||||
|
||||
##### 默认凭据
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "a",
|
||||
"credential_path": "/path/to/auth.json",
|
||||
"usages_path": "/path/to/usages.json",
|
||||
"detour": "",
|
||||
"reserve_5h": 20,
|
||||
"reserve_weekly": 20
|
||||
}
|
||||
```
|
||||
|
||||
单个 OAuth 凭据文件。`type` 字段可以省略(默认为 `default`)。即使文件尚不存在,服务也可以启动,并会自动重载文件更新。
|
||||
|
||||
- `credential_path`:凭据文件的路径。默认值与顶层 `credential_path` 相同。
|
||||
- `usages_path`:此凭据的可选使用跟踪文件。
|
||||
- `detour`:此凭据用于连接 OpenAI API 的出站标签。
|
||||
- `reserve_5h`:主要速率限制窗口的保留阈值(1-99)。凭据在利用率达到 (100-N)% 时暂停。
|
||||
- `reserve_weekly`:次要(每周)速率限制窗口的保留阈值(1-99)。凭据在利用率达到 (100-N)% 时暂停。
|
||||
|
||||
##### 均衡凭据
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "pool",
|
||||
"type": "balancer",
|
||||
"strategy": "",
|
||||
"credentials": ["a", "b"],
|
||||
"poll_interval": "60s"
|
||||
}
|
||||
```
|
||||
|
||||
根据选择的策略将会话分配给默认凭据。会话保持粘性,直到分配的凭据触发速率限制。
|
||||
|
||||
- `strategy`:选择策略。可选值:`least_used` `round_robin` `random`。默认使用 `least_used`。
|
||||
- `credentials`:==必填== 默认凭据标签列表。
|
||||
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`。
|
||||
|
||||
##### 回退凭据
|
||||
|
||||
```json
|
||||
{
|
||||
"tag": "backup",
|
||||
"type": "fallback",
|
||||
"credentials": ["a", "b"],
|
||||
"poll_interval": "30s"
|
||||
}
|
||||
```
|
||||
|
||||
按顺序使用凭据。当前凭据耗尽后切换到下一个。
|
||||
|
||||
- `credentials`:==必填== 有序的默认凭据标签列表。
|
||||
- `poll_interval`:轮询上游使用 API 的间隔。默认 `60s`。
|
||||
|
||||
#### usages_path
|
||||
|
||||
用于存储聚合 API 使用统计信息的文件路径。
|
||||
@@ -133,8 +56,6 @@ OpenAI OAuth 凭据文件的路径。
|
||||
|
||||
统计文件每分钟自动保存一次,并在服务关闭时保存。
|
||||
|
||||
与 `credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `usages_path`。
|
||||
|
||||
#### users
|
||||
|
||||
用于令牌身份验证的授权用户列表。
|
||||
@@ -146,8 +67,7 @@ OpenAI OAuth 凭据文件的路径。
|
||||
```json
|
||||
{
|
||||
"name": "",
|
||||
"token": "",
|
||||
"credential": ""
|
||||
"token": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -155,7 +75,6 @@ OpenAI OAuth 凭据文件的路径。
|
||||
|
||||
- `name`:用于跟踪的用户名标识符。
|
||||
- `token`:用于身份验证的 Bearer 令牌。客户端通过设置 `Authorization: Bearer <token>` 头进行身份验证。
|
||||
- `credential`:此用户使用的凭据标签。设置 `credentials` 时==必填==。
|
||||
|
||||
#### headers
|
||||
|
||||
@@ -167,8 +86,6 @@ OpenAI OAuth 凭据文件的路径。
|
||||
|
||||
用于连接 OpenAI API 的出站标签。
|
||||
|
||||
与 `credentials` 冲突。在多凭据模式下,在各个默认凭据上使用 `detour`。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
@@ -194,24 +111,17 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
在 `~/.codex/config.toml` 中添加:
|
||||
|
||||
```toml
|
||||
# profile = "ocm" # 设为默认配置
|
||||
|
||||
|
||||
[model_providers.ocm]
|
||||
name = "OCM Proxy"
|
||||
base_url = "http://127.0.0.1:8080/v1"
|
||||
supports_websockets = true
|
||||
|
||||
[profiles.ocm]
|
||||
model_provider = "ocm"
|
||||
# model = "gpt-5.4" # 如果最新模型尚未公开发布
|
||||
# model_reasoning_effort = "xhigh"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = false
|
||||
```
|
||||
|
||||
然后运行:
|
||||
|
||||
```bash
|
||||
codex --profile ocm
|
||||
codex --model-provider ocm
|
||||
```
|
||||
|
||||
### 带身份验证的示例
|
||||
@@ -229,11 +139,11 @@ codex --profile ocm
|
||||
"users": [
|
||||
{
|
||||
"name": "alice",
|
||||
"token": "sk-ocm-hello-world"
|
||||
"token": "sk-alice-secret-token"
|
||||
},
|
||||
{
|
||||
"name": "bob",
|
||||
"token": "sk-ocm-hello-bob"
|
||||
"token": "sk-bob-secret-token"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -246,71 +156,16 @@ codex --profile ocm
|
||||
在 `~/.codex/config.toml` 中添加:
|
||||
|
||||
```toml
|
||||
# profile = "ocm" # 设为默认配置
|
||||
|
||||
[model_providers.ocm]
|
||||
name = "OCM Proxy"
|
||||
base_url = "http://127.0.0.1:8080/v1"
|
||||
supports_websockets = true
|
||||
experimental_bearer_token = "sk-ocm-hello-world"
|
||||
|
||||
[profiles.ocm]
|
||||
model_provider = "ocm"
|
||||
# model = "gpt-5.4" # 如果最新模型尚未公开发布
|
||||
# model_reasoning_effort = "xhigh"
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = false
|
||||
experimental_bearer_token = "sk-alice-secret-token"
|
||||
```
|
||||
|
||||
然后运行:
|
||||
|
||||
```bash
|
||||
codex --profile ocm
|
||||
```
|
||||
|
||||
### 多凭据示例
|
||||
|
||||
#### 服务端
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"type": "ocm",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8080,
|
||||
"credentials": [
|
||||
{
|
||||
"tag": "a",
|
||||
"credential_path": "/home/user/.codex-a/auth.json",
|
||||
"usages_path": "/data/usages-a.json",
|
||||
"reserve_5h": 20,
|
||||
"reserve_weekly": 20
|
||||
},
|
||||
{
|
||||
"tag": "b",
|
||||
"credential_path": "/home/user/.codex-b/auth.json",
|
||||
"reserve_5h": 10,
|
||||
"reserve_weekly": 10
|
||||
},
|
||||
{
|
||||
"tag": "pool",
|
||||
"type": "balancer",
|
||||
"poll_interval": "60s",
|
||||
"credentials": ["a", "b"]
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"name": "alice",
|
||||
"token": "sk-ocm-hello-world",
|
||||
"credential": "pool"
|
||||
},
|
||||
{
|
||||
"name": "bob",
|
||||
"token": "sk-ocm-hello-bob",
|
||||
"credential": "a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
codex --model-provider ocm
|
||||
```
|
||||
|
||||
@@ -5,8 +5,7 @@ icon: material/new-box
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [alidns.security_token](#security_token)
|
||||
:material-plus: [cloudflare.zone_token](#zone_token)
|
||||
:material-plus: [acmedns](#acmedns)
|
||||
:material-plus: [cloudflare.zone_token](#zone_token)
|
||||
|
||||
### Structure
|
||||
|
||||
@@ -55,19 +54,3 @@ The Security Token for STS temporary credentials.
|
||||
Optional API token with `Zone:Read` permission.
|
||||
|
||||
When provided, allows `api_token` to be scoped to a single zone.
|
||||
|
||||
#### ACME-DNS
|
||||
|
||||
!!! question "Since sing-box 1.13.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "acmedns",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"subdomain": "",
|
||||
"server_url": ""
|
||||
}
|
||||
```
|
||||
|
||||
See [ACME-DNS](https://github.com/joohoi/acme-dns) for details.
|
||||
|
||||
@@ -5,8 +5,7 @@ icon: material/new-box
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [alidns.security_token](#security_token)
|
||||
:material-plus: [cloudflare.zone_token](#zone_token)
|
||||
:material-plus: [acmedns](#acmedns)
|
||||
:material-plus: [cloudflare.zone_token](#zone_token)
|
||||
|
||||
### 结构
|
||||
|
||||
@@ -55,19 +54,3 @@ icon: material/new-box
|
||||
具有 `Zone:Read` 权限的可选 API 令牌。
|
||||
|
||||
提供后可将 `api_token` 限定到单个区域。
|
||||
|
||||
#### ACME-DNS
|
||||
|
||||
!!! question "自 sing-box 1.13.0 起"
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "acmedns",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"subdomain": "",
|
||||
"server_url": ""
|
||||
}
|
||||
```
|
||||
|
||||
参阅 [ACME-DNS](https://github.com/joohoi/acme-dns)。
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
icon: material/lan
|
||||
---
|
||||
|
||||
# Neighbor Resolution
|
||||
|
||||
Match LAN devices by MAC address and hostname using
|
||||
[`source_mac_address`](/configuration/route/rule/#source_mac_address) and
|
||||
[`source_hostname`](/configuration/route/rule/#source_hostname) rule items.
|
||||
|
||||
Neighbor resolution is automatically enabled when these rule items exist.
|
||||
Use [`route.find_neighbor`](/configuration/route/#find_neighbor) to force enable it for logging without rules.
|
||||
|
||||
## Linux
|
||||
|
||||
Works natively. No special setup required.
|
||||
|
||||
Hostname resolution requires DHCP lease files,
|
||||
automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea).
|
||||
Custom paths can be set via [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files).
|
||||
|
||||
## Android
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported in graphical clients.
|
||||
|
||||
Requires Android 11 or above and ROOT.
|
||||
|
||||
Must use [VPNHotspot](https://github.com/Mygod/VPNHotspot) to share the VPN connection.
|
||||
ROM built-in features like "Use VPN for connected devices" can share VPN
|
||||
but cannot provide MAC address or hostname information.
|
||||
|
||||
Set **IP Masquerade Mode** to **None** in VPNHotspot settings.
|
||||
|
||||
Only route/DNS rules are supported. TUN include/exclude routes are not supported.
|
||||
|
||||
### Hostname Visibility
|
||||
|
||||
Hostname is only visible in sing-box if it is visible in VPNHotspot.
|
||||
For Apple devices, change **Private Wi-Fi Address** from **Rotating** to **Fixed** in the Wi-Fi settings
|
||||
of the connected network. Non-Apple devices are always visible.
|
||||
|
||||
## macOS
|
||||
|
||||
Requires the standalone version (macOS system extension).
|
||||
The App Store version can share the VPN as a hotspot but does not support MAC address or hostname reading.
|
||||
|
||||
See [VPN Hotspot](/manual/misc/vpn-hotspot/#macos) for Internet Sharing setup.
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
icon: material/lan
|
||||
---
|
||||
|
||||
# 邻居解析
|
||||
|
||||
通过
|
||||
[`source_mac_address`](/configuration/route/rule/#source_mac_address) 和
|
||||
[`source_hostname`](/configuration/route/rule/#source_hostname) 规则项匹配局域网设备的 MAC 地址和主机名。
|
||||
|
||||
当这些规则项存在时,邻居解析自动启用。
|
||||
使用 [`route.find_neighbor`](/configuration/route/#find_neighbor) 可在没有规则时强制启用以输出日志。
|
||||
|
||||
## Linux
|
||||
|
||||
原生支持,无需特殊设置。
|
||||
|
||||
主机名解析需要 DHCP 租约文件,
|
||||
自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。
|
||||
可通过 [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files) 设置自定义路径。
|
||||
|
||||
## Android
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅在图形客户端中支持。
|
||||
|
||||
需要 Android 11 或以上版本和 ROOT。
|
||||
|
||||
必须使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot) 共享 VPN 连接。
|
||||
ROM 自带的「通过 VPN 共享连接」等功能可以共享 VPN,
|
||||
但无法提供 MAC 地址或主机名信息。
|
||||
|
||||
在 VPNHotspot 设置中将 **IP 遮掩模式** 设为 **无**。
|
||||
|
||||
仅支持路由/DNS 规则。不支持 TUN 的 include/exclude 路由。
|
||||
|
||||
### 设备可见性
|
||||
|
||||
MAC 地址和主机名仅在 VPNHotspot 中可见时 sing-box 才能读取。
|
||||
对于 Apple 设备,需要在所连接网络的 Wi-Fi 设置中将**私有无线局域网地址**从**轮替**改为**固定**。
|
||||
非 Apple 设备始终可见。
|
||||
|
||||
## macOS
|
||||
|
||||
需要独立版本(macOS 系统扩展)。
|
||||
App Store 版本可以共享 VPN 热点但不支持 MAC 地址或主机名读取。
|
||||
|
||||
参阅 [VPN 热点](/manual/misc/vpn-hotspot/#macos) 了解互联网共享设置。
|
||||
@@ -418,18 +418,9 @@ Enable kernel TLS receive support.
|
||||
|
||||
==Client only==
|
||||
|
||||
!!! failure "Not Recommended"
|
||||
|
||||
uTLS has had repeated fingerprinting vulnerabilities discovered by researchers.
|
||||
|
||||
uTLS is a Go library that attempts to imitate browser TLS fingerprints by copying
|
||||
ClientHello structure. However, browsers use completely different TLS stacks
|
||||
(Chrome uses BoringSSL, Firefox uses NSS) with distinct implementation behaviors
|
||||
that cannot be replicated by simply copying the handshake format, making detection possible.
|
||||
Additionally, the library lacks active maintenance and has poor code quality,
|
||||
making it unsuitable for censorship circumvention.
|
||||
|
||||
For TLS fingerprint resistance, use [NaiveProxy](/configuration/inbound/naive/) instead.
|
||||
!!! failure ""
|
||||
|
||||
There is no evidence that GFW detects and blocks servers based on TLS client fingerprinting, and using an imperfect emulation that has not been security reviewed could pose security risks.
|
||||
|
||||
uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance.
|
||||
|
||||
|
||||
@@ -417,16 +417,9 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/
|
||||
|
||||
==仅客户端==
|
||||
|
||||
!!! failure "不推荐"
|
||||
!!! failure ""
|
||||
|
||||
uTLS 已被研究人员多次发现其指纹可被识别的漏洞。
|
||||
|
||||
uTLS 是一个试图通过复制 ClientHello 结构来模仿浏览器 TLS 指纹的 Go 库。
|
||||
然而,浏览器使用完全不同的 TLS 实现(Chrome 使用 BoringSSL,Firefox 使用 NSS),
|
||||
其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。
|
||||
此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。
|
||||
|
||||
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/configuration/inbound/naive/)。
|
||||
没有证据表明 GFW 根据 TLS 客户端指纹检测并阻止服务器,并且,使用一个未经安全审查的不完美模拟可能带来安全隐患。
|
||||
|
||||
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。
|
||||
|
||||
|
||||
@@ -57,49 +57,25 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
|
||||
| `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
|
||||
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
|
||||
| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
|
||||
| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale). |
|
||||
| `with_ccm` | :material-check: | Build with Claude Code Multiplexer service support. |
|
||||
| `with_ocm` | :material-check: | Build with OpenAI Codex Multiplexer service support. |
|
||||
| `with_naive_outbound` | :material-check: | Build with NaiveProxy outbound support, see [NaiveProxy outbound](/configuration/outbound/naive/). |
|
||||
| `badlinkname` | :material-check: | Enable `go:linkname` access to internal standard library functions. Required because the Go standard library does not expose many low-level APIs needed by this project, and reimplementing them externally is impractical. Used for kTLS (kernel TLS offload) and raw TLS record manipulation. |
|
||||
| `tfogo_checklinkname0` | :material-check: | Companion to `badlinkname`. Go 1.23+ enforces `go:linkname` restrictions via the linker; this tag signals the build uses `-checklinkname=0` to bypass that enforcement. |
|
||||
| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale) |
|
||||
| `with_naive_outbound` | :material-close:️ | Build with NaiveProxy outbound support, see [NaiveProxy outbound](/configuration/outbound/naive/). |
|
||||
|
||||
It is not recommended to change the default build tag list unless you really know what you are adding.
|
||||
|
||||
## :material-wrench: Linker Flags
|
||||
|
||||
The following `-ldflags` are used in official builds:
|
||||
|
||||
| Flag | Description |
|
||||
|-------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `-X 'internal/godebug.defaultGODEBUG=multipathtcp=0'` | Go 1.24 enabled Multipath TCP for listeners by default (`multipathtcp=2`). This may cause errors on low-level sockets, and sing-box has its own MPTCP control (`tcp_multi_path` option). This flag disables the Go default. |
|
||||
| `-checklinkname=0` | Go 1.23+ linker rejects unauthorized `go:linkname` usage. This flag disables the check, required together with the `badlinkname` build tag. |
|
||||
|
||||
## :material-package-variant: For Downstream Packagers
|
||||
|
||||
The default build tag lists and linker flags are available as files in the repository for downstream packagers to reference directly:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `release/DEFAULT_BUILD_TAGS` | Default for Linux (common architectures), Darwin, and Android. |
|
||||
| `release/DEFAULT_BUILD_TAGS_WINDOWS` | Default for Windows (includes `with_purego`). |
|
||||
| `release/DEFAULT_BUILD_TAGS_OTHERS` | Default for other platforms (no `with_naive_outbound`). |
|
||||
| `release/LDFLAGS` | Required linker flags (see above). |
|
||||
|
||||
## :material-layers: with_naive_outbound
|
||||
|
||||
NaiveProxy outbound requires special build configurations depending on your target platform.
|
||||
|
||||
### Supported Platforms
|
||||
|
||||
| Platform | Architectures | Mode | Requirements |
|
||||
|-----------------|--------------------------------------------------------|--------|-----------------------------------------------------------------|
|
||||
| Linux | amd64, arm64 | purego | None (library included in official releases) |
|
||||
| Linux | 386, amd64, arm, arm64, mipsle, mips64le, riscv64, loong64 | CGO | Chromium toolchain, glibc >= 2.31 (loong64: >= 2.36) at runtime |
|
||||
| Linux (musl) | 386, amd64, arm, arm64, mipsle, riscv64, loong64 | CGO | Chromium toolchain |
|
||||
| Windows | amd64, arm64 | purego | None (library included in official releases) |
|
||||
| Apple platforms | * | CGO | Xcode |
|
||||
| Android | * | CGO | Android NDK |
|
||||
| Platform | Architectures | Mode | Requirements |
|
||||
|-----------------|------------------------|--------|---------------------------------------------------|
|
||||
| Linux | amd64, arm64 | purego | None (library included in official releases) |
|
||||
| Linux | 386, amd64, arm, arm64 | CGO | Chromium toolchain, glibc >= 2.31 at runtime |
|
||||
| Linux (musl) | 386, amd64, arm, arm64 | CGO | Chromium toolchain |
|
||||
| Windows | amd64, arm64 | purego | None (library included in official releases) |
|
||||
| Apple platforms | * | CGO | Xcode |
|
||||
| Android | * | CGO | Android NDK |
|
||||
|
||||
### Windows
|
||||
|
||||
|
||||
@@ -61,49 +61,25 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
|
||||
| `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |
|
||||
| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). |
|
||||
| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). |
|
||||
| `with_tailscale` | :material-check: | 构建 Tailscale 支持,参阅 [Tailscale 端点](/configuration/endpoint/tailscale)。 |
|
||||
| `with_ccm` | :material-check: | 构建 Claude Code Multiplexer 服务支持。 |
|
||||
| `with_ocm` | :material-check: | 构建 OpenAI Codex Multiplexer 服务支持。 |
|
||||
| `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/configuration/outbound/naive/)。 |
|
||||
| `badlinkname` | :material-check: | 启用 `go:linkname` 以访问标准库内部函数。Go 标准库未提供本项目需要的许多底层 API,且在外部重新实现不切实际。用于 kTLS(内核 TLS 卸载)和原始 TLS 记录操作。 |
|
||||
| `tfogo_checklinkname0` | :material-check: | `badlinkname` 的伴随标记。Go 1.23+ 链接器强制限制 `go:linkname` 使用;此标记表示构建使用 `-checklinkname=0` 以绕过该限制。 |
|
||||
| `with_tailscale` | :material-check: | Build with Tailscale support, see [Tailscale endpoint](/configuration/endpoint/tailscale) |
|
||||
| `with_naive_outbound` | :material-close:️ | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/zh/configuration/outbound/naive/)。 |
|
||||
|
||||
除非您确实知道您正在启用什么,否则不建议更改默认构建标签列表。
|
||||
|
||||
## :material-wrench: 链接器标志
|
||||
|
||||
以下 `-ldflags` 在官方构建中使用:
|
||||
|
||||
| 标志 | 说明 |
|
||||
|-------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `-X 'internal/godebug.defaultGODEBUG=multipathtcp=0'` | Go 1.24 默认为监听器启用 Multipath TCP(`multipathtcp=2`)。这可能在底层 socket 上导致错误,且 sing-box 有自己的 MPTCP 控制(`tcp_multi_path` 选项)。此标志禁用 Go 的默认行为。 |
|
||||
| `-checklinkname=0` | Go 1.23+ 链接器拒绝未授权的 `go:linkname` 使用。此标志禁用该检查,需要与 `badlinkname` 构建标记一起使用。 |
|
||||
|
||||
## :material-package-variant: 下游打包者
|
||||
|
||||
默认构建标签列表和链接器标志以文件形式存放在仓库中,供下游打包者直接引用:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `release/DEFAULT_BUILD_TAGS` | Linux(常见架构)、Darwin 和 Android 的默认标签。 |
|
||||
| `release/DEFAULT_BUILD_TAGS_WINDOWS` | Windows 的默认标签(包含 `with_purego`)。 |
|
||||
| `release/DEFAULT_BUILD_TAGS_OTHERS` | 其他平台的默认标签(不含 `with_naive_outbound`)。 |
|
||||
| `release/LDFLAGS` | 必需的链接器标志(参见上文)。 |
|
||||
|
||||
## :material-layers: with_naive_outbound
|
||||
|
||||
NaiveProxy 出站需要根据目标平台进行特殊的构建配置。
|
||||
|
||||
### 支持的平台
|
||||
|
||||
| 平台 | 架构 | 模式 | 要求 |
|
||||
|--------------|----------------------------------------------------------|--------|-----------------------------------------------------|
|
||||
| Linux | amd64, arm64 | purego | 无(官方发布版本已包含库文件) |
|
||||
| Linux | 386, amd64, arm, arm64, mipsle, mips64le, riscv64, loong64 | CGO | Chromium 工具链,运行时需要 glibc >= 2.31(loong64: >= 2.36) |
|
||||
| Linux (musl) | 386, amd64, arm, arm64, mipsle, riscv64, loong64 | CGO | Chromium 工具链 |
|
||||
| Windows | amd64, arm64 | purego | 无(官方发布版本已包含库文件) |
|
||||
| Apple 平台 | * | CGO | Xcode |
|
||||
| Android | * | CGO | Android NDK |
|
||||
| 平台 | 架构 | 模式 | 要求 |
|
||||
|---------------|------------------------|--------|--------------------------------|
|
||||
| Linux | amd64, arm64 | purego | 无(官方发布版本已包含库文件) |
|
||||
| Linux | 386, amd64, arm, arm64 | CGO | Chromium 工具链,运行时需要 glibc >= 2.31 |
|
||||
| Linux (musl) | 386, amd64, arm, arm64 | CGO | Chromium 工具链 |
|
||||
| Windows | amd64, arm64 | purego | 无(官方发布版本已包含库文件) |
|
||||
| Apple 平台 | * | CGO | Xcode |
|
||||
| Android | * | CGO | Android NDK |
|
||||
|
||||
### Windows
|
||||
|
||||
|
||||
@@ -47,17 +47,6 @@ elif command -v rpm >/dev/null 2>&1; then
|
||||
arch=$(uname -m)
|
||||
package_suffix=".rpm"
|
||||
package_install="rpm -i"
|
||||
elif command -v apk >/dev/null 2>&1 && [ -f /etc/os-release ] && grep -q OPENWRT_ARCH /etc/os-release; then
|
||||
os="openwrt"
|
||||
. /etc/os-release
|
||||
arch="$OPENWRT_ARCH"
|
||||
package_suffix=".apk"
|
||||
package_install="apk add --allow-untrusted"
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
os="linux"
|
||||
arch=$(apk --print-arch)
|
||||
package_suffix=".apk"
|
||||
package_install="apk add --allow-untrusted"
|
||||
elif command -v opkg >/dev/null 2>&1; then
|
||||
os="openwrt"
|
||||
. /etc/os-release
|
||||
|
||||
@@ -4,7 +4,8 @@ icon: material/horse
|
||||
|
||||
# Trojan
|
||||
|
||||
Trojan is the most commonly used TLS proxy made in China. It can be used in various combinations.
|
||||
Torjan is the most commonly used TLS proxy made in China. It can be used in various combinations,
|
||||
but only the combination of uTLS and multiplexing is recommended.
|
||||
|
||||
| Protocol and implementation combination | Specification | Resists passive detection | Resists active probes |
|
||||
|-----------------------------------------|----------------------------------------------------------------------|---------------------------|-----------------------|
|
||||
@@ -139,7 +140,11 @@ Trojan is the most commonly used TLS proxy made in China. It can be used in vari
|
||||
"password": "password",
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": "example.org"
|
||||
"server_name": "example.org",
|
||||
"utls": {
|
||||
"enabled": true,
|
||||
"fingerprint": "firefox"
|
||||
}
|
||||
},
|
||||
"multiplex": {
|
||||
"enabled": true
|
||||
@@ -166,7 +171,11 @@ Trojan is the most commonly used TLS proxy made in China. It can be used in vari
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": "example.org",
|
||||
"certificate_path": "/path/to/certificate.pem"
|
||||
"certificate_path": "/path/to/certificate.pem",
|
||||
"utls": {
|
||||
"enabled": true,
|
||||
"fingerprint": "firefox"
|
||||
}
|
||||
},
|
||||
"multiplex": {
|
||||
"enabled": true
|
||||
@@ -189,7 +198,11 @@ Trojan is the most commonly used TLS proxy made in China. It can be used in vari
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": "example.org",
|
||||
"insecure": true
|
||||
"insecure": true,
|
||||
"utls": {
|
||||
"enabled": true,
|
||||
"fingerprint": "firefox"
|
||||
}
|
||||
},
|
||||
"multiplex": {
|
||||
"enabled": true
|
||||
|
||||
@@ -45,7 +45,6 @@ type CacheFile struct {
|
||||
storeRDRC bool
|
||||
rdrcTimeout time.Duration
|
||||
DB *bbolt.DB
|
||||
resetAccess sync.Mutex
|
||||
saveMetadataTimer *time.Timer
|
||||
saveFakeIPAccess sync.RWMutex
|
||||
saveDomain map[netip.Addr]string
|
||||
@@ -170,55 +169,13 @@ func (c *CacheFile) Close() error {
|
||||
return c.DB.Close()
|
||||
}
|
||||
|
||||
func (c *CacheFile) view(fn func(tx *bbolt.Tx) error) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.resetDB()
|
||||
err = E.New("database corrupted: ", r)
|
||||
}
|
||||
}()
|
||||
return c.DB.View(fn)
|
||||
}
|
||||
|
||||
func (c *CacheFile) batch(fn func(tx *bbolt.Tx) error) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.resetDB()
|
||||
err = E.New("database corrupted: ", r)
|
||||
}
|
||||
}()
|
||||
return c.DB.Batch(fn)
|
||||
}
|
||||
|
||||
func (c *CacheFile) update(fn func(tx *bbolt.Tx) error) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.resetDB()
|
||||
err = E.New("database corrupted: ", r)
|
||||
}
|
||||
}()
|
||||
return c.DB.Update(fn)
|
||||
}
|
||||
|
||||
func (c *CacheFile) resetDB() {
|
||||
c.resetAccess.Lock()
|
||||
defer c.resetAccess.Unlock()
|
||||
c.DB.Close()
|
||||
os.Remove(c.path)
|
||||
db, err := bbolt.Open(c.path, 0o666, &bbolt.Options{Timeout: time.Second})
|
||||
if err == nil {
|
||||
_ = filemanager.Chown(c.ctx, c.path)
|
||||
c.DB = db
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CacheFile) StoreFakeIP() bool {
|
||||
return c.storeFakeIP
|
||||
}
|
||||
|
||||
func (c *CacheFile) LoadMode() string {
|
||||
var mode string
|
||||
c.view(func(t *bbolt.Tx) error {
|
||||
c.DB.View(func(t *bbolt.Tx) error {
|
||||
bucket := t.Bucket(bucketMode)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
@@ -236,7 +193,7 @@ func (c *CacheFile) LoadMode() string {
|
||||
}
|
||||
|
||||
func (c *CacheFile) StoreMode(mode string) error {
|
||||
return c.batch(func(t *bbolt.Tx) error {
|
||||
return c.DB.Batch(func(t *bbolt.Tx) error {
|
||||
bucket, err := t.CreateBucketIfNotExists(bucketMode)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -273,7 +230,7 @@ func (c *CacheFile) createBucket(t *bbolt.Tx, key []byte) (*bbolt.Bucket, error)
|
||||
|
||||
func (c *CacheFile) LoadSelected(group string) string {
|
||||
var selected string
|
||||
c.view(func(t *bbolt.Tx) error {
|
||||
c.DB.View(func(t *bbolt.Tx) error {
|
||||
bucket := c.bucket(t, bucketSelected)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
@@ -288,7 +245,7 @@ func (c *CacheFile) LoadSelected(group string) string {
|
||||
}
|
||||
|
||||
func (c *CacheFile) StoreSelected(group, selected string) error {
|
||||
return c.batch(func(t *bbolt.Tx) error {
|
||||
return c.DB.Batch(func(t *bbolt.Tx) error {
|
||||
bucket, err := c.createBucket(t, bucketSelected)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -298,7 +255,7 @@ func (c *CacheFile) StoreSelected(group, selected string) error {
|
||||
}
|
||||
|
||||
func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) {
|
||||
c.view(func(t *bbolt.Tx) error {
|
||||
c.DB.View(func(t *bbolt.Tx) error {
|
||||
bucket := c.bucket(t, bucketExpand)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
@@ -314,7 +271,7 @@ func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) {
|
||||
}
|
||||
|
||||
func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
|
||||
return c.batch(func(t *bbolt.Tx) error {
|
||||
return c.DB.Batch(func(t *bbolt.Tx) error {
|
||||
bucket, err := c.createBucket(t, bucketExpand)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -329,7 +286,7 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
|
||||
|
||||
func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary {
|
||||
var savedSet adapter.SavedBinary
|
||||
err := c.view(func(t *bbolt.Tx) error {
|
||||
err := c.DB.View(func(t *bbolt.Tx) error {
|
||||
bucket := c.bucket(t, bucketRuleSet)
|
||||
if bucket == nil {
|
||||
return os.ErrNotExist
|
||||
@@ -347,7 +304,7 @@ func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary {
|
||||
}
|
||||
|
||||
func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error {
|
||||
return c.batch(func(t *bbolt.Tx) error {
|
||||
return c.DB.Batch(func(t *bbolt.Tx) error {
|
||||
bucket, err := c.createBucket(t, bucketRuleSet)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -23,7 +23,7 @@ var (
|
||||
|
||||
func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
|
||||
var metadata adapter.FakeIPMetadata
|
||||
err := c.batch(func(tx *bbolt.Tx) error {
|
||||
err := c.DB.Batch(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket(bucketFakeIP)
|
||||
if bucket == nil {
|
||||
return os.ErrNotExist
|
||||
@@ -45,7 +45,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
|
||||
}
|
||||
|
||||
func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error {
|
||||
return c.batch(func(tx *bbolt.Tx) error {
|
||||
return c.DB.Batch(func(tx *bbolt.Tx) error {
|
||||
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -69,7 +69,7 @@ func (c *CacheFile) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) {
|
||||
}
|
||||
|
||||
func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error {
|
||||
return c.batch(func(tx *bbolt.Tx) error {
|
||||
return c.DB.Batch(func(tx *bbolt.Tx) error {
|
||||
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -136,7 +136,7 @@ func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) {
|
||||
return cachedDomain, true
|
||||
}
|
||||
var domain string
|
||||
_ = c.view(func(tx *bbolt.Tx) error {
|
||||
_ = c.DB.View(func(tx *bbolt.Tx) error {
|
||||
bucket := tx.Bucket(bucketFakeIP)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
@@ -163,7 +163,7 @@ func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bo
|
||||
return cachedAddress, true
|
||||
}
|
||||
var address netip.Addr
|
||||
_ = c.view(func(tx *bbolt.Tx) error {
|
||||
_ = c.DB.View(func(tx *bbolt.Tx) error {
|
||||
var bucket *bbolt.Bucket
|
||||
if isIPv6 {
|
||||
bucket = tx.Bucket(bucketFakeIPDomain6)
|
||||
@@ -180,7 +180,7 @@ func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bo
|
||||
}
|
||||
|
||||
func (c *CacheFile) FakeIPReset() error {
|
||||
return c.batch(func(tx *bbolt.Tx) error {
|
||||
return c.DB.Batch(func(tx *bbolt.Tx) error {
|
||||
err := tx.DeleteBucket(bucketFakeIP)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -31,7 +31,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (
|
||||
copy(key[2:], qName)
|
||||
defer buf.Put(key)
|
||||
var deleteCache bool
|
||||
err := c.view(func(tx *bbolt.Tx) error {
|
||||
err := c.DB.View(func(tx *bbolt.Tx) error {
|
||||
bucket := c.bucket(tx, bucketRDRC)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
@@ -56,7 +56,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (
|
||||
return
|
||||
}
|
||||
if deleteCache {
|
||||
c.update(func(tx *bbolt.Tx) error {
|
||||
c.DB.Update(func(tx *bbolt.Tx) error {
|
||||
bucket := c.bucket(tx, bucketRDRC)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
@@ -72,7 +72,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (
|
||||
}
|
||||
|
||||
func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) error {
|
||||
return c.batch(func(tx *bbolt.Tx) error {
|
||||
return c.DB.Batch(func(tx *bbolt.Tx) error {
|
||||
bucket, err := c.createBucket(tx, bucketRDRC)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,7 +2,6 @@ package clashapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
@@ -28,7 +27,7 @@ func (s *Server) setupMetaAPI(r chi.Router) {
|
||||
})
|
||||
r.Mount("/", middleware.Profiler())
|
||||
}
|
||||
r.Get("/memory", memory(s.ctx, s.trafficManager))
|
||||
r.Get("/memory", memory(s.trafficManager))
|
||||
r.Mount("/group", groupRouter(s))
|
||||
r.Mount("/upgrade", upgradeRouter(s))
|
||||
}
|
||||
@@ -38,7 +37,7 @@ type Memory struct {
|
||||
OSLimit uint64 `json:"oslimit"` // maybe we need it in the future
|
||||
}
|
||||
|
||||
func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
||||
func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var conn net.Conn
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
@@ -47,7 +46,6 @@ func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w h
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
}
|
||||
|
||||
if conn == nil {
|
||||
@@ -60,12 +58,7 @@ func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w h
|
||||
buf := &bytes.Buffer{}
|
||||
var err error
|
||||
first := true
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-tick.C:
|
||||
}
|
||||
for range tick.C {
|
||||
buf.Reset()
|
||||
|
||||
inuse := trafficManager.Snapshot().Memory
|
||||
|
||||
@@ -2,7 +2,6 @@ package clashapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -18,15 +17,15 @@ import (
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
func connectionRouter(ctx context.Context, router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
|
||||
func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", getConnections(ctx, trafficManager))
|
||||
r.Get("/", getConnections(trafficManager))
|
||||
r.Delete("/", closeAllConnections(router, trafficManager))
|
||||
r.Delete("/{id}", closeConnection(trafficManager))
|
||||
return r
|
||||
}
|
||||
|
||||
func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
||||
func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Upgrade") != "websocket" {
|
||||
snapshot := trafficManager.Snapshot()
|
||||
@@ -38,7 +37,6 @@ func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
intervalStr := r.URL.Query().Get("interval")
|
||||
interval := 1000
|
||||
@@ -69,12 +67,7 @@ func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager)
|
||||
|
||||
tick := time.NewTicker(time.Millisecond * time.Duration(interval))
|
||||
defer tick.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-tick.C:
|
||||
}
|
||||
for range tick.C {
|
||||
if err = sendSnapshot(); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -115,13 +115,13 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
||||
chiRouter.Group(func(r chi.Router) {
|
||||
r.Use(authentication(options.Secret))
|
||||
r.Get("/", hello(options.ExternalUI != ""))
|
||||
r.Get("/logs", getLogs(s.ctx, logFactory))
|
||||
r.Get("/traffic", traffic(s.ctx, trafficManager))
|
||||
r.Get("/logs", getLogs(logFactory))
|
||||
r.Get("/traffic", traffic(trafficManager))
|
||||
r.Get("/version", version)
|
||||
r.Mount("/configs", configRouter(s, logFactory))
|
||||
r.Mount("/proxies", proxyRouter(s, s.router))
|
||||
r.Mount("/rules", ruleRouter(s.router))
|
||||
r.Mount("/connections", connectionRouter(s.ctx, s.router, trafficManager))
|
||||
r.Mount("/connections", connectionRouter(s.router, trafficManager))
|
||||
r.Mount("/providers/proxies", proxyProviderRouter())
|
||||
r.Mount("/providers/rules", ruleProviderRouter())
|
||||
r.Mount("/script", scriptRouter())
|
||||
@@ -303,7 +303,7 @@ type Traffic struct {
|
||||
Down int64 `json:"down"`
|
||||
}
|
||||
|
||||
func traffic(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
||||
func traffic(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var conn net.Conn
|
||||
if r.Header.Get("Upgrade") == "websocket" {
|
||||
@@ -324,12 +324,7 @@ func traffic(ctx context.Context, trafficManager *trafficontrol.Manager) func(w
|
||||
defer tick.Stop()
|
||||
buf := &bytes.Buffer{}
|
||||
uploadTotal, downloadTotal := trafficManager.Total()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-tick.C:
|
||||
}
|
||||
for range tick.C {
|
||||
buf.Reset()
|
||||
uploadTotalNew, downloadTotalNew := trafficManager.Total()
|
||||
err := json.NewEncoder(buf).Encode(Traffic{
|
||||
@@ -360,7 +355,7 @@ type Log struct {
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
|
||||
func getLogs(ctx context.Context, logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) {
|
||||
func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
levelText := r.URL.Query().Get("level")
|
||||
if levelText == "" {
|
||||
@@ -399,8 +394,6 @@ func getLogs(ctx context.Context, logFactory log.ObservableFactory) func(w http.
|
||||
var logEntry log.Entry
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-done:
|
||||
return
|
||||
case logEntry = <-subscription:
|
||||
|
||||
@@ -10,31 +10,11 @@ import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
type ConnectionEventType int
|
||||
|
||||
const (
|
||||
ConnectionEventNew ConnectionEventType = iota
|
||||
ConnectionEventUpdate
|
||||
ConnectionEventClosed
|
||||
)
|
||||
|
||||
type ConnectionEvent struct {
|
||||
Type ConnectionEventType
|
||||
ID uuid.UUID
|
||||
Metadata *TrackerMetadata
|
||||
UplinkDelta int64
|
||||
DownlinkDelta int64
|
||||
ClosedAt time.Time
|
||||
}
|
||||
|
||||
const closedConnectionsLimit = 1000
|
||||
|
||||
type Manager struct {
|
||||
uploadTotal atomic.Int64
|
||||
downloadTotal atomic.Int64
|
||||
@@ -42,52 +22,29 @@ type Manager struct {
|
||||
connections compatible.Map[uuid.UUID, Tracker]
|
||||
closedConnectionsAccess sync.Mutex
|
||||
closedConnections list.List[TrackerMetadata]
|
||||
memory uint64
|
||||
|
||||
eventSubscriber *observable.Subscriber[ConnectionEvent]
|
||||
// process *process.Process
|
||||
memory uint64
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{}
|
||||
}
|
||||
|
||||
func (m *Manager) SetEventHook(subscriber *observable.Subscriber[ConnectionEvent]) {
|
||||
m.eventSubscriber = subscriber
|
||||
}
|
||||
|
||||
func (m *Manager) Join(c Tracker) {
|
||||
metadata := c.Metadata()
|
||||
m.connections.Store(metadata.ID, c)
|
||||
if m.eventSubscriber != nil {
|
||||
m.eventSubscriber.Emit(ConnectionEvent{
|
||||
Type: ConnectionEventNew,
|
||||
ID: metadata.ID,
|
||||
Metadata: metadata,
|
||||
})
|
||||
}
|
||||
m.connections.Store(c.Metadata().ID, c)
|
||||
}
|
||||
|
||||
func (m *Manager) Leave(c Tracker) {
|
||||
metadata := c.Metadata()
|
||||
_, loaded := m.connections.LoadAndDelete(metadata.ID)
|
||||
if loaded {
|
||||
closedAt := time.Now()
|
||||
metadata.ClosedAt = closedAt
|
||||
metadataCopy := *metadata
|
||||
metadata.ClosedAt = time.Now()
|
||||
m.closedConnectionsAccess.Lock()
|
||||
if m.closedConnections.Len() >= closedConnectionsLimit {
|
||||
defer m.closedConnectionsAccess.Unlock()
|
||||
if m.closedConnections.Len() >= 1000 {
|
||||
m.closedConnections.PopFront()
|
||||
}
|
||||
m.closedConnections.PushBack(metadataCopy)
|
||||
m.closedConnectionsAccess.Unlock()
|
||||
if m.eventSubscriber != nil {
|
||||
m.eventSubscriber.Emit(ConnectionEvent{
|
||||
Type: ConnectionEventClosed,
|
||||
ID: metadata.ID,
|
||||
Metadata: &metadataCopy,
|
||||
ClosedAt: closedAt,
|
||||
})
|
||||
}
|
||||
m.closedConnections.PushBack(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,8 +64,8 @@ func (m *Manager) ConnectionsLen() int {
|
||||
return m.connections.Len()
|
||||
}
|
||||
|
||||
func (m *Manager) Connections() []*TrackerMetadata {
|
||||
var connections []*TrackerMetadata
|
||||
func (m *Manager) Connections() []TrackerMetadata {
|
||||
var connections []TrackerMetadata
|
||||
m.connections.Range(func(_ uuid.UUID, value Tracker) bool {
|
||||
connections = append(connections, value.Metadata())
|
||||
return true
|
||||
@@ -116,18 +73,10 @@ func (m *Manager) Connections() []*TrackerMetadata {
|
||||
return connections
|
||||
}
|
||||
|
||||
func (m *Manager) ClosedConnections() []*TrackerMetadata {
|
||||
func (m *Manager) ClosedConnections() []TrackerMetadata {
|
||||
m.closedConnectionsAccess.Lock()
|
||||
values := m.closedConnections.Array()
|
||||
m.closedConnectionsAccess.Unlock()
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
connections := make([]*TrackerMetadata, len(values))
|
||||
for i := range values {
|
||||
connections[i] = &values[i]
|
||||
}
|
||||
return connections
|
||||
defer m.closedConnectionsAccess.Unlock()
|
||||
return m.closedConnections.Array()
|
||||
}
|
||||
|
||||
func (m *Manager) Connection(id uuid.UUID) Tracker {
|
||||
@@ -175,7 +124,7 @@ func (s *Snapshot) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"downloadTotal": s.Download,
|
||||
"uploadTotal": s.Upload,
|
||||
"connections": common.Map(s.Connections, func(t Tracker) *TrackerMetadata { return t.Metadata() }),
|
||||
"connections": common.Map(s.Connections, func(t Tracker) TrackerMetadata { return t.Metadata() }),
|
||||
"memory": s.Memory,
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user