Compare commits

..

22 Commits

Author SHA1 Message Date
Sergei Maklagin
20bf40e822 Update dependencies 2026-03-11 17:32:12 +03:00
Sergei Maklagin
861aff60f0 Update dependencies 2026-03-11 06:45:24 +03:00
Sergei Maklagin
2df1cffb0f Resolve conflicts 2026-03-10 08:43:01 +03:00
Sergei Maklagin
bc6ca6e2ea Update sing-box core 2026-03-10 04:50:32 +03:00
Sergei Maklagin
0503006f48 Add Kmutex 2026-03-03 00:51:20 +03:00
Sergei Maklagin
517f5152e7 Add migrate 2026-03-03 00:51:04 +03:00
Sergei Maklagin
b1b7aa81cd Update Amnezia H1-H4 format 2026-03-02 21:48:54 +03:00
Sergei Maklagin
195e941c35 Fix typo 2026-03-02 21:27:47 +03:00
Sergei Maklagin
35bc351564 Fix failover 2026-03-02 19:48:06 +03:00
Sergei Maklagin
290dbed7b8 Fix failover 2026-03-02 19:33:59 +03:00
Sergei Maklagin
d7a8207f44 Update AmneziaWG 2026-03-02 19:33:07 +03:00
Sergei Maklagin
57c5ca13eb Fix bond outbound 2026-03-02 19:31:23 +03:00
Sergei Maklagin
7fc33134fb Update AmneziaWG 2026-03-01 16:42:21 +03:00
Sergei Maklagin
881ab6d436 Fix examples 2026-02-27 00:47:22 +03:00
Sergei Maklagin
0443b93328 Update README.md 2026-02-27 00:19:37 +03:00
Sergei Maklagin
75557830a8 Merge branch 'extended' into extended-next 2026-02-26 22:58:59 +03:00
Sergei Maklagin
9d5273ba1e Resolve conflicts 2026-02-26 22:58:45 +03:00
Sergei Maklagin
5f2a65f01b Add examples 2026-02-26 22:57:44 +03:00
Sergei Maklagin
06a519db27 Resolve conflicts 2026-02-26 22:57:25 +03:00
Sergei Maklagin
65e73fe817 Add examples 2026-02-26 22:55:24 +03:00
Sergei Maklagin
c0aa3480c5 Add admin panel, manager, node_manager, bandwidth limiter, connection limiter, bonding, failover, vless encryption, mkcp transport 2026-02-26 22:44:31 +03:00
Sergei Maklagin
69f6c75dd7 Add vless encryption 2026-02-26 18:03:59 +03:00
321 changed files with 17344 additions and 5892 deletions

View File

@@ -4,7 +4,6 @@
--license GPL-3.0-or-later --license GPL-3.0-or-later
--description "The universal proxy platform." --description "The universal proxy platform."
--url "https://sing-box.sagernet.org/" --url "https://sing-box.sagernet.org/"
--vendor SagerNet
--maintainer "nekohasekai <contact-git@sekai.icu>" --maintainer "nekohasekai <contact-git@sekai.icu>"
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues" --deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes --no-deb-generate-changes

View File

@@ -1 +1 @@
e4926ba205fae5351e3d3eeafff7e7029654424a cba7b9ac0399055aa49fbdc57c03c374f58e1597

View File

@@ -1,94 +0,0 @@
#!/usr/bin/env bash
set -e -o pipefail
prepare_apk_root() {
# apk mkpkg resolves owner/group names through --root/etc/{passwd,group}.
APK_ROOT_DIR=$(mktemp -d)
mkdir -p "$APK_ROOT_DIR/etc"
cat > "$APK_ROOT_DIR/etc/passwd" <<EOF
root:x:$(id -u):$(id -g):root:/root:/sbin/nologin
EOF
cat > "$APK_ROOT_DIR/etc/group" <<EOF
root:x:$(id -g):root
EOF
}
ARCHITECTURE="$1"
VERSION="$2"
BINARY_PATH="$3"
OUTPUT_PATH="$4"
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
exit 1
fi
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
# Convert version to APK format:
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
# 1.13.0 -> 1.13.0-r0
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
APK_VERSION="${APK_VERSION}-r0"
ROOT_DIR=$(mktemp -d)
prepare_apk_root
trap 'rm -rf "$ROOT_DIR" "$APK_ROOT_DIR"' EXIT
# Binary
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
# Config files
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
install -Dm755 "$PROJECT/release/config/sing-box.initd" "$ROOT_DIR/etc/init.d/sing-box"
install -Dm644 "$PROJECT/release/config/sing-box.confd" "$ROOT_DIR/etc/conf.d/sing-box"
# Service files
install -Dm644 "$PROJECT/release/config/sing-box.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box.service"
install -Dm644 "$PROJECT/release/config/sing-box@.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box@.service"
# Completions
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
# License
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
# APK metadata
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
mkdir -p "$PACKAGES_DIR"
# .conffiles
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
/etc/conf.d/sing-box
/etc/init.d/sing-box
/etc/sing-box/config.json
EOF
# .conffiles_static (sha256 checksums)
while IFS= read -r conffile; do
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
echo "$conffile $sha256"
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
# .list (all files, excluding lib/apk/packages/ metadata)
(cd "$ROOT_DIR" && find . -type f -o -type l) \
| sed 's|^\./|/|' \
| grep -v '^/lib/apk/packages/' \
| sort > "$PACKAGES_DIR/.list"
# Build APK
apk --root "$APK_ROOT_DIR" mkpkg \
--info "name:sing-box" \
--info "version:${APK_VERSION}" \
--info "description:The universal proxy platform." \
--info "arch:${ARCHITECTURE}" \
--info "license:GPL-3.0-or-later with name use or association addition" \
--info "origin:sing-box" \
--info "url:https://sing-box.sagernet.org/" \
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
--files "$ROOT_DIR" \
--output "$OUTPUT_PATH"

View File

@@ -1,93 +0,0 @@
#!/usr/bin/env bash
set -e -o pipefail
prepare_apk_root() {
# apk mkpkg resolves owner/group names through --root/etc/{passwd,group}.
APK_ROOT_DIR=$(mktemp -d)
mkdir -p "$APK_ROOT_DIR/etc"
cat > "$APK_ROOT_DIR/etc/passwd" <<EOF
root:x:$(id -u):$(id -g):root:/root:/sbin/nologin
EOF
cat > "$APK_ROOT_DIR/etc/group" <<EOF
root:x:$(id -g):root
EOF
}
ARCHITECTURE="$1"
VERSION="$2"
BINARY_PATH="$3"
OUTPUT_PATH="$4"
if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then
echo "Usage: $0 <architecture> <version> <binary_path> <output_path>"
exit 1
fi
PROJECT=$(cd "$(dirname "$0")/.."; pwd)
# Convert version to APK format:
# 1.13.0-beta.8 -> 1.13.0_beta8-r0
# 1.13.0-rc.3 -> 1.13.0_rc3-r0
# 1.13.0 -> 1.13.0-r0
APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/')
APK_VERSION="${APK_VERSION}-r0"
ROOT_DIR=$(mktemp -d)
prepare_apk_root
trap 'rm -rf "$ROOT_DIR" "$APK_ROOT_DIR"' EXIT
# Binary
install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box"
# Config files
install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json"
install -Dm644 "$PROJECT/release/config/openwrt.conf" "$ROOT_DIR/etc/config/sing-box"
install -Dm755 "$PROJECT/release/config/openwrt.init" "$ROOT_DIR/etc/init.d/sing-box"
install -Dm644 "$PROJECT/release/config/openwrt.keep" "$ROOT_DIR/lib/upgrade/keep.d/sing-box"
# Completions
install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash"
install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish"
install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box"
# License
install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE"
# APK metadata
PACKAGES_DIR="$ROOT_DIR/lib/apk/packages"
mkdir -p "$PACKAGES_DIR"
# .conffiles
cat > "$PACKAGES_DIR/.conffiles" <<'EOF'
/etc/config/sing-box
/etc/sing-box/config.json
EOF
# .conffiles_static (sha256 checksums)
while IFS= read -r conffile; do
sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1)
echo "$conffile $sha256"
done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static"
# .list (all files, excluding lib/apk/packages/ metadata)
(cd "$ROOT_DIR" && find . -type f -o -type l) \
| sed 's|^\./|/|' \
| grep -v '^/lib/apk/packages/' \
| sort > "$PACKAGES_DIR/.list"
# Build APK
apk --root "$APK_ROOT_DIR" mkpkg \
--info "name:sing-box" \
--info "version:${APK_VERSION}" \
--info "description:The universal proxy platform." \
--info "arch:${ARCHITECTURE}" \
--info "license:GPL-3.0-or-later" \
--info "origin:sing-box" \
--info "url:https://sing-box.sagernet.org/" \
--info "maintainer:nekohasekai <contact-git@sekai.icu>" \
--info "depends:ca-bundle kmod-inet-diag kmod-tun firewall4 kmod-nft-queue" \
--info "provider-priority:100" \
--script "pre-deinstall:${PROJECT}/release/config/openwrt.prerm" \
--files "$ROOT_DIR" \
--output "$OUTPUT_PATH"

View File

@@ -1,33 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
branches=$(git branch -r --contains HEAD)
if echo "$branches" | grep -q 'origin/stable'; then
track=stable
elif echo "$branches" | grep -q 'origin/testing'; then
track=testing
elif echo "$branches" | grep -q 'origin/oldstable'; then
track=oldstable
else
echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2
exit 1
fi
if [[ "$track" == "stable" ]]; then
tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
if [[ -n "$tag" && "$tag" == *"-"* ]]; then
track=beta
fi
fi
case "$track" in
stable) name=sing-box; docker_tag=latest ;;
beta) name=sing-box-beta; docker_tag=latest-beta ;;
testing) name=sing-box-testing; docker_tag=latest-testing ;;
oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;;
esac
echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2
echo "TRACK=${track}" >> "$GITHUB_ENV"
echo "NAME=${name}" >> "$GITHUB_ENV"
echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV"

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="1.25.9"
PATCH_COMMITS=(
"afe69d3cec1c6dcf0f1797b20546795730850070"
"1ed289b0cf87dc5aae9c6fe1aa5f200a83412938"
)
CURL_ARGS=(
-fL
--silent
--show-error
)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
fi
mkdir -p "$HOME/go"
cd "$HOME/go"
wget "https://dl.google.com/go/go${VERSION}.darwin-arm64.tar.gz"
tar -xzf "go${VERSION}.darwin-arm64.tar.gz"
#cp -a go go_bootstrap
mv go go_osx
cd go_osx
# these patch URLs only work on golang1.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/SagerNet/go/commits/release-branch.go1.25/
# revert:
# 33d3f603c1: "cmd/link/internal/ld: use 12.0.0 OS/SDK versions for macOS linking"
# 937368f84e: "crypto/x509: change how we retrieve chains on darwin"
for patch_commit in "${PATCH_COMMITS[@]}"; do
curl "${CURL_ARGS[@]}" "https://github.com/SagerNet/go/commit/${patch_commit}.diff" | patch --verbose -p 1
done
# Rebuild is not needed: we build with CGO_ENABLED=1, so Apple's external
# linker handles LC_BUILD_VERSION via MACOSX_DEPLOYMENT_TARGET, and the
# stdlib (crypto/x509) is compiled from patched src automatically.
#cd src
#GOROOT_BOOTSTRAP="$HOME/go/go_bootstrap" ./make.bash
#cd ../..
#rm -rf go_bootstrap "go${VERSION}.darwin-arm64.tar.gz"

View File

@@ -1,35 +1,16 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail VERSION="1.25.7"
VERSION="1.25.9" mkdir -p $HOME/go
PATCH_COMMITS=( cd $HOME/go
"466f6c7a29bc098b0d4c987b803c779222894a11"
"1bdabae205052afe1dadb2ad6f1ba612cdbc532a"
"a90777dcf692dd2168577853ba743b4338721b06"
"f6bddda4e8ff58a957462a1a09562924d5f3d05c"
"bed309eff415bcb3c77dd4bc3277b682b89a388d"
"34b899c2fb39b092db4fa67c4417e41dc046be4b"
)
CURL_ARGS=(
-fL
--silent
--show-error
)
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
fi
mkdir -p "$HOME/go"
cd "$HOME/go"
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz" wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz" tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go go_win7 mv go go_win7
cd go_win7 cd go_win7
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# these patch URLs only work on golang1.25.x # this patch file only works on golang1.25.x
# that means after golang1.26 release it must be changed # that means after golang1.26 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/ # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
# revert: # revert:
@@ -37,10 +18,10 @@ cd go_win7
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
# fixes:
# bed309eff415bcb3c77dd4bc3277b682b89a388d: "Fix os.RemoveAll not working on Windows7"
# 34b899c2fb39b092db4fa67c4417e41dc046be4b: "Revert \"os: remove 5ms sleep on Windows in (*Process).Wait\""
for patch_commit in "${PATCH_COMMITS[@]}"; do alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1
done curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1

View File

@@ -47,7 +47,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.9 go-version: ~1.25.7
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -72,27 +72,27 @@ jobs:
include: include:
- { os: linux, arch: amd64, variant: purego, naive: true } - { os: linux, arch: amd64, variant: purego, naive: true }
- { os: linux, arch: amd64, variant: glibc, naive: true } - { os: linux, arch: amd64, variant: glibc, naive: true }
- { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, alpine: x86_64, openwrt: "x86_64" } - { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
- { os: linux, arch: arm64, variant: purego, naive: true } - { os: linux, arch: arm64, variant: purego, naive: true }
- { os: linux, arch: arm64, variant: glibc, naive: true } - { os: linux, arch: arm64, variant: glibc, naive: true }
- { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, alpine: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" } - { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
- { os: linux, arch: "386", go386: sse2 } - { os: linux, arch: "386", go386: sse2 }
- { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 } - { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 }
- { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, alpine: x86, openwrt: "i386_pentium4" } - { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
- { os: linux, arch: arm, goarm: "7" } - { os: linux, arch: arm, goarm: "7" }
- { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" } - { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" }
- { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, alpine: armv7, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" } - { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" }
- { os: linux, arch: mipsle, gomips: hardfloat, naive: true, variant: glibc } - { os: linux, arch: mipsle, gomips: hardfloat, naive: true, variant: glibc }
- { os: linux, arch: mipsle, gomips: softfloat, naive: true, variant: musl, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" } - { os: linux, arch: mipsle, gomips: softfloat, naive: true, variant: musl, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
- { os: linux, arch: mips64le, gomips: hardfloat, naive: true, variant: glibc, debian: mips64el, rpm: mips64el } - { os: linux, arch: mips64le, gomips: hardfloat, naive: true, variant: glibc, debian: mips64el, rpm: mips64el }
- { os: linux, arch: riscv64, naive: true, variant: glibc } - { os: linux, arch: riscv64, naive: true, variant: glibc }
- { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, alpine: riscv64, openwrt: "riscv64_generic" } - { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
- { os: linux, arch: loong64, naive: true, variant: glibc } - { os: linux, arch: loong64, naive: true, variant: glibc }
- { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, alpine: loongarch64, openwrt: "loongarch64_generic" } - { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" } - { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" } - { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
@@ -121,10 +121,15 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
if: ${{ ! matrix.legacy_win7 }} if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.9 go-version: ~1.25.7
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
go-version: ~1.24.10
- name: Cache Go for Windows 7 - name: Cache Go for Windows 7
if: matrix.legacy_win7 if: matrix.legacy_win7
id: cache-go-for-windows7 id: cache-go-for-windows7
@@ -132,11 +137,9 @@ jobs:
with: with:
path: | path: |
~/go/go_win7 ~/go/go_win7
key: go_win7_1258 key: go_win7_1255
- name: Setup Go for Windows 7 - name: Setup Go for Windows 7
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true' if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |- run: |-
.github/setup_go_for_windows7.sh .github/setup_go_for_windows7.sh
- name: Setup Go for Windows 7 - name: Setup Go for Windows 7
@@ -396,30 +399,6 @@ jobs:
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk" .github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
done done
rm "dist/openwrt.deb" rm "dist/openwrt.deb"
- name: Install apk-tools
if: matrix.openwrt != '' || matrix.alpine != ''
run: |-
docker run --rm -v /usr/local/bin:/mnt alpine:edge sh -c "apk add --no-cache apk-tools-static && cp /sbin/apk.static /mnt/apk && chmod +x /mnt/apk"
- name: Package OpenWrt APK
if: matrix.openwrt != ''
run: |-
set -xeuo pipefail
for architecture in ${{ matrix.openwrt }}; do
.github/build_openwrt_apk.sh \
"$architecture" \
"${{ needs.calculate_version.outputs.version }}" \
"dist/sing-box" \
"dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.apk"
done
- name: Package Alpine APK
if: matrix.alpine != ''
run: |-
set -xeuo pipefail
.github/build_alpine_apk.sh \
"${{ matrix.alpine }}" \
"${{ needs.calculate_version.outputs.version }}" \
"dist/sing-box" \
"dist/sing-box_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.alpine }}.apk"
- name: Archive - name: Archive
run: | run: |
set -xeuo pipefail set -xeuo pipefail
@@ -455,36 +434,22 @@ jobs:
include: include:
- { arch: amd64 } - { arch: amd64 }
- { arch: arm64 } - { arch: arm64 }
- { arch: amd64, legacy_osx: true, legacy_name: "macos-10.13" } - { arch: amd64, legacy_go124: true, legacy_name: "macos-11" }
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
if: ${{ ! matrix.legacy_osx }} if: ${{ ! matrix.legacy_go124 }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.3 go-version: ^1.25.3
- name: Cache Go for macOS 10.13 - name: Setup Go 1.24
if: matrix.legacy_osx if: matrix.legacy_go124
id: cache-go-for-macos1013 uses: actions/setup-go@v5
uses: actions/cache@v4
with: with:
path: | go-version: ~1.24.6
~/go/go_osx
key: go_osx_1258
- name: Setup Go for macOS 10.13
if: matrix.legacy_osx && steps.cache-go-for-macos1013.outputs.cache-hit != 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |-
.github/setup_go_for_macos1013.sh
- name: Setup Go for macOS 10.13
if: matrix.legacy_osx
run: |-
echo "PATH=$HOME/go/go_osx/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_osx" >> $GITHUB_ENV
- name: Set tag - name: Set tag
run: |- run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
@@ -492,7 +457,7 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail set -xeuo pipefail
if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then if [[ "${{ matrix.legacy_go124 }}" != "true" ]]; then
TAGS=$(cat release/DEFAULT_BUILD_TAGS) TAGS=$(cat release/DEFAULT_BUILD_TAGS)
else else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
@@ -512,7 +477,6 @@ jobs:
CGO_ENABLED: "1" CGO_ENABLED: "1"
GOOS: darwin GOOS: darwin
GOARCH: ${{ matrix.arch }} GOARCH: ${{ matrix.arch }}
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.legacy_osx && '10.13' || '' }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set name - name: Set name
run: |- run: |-
@@ -641,7 +605,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.9 go-version: ~1.25.7
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -731,7 +695,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.9 go-version: ~1.25.7
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -830,7 +794,7 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.9 go-version: ~1.25.7
- name: Set tag - name: Set tag
if: matrix.if if: matrix.if
run: |- run: |-

View File

@@ -19,6 +19,7 @@ env:
jobs: jobs:
build_binary: build_binary:
name: Build binary name: Build binary
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: true
@@ -55,7 +56,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.9 go-version: ~1.25.7
- name: Clone cronet-go - name: Clone cronet-go
if: matrix.naive if: matrix.naive
run: | run: |
@@ -259,13 +260,13 @@ jobs:
fi fi
echo "ref=$ref" echo "ref=$ref"
echo "ref=$ref" >> $GITHUB_OUTPUT echo "ref=$ref" >> $GITHUB_OUTPUT
- name: Checkout if [[ $ref == *"-"* ]]; then
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 latest=latest-beta
with: else
ref: ${{ steps.ref.outputs.ref }} latest=latest
fetch-depth: 0 fi
- name: Detect track echo "latest=$latest"
run: bash .github/detect_track.sh echo "latest=$latest" >> $GITHUB_OUTPUT
- name: Download digests - name: Download digests
uses: actions/download-artifact@v5 uses: actions/download-artifact@v5
with: with:
@@ -285,11 +286,11 @@ jobs:
working-directory: /tmp/digests working-directory: /tmp/digests
run: | run: |
docker buildx imagetools create \ docker buildx imagetools create \
-t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \ -t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \ -t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image - name: Inspect image
if: github.event_name != 'push' if: github.event_name != 'push'
run: | run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }} docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }} docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}

View File

@@ -11,6 +11,11 @@ on:
description: "Version name" description: "Version name"
required: true required: true
type: string type: string
forceBeta:
description: "Force beta"
required: false
type: boolean
default: false
release: release:
types: types:
- published - published
@@ -18,6 +23,7 @@ on:
jobs: jobs:
calculate_version: calculate_version:
name: Calculate version name: Calculate version
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
version: ${{ steps.outputs.outputs.version }} version: ${{ steps.outputs.outputs.version }}
@@ -29,7 +35,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.9 go-version: ~1.25.7
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -72,7 +78,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.25.9 go-version: ~1.25.7
- name: Clone cronet-go - name: Clone cronet-go
if: matrix.naive if: matrix.naive
run: | run: |
@@ -162,8 +168,14 @@ jobs:
- name: Set mtime - name: Set mtime
run: |- run: |-
TZ=UTC touch -t '197001010000' dist/sing-box TZ=UTC touch -t '197001010000' dist/sing-box
- name: Detect track - name: Set name
run: bash .github/detect_track.sh if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
run: |-
echo "NAME=sing-box" >> "$GITHUB_ENV"
- name: Set beta name
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta
run: |-
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
- name: Set version - name: Set version
run: |- run: |-
PKG_VERSION="${{ needs.calculate_version.outputs.version }}" PKG_VERSION="${{ needs.calculate_version.outputs.version }}"

191
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,191 @@
version: 2
project_name: sing-box
builds:
- &template
id: main
main: ./cmd/sing-box
flags:
- -v
- -trimpath
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
- GOTOOLCHAIN=local
targets:
- linux_386
- linux_amd64_v1
- linux_arm64
- linux_arm_6
- linux_arm_7
- linux_s390x
- linux_riscv64
- linux_mips
- linux_mips_softfloat
- linux_mipsle
- linux_mipsle_softfloat
- linux_mips64
- linux_mips64le
- windows_amd64_v1
- windows_386
- windows_arm64
- darwin_amd64_v1
- darwin_arm64
mod_timestamp: '{{ .CommitTimestamp }}'
- id: manager
main: ./cmd/sing-box
flags:
- -v
- -trimpath
ldflags:
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
- -s
- -buildid=
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
- with_manager
- with_admin_panel
env:
- CGO_ENABLED=0
- GOTOOLCHAIN=local
targets:
- linux_386
- linux_amd64_v1
- linux_arm64
- linux_arm_6
- linux_arm_7
- linux_s390x
- linux_riscv64
- linux_mips
- linux_mips_softfloat
- linux_mipsle
- linux_mipsle_softfloat
- linux_mips64
- linux_mips64le
- windows_amd64_v1
- windows_386
- windows_arm64
- darwin_amd64_v1
- darwin_arm64
mod_timestamp: '{{ .CommitTimestamp }}'
- id: legacy
<<: *template
tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_utls
- with_acme
- with_clash_api
- with_tailscale
env:
- CGO_ENABLED=0
targets:
- windows_amd64_v1
- windows_386
- id: android
<<: *template
env:
- CGO_ENABLED=1
- GOTOOLCHAIN=local
overrides:
- goos: android
goarch: arm
goarm: 7
env:
- CC=armv7a-linux-androideabi21-clang
- CXX=armv7a-linux-androideabi21-clang++
- goos: android
goarch: arm64
env:
- CC=aarch64-linux-android21-clang
- CXX=aarch64-linux-android21-clang++
- goos: android
goarch: 386
env:
- CC=i686-linux-android21-clang
- CXX=i686-linux-android21-clang++
- goos: android
goarch: amd64
goamd64: v1
env:
- CC=x86_64-linux-android21-clang
- CXX=x86_64-linux-android21-clang++
targets:
- android_arm_7
- android_arm64
- android_386
- android_amd64
archives:
- &template
id: archive
builds:
- main
- android
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
wrap_in_directory: true
files:
- LICENSE
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
- id: archive_with_manager
builds:
- manager
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
wrap_in_directory: true
files:
- LICENSE
name_template: '{{ .ProjectName }}-{{ .Version }}-with-manager-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ if and .Mips (not (eq .Mips "hardfloat")) }}-{{ .Mips }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
- id: archive-legacy
<<: *template
builds:
- legacy
name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}-legacy'
source:
enabled: false
name_template: '{{ .ProjectName }}-{{ .Version }}.source'
prefix_template: '{{ .ProjectName }}-{{ .Version }}/'
checksum:
disable: true
name_template: '{{ .ProjectName }}-{{ .Version }}.checksum'
signs:
- artifacts: checksum
release:
github:
owner: shtorm-7
name: sing-box-extended
draft: true
prerelease: auto
mode: replace
ids:
- archive
- package
skip_upload: true

View File

@@ -70,14 +70,10 @@ update_certificates:
go run ./cmd/internal/update_certificates go run ./cmd/internal/update_certificates
release: release:
go run ./cmd/internal/build goreleaser release --clean --skip publish go run ./cmd/internal/build goreleaser release --skip=validate --clean -p 3 --skip publish
mkdir dist/release mkdir dist/release
mv dist/*.tar.gz \ mv dist/*.tar.gz \
dist/*.zip \ dist/*.zip \
dist/*.deb \
dist/*.rpm \
dist/*_amd64.pkg.tar.zst \
dist/*_arm64.pkg.tar.zst \
dist/release dist/release
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
rm -r dist/release rm -r dist/release
@@ -101,7 +97,7 @@ upload_android:
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
rm -rf dist/release_android rm -rf dist/release_android
release_android: lib_android update_android_version build_android upload_android release_android: lib_android update_android_version build_android
publish_android: publish_android:
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop

View File

@@ -1,52 +1,70 @@
<<<<<<< HEAD
# sing-box-extended # sing-box-extended
Sing-box with extended features. Sing-box with extended features.
## Features ## 🔥 Features
* Amnezia 1.5 ### 🌐 Outbounds
* WARP - **WARP** — Cloudflare WARP integration through WireGuard
* Tunneling - **Tunnel** — Protocol for creating tunnels across nodes
* Mieru - **Bond** — Link aggregation for increased throughput
* XHTTP - **Mieru** — Secure, hard to classify, hard to probe network protocol
* SDNS (DNSCrypt) - **Failover** — Automatic outbound switching for high availability
* Extended Wireguard options
* Unified delay
=======
# sing-box
>>>>>>> v1.13.11
## Examples ### 🚦 Limiters
- **Bandwidth Limiter** — Upload / download rate limiting
- **Connection Limiter** — Concurrent connection control
### 🛡 Encryption & Obfuscation
- **Amnezia 1.5** — WireGuard traffic obfuscation
- **VLESS encryption** — XRAY encryption for VLESS protocol
### 🔄 Transports
- **mKCP** — Reliable UDP-based transport
- **XHTTP** — Modern XRAY transport
### 🛠 Services
- **Admin Panel** — Web-based management interface
- **Manager** — Management service for configuring squads, nodes, users, limiters
- **Node Manager** — Service for connecting nodes to remote manager
### ⚙ Miscellaneous
- **SDNS (DNSCrypt)** — Encrypted DNS queries for enhanced privacy
- **Extended WireGuard options** — Advanced configuration capabilities
- **Unified Delay** — Unified latency measurement
## 📚 Examples
Configuration examples are available here:
https://github.com/shtorm-7/sing-box-extended/tree/extended/examples https://github.com/shtorm-7/sing-box-extended/tree/extended/examples
## Support the project ## Support the Project
If you want to support the project, you can donate to the following addresses. If you want to support the project, you can donate to the following addresses.
#### TRX (Tron) ### TRX (Tron)
``` ```
TSWU6VUZ4FcUghYDmbbhK15gRVvhvBgW3F TSWU6VUZ4FcUghYDmbbhK15gRVvhvBgW3F
``` ```
#### TON ### TON
``` ```
UQAyD2UuT5kCP6lZQlhFL0hyNibDXNE4nIo_RSLVSYAtD7N1 UQAyD2UuT5kCP6lZQlhFL0hyNibDXNE4nIo_RSLVSYAtD7N1
``` ```
#### Solana ### Solana
``` ```
CJu8ickwRCwNE71uVFjYf1UveyCkRp9Xo44rhPcQpeFL CJu8ickwRCwNE71uVFjYf1UveyCkRp9Xo44rhPcQpeFL
``` ```
#### Bitcoin ### Bitcoin
``` ```
bc1qqx97p8k4dchqkyd47s4vf74hrqdfnmhqvcja7x bc1qqx97p8k4dchqkyd47s4vf74hrqdfnmhqvcja7x
``` ```
#### Ethereum ### Ethereum
``` ```
0xAcc5919C22F2B3fAa0ec7E8BaD142da5B375FBF6 0xAcc5919C22F2B3fAa0ec7E8BaD142da5B375FBF6
``` ```
## License ## 📄 License
``` ```
Copyright (C) 2022 by nekohasekai <contact-sagernet@sekai.icu> Copyright (C) 2022 by nekohasekai <contact-sagernet@sekai.icu>

View File

@@ -68,8 +68,6 @@ type DNSTransport interface {
Type() string Type() string
Tag() string Tag() string
Dependencies() []string Dependencies() []string
// Reset closes the transport's existing connections so later requests use fresh connections.
// Exchanges that are currently using those connections may fail.
Reset() Reset()
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"os" "os"
"sync" "sync"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
@@ -11,6 +12,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
) )
var _ adapter.EndpointManager = (*Manager)(nil) var _ adapter.EndpointManager = (*Manager)(nil)
@@ -49,12 +51,13 @@ func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Unlock() m.access.Unlock()
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name) m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(endpoint, stage) err := adapter.LegacyStart(endpoint, stage)
done()
if err != nil { if err != nil {
return E.Cause(err, stage, " ", name) return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -72,13 +75,14 @@ func (m *Manager) Close() error {
var err error var err error
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name) m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name) monitor.Start("close ", name)
err = E.Append(err, endpoint.Close(), func(err error) error { err = E.Append(err, endpoint.Close(), func(err error) error {
return E.Cause(err, "close ", name) return E.Cause(err, "close ", name)
}) })
monitor.Finish() monitor.Finish()
done() m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -129,12 +133,13 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
if m.started { if m.started {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
for _, stage := range adapter.ListStartStages { for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name) m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(endpoint, stage) err = adapter.LegacyStart(endpoint, stage)
done()
if err != nil { if err != nil {
return E.Cause(err, stage, " ", name) return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded { if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {

View File

@@ -30,6 +30,7 @@ type UDPInjectableInbound interface {
type InboundRegistry interface { type InboundRegistry interface {
option.InboundOptionsRegistry option.InboundOptionsRegistry
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error) Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
UnsafeCreate(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error)
} }
type InboundManager interface { type InboundManager interface {
@@ -103,10 +104,6 @@ type InboundContext struct {
func (c *InboundContext) ResetRuleCache() { func (c *InboundContext) ResetRuleCache() {
c.IPCIDRMatchSource = false c.IPCIDRMatchSource = false
c.IPCIDRAcceptEmpty = false c.IPCIDRAcceptEmpty = false
c.ResetRuleMatchCache()
}
func (c *InboundContext) ResetRuleMatchCache() {
c.SourceAddressMatch = false c.SourceAddressMatch = false
c.SourcePortMatch = false c.SourcePortMatch = false
c.DestinationAddressMatch = false c.DestinationAddressMatch = false

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"os" "os"
"sync" "sync"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
@@ -11,6 +12,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
) )
var _ adapter.InboundManager = (*Manager)(nil) var _ adapter.InboundManager = (*Manager)(nil)
@@ -46,12 +48,13 @@ func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Unlock() m.access.Unlock()
for _, inbound := range inbounds { for _, inbound := range inbounds {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name) m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(inbound, stage) err := adapter.LegacyStart(inbound, stage)
done()
if err != nil { if err != nil {
return E.Cause(err, stage, " ", name) return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -69,13 +72,14 @@ func (m *Manager) Close() error {
var err error var err error
for _, inbound := range inbounds { for _, inbound := range inbounds {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name) m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name) monitor.Start("close ", name)
err = E.Append(err, inbound.Close(), func(err error) error { err = E.Append(err, inbound.Close(), func(err error) error {
return E.Cause(err, "close ", name) return E.Cause(err, "close ", name)
}) })
monitor.Finish() monitor.Finish()
done() m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -129,12 +133,13 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
if m.started { if m.started {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
for _, stage := range adapter.ListStartStages { for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name) m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(inbound, stage) err = adapter.LegacyStart(inbound, stage)
done()
if err != nil { if err != nil {
return E.Cause(err, stage, " ", name) return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
if existsInbound, loaded := m.inboundByTag[tag]; loaded { if existsInbound, loaded := m.inboundByTag[tag]; loaded {

View File

@@ -57,6 +57,10 @@ func (m *Registry) CreateOptions(outboundType string) (any, bool) {
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) { func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
return m.UnsafeCreate(ctx, router, logger, tag, outboundType, options)
}
func (m *Registry) UnsafeCreate(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
constructor, loaded := m.constructor[outboundType] constructor, loaded := m.constructor[outboundType]
if !loaded { if !loaded {
return nil, E.New("outbound type not found: " + outboundType) return nil, E.New("outbound type not found: " + outboundType)

View File

@@ -77,38 +77,26 @@ func getServiceName(service any) string {
func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) error { func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) error {
for _, service := range services { for _, service := range services {
name := getServiceName(service) name := getServiceName(service)
done := LogElapsed(logger, stage, " ", name) logger.Trace(stage, " ", name)
startTime := time.Now()
err := service.Start(stage) err := service.Start(stage)
done()
if err != nil { if err != nil {
return err return err
} }
logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error { func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error {
for _, service := range services { for _, service := range services {
done := LogElapsed(logger, stage, " ", service.Name()) logger.Trace(stage, " ", service.Name())
startTime := time.Now()
err := service.Start(stage) err := service.Start(stage)
done()
if err != nil { if err != nil {
return E.Cause(err, stage.String(), " ", service.Name()) return E.Cause(err, stage.String(), " ", service.Name())
} }
logger.Trace(stage, " ", service.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
func LogElapsed(logger log.ContextLogger, description ...any) func() {
prefix := F.ToString(description...)
startTime := time.Now()
timer := time.AfterFunc(time.Second, func() {
logger.Trace(prefix, "...")
})
return func() {
if timer.Stop() {
return
}
logger.Trace(prefix, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}

View File

@@ -1,9 +1,6 @@
package adapter package adapter
import ( import (
"encoding/hex"
"net"
"strings"
"time" "time"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
@@ -54,24 +51,6 @@ type WIFIState struct {
BSSID string BSSID string
} }
func NormalizeWIFIBSSID(bssid string) string {
bssid = strings.TrimSpace(bssid)
if bssid == "" {
return ""
}
parsed, err := net.ParseMAC(bssid)
if err == nil && len(parsed) == 6 {
return parsed.String()
}
if len(bssid) == 12 {
decoded, err := hex.DecodeString(bssid)
if err == nil {
return net.HardwareAddr(decoded).String()
}
}
return bssid
}
type NetworkInterface struct { type NetworkInterface struct {
control.Interface control.Interface
Type C.InterfaceType Type C.InterfaceType

View File

@@ -35,6 +35,7 @@ type DirectRouteOutbound interface {
type OutboundRegistry interface { type OutboundRegistry interface {
option.OutboundOptionsRegistry option.OutboundOptionsRegistry
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error) CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
UnsafeCreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
} }
type OutboundManager interface { type OutboundManager interface {

View File

@@ -6,6 +6,7 @@ import (
"os" "os"
"strings" "strings"
"sync" "sync"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
@@ -13,6 +14,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
) )
@@ -82,12 +84,13 @@ func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Unlock() m.access.Unlock()
for _, outbound := range outbounds { for _, outbound := range outbounds {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name) m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(outbound, stage) err := adapter.LegacyStart(outbound, stage)
done()
if err != nil { if err != nil {
return E.Cause(err, stage, " ", name) return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
return nil return nil
@@ -114,25 +117,27 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
canContinue = true canContinue = true
name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]" name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]"
if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter { if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
done := adapter.LogElapsed(m.logger, "start ", name) m.logger.Trace("start ", name)
startTime := time.Now()
monitor.Start("start ", name) monitor.Start("start ", name)
err := starter.Start(adapter.StartStateStart) err := starter.Start(adapter.StartStateStart)
monitor.Finish() monitor.Finish()
done()
if err != nil { if err != nil {
return E.Cause(err, "start ", name) return E.Cause(err, "start ", name)
} }
m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} else if starter, isStarter := outboundToStart.(interface { } else if starter, isStarter := outboundToStart.(interface {
Start() error Start() error
}); isStarter { }); isStarter {
done := adapter.LogElapsed(m.logger, "start ", name) m.logger.Trace("start ", name)
startTime := time.Now()
monitor.Start("start ", name) monitor.Start("start ", name)
err := starter.Start() err := starter.Start()
monitor.Finish() monitor.Finish()
done()
if err != nil { if err != nil {
return E.Cause(err, "start ", name) return E.Cause(err, "start ", name)
} }
m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
if len(started) == len(outbounds) { if len(started) == len(outbounds) {
@@ -180,13 +185,14 @@ func (m *Manager) Close() error {
for _, outbound := range outbounds { for _, outbound := range outbounds {
if closer, isCloser := outbound.(io.Closer); isCloser { if closer, isCloser := outbound.(io.Closer); isCloser {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name) m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name) monitor.Start("close ", name)
err = E.Append(err, closer.Close(), func(err error) error { err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close ", name) return E.Cause(err, "close ", name)
}) })
monitor.Finish() monitor.Finish()
done() m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
return nil return nil
@@ -269,12 +275,13 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
if m.started { if m.started {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
for _, stage := range adapter.ListStartStages { for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name) m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(outbound, stage) err = adapter.LegacyStart(outbound, stage)
done()
if err != nil { if err != nil {
return E.Cause(err, stage, " ", name) return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
m.access.Lock() m.access.Lock()

View File

@@ -57,6 +57,10 @@ func (r *Registry) CreateOptions(outboundType string) (any, bool) {
func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) { func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
r.access.Lock() r.access.Lock()
defer r.access.Unlock() defer r.access.Unlock()
return r.UnsafeCreateOutbound(ctx, router, logger, tag, outboundType, options)
}
func (r *Registry) UnsafeCreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
constructor, loaded := r.constructors[outboundType] constructor, loaded := r.constructors[outboundType]
if !loaded { if !loaded {
return nil, E.New("outbound type not found: " + outboundType) return nil, E.New("outbound type not found: " + outboundType)

View File

@@ -1,8 +1,6 @@
package adapter package adapter
import ( import (
"net/netip"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
@@ -38,8 +36,6 @@ type PlatformInterface interface {
UsePlatformNotification() bool UsePlatformNotification() bool
SendNotification(notification *Notification) error SendNotification(notification *Notification) error
MyInterfaceAddress() []netip.Addr
} }
type FindConnectionOwnerRequest struct { type FindConnectionOwnerRequest struct {
@@ -51,11 +47,11 @@ type FindConnectionOwnerRequest struct {
} }
type ConnectionOwner struct { type ConnectionOwner struct {
ProcessID uint32 ProcessID uint32
UserId int32 UserId int32
UserName string UserName string
ProcessPath string ProcessPath string
AndroidPackageNames []string AndroidPackageName string
} }
type Notification struct { type Notification struct {

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"os" "os"
"sync" "sync"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
@@ -11,6 +12,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
) )
var _ adapter.ServiceManager = (*Manager)(nil) var _ adapter.ServiceManager = (*Manager)(nil)
@@ -44,12 +46,13 @@ func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Unlock() m.access.Unlock()
for _, service := range services { for _, service := range services {
name := "service/" + service.Type() + "[" + service.Tag() + "]" name := "service/" + service.Type() + "[" + service.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name) m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(service, stage) err := adapter.LegacyStart(service, stage)
done()
if err != nil { if err != nil {
return E.Cause(err, stage, " ", name) return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -67,13 +70,14 @@ func (m *Manager) Close() error {
var err error var err error
for _, service := range services { for _, service := range services {
name := "service/" + service.Type() + "[" + service.Tag() + "]" name := "service/" + service.Type() + "[" + service.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name) m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name) monitor.Start("close ", name)
err = E.Append(err, service.Close(), func(err error) error { err = E.Append(err, service.Close(), func(err error) error {
return E.Cause(err, "close ", name) return E.Cause(err, "close ", name)
}) })
monitor.Finish() monitor.Finish()
done() m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
return nil return nil
} }
@@ -124,12 +128,13 @@ func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag stri
if m.started { if m.started {
name := "service/" + service.Type() + "[" + service.Tag() + "]" name := "service/" + service.Type() + "[" + service.Tag() + "]"
for _, stage := range adapter.ListStartStages { for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name) m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(service, stage) err = adapter.LegacyStart(service, stage)
done()
if err != nil { if err != nil {
return E.Cause(err, stage, " ", name) return E.Cause(err, stage, " ", name)
} }
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
} }
if existsService, loaded := m.serviceByTag[tag]; loaded { if existsService, loaded := m.serviceByTag[tag]; loaded {

26
box.go
View File

@@ -20,6 +20,7 @@ import (
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport/local"
"github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
@@ -160,6 +161,7 @@ func New(options Options) (*Box, error) {
if err != nil { if err != nil {
return nil, E.Cause(err, "create log factory") return nil, E.Cause(err, "create log factory")
} }
service.MustRegister[log.Factory](ctx, logFactory)
var internalServices []adapter.LifecycleService var internalServices []adapter.LifecycleService
certificateOptions := common.PtrValueOrDefault(options.Certificate) certificateOptions := common.PtrValueOrDefault(options.Certificate)
@@ -329,12 +331,11 @@ func New(options Options) (*Box, error) {
) )
}) })
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) { dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
return dnsTransportRegistry.CreateDNSTransport( return local.NewTransport(
ctx, ctx,
logFactory.NewLogger("dns/local"), logFactory.NewLogger("dns/local"),
"local", "local",
C.DNSTypeLocal, option.LocalDNSServerOptions{},
&option.LocalDNSServerOptions{},
) )
}) })
if platformInterface != nil { if platformInterface != nil {
@@ -519,24 +520,27 @@ func (s *Box) Close() error {
{"dns-transport", s.dnsTransport}, {"dns-transport", s.dnsTransport},
{"network", s.network}, {"network", s.network},
} { } {
done := adapter.LogElapsed(s.logger, "close ", closeItem.name) s.logger.Trace("close ", closeItem.name)
startTime := time.Now()
err = E.Append(err, closeItem.service.Close(), func(err error) error { err = E.Append(err, closeItem.service.Close(), func(err error) error {
return E.Cause(err, "close ", closeItem.name) return E.Cause(err, "close ", closeItem.name)
}) })
done() s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
for _, lifecycleService := range s.internalService { for _, lifecycleService := range s.internalService {
done := adapter.LogElapsed(s.logger, "close ", lifecycleService.Name()) s.logger.Trace("close ", lifecycleService.Name())
startTime := time.Now()
err = E.Append(err, lifecycleService.Close(), func(err error) error { err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name()) return E.Cause(err, "close ", lifecycleService.Name())
}) })
done() s.logger.Trace("close ", lifecycleService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
} }
done := adapter.LogElapsed(s.logger, "close logger") s.logger.Trace("close logger")
startTime := time.Now()
err = E.Append(err, s.logFactory.Close(), func(err error) error { err = E.Append(err, s.logFactory.Close(), func(err error) error {
return E.Cause(err, "close logger") return E.Cause(err, "close logger")
}) })
done() s.logger.Trace("close logger completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
return err return err
} }
@@ -556,10 +560,6 @@ func (s *Box) Outbound() adapter.OutboundManager {
return s.outbound return s.outbound
} }
func (s *Box) Endpoint() adapter.EndpointManager {
return s.endpoint
}
func (s *Box) LogFactory() log.Factory { func (s *Box) LogFactory() log.Factory {
return s.logFactory return s.logFactory
} }

View File

@@ -1,5 +1,3 @@
//go:build with_quic
package main package main
import ( import (

View File

@@ -149,10 +149,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else { } else {
dialer.Timeout = C.TCPConnectTimeout dialer.Timeout = C.TCPConnectTimeout
} }
if options.DisableTCPKeepAlive { if !options.DisableTCPKeepAlive {
dialer.KeepAlive = -1
dialer.KeepAliveConfig.Enable = false
} else {
keepIdle := time.Duration(options.TCPKeepAlive) keepIdle := time.Duration(options.TCPKeepAlive)
if keepIdle == 0 { if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial keepIdle = C.TCPKeepAliveInitial
@@ -242,7 +239,7 @@ func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefaul
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) { func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
if !address.IsValid() { if !address.IsValid() {
return nil, E.New("invalid address") return nil, E.New("invalid address")
} else if address.IsDomain() { } else if address.IsFqdn() {
return nil, E.New("domain not resolved") return nil, E.New("domain not resolved")
} }
if d.networkStrategy == nil { if d.networkStrategy == nil {
@@ -332,9 +329,9 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer { func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
if !destination.Is6() { if !destination.Is6() {
return d.dialer4.Dialer
} else {
return d.dialer6.Dialer return d.dialer6.Dialer
} else {
return d.dialer4.Dialer
} }
} }

View File

@@ -184,12 +184,6 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) { func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) {
interfaces := networkManager.NetworkInterfaces() interfaces := networkManager.NetworkInterfaces()
myInterface := networkManager.InterfaceMonitor().MyInterface()
if myInterface != "" {
interfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
return it.Name != myInterface
})
}
switch strategy { switch strategy {
case C.NetworkStrategyDefault: case C.NetworkStrategyDefault:
if len(interfaceType) == 0 { if len(interfaceType) == 0 {

View File

@@ -96,7 +96,7 @@ func (d *resolveDialer) DialContext(ctx context.Context, network string, destina
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !destination.IsDomain() { if !destination.IsFqdn() {
return d.dialer.DialContext(ctx, network, destination) return d.dialer.DialContext(ctx, network, destination)
} }
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -116,7 +116,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !destination.IsDomain() { if !destination.IsFqdn() {
return d.dialer.ListenPacket(ctx, destination) return d.dialer.ListenPacket(ctx, destination)
} }
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -144,7 +144,7 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !destination.IsDomain() { if !destination.IsFqdn() {
return d.dialer.DialContext(ctx, network, destination) return d.dialer.DialContext(ctx, network, destination)
} }
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
@@ -167,7 +167,7 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !destination.IsDomain() { if !destination.IsFqdn() {
return d.dialer.ListenPacket(ctx, destination) return d.dialer.ListenPacket(ctx, destination)
} }
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)

57
common/kmutex/mutex.go Normal file
View File

@@ -0,0 +1,57 @@
package kmutex
import "sync"
type Kmutex[T comparable] struct {
l sync.Locker
s map[T]*klock
}
type klock struct {
cond *sync.Cond
ref uint64
}
// Create new Kmutex
func New[T comparable]() *Kmutex[T] {
l := sync.Mutex{}
return &Kmutex[T]{
l: &l,
s: make(map[T]*klock),
}
}
// Unlock Kmutex by unique ID
func (km *Kmutex[T]) Unlock(key T) {
km.l.Lock()
defer km.l.Unlock()
kl, ok := km.s[key]
if !ok || kl.ref == 0 {
panic("unlock of unlocked kmutex")
}
kl.ref--
if kl.ref == 0 {
delete(km.s, key)
return
}
kl.cond.Signal()
}
// Lock Kmutex by unique ID
func (km *Kmutex[T]) Lock(key T) {
km.l.Lock()
defer km.l.Unlock()
for {
kl, ok := km.s[key]
if !ok {
km.s[key] = &klock{
cond: sync.NewCond(km.l),
ref: 1,
}
return
}
kl.ref++
kl.cond.Wait()
return
}
}

View File

@@ -0,0 +1,96 @@
package kmutex
import (
"sync"
"testing"
"time"
)
// Number of unique resources to access
const number = 100
func makeIds(count int) []int {
ids := make([]int, count)
for i := 0; i < count; i++ {
ids[i] = i
}
return ids
}
func TestKmutex(t *testing.T) {
km := New[int]()
ids := makeIds(number)
resources := make([]int, number)
wg := sync.WaitGroup{}
lc := make(chan int)
uc := make(chan int)
// Start 10n goroutines accessing n resources 10 times each
for i := 0; i < 10*number; i++ {
wg.Add(1)
go func(k int) {
for j := 0; j < 10; j++ {
lc <- k
km.Lock(ids[k])
// read and write resource to check for race
resources[k] = resources[k] + 1
km.Unlock(ids[k])
uc <- k
}
wg.Done()
}(i % len(ids))
}
to := time.After(time.Second)
counts := make(map[int]int)
var lCount, ulCount int
loop:
for {
select {
case k := <-lc:
counts[k] = counts[k] + 1
lCount++
case k := <-uc:
counts[k] = counts[k] - 1
ulCount++
case <-to:
t.Fatal("timed out waiting for results")
break loop
}
expectCount := 100 * number
if lCount == expectCount && ulCount == expectCount {
// Have all results
break
}
}
for k, c := range counts {
if c != 0 {
t.Errorf("Key %d count != 0: %d\n", k, c)
}
}
wg.Wait()
}
func BenchmarkKmutex1000(b *testing.B) {
km := New[int]()
ids := makeIds(number)
resources := make([]int, number)
wg := sync.WaitGroup{}
// Start 1000 goroutines accessing 100 resources N times each
b.ResetTimer()
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(k int) {
for j := 0; j < b.N; j++ {
km.Lock(ids[k])
// read and write resource to check for race
resources[k] = resources[k] + 1
km.Unlock(ids[k])
}
wg.Done()
}(i % len(ids))
}
wg.Wait()
}

View File

@@ -12,7 +12,6 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"unsafe"
) )
func (c *Conn) Read(b []byte) (int, error) { func (c *Conn) Read(b []byte) (int, error) {
@@ -230,7 +229,7 @@ func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) {
record := c.rawConn.RawInput.Next(recordHeaderLen + n) record := c.rawConn.RawInput.Next(recordHeaderLen + n)
data, typ, err = c.rawConn.In.Decrypt(record) data, typ, err = c.rawConn.In.Decrypt(record)
if err != nil { if err != nil {
err = c.rawConn.In.SetErrorLocked(c.sendAlert(*(*uint8)((*[2]unsafe.Pointer)(unsafe.Pointer(&err))[1]))) err = c.rawConn.In.SetErrorLocked(c.sendAlert(uint8(err.(tls.AlertError))))
return return
} }
return return

View File

@@ -151,7 +151,6 @@ func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (
if err != nil { if err != nil {
return common.DefaultValue[T](), E.Cause(err, "get current netns") return common.DefaultValue[T](), E.Cause(err, "get current netns")
} }
defer currentNs.Close()
defer netns.Set(currentNs) defer netns.Set(currentNs)
var targetNs netns.NsHandle var targetNs netns.NsHandle
if strings.HasPrefix(nameOrPath, "/") { if strings.HasPrefix(nameOrPath, "/") {

View File

@@ -37,10 +37,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
if l.listenOptions.ReuseAddr { if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr()) listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
} }
if l.listenOptions.DisableTCPKeepAlive { if !l.listenOptions.DisableTCPKeepAlive {
listenConfig.KeepAlive = -1
listenConfig.KeepAliveConfig.Enable = false
} else {
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive) keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
if keepIdle == 0 { if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial keepIdle = C.TCPKeepAliveInitial

View File

@@ -0,0 +1,98 @@
package source
import (
"io"
"io/fs"
"io/ioutil"
"strconv"
"strings"
"github.com/golang-migrate/migrate/v4/source"
E "github.com/sagernet/sing/common/exceptions"
)
type RawDriver struct {
migrations *source.Migrations
rawMigrations map[string]string
}
func NewRawDriver(rawMigrations map[string]string) *RawDriver {
return &RawDriver{rawMigrations: rawMigrations}
}
func (d *RawDriver) Init() error {
ms := source.NewMigrations()
for key := range d.rawMigrations {
m, err := source.DefaultParse(key)
if err != nil {
continue
}
if !ms.Append(m) {
return source.ErrDuplicateMigration{
Migration: *m,
}
}
}
d.migrations = ms
return nil
}
func (d *RawDriver) Open(url string) (source.Driver, error) {
return nil, E.New("open() cannot be called")
}
func (d *RawDriver) Close() error {
return nil
}
func (d *RawDriver) First() (version uint, err error) {
if version, ok := d.migrations.First(); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "first",
Err: fs.ErrNotExist,
}
}
func (d *RawDriver) Prev(version uint) (prevVersion uint, err error) {
if version, ok := d.migrations.Prev(version); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "prev for version " + strconv.FormatUint(uint64(version), 10),
Err: fs.ErrNotExist,
}
}
func (d *RawDriver) Next(version uint) (nextVersion uint, err error) {
if version, ok := d.migrations.Next(version); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "next for version " + strconv.FormatUint(uint64(version), 10),
Err: fs.ErrNotExist,
}
}
func (d *RawDriver) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := d.migrations.Up(version); ok {
body := ioutil.NopCloser(strings.NewReader(d.rawMigrations[m.Raw]))
return body, m.Identifier, nil
}
return nil, "", &fs.PathError{
Op: "read up for version " + strconv.FormatUint(uint64(version), 10),
Err: fs.ErrNotExist,
}
}
func (d *RawDriver) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := d.migrations.Down(version); ok {
body := ioutil.NopCloser(strings.NewReader(d.rawMigrations[m.Raw]))
return body, m.Identifier, nil
}
return nil, "", &fs.PathError{
Op: "read down for version " + strconv.FormatUint(uint64(version), 10),
Err: fs.ErrNotExist,
}
}

View File

@@ -38,6 +38,9 @@ func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.Conte
} }
} }
service, err := mux.NewService(mux.ServiceOptions{ service, err := mux.NewService(mux.ServiceOptions{
NewConnectionContext: func(ctx context.Context, conn net.Conn) context.Context {
return log.ContextWithNewMuxID(ctx)
},
NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context { NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context {
return log.ContextWithNewID(ctx) return log.ContextWithNewID(ctx)
}, },

View File

@@ -14,7 +14,6 @@ import (
type Searcher interface { type Searcher interface {
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
Close() error
} }
var ErrNotFound = E.New("process not found") var ErrNotFound = E.New("process not found")
@@ -29,7 +28,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
if err != nil { if err != nil {
return nil, err return nil, err
} }
if info.UserId != -1 && info.UserName == "" { if info.UserId != -1 {
osUser, _ := user.LookupId(F.ToString(info.UserId)) osUser, _ := user.LookupId(F.ToString(info.UserId))
if osUser != nil { if osUser != nil {
info.UserName = osUser.Username info.UserName = osUser.Username

View File

@@ -6,7 +6,6 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
) )
var _ Searcher = (*androidSearcher)(nil) var _ Searcher = (*androidSearcher)(nil)
@@ -19,30 +18,22 @@ func NewSearcher(config Config) (Searcher, error) {
return &androidSearcher{config.PackageManager}, nil return &androidSearcher{config.PackageManager}, nil
} }
func (s *androidSearcher) Close() error {
return nil
}
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
family, protocol, err := socketDiagSettings(network, source) _, uid, err := resolveSocketByNetlink(network, source, destination)
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, uid, err := querySocketDiagOnce(family, protocol, source) if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
if err != nil { return &adapter.ConnectionOwner{
return nil, err UserId: int32(uid),
AndroidPackageName: sharedPackage,
}, nil
} }
appID := uid % 100000 if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
var packageNames []string return &adapter.ConnectionOwner{
if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded { UserId: int32(uid),
packageNames = append(packageNames, sharedPackage) AndroidPackageName: packageName,
}, nil
} }
if packages, loaded := s.packageManager.PackagesByID(appID); loaded { return &adapter.ConnectionOwner{UserId: int32(uid)}, nil
packageNames = append(packageNames, packages...)
}
packageNames = common.Uniq(packageNames)
return &adapter.ConnectionOwner{
UserId: int32(uid),
AndroidPackageNames: packageNames,
}, nil
} }

View File

@@ -1,15 +1,19 @@
//go:build darwin
package process package process
import ( import (
"context" "context"
"encoding/binary"
"net/netip" "net/netip"
"os"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
"unsafe"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
N "github.com/sagernet/sing/common/network"
"golang.org/x/sys/unix"
) )
var _ Searcher = (*darwinSearcher)(nil) var _ Searcher = (*darwinSearcher)(nil)
@@ -20,12 +24,12 @@ func NewSearcher(_ Config) (Searcher, error) {
return &darwinSearcher{}, nil return &darwinSearcher{}, nil
} }
func (d *darwinSearcher) Close() error {
return nil
}
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
return FindDarwinConnectionOwner(network, source, destination) processName, err := findProcessName(network, source.Addr(), int(source.Port()))
if err != nil {
return nil, err
}
return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
} }
var structSize = func() int { var structSize = func() int {
@@ -43,3 +47,107 @@ var structSize = func() int {
return 384 return 384
} }
}() }()
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
var spath string
switch network {
case N.NetworkTCP:
spath = "net.inet.tcp.pcblist_n"
case N.NetworkUDP:
spath = "net.inet.udp.pcblist_n"
default:
return "", os.ErrInvalid
}
isIPv4 := ip.Is4()
value, err := unix.SysctlRaw(spath)
if err != nil {
return "", err
}
buf := value
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
itemSize := structSize
if network == N.NetworkTCP {
// rup8(sizeof(xtcpcb_n))
itemSize += 208
}
var fallbackUDPProcess string
// skip the first xinpgen(24 bytes) block
for i := 24; i+itemSize <= len(buf); i += itemSize {
// offset of xinpcb_n and xsocket_n
inp, so := i, i+104
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
if uint16(port) != srcPort {
continue
}
// xinpcb_n.inp_vflag
flag := buf[inp+44]
var srcIP netip.Addr
srcIsIPv4 := false
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80]))
srcIsIPv4 = true
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80]))
default:
continue
}
if ip == srcIP {
// xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid)
}
// udp packet connection may be not equal with srcIP
if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
pid := readNativeUint32(buf[so+68 : so+72])
fallbackUDPProcess, _ = getExecPathFromPID(pid)
}
}
if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 {
return fallbackUDPProcess, nil
}
return "", ErrNotFound
}
func getExecPathFromPID(pid uint32) (string, error) {
const (
procpidpathinfo = 0xb
procpidpathinfosize = 1024
proccallnumpidinfo = 0x2
)
buf := make([]byte, procpidpathinfosize)
_, _, errno := syscall.Syscall6(
syscall.SYS_PROC_INFO,
proccallnumpidinfo,
uintptr(pid),
procpidpathinfo,
0,
uintptr(unsafe.Pointer(&buf[0])),
procpidpathinfosize)
if errno != 0 {
return "", errno
}
return unix.ByteSliceToString(buf), nil
}
func readNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}

View File

@@ -1,269 +0,0 @@
//go:build darwin
package process
import (
"encoding/binary"
"net/netip"
"os"
"sync"
"syscall"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
N "github.com/sagernet/sing/common/network"
"golang.org/x/sys/unix"
)
const (
darwinSnapshotTTL = 200 * time.Millisecond
darwinXinpgenSize = 24
darwinXsocketOffset = 104
darwinXinpcbForeignPort = 16
darwinXinpcbLocalPort = 18
darwinXinpcbVFlag = 44
darwinXinpcbForeignAddr = 48
darwinXinpcbLocalAddr = 64
darwinXinpcbIPv4Addr = 12
darwinXsocketUID = 64
darwinXsocketLastPID = 68
darwinTCPExtraStructSize = 208
)
type darwinConnectionEntry struct {
localAddr netip.Addr
remoteAddr netip.Addr
localPort uint16
remotePort uint16
pid uint32
uid int32
}
type darwinConnectionMatchKind uint8
const (
darwinConnectionMatchExact darwinConnectionMatchKind = iota
darwinConnectionMatchLocalFallback
darwinConnectionMatchWildcardFallback
)
type darwinSnapshot struct {
createdAt time.Time
entries []darwinConnectionEntry
}
type darwinConnectionFinder struct {
access sync.Mutex
ttl time.Duration
snapshots map[string]darwinSnapshot
builder func(string) (darwinSnapshot, error)
}
var sharedDarwinConnectionFinder = newDarwinConnectionFinder(darwinSnapshotTTL)
func newDarwinConnectionFinder(ttl time.Duration) *darwinConnectionFinder {
return &darwinConnectionFinder{
ttl: ttl,
snapshots: make(map[string]darwinSnapshot),
builder: buildDarwinSnapshot,
}
}
func FindDarwinConnectionOwner(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
return sharedDarwinConnectionFinder.find(network, source, destination)
}
func (f *darwinConnectionFinder) find(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
networkName := N.NetworkName(network)
source = normalizeDarwinAddrPort(source)
destination = normalizeDarwinAddrPort(destination)
var lastOwner *adapter.ConnectionOwner
for attempt := 0; attempt < 2; attempt++ {
snapshot, fromCache, err := f.loadSnapshot(networkName, attempt > 0)
if err != nil {
return nil, err
}
entry, matchKind, err := matchDarwinConnectionEntry(snapshot.entries, networkName, source, destination)
if err != nil {
if err == ErrNotFound && fromCache {
continue
}
return nil, err
}
if fromCache && matchKind != darwinConnectionMatchExact {
continue
}
owner := &adapter.ConnectionOwner{
UserId: entry.uid,
}
lastOwner = owner
if entry.pid == 0 {
return owner, nil
}
processPath, err := getExecPathFromPID(entry.pid)
if err == nil {
owner.ProcessPath = processPath
return owner, nil
}
if fromCache {
continue
}
return owner, nil
}
if lastOwner != nil {
return lastOwner, nil
}
return nil, ErrNotFound
}
func (f *darwinConnectionFinder) loadSnapshot(network string, forceRefresh bool) (darwinSnapshot, bool, error) {
f.access.Lock()
defer f.access.Unlock()
if !forceRefresh {
if snapshot, loaded := f.snapshots[network]; loaded && time.Since(snapshot.createdAt) < f.ttl {
return snapshot, true, nil
}
}
snapshot, err := f.builder(network)
if err != nil {
return darwinSnapshot{}, false, err
}
f.snapshots[network] = snapshot
return snapshot, false, nil
}
func buildDarwinSnapshot(network string) (darwinSnapshot, error) {
spath, itemSize, err := darwinSnapshotSettings(network)
if err != nil {
return darwinSnapshot{}, err
}
value, err := unix.SysctlRaw(spath)
if err != nil {
return darwinSnapshot{}, err
}
return darwinSnapshot{
createdAt: time.Now(),
entries: parseDarwinSnapshot(value, itemSize),
}, nil
}
func darwinSnapshotSettings(network string) (string, int, error) {
itemSize := structSize
switch network {
case N.NetworkTCP:
return "net.inet.tcp.pcblist_n", itemSize + darwinTCPExtraStructSize, nil
case N.NetworkUDP:
return "net.inet.udp.pcblist_n", itemSize, nil
default:
return "", 0, os.ErrInvalid
}
}
func parseDarwinSnapshot(buf []byte, itemSize int) []darwinConnectionEntry {
entries := make([]darwinConnectionEntry, 0, (len(buf)-darwinXinpgenSize)/itemSize)
for i := darwinXinpgenSize; i+itemSize <= len(buf); i += itemSize {
inp := i
so := i + darwinXsocketOffset
entry, ok := parseDarwinConnectionEntry(buf[inp:so], buf[so:so+structSize-darwinXsocketOffset])
if ok {
entries = append(entries, entry)
}
}
return entries
}
func parseDarwinConnectionEntry(inp []byte, so []byte) (darwinConnectionEntry, bool) {
if len(inp) < darwinXsocketOffset || len(so) < structSize-darwinXsocketOffset {
return darwinConnectionEntry{}, false
}
entry := darwinConnectionEntry{
remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]),
localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]),
pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]),
uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])),
}
flag := inp[darwinXinpcbVFlag]
switch {
case flag&0x1 != 0:
entry.remoteAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr : darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr+4]))
entry.localAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr : darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr+4]))
return entry, true
case flag&0x2 != 0:
entry.remoteAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16]))
entry.localAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16]))
return entry, true
default:
return darwinConnectionEntry{}, false
}
}
func matchDarwinConnectionEntry(entries []darwinConnectionEntry, network string, source netip.AddrPort, destination netip.AddrPort) (darwinConnectionEntry, darwinConnectionMatchKind, error) {
sourceAddr := source.Addr()
if !sourceAddr.IsValid() {
return darwinConnectionEntry{}, darwinConnectionMatchExact, os.ErrInvalid
}
var localFallback darwinConnectionEntry
var hasLocalFallback bool
var wildcardFallback darwinConnectionEntry
var hasWildcardFallback bool
for _, entry := range entries {
if entry.localPort != source.Port() || sourceAddr.BitLen() != entry.localAddr.BitLen() {
continue
}
if entry.localAddr == sourceAddr && destination.IsValid() && entry.remotePort == destination.Port() && entry.remoteAddr == destination.Addr() {
return entry, darwinConnectionMatchExact, nil
}
if !destination.IsValid() && entry.localAddr == sourceAddr {
return entry, darwinConnectionMatchExact, nil
}
if network != N.NetworkUDP {
continue
}
if !hasLocalFallback && entry.localAddr == sourceAddr {
hasLocalFallback = true
localFallback = entry
}
if !hasWildcardFallback && entry.localAddr.IsUnspecified() {
hasWildcardFallback = true
wildcardFallback = entry
}
}
if hasLocalFallback {
return localFallback, darwinConnectionMatchLocalFallback, nil
}
if hasWildcardFallback {
return wildcardFallback, darwinConnectionMatchWildcardFallback, nil
}
return darwinConnectionEntry{}, darwinConnectionMatchExact, ErrNotFound
}
func normalizeDarwinAddrPort(addrPort netip.AddrPort) netip.AddrPort {
if !addrPort.IsValid() {
return addrPort
}
return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())
}
func getExecPathFromPID(pid uint32) (string, error) {
const (
procpidpathinfo = 0xb
procpidpathinfosize = 1024
proccallnumpidinfo = 0x2
)
buf := make([]byte, procpidpathinfosize)
_, _, errno := syscall.Syscall6(
syscall.SYS_PROC_INFO,
proccallnumpidinfo,
uintptr(pid),
procpidpathinfo,
0,
uintptr(unsafe.Pointer(&buf[0])),
procpidpathinfosize)
if errno != 0 {
return "", errno
}
return unix.ByteSliceToString(buf), nil
}

View File

@@ -4,82 +4,33 @@ package process
import ( import (
"context" "context"
"errors"
"net/netip" "net/netip"
"syscall"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
) )
var _ Searcher = (*linuxSearcher)(nil) var _ Searcher = (*linuxSearcher)(nil)
type linuxSearcher struct { type linuxSearcher struct {
logger log.ContextLogger logger log.ContextLogger
diagConns [4]*socketDiagConn
processPathCache *uidProcessPathCache
} }
func NewSearcher(config Config) (Searcher, error) { func NewSearcher(config Config) (Searcher, error) {
searcher := &linuxSearcher{ return &linuxSearcher{config.Logger}, nil
logger: config.Logger,
processPathCache: newUIDProcessPathCache(time.Second),
}
for _, family := range []uint8{syscall.AF_INET, syscall.AF_INET6} {
for _, protocol := range []uint8{syscall.IPPROTO_TCP, syscall.IPPROTO_UDP} {
searcher.diagConns[socketDiagConnIndex(family, protocol)] = newSocketDiagConn(family, protocol)
}
}
return searcher, nil
}
func (s *linuxSearcher) Close() error {
var errs []error
for _, conn := range s.diagConns {
if conn == nil {
continue
}
errs = append(errs, conn.Close())
}
return E.Errors(errs...)
} }
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
inode, uid, err := s.resolveSocketByNetlink(network, source, destination) inode, uid, err := resolveSocketByNetlink(network, source, destination)
if err != nil { if err != nil {
return nil, err return nil, err
} }
processInfo := &adapter.ConnectionOwner{ processPath, err := resolveProcessNameByProcSearch(inode, uid)
UserId: int32(uid),
}
processPath, err := s.processPathCache.findProcessPath(inode, uid)
if err != nil { if err != nil {
s.logger.DebugContext(ctx, "find process path: ", err) s.logger.DebugContext(ctx, "find process path: ", err)
} else {
processInfo.ProcessPath = processPath
} }
return processInfo, nil return &adapter.ConnectionOwner{
} UserId: int32(uid),
ProcessPath: processPath,
func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { }, nil
family, protocol, err := socketDiagSettings(network, source)
if err != nil {
return 0, 0, err
}
conn := s.diagConns[socketDiagConnIndex(family, protocol)]
if conn == nil {
return 0, 0, E.New("missing socket diag connection for family=", family, " protocol=", protocol)
}
if destination.IsValid() && source.Addr().BitLen() == destination.Addr().BitLen() {
inode, uid, err = conn.query(source, destination)
if err == nil {
return inode, uid, nil
}
if !errors.Is(err, ErrNotFound) {
return 0, 0, err
}
}
return querySocketDiagOnce(family, protocol, source)
} }

View File

@@ -3,67 +3,43 @@
package process package process
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"errors" "fmt"
"net"
"net/netip" "net/netip"
"os" "os"
"path/filepath" "path"
"strings" "strings"
"sync"
"syscall" "syscall"
"time"
"unicode" "unicode"
"unsafe"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
) )
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
var nativeEndian = func() binary.ByteOrder {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
return binary.BigEndian
}
return binary.LittleEndian
}()
const ( const (
sizeOfSocketDiagRequestData = 56 sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData socketDiagByFamily = 20
socketDiagResponseMinSize = 72 pathProc = "/proc"
socketDiagByFamily = 20
pathProc = "/proc"
) )
type socketDiagConn struct { func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
access sync.Mutex var family uint8
family uint8 var protocol uint8
protocol uint8
fd int
}
type uidProcessPathCache struct {
cache freelru.Cache[uint32, *uidProcessPaths]
}
type uidProcessPaths struct {
entries map[uint32]string
}
func newSocketDiagConn(family, protocol uint8) *socketDiagConn {
return &socketDiagConn{
family: family,
protocol: protocol,
fd: -1,
}
}
func socketDiagConnIndex(family, protocol uint8) int {
index := 0
if protocol == syscall.IPPROTO_UDP {
index += 2
}
if family == syscall.AF_INET6 {
index++
}
return index
}
func socketDiagSettings(network string, source netip.AddrPort) (family, protocol uint8, err error) {
switch network { switch network {
case N.NetworkTCP: case N.NetworkTCP:
protocol = syscall.IPPROTO_TCP protocol = syscall.IPPROTO_TCP
@@ -72,308 +48,151 @@ func socketDiagSettings(network string, source netip.AddrPort) (family, protocol
default: default:
return 0, 0, os.ErrInvalid return 0, 0, os.ErrInvalid
} }
switch {
case source.Addr().Is4(): if source.Addr().Is4() {
family = syscall.AF_INET family = syscall.AF_INET
case source.Addr().Is6(): } else {
family = syscall.AF_INET6 family = syscall.AF_INET6
default:
return 0, 0, os.ErrInvalid
} }
return family, protocol, nil
}
func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache { req := packSocketDiagRequest(family, protocol, source)
cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32))
cache.SetLifetime(ttl)
return &uidProcessPathCache{cache: cache}
}
func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) { socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
if cached, ok := c.cache.Get(uid); ok {
if processPath, found := cached.entries[targetInode]; found {
return processPath, nil
}
}
processPaths, err := buildProcessPathByUIDCache(uid)
if err != nil {
return "", err
}
c.cache.Add(uid, &uidProcessPaths{entries: processPaths})
processPath, found := processPaths[targetInode]
if !found {
return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found")
}
return processPath, nil
}
func (c *socketDiagConn) Close() error {
c.access.Lock()
defer c.access.Unlock()
return c.closeLocked()
}
func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
c.access.Lock()
defer c.access.Unlock()
request := packSocketDiagRequest(c.family, c.protocol, source, destination, false)
for attempt := 0; attempt < 2; attempt++ {
err = c.ensureOpenLocked()
if err != nil {
return 0, 0, E.Cause(err, "dial netlink")
}
inode, uid, err = querySocketDiag(c.fd, request)
if err == nil || errors.Is(err, ErrNotFound) {
return inode, uid, err
}
if !shouldRetrySocketDiag(err) {
return 0, 0, err
}
_ = c.closeLocked()
}
return 0, 0, err
}
func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) {
fd, err := openSocketDiag()
if err != nil { if err != nil {
return 0, 0, E.Cause(err, "dial netlink") return 0, 0, E.Cause(err, "dial netlink")
} }
defer syscall.Close(fd) defer syscall.Close(socket)
return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
}
func (c *socketDiagConn) ensureOpenLocked() error { syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
if c.fd != -1 { syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
return nil
}
fd, err := openSocketDiag()
if err != nil {
return err
}
c.fd = fd
return nil
}
func openSocketDiag() (int, error) { err = syscall.Connect(socket, &syscall.SockaddrNetlink{
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM|syscall.SOCK_CLOEXEC, syscall.NETLINK_INET_DIAG)
if err != nil {
return -1, err
}
timeout := &syscall.Timeval{Usec: 100}
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil {
syscall.Close(fd)
return -1, err
}
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil {
syscall.Close(fd)
return -1, err
}
if err = syscall.Connect(fd, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK, Family: syscall.AF_NETLINK,
Pad: 0,
Pid: 0, Pid: 0,
Groups: 0, Groups: 0,
}); err != nil { })
syscall.Close(fd) if err != nil {
return -1, err return
} }
return fd, nil
}
func (c *socketDiagConn) closeLocked() error { _, err = syscall.Write(socket, req)
if c.fd == -1 {
return nil
}
err := syscall.Close(c.fd)
c.fd = -1
return err
}
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort, destination netip.AddrPort, dump bool) []byte {
request := make([]byte, sizeOfSocketDiagRequest)
binary.NativeEndian.PutUint32(request[0:4], sizeOfSocketDiagRequest)
binary.NativeEndian.PutUint16(request[4:6], socketDiagByFamily)
flags := uint16(syscall.NLM_F_REQUEST)
if dump {
flags |= syscall.NLM_F_DUMP
}
binary.NativeEndian.PutUint16(request[6:8], flags)
binary.NativeEndian.PutUint32(request[8:12], 0)
binary.NativeEndian.PutUint32(request[12:16], 0)
request[16] = family
request[17] = protocol
request[18] = 0
request[19] = 0
if dump {
binary.NativeEndian.PutUint32(request[20:24], 0xFFFFFFFF)
}
requestSource := source
requestDestination := destination
if protocol == syscall.IPPROTO_UDP && !dump && destination.IsValid() {
// udp_dump_one expects the exact-match endpoints reversed for historical reasons.
requestSource, requestDestination = destination, source
}
binary.BigEndian.PutUint16(request[24:26], requestSource.Port())
binary.BigEndian.PutUint16(request[26:28], requestDestination.Port())
if family == syscall.AF_INET6 {
copy(request[28:44], requestSource.Addr().AsSlice())
if requestDestination.IsValid() {
copy(request[44:60], requestDestination.Addr().AsSlice())
}
} else {
copy(request[28:32], requestSource.Addr().AsSlice())
if requestDestination.IsValid() {
copy(request[44:48], requestDestination.Addr().AsSlice())
}
}
binary.NativeEndian.PutUint32(request[60:64], 0)
binary.NativeEndian.PutUint64(request[64:72], 0xFFFFFFFFFFFFFFFF)
return request
}
func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) {
_, err = syscall.Write(fd, request)
if err != nil { if err != nil {
return 0, 0, E.Cause(err, "write netlink request") return 0, 0, E.Cause(err, "write netlink request")
} }
buffer := make([]byte, 64<<10)
n, err := syscall.Read(fd, buffer) buffer := buf.New()
defer buffer.Release()
n, err := syscall.Read(socket, buffer.FreeBytes())
if err != nil { if err != nil {
return 0, 0, E.Cause(err, "read netlink response") return 0, 0, E.Cause(err, "read netlink response")
} }
messages, err := syscall.ParseNetlinkMessage(buffer[:n])
buffer.Truncate(n)
messages, err := syscall.ParseNetlinkMessage(buffer.Bytes())
if err != nil { if err != nil {
return 0, 0, E.Cause(err, "parse netlink message") return 0, 0, E.Cause(err, "parse netlink message")
} else if len(messages) == 0 {
return 0, 0, E.New("unexcepted netlink response")
} }
return unpackSocketDiagMessages(messages)
message := messages[0]
if message.Header.Type&syscall.NLMSG_ERROR != 0 {
return 0, 0, E.New("netlink message: NLMSG_ERROR")
}
inode, uid = unpackSocketDiagResponse(&messages[0])
return
} }
func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) { func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte {
for _, message := range messages { s := make([]byte, 16)
switch message.Header.Type { copy(s, source.Addr().AsSlice())
case syscall.NLMSG_DONE:
continue buf := make([]byte, sizeOfSocketDiagRequest)
case syscall.NLMSG_ERROR:
err = unpackSocketDiagError(&message) nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
if err != nil { nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
return 0, 0, err nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
} nativeEndian.PutUint32(buf[8:12], 0)
case socketDiagByFamily: nativeEndian.PutUint32(buf[12:16], 0)
inode, uid = unpackSocketDiagResponse(&message)
if inode != 0 || uid != 0 { buf[16] = family
return inode, uid, nil buf[17] = protocol
} buf[18] = 0
} buf[19] = 0
} nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
return 0, 0, ErrNotFound
binary.BigEndian.PutUint16(buf[24:26], source.Port())
binary.BigEndian.PutUint16(buf[26:28], 0)
copy(buf[28:44], s)
copy(buf[44:60], net.IPv6zero)
nativeEndian.PutUint32(buf[60:64], 0)
nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF)
return buf
} }
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) { func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
if len(msg.Data) < socketDiagResponseMinSize { if len(msg.Data) < 72 {
return 0, 0 return 0, 0
} }
uid = binary.NativeEndian.Uint32(msg.Data[64:68])
inode = binary.NativeEndian.Uint32(msg.Data[68:72]) data := msg.Data
return inode, uid
uid = nativeEndian.Uint32(data[64:68])
inode = nativeEndian.Uint32(data[68:72])
return
} }
func unpackSocketDiagError(msg *syscall.NetlinkMessage) error { func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
if len(msg.Data) < 4 {
return E.New("netlink message: NLMSG_ERROR")
}
errno := int32(binary.NativeEndian.Uint32(msg.Data[:4]))
if errno == 0 {
return nil
}
if errno < 0 {
errno = -errno
}
sysErr := syscall.Errno(errno)
switch sysErr {
case syscall.ENOENT, syscall.ESRCH:
return ErrNotFound
default:
return E.New("netlink message: ", sysErr)
}
}
func shouldRetrySocketDiag(err error) bool {
return err != nil && !errors.Is(err, ErrNotFound)
}
func buildProcessPathByUIDCache(uid uint32) (map[uint32]string, error) {
files, err := os.ReadDir(pathProc) files, err := os.ReadDir(pathProc)
if err != nil { if err != nil {
return nil, err return "", err
} }
buffer := make([]byte, syscall.PathMax) buffer := make([]byte, syscall.PathMax)
processPaths := make(map[uint32]string) socket := []byte(fmt.Sprintf("socket:[%d]", inode))
for _, file := range files {
if !file.IsDir() || !isPid(file.Name()) { for _, f := range files {
if !f.IsDir() || !isPid(f.Name()) {
continue continue
} }
info, err := file.Info()
info, err := f.Info()
if err != nil { if err != nil {
if isIgnorableProcError(err) { return "", err
continue
}
return nil, err
} }
if info.Sys().(*syscall.Stat_t).Uid != uid { if info.Sys().(*syscall.Stat_t).Uid != uid {
continue continue
} }
processPath := filepath.Join(pathProc, file.Name())
fdPath := filepath.Join(processPath, "fd") processPath := path.Join(pathProc, f.Name())
exePath, err := os.Readlink(filepath.Join(processPath, "exe")) fdPath := path.Join(processPath, "fd")
if err != nil {
if isIgnorableProcError(err) {
continue
}
return nil, err
}
fds, err := os.ReadDir(fdPath) fds, err := os.ReadDir(fdPath)
if err != nil { if err != nil {
continue continue
} }
for _, fd := range fds { for _, fd := range fds {
n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer) n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
if err != nil { if err != nil {
continue continue
} }
inode, ok := parseSocketInode(buffer[:n])
if !ok { if bytes.Equal(buffer[:n], socket) {
continue return os.Readlink(path.Join(processPath, "exe"))
}
if _, loaded := processPaths[inode]; !loaded {
processPaths[inode] = exePath
} }
} }
} }
return processPaths, nil
}
func isIgnorableProcError(err error) bool { return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
return os.IsNotExist(err) || os.IsPermission(err)
}
func parseSocketInode(link []byte) (uint32, bool) {
const socketPrefix = "socket:["
if len(link) <= len(socketPrefix) || string(link[:len(socketPrefix)]) != socketPrefix || link[len(link)-1] != ']' {
return 0, false
}
var inode uint64
for _, char := range link[len(socketPrefix) : len(link)-1] {
if char < '0' || char > '9' {
return 0, false
}
inode = inode*10 + uint64(char-'0')
if inode > uint64(^uint32(0)) {
return 0, false
}
}
return uint32(inode), true
} }
func isPid(s string) bool { func isPid(s string) bool {

View File

@@ -1,60 +0,0 @@
//go:build linux
package process
import (
"net"
"net/netip"
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestQuerySocketDiagUDPExact(t *testing.T) {
t.Parallel()
server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer server.Close()
client, err := net.DialUDP("udp4", nil, server.LocalAddr().(*net.UDPAddr))
require.NoError(t, err)
defer client.Close()
err = client.SetDeadline(time.Now().Add(time.Second))
require.NoError(t, err)
_, err = client.Write([]byte{0})
require.NoError(t, err)
err = server.SetReadDeadline(time.Now().Add(time.Second))
require.NoError(t, err)
buffer := make([]byte, 1)
_, _, err = server.ReadFromUDP(buffer)
require.NoError(t, err)
source := addrPortFromUDPAddr(t, client.LocalAddr())
destination := addrPortFromUDPAddr(t, client.RemoteAddr())
fd, err := openSocketDiag()
require.NoError(t, err)
defer syscall.Close(fd)
inode, uid, err := querySocketDiag(fd, packSocketDiagRequest(syscall.AF_INET, syscall.IPPROTO_UDP, source, destination, false))
require.NoError(t, err)
require.NotZero(t, inode)
require.EqualValues(t, os.Getuid(), uid)
}
func addrPortFromUDPAddr(t *testing.T, addr net.Addr) netip.AddrPort {
t.Helper()
udpAddr, ok := addr.(*net.UDPAddr)
require.True(t, ok)
ip, ok := netip.AddrFromSlice(udpAddr.IP)
require.True(t, ok)
return netip.AddrPortFrom(ip.Unmap(), uint16(udpAddr.Port))
}

View File

@@ -28,10 +28,6 @@ func initWin32API() error {
return winiphlpapi.LoadExtendedTable() return winiphlpapi.LoadExtendedTable()
} }
func (s *windowsSearcher) Close() error {
return nil
}
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
pid, err := winiphlpapi.FindPid(network, source) pid, err := winiphlpapi.FindPid(network, source)
if err != nil { if err != nil {

25
common/vision/hook.go Normal file
View File

@@ -0,0 +1,25 @@
package vision
import (
"context"
"net"
)
type Hook func(net.Conn)
type hookKey struct{}
func WithHook(ctx context.Context, hook Hook) context.Context {
if hook == nil {
return ctx
}
return context.WithValue(ctx, hookKey{}, hook)
}
func HookFromContext(ctx context.Context) (Hook, bool) {
if ctx == nil {
return nil, false
}
hook, ok := ctx.Value(hookKey{}).(Hook)
return hook, ok
}

View File

@@ -0,0 +1,18 @@
package cpuid
import (
"runtime"
"golang.org/x/sys/cpu"
)
var (
// Keep in sync with crypto/tls/cipher_suites.go.
hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ
hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasGHASH
hasGCMAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le"
// HasAESGCM indicates whether the CPU has AES-GCM hardware acceleration.
HasAESGCM = hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X || hasGCMAsmPPC64
)

View File

@@ -71,6 +71,13 @@ func (c *Range) UnmarshalJSON(content []byte) error {
return nil return nil
} }
func (c *Range) String() string {
if c.From == c.To {
return strconv.FormatInt(int64(c.From), 10)
}
return fmt.Sprintf("%d-%d", c.From, c.To)
}
func (c Range) Rand() int32 { func (c Range) Rand() int32 {
return int32(crypto.RandBetween(int64(c.From), int64(c.To))) return int32(crypto.RandBetween(int64(c.From), int64(c.To)))
} }

View File

@@ -1,43 +1,53 @@
package constant package constant
const ( const (
TypeTun = "tun" TypeTun = "tun"
TypeRedirect = "redirect" TypeRedirect = "redirect"
TypeTProxy = "tproxy" TypeTProxy = "tproxy"
TypeDirect = "direct" TypeDirect = "direct"
TypeBlock = "block" TypeBlock = "block"
TypeDNS = "dns" TypeDNS = "dns"
TypeSOCKS = "socks" TypeSOCKS = "socks"
TypeHTTP = "http" TypeHTTP = "http"
TypeMixed = "mixed" TypeMixed = "mixed"
TypeShadowsocks = "shadowsocks" TypeShadowsocks = "shadowsocks"
TypeVMess = "vmess" TypeVMess = "vmess"
TypeTrojan = "trojan" TypeTrojan = "trojan"
TypeNaive = "naive" TypeNaive = "naive"
TypeWireGuard = "wireguard" TypeWireGuard = "wireguard"
TypeWARP = "warp" TypeWARP = "warp"
TypeHysteria = "hysteria" TypeHysteria = "hysteria"
TypeTor = "tor" TypeTor = "tor"
TypeSSH = "ssh" TypeSSH = "ssh"
TypeShadowTLS = "shadowtls" TypeShadowTLS = "shadowtls"
TypeMieru = "mieru" TypeMieru = "mieru"
TypeAnyTLS = "anytls" TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr" TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless" TypeVLESS = "vless"
TypeTUIC = "tuic" TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2" TypeHysteria2 = "hysteria2"
TypeTunnelClient = "tunnel_client" TypeBond = "bond"
TypeTunnelServer = "tunnel_server" TypeTunnelServer = "tunnel-server"
TypeTailscale = "tailscale" TypeTunnelClient = "tunnel-client"
TypeDERP = "derp" TypeTailscale = "tailscale"
TypeResolved = "resolved" TypeConnectionLimiter = "connection-limiter"
TypeSSMAPI = "ssm-api" TypeBandwidthLimiter = "bandwidth-limiter"
TypeCCM = "ccm" TypeTrafficLimiter = "traffic-limiter"
TypeOCM = "ocm" TypeAdminPanel = "admin-panel"
TypeOOMKiller = "oom-killer" TypeNodeManagerServer = "node-manager-server"
TypeNodeManagerClient = "node-manager-client"
TypeDERP = "derp"
TypeManager = "manager"
TypeNode = "node"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
TypeOOMKiller = "oom-killer"
) )
const ( const (
TypeFailover = "failover"
TypeSelector = "selector" TypeSelector = "selector"
TypeURLTest = "urltest" TypeURLTest = "urltest"
) )
@@ -90,10 +100,14 @@ func ProxyDisplayName(proxyType string) string {
return "TUIC" return "TUIC"
case TypeHysteria2: case TypeHysteria2:
return "Hysteria2" return "Hysteria2"
case TypeBond:
return "Bond"
case TypeMieru: case TypeMieru:
return "Mieru" return "Mieru"
case TypeAnyTLS: case TypeAnyTLS:
return "AnyTLS" return "AnyTLS"
case TypeFailover:
return "Failover"
case TypeTailscale: case TypeTailscale:
return "Tailscale" return "Tailscale"
case TypeSelector: case TypeSelector:
@@ -101,9 +115,9 @@ func ProxyDisplayName(proxyType string) string {
case TypeURLTest: case TypeURLTest:
return "URLTest" return "URLTest"
case TypeTunnelClient: case TypeTunnelClient:
return "Tunnel Client" return "Tunnel client"
case TypeTunnelServer: case TypeTunnelServer:
return "Tunnel Server" return "Tunnel server"
default: default:
return "Unknown" return "Unknown"
} }

View File

@@ -7,4 +7,5 @@ const (
V2RayTransportTypeGRPC = "grpc" V2RayTransportTypeGRPC = "grpc"
V2RayTransportTypeHTTPUpgrade = "httpupgrade" V2RayTransportTypeHTTPUpgrade = "httpupgrade"
V2RayTransportTypeXHTTP = "xhttp" V2RayTransportTypeXHTTP = "xhttp"
V2RayTransportTypeKCP = "mkcp"
) )

View File

@@ -69,7 +69,7 @@ type OverrideOptions struct {
} }
func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) { func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
ctx := service.ExtendContext(s.ctx) ctx := s.ctx
service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager)) service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
ctx, cancel := context.WithCancel(include.Context(ctx)) ctx, cancel := context.WithCancel(include.Context(ctx))
options, err := parseConfig(ctx, profileContent) options, err := parseConfig(ctx, profileContent)

View File

@@ -168,7 +168,7 @@ func (s *StartedService) waitForStarted(ctx context.Context) error {
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error { func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
s.serviceAccess.Lock() s.serviceAccess.Lock()
switch s.serviceStatus.Status { switch s.serviceStatus.Status {
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING, ServiceStatus_FATAL: case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING:
default: default:
s.serviceAccess.Unlock() s.serviceAccess.Unlock()
return os.ErrInvalid return os.ErrInvalid
@@ -226,14 +226,13 @@ func (s *StartedService) CloseService() error {
return os.ErrInvalid return os.ErrInvalid
} }
s.updateStatus(ServiceStatus_STOPPING) s.updateStatus(ServiceStatus_STOPPING)
instance := s.instance if s.instance != nil {
s.instance = nil err := s.instance.Close()
if instance != nil {
err := instance.Close()
if err != nil { if err != nil {
return s.updateStatusError(err) return s.updateStatusError(err)
} }
} }
s.instance = nil
s.startedAt = time.Time{} s.startedAt = time.Time{}
s.updateStatus(ServiceStatus_IDLE) s.updateStatus(ServiceStatus_IDLE)
s.serviceAccess.Unlock() s.serviceAccess.Unlock()
@@ -950,11 +949,11 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
var processInfo *ProcessInfo var processInfo *ProcessInfo
if metadata.Metadata.ProcessInfo != nil { if metadata.Metadata.ProcessInfo != nil {
processInfo = &ProcessInfo{ processInfo = &ProcessInfo{
ProcessId: metadata.Metadata.ProcessInfo.ProcessID, ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
UserId: metadata.Metadata.ProcessInfo.UserId, UserId: metadata.Metadata.ProcessInfo.UserId,
UserName: metadata.Metadata.ProcessInfo.UserName, UserName: metadata.Metadata.ProcessInfo.UserName,
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath, ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames, PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName,
} }
} }
return &Connection{ return &Connection{

View File

@@ -1460,7 +1460,7 @@ type ProcessInfo struct {
UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"` UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"`
UserName string `protobuf:"bytes,3,opt,name=userName,proto3" json:"userName,omitempty"` UserName string `protobuf:"bytes,3,opt,name=userName,proto3" json:"userName,omitempty"`
ProcessPath string `protobuf:"bytes,4,opt,name=processPath,proto3" json:"processPath,omitempty"` ProcessPath string `protobuf:"bytes,4,opt,name=processPath,proto3" json:"processPath,omitempty"`
PackageNames []string `protobuf:"bytes,5,rep,name=packageNames,proto3" json:"packageNames,omitempty"` PackageName string `protobuf:"bytes,5,opt,name=packageName,proto3" json:"packageName,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -1523,11 +1523,11 @@ func (x *ProcessInfo) GetProcessPath() string {
return "" return ""
} }
func (x *ProcessInfo) GetPackageNames() []string { func (x *ProcessInfo) GetPackageName() string {
if x != nil { if x != nil {
return x.PackageNames return x.PackageName
} }
return nil return ""
} }
type CloseConnectionRequest struct { type CloseConnectionRequest struct {
@@ -1884,13 +1884,13 @@ const file_daemon_started_service_proto_rawDesc = "" +
"\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" + "\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" +
"\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" + "\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" +
"\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" + "\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" +
"\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa5\x01\n" + "\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa3\x01\n" +
"\vProcessInfo\x12\x1c\n" + "\vProcessInfo\x12\x1c\n" +
"\tprocessId\x18\x01 \x01(\rR\tprocessId\x12\x16\n" + "\tprocessId\x18\x01 \x01(\rR\tprocessId\x12\x16\n" +
"\x06userId\x18\x02 \x01(\x05R\x06userId\x12\x1a\n" + "\x06userId\x18\x02 \x01(\x05R\x06userId\x12\x1a\n" +
"\buserName\x18\x03 \x01(\tR\buserName\x12 \n" + "\buserName\x18\x03 \x01(\tR\buserName\x12 \n" +
"\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12\"\n" + "\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12 \n" +
"\fpackageNames\x18\x05 \x03(\tR\fpackageNames\"(\n" + "\vpackageName\x18\x05 \x01(\tR\vpackageName\"(\n" +
"\x16CloseConnectionRequest\x12\x0e\n" + "\x16CloseConnectionRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"K\n" + "\x02id\x18\x01 \x01(\tR\x02id\"K\n" +
"\x12DeprecatedWarnings\x125\n" + "\x12DeprecatedWarnings\x125\n" +

View File

@@ -195,7 +195,7 @@ message ProcessInfo {
int32 userId = 2; int32 userId = 2;
string userName = 3; string userName = 3;
string processPath = 4; string processPath = 4;
repeated string packageNames = 5; string packageName = 5;
} }
message CloseConnectionRequest { message CloseConnectionRequest {

View File

@@ -283,9 +283,6 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
if timeToLive == 0 { if timeToLive == 0 {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList { for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive { if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl timeToLive = record.Header().Ttl
} }
@@ -297,9 +294,6 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} }
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList { for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = timeToLive record.Header().Ttl = timeToLive
} }
} }
@@ -330,20 +324,16 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
} else { } else {
strategy = options.Strategy strategy = options.Strategy
} }
lookupOptions := options
if options.LookupStrategy != C.DomainStrategyAsIS {
lookupOptions.Strategy = strategy
}
if strategy == C.DomainStrategyIPv4Only { if strategy == C.DomainStrategyIPv4Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker) return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
} else if strategy == C.DomainStrategyIPv6Only { } 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 response4 []netip.Addr
var response6 []netip.Addr var response6 []netip.Addr
var group task.Group var group task.Group
group.Append("exchange4", func(ctx context.Context) error { group.Append("exchange4", func(ctx context.Context) error {
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker) response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
if err != nil { if err != nil {
return err return err
} }
@@ -351,7 +341,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
return nil return nil
}) })
group.Append("exchange6", func(ctx context.Context) error { group.Append("exchange6", func(ctx context.Context) error {
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker) response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
if err != nil { if err != nil {
return err return err
} }
@@ -387,21 +377,21 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
} }
if c.disableExpire { if c.disableExpire {
if !c.independentCache { if !c.independentCache {
c.cache.Add(question, message.Copy()) c.cache.Add(question, message)
} else { } else {
c.transportCache.Add(transportCacheKey{ c.transportCache.Add(transportCacheKey{
Question: question, Question: question,
transportTag: transport.Tag(), transportTag: transport.Tag(),
}, message.Copy()) }, message)
} }
} else { } else {
if !c.independentCache { if !c.independentCache {
c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive)) c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
} else { } else {
c.transportCache.AddWithLifetime(transportCacheKey{ c.transportCache.AddWithLifetime(transportCacheKey{
Question: question, Question: question,
transportTag: transport.Tag(), transportTag: transport.Tag(),
}, message.Copy(), time.Second*time.Duration(timeToLive)) }, message, time.Second*time.Duration(timeToLive))
} }
} }
} }
@@ -492,9 +482,6 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
var originTTL int var originTTL int
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList { for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL { if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
originTTL = int(record.Header().Ttl) originTTL = int(record.Header().Ttl)
} }
@@ -509,18 +496,12 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
duration := uint32(originTTL - nowTTL) duration := uint32(originTTL - nowTTL)
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList { for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = record.Header().Ttl - duration record.Header().Ttl = record.Header().Ttl - duration
} }
} }
} else { } else {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList { for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = uint32(nowTTL) record.Header().Ttl = uint32(nowTTL)
} }
} }

View File

@@ -195,16 +195,7 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
} }
} }
} }
transport := r.transport.Default() return r.transport.Default(), nil, -1
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
return transport, nil, -1
} }
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) { func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
@@ -354,7 +345,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
transport := options.Transport transport := options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS { if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy() options.Strategy = r.defaultDomainStrategy
} }
if !options.ClientSubnet.IsValid() { if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet() options.ClientSubnet = legacyTransport.LegacyClientSubnet()

145
dns/transport/base.go Normal file
View File

@@ -0,0 +1,145 @@
package transport
import (
"context"
"os"
"sync"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
type TransportState int
const (
StateNew TransportState = iota
StateStarted
StateClosing
StateClosed
)
var (
ErrTransportClosed = os.ErrClosed
ErrConnectionReset = E.New("connection reset")
)
type BaseTransport struct {
dns.TransportAdapter
Logger logger.ContextLogger
mutex sync.Mutex
state TransportState
inFlight int32
queriesComplete chan struct{}
closeCtx context.Context
closeCancel context.CancelFunc
}
func NewBaseTransport(adapter dns.TransportAdapter, logger logger.ContextLogger) *BaseTransport {
ctx, cancel := context.WithCancel(context.Background())
return &BaseTransport{
TransportAdapter: adapter,
Logger: logger,
state: StateNew,
closeCtx: ctx,
closeCancel: cancel,
}
}
func (t *BaseTransport) State() TransportState {
t.mutex.Lock()
defer t.mutex.Unlock()
return t.state
}
func (t *BaseTransport) SetStarted() error {
t.mutex.Lock()
defer t.mutex.Unlock()
switch t.state {
case StateNew:
t.state = StateStarted
return nil
case StateStarted:
return nil
default:
return ErrTransportClosed
}
}
func (t *BaseTransport) BeginQuery() bool {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.state != StateStarted {
return false
}
t.inFlight++
return true
}
func (t *BaseTransport) EndQuery() {
t.mutex.Lock()
if t.inFlight > 0 {
t.inFlight--
}
if t.inFlight == 0 && t.queriesComplete != nil {
close(t.queriesComplete)
t.queriesComplete = nil
}
t.mutex.Unlock()
}
func (t *BaseTransport) CloseContext() context.Context {
return t.closeCtx
}
func (t *BaseTransport) Shutdown(ctx context.Context) error {
t.mutex.Lock()
if t.state >= StateClosing {
t.mutex.Unlock()
return nil
}
if t.state == StateNew {
t.state = StateClosed
t.mutex.Unlock()
t.closeCancel()
return nil
}
t.state = StateClosing
if t.inFlight == 0 {
t.state = StateClosed
t.mutex.Unlock()
t.closeCancel()
return nil
}
t.queriesComplete = make(chan struct{})
queriesComplete := t.queriesComplete
t.mutex.Unlock()
t.closeCancel()
select {
case <-queriesComplete:
t.mutex.Lock()
t.state = StateClosed
t.mutex.Unlock()
return nil
case <-ctx.Done():
t.mutex.Lock()
t.state = StateClosed
t.mutex.Unlock()
return ctx.Err()
}
}
func (t *BaseTransport) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout)
defer cancel()
return t.Shutdown(ctx)
}

View File

@@ -1,547 +0,0 @@
package transport
import (
"context"
"net"
"sync"
"time"
"github.com/sagernet/sing/common/x/list"
)
type ConnPoolMode int
const (
ConnPoolSingle ConnPoolMode = iota
ConnPoolOrdered
)
type ConnPoolOptions[T comparable] struct {
Mode ConnPoolMode
IsAlive func(T) bool
Close func(T, error)
}
type ConnPool[T comparable] struct {
options ConnPoolOptions[T]
access sync.Mutex
closed bool
state *connPoolState[T]
}
type connPoolState[T comparable] struct {
ctx context.Context
cancel context.CancelCauseFunc
all map[T]struct{}
idle list.List[T]
idleElements map[T]*list.Element[T]
shared T
hasShared bool
sharedClaimed bool
sharedCtx context.Context
sharedCancel context.CancelCauseFunc
connecting *connPoolConnect[T]
}
type connPoolConnect[T comparable] struct {
done chan struct{}
err error
}
type connPoolDialContext struct {
context.Context
parent context.Context
}
func (c connPoolDialContext) Deadline() (time.Time, bool) {
return c.parent.Deadline()
}
func (c connPoolDialContext) Value(key any) any {
return c.parent.Value(key)
}
func NewConnPool[T comparable](options ConnPoolOptions[T]) *ConnPool[T] {
return &ConnPool[T]{
options: options,
state: newConnPoolState[T](options.Mode),
}
}
func newConnPoolState[T comparable](mode ConnPoolMode) *connPoolState[T] {
ctx, cancel := context.WithCancelCause(context.Background())
state := &connPoolState[T]{
ctx: ctx,
cancel: cancel,
all: make(map[T]struct{}),
}
if mode == ConnPoolOrdered {
state.idleElements = make(map[T]*list.Element[T])
}
return state
}
func (p *ConnPool[T]) Acquire(ctx context.Context, dial func(context.Context) (T, error)) (T, bool, error) {
switch p.options.Mode {
case ConnPoolSingle:
conn, _, created, err := p.acquireShared(ctx, dial)
return conn, created, err
case ConnPoolOrdered:
return p.acquireOrdered(ctx, dial)
default:
var zero T
return zero, false, net.ErrClosed
}
}
func (p *ConnPool[T]) AcquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) {
if p.options.Mode != ConnPoolSingle {
var zero T
return zero, nil, false, net.ErrClosed
}
return p.acquireShared(ctx, dial)
}
func (p *ConnPool[T]) Release(conn T, reuse bool) {
var (
closeConn bool
closeErr error
)
p.access.Lock()
if p.closed || p.state == nil {
closeConn = true
closeErr = net.ErrClosed
p.access.Unlock()
if closeConn {
p.options.Close(conn, closeErr)
}
return
}
currentState := p.state
_, tracked := currentState.all[conn]
if !tracked {
closeConn = true
closeErr = p.closeCause(currentState)
p.access.Unlock()
if closeConn {
p.options.Close(conn, closeErr)
}
return
}
if !reuse || !p.options.IsAlive(conn) {
delete(currentState.all, conn)
switch p.options.Mode {
case ConnPoolSingle:
if currentState.hasShared && currentState.shared == conn {
var zero T
currentState.shared = zero
currentState.hasShared = false
currentState.sharedClaimed = false
currentState.sharedCtx = nil
if currentState.sharedCancel != nil {
currentState.sharedCancel(net.ErrClosed)
currentState.sharedCancel = nil
}
}
case ConnPoolOrdered:
if element, loaded := currentState.idleElements[conn]; loaded {
currentState.idle.Remove(element)
delete(currentState.idleElements, conn)
}
}
closeConn = true
closeErr = net.ErrClosed
p.access.Unlock()
if closeConn {
p.options.Close(conn, closeErr)
}
return
}
if p.options.Mode == ConnPoolOrdered {
if _, loaded := currentState.idleElements[conn]; !loaded {
currentState.idleElements[conn] = currentState.idle.PushBack(conn)
}
}
p.access.Unlock()
}
func (p *ConnPool[T]) Invalidate(conn T, cause error) {
p.access.Lock()
if p.closed || p.state == nil {
p.access.Unlock()
p.options.Close(conn, cause)
return
}
currentState := p.state
_, tracked := currentState.all[conn]
if !tracked {
p.access.Unlock()
return
}
delete(currentState.all, conn)
switch p.options.Mode {
case ConnPoolSingle:
if currentState.hasShared && currentState.shared == conn {
var zero T
currentState.shared = zero
currentState.hasShared = false
currentState.sharedClaimed = false
currentState.sharedCtx = nil
if currentState.sharedCancel != nil {
currentState.sharedCancel(cause)
currentState.sharedCancel = nil
}
}
case ConnPoolOrdered:
if element, loaded := currentState.idleElements[conn]; loaded {
currentState.idle.Remove(element)
delete(currentState.idleElements, conn)
}
}
p.access.Unlock()
p.options.Close(conn, cause)
}
func (p *ConnPool[T]) Reset() {
p.access.Lock()
if p.closed {
p.access.Unlock()
return
}
oldState := p.state
p.state = newConnPoolState[T](p.options.Mode)
p.access.Unlock()
p.closeState(oldState, net.ErrClosed)
}
func (p *ConnPool[T]) Close() error {
p.access.Lock()
if p.closed {
p.access.Unlock()
return nil
}
p.closed = true
oldState := p.state
p.state = nil
p.access.Unlock()
p.closeState(oldState, net.ErrClosed)
return nil
}
func (p *ConnPool[T]) acquireOrdered(ctx context.Context, dial func(context.Context) (T, error)) (T, bool, error) {
var zero T
for {
var (
staleConn T
hasStale bool
)
p.access.Lock()
if p.closed {
p.access.Unlock()
return zero, false, net.ErrClosed
}
currentState := p.state
if element := currentState.idle.Front(); element != nil {
conn := currentState.idle.Remove(element)
delete(currentState.idleElements, conn)
if p.options.IsAlive(conn) {
p.access.Unlock()
return conn, false, nil
}
delete(currentState.all, conn)
staleConn = conn
hasStale = true
}
p.access.Unlock()
if hasStale {
p.options.Close(staleConn, net.ErrClosed)
continue
}
conn, err := p.dial(ctx, currentState, dial)
if err != nil {
return zero, false, err
}
p.access.Lock()
if p.closed {
p.access.Unlock()
p.options.Close(conn, net.ErrClosed)
return zero, false, net.ErrClosed
}
if p.state != currentState {
cause := p.closeCause(currentState)
p.access.Unlock()
p.options.Close(conn, cause)
return zero, false, cause
}
currentState.all[conn] = struct{}{}
p.access.Unlock()
return conn, true, nil
}
}
func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) {
var zero T
for {
var (
staleConn T
hasStale bool
state *connPoolConnect[T]
current *connPoolState[T]
startDial bool
)
p.access.Lock()
if p.closed {
p.access.Unlock()
return zero, nil, false, net.ErrClosed
}
current = p.state
if current.hasShared {
conn := current.shared
if p.options.IsAlive(conn) {
created := !current.sharedClaimed
current.sharedClaimed = true
connCtx := current.sharedCtx
p.access.Unlock()
return conn, connCtx, created, nil
}
delete(current.all, conn)
var zeroConn T
current.shared = zeroConn
current.hasShared = false
current.sharedClaimed = false
current.sharedCtx = nil
if current.sharedCancel != nil {
current.sharedCancel(net.ErrClosed)
current.sharedCancel = nil
}
staleConn = conn
hasStale = true
p.access.Unlock()
p.options.Close(staleConn, net.ErrClosed)
continue
}
if current.connecting == nil {
current.connecting = &connPoolConnect[T]{
done: make(chan struct{}),
}
startDial = true
}
state = current.connecting
p.access.Unlock()
if hasStale {
continue
}
if startDial {
go p.connectSingle(current, state, ctx, dial)
}
select {
case <-state.done:
conn, connCtx, created, retry, err := p.collectShared(current, state, startDial)
if retry {
continue
}
return conn, connCtx, created, err
case <-ctx.Done():
return zero, nil, false, ctx.Err()
case <-current.ctx.Done():
p.access.Lock()
closed := p.closed
p.access.Unlock()
if closed {
return zero, nil, false, net.ErrClosed
}
}
}
}
func (p *ConnPool[T]) connectSingle(current *connPoolState[T], state *connPoolConnect[T], ctx context.Context, dial func(context.Context) (T, error)) {
conn, err := p.dial(ctx, current, dial)
if err != nil {
p.access.Lock()
if current.connecting == state {
current.connecting = nil
}
state.err = err
p.access.Unlock()
close(state.done)
return
}
var closeErr error
p.access.Lock()
if current.connecting == state {
current.connecting = nil
}
if p.closed {
closeErr = net.ErrClosed
state.err = closeErr
} else if p.state != current {
closeErr = p.closeCause(current)
state.err = closeErr
} else {
sharedCtx, sharedCancel := context.WithCancelCause(current.ctx)
current.shared = conn
current.hasShared = true
current.sharedClaimed = false
current.sharedCtx = sharedCtx
current.sharedCancel = sharedCancel
current.all[conn] = struct{}{}
}
p.access.Unlock()
if closeErr != nil {
p.options.Close(conn, closeErr)
}
close(state.done)
}
func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolConnect[T], startDial bool) (T, context.Context, bool, bool, error) {
var zero T
p.access.Lock()
if state.err != nil {
err := state.err
p.access.Unlock()
if startDial {
return zero, nil, false, false, err
}
return zero, nil, false, true, nil
}
if p.closed {
p.access.Unlock()
return zero, nil, false, false, net.ErrClosed
}
if p.state != current {
cause := p.closeCause(current)
p.access.Unlock()
return zero, nil, false, false, cause
}
if !current.hasShared {
p.access.Unlock()
return zero, nil, false, true, nil
}
conn := current.shared
if !p.options.IsAlive(conn) {
delete(current.all, conn)
var zeroConn T
current.shared = zeroConn
current.hasShared = false
current.sharedClaimed = false
current.sharedCtx = nil
if current.sharedCancel != nil {
current.sharedCancel(net.ErrClosed)
current.sharedCancel = nil
}
p.access.Unlock()
p.options.Close(conn, net.ErrClosed)
return zero, nil, false, true, nil
}
created := !current.sharedClaimed
current.sharedClaimed = true
connCtx := current.sharedCtx
p.access.Unlock()
return conn, connCtx, created, false, nil
}
func (p *ConnPool[T]) dial(ctx context.Context, current *connPoolState[T], dial func(context.Context) (T, error)) (T, error) {
var zero T
if err := ctx.Err(); err != nil {
return zero, err
}
if cause := context.Cause(current.ctx); cause != nil {
return zero, cause
}
dialCtx, cancel := context.WithCancelCause(current.ctx)
var (
stateAccess sync.Mutex
dialComplete bool
)
stopCancel := context.AfterFunc(ctx, func() {
stateAccess.Lock()
if !dialComplete {
cancel(context.Cause(ctx))
}
stateAccess.Unlock()
})
select {
case <-ctx.Done():
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
cancel(context.Cause(ctx))
return zero, ctx.Err()
default:
}
conn, err := dial(connPoolDialContext{
Context: dialCtx,
parent: ctx,
})
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
if err != nil {
if cause := context.Cause(dialCtx); cause != nil {
return zero, cause
}
return zero, err
}
if cause := context.Cause(dialCtx); cause != nil {
p.options.Close(conn, cause)
return zero, cause
}
return conn, nil
}
func (p *ConnPool[T]) closeState(state *connPoolState[T], cause error) {
if state == nil {
return
}
state.cancel(cause)
if state.sharedCancel != nil {
state.sharedCancel(cause)
}
for conn := range state.all {
p.options.Close(conn, cause)
}
}
func (p *ConnPool[T]) closeCause(state *connPoolState[T]) error {
_ = state
return net.ErrClosed
}

287
dns/transport/connector.go Normal file
View File

@@ -0,0 +1,287 @@
package transport
import (
"context"
"net"
"sync"
"time"
E "github.com/sagernet/sing/common/exceptions"
)
type ConnectorCallbacks[T any] struct {
IsClosed func(connection T) bool
Close func(connection T)
Reset func(connection T)
}
type Connector[T any] struct {
dial func(ctx context.Context) (T, error)
callbacks ConnectorCallbacks[T]
access sync.Mutex
connection T
hasConnection bool
connectionCancel context.CancelFunc
connecting chan struct{}
closeCtx context.Context
closed bool
}
func NewConnector[T any](closeCtx context.Context, dial func(context.Context) (T, error), callbacks ConnectorCallbacks[T]) *Connector[T] {
return &Connector[T]{
dial: dial,
callbacks: callbacks,
closeCtx: closeCtx,
}
}
func NewSingleflightConnector(closeCtx context.Context, dial func(context.Context) (*Connection, error)) *Connector[*Connection] {
return NewConnector(closeCtx, dial, ConnectorCallbacks[*Connection]{
IsClosed: func(connection *Connection) bool {
return connection.IsClosed()
},
Close: func(connection *Connection) {
connection.CloseWithError(ErrTransportClosed)
},
Reset: func(connection *Connection) {
connection.CloseWithError(ErrConnectionReset)
},
})
}
type contextKeyConnecting struct{}
var errRecursiveConnectorDial = E.New("recursive connector dial")
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
var zero T
for {
c.access.Lock()
if c.closed {
c.access.Unlock()
return zero, ErrTransportClosed
}
if c.hasConnection && !c.callbacks.IsClosed(c.connection) {
connection := c.connection
c.access.Unlock()
return connection, nil
}
c.hasConnection = false
if c.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
if isRecursiveConnectorDial(ctx, c) {
c.access.Unlock()
return zero, errRecursiveConnectorDial
}
if c.connecting != nil {
connecting := c.connecting
c.access.Unlock()
select {
case <-connecting:
continue
case <-ctx.Done():
return zero, ctx.Err()
case <-c.closeCtx.Done():
return zero, ErrTransportClosed
}
}
if err := ctx.Err(); err != nil {
c.access.Unlock()
return zero, err
}
c.connecting = make(chan struct{})
c.access.Unlock()
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
connection, cancel, err := c.dialWithCancellation(dialContext)
c.access.Lock()
close(c.connecting)
c.connecting = nil
if err != nil {
c.access.Unlock()
return zero, err
}
if c.closed {
cancel()
c.callbacks.Close(connection)
c.access.Unlock()
return zero, ErrTransportClosed
}
if err = ctx.Err(); err != nil {
cancel()
c.callbacks.Close(connection)
c.access.Unlock()
return zero, err
}
c.connection = connection
c.hasConnection = true
c.connectionCancel = cancel
result := c.connection
c.access.Unlock()
return result, nil
}
}
func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T]) bool {
dialConnector, loaded := ctx.Value(contextKeyConnecting{}).(*Connector[T])
return loaded && dialConnector == connector
}
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) {
var zero T
if err := ctx.Err(); err != nil {
return zero, nil, err
}
connCtx, cancel := context.WithCancel(c.closeCtx)
var (
stateAccess sync.Mutex
dialComplete bool
)
stopCancel := context.AfterFunc(ctx, func() {
stateAccess.Lock()
if !dialComplete {
cancel()
}
stateAccess.Unlock()
})
select {
case <-ctx.Done():
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
cancel()
return zero, nil, ctx.Err()
default:
}
connection, err := c.dial(valueContext{connCtx, ctx})
stateAccess.Lock()
dialComplete = true
stateAccess.Unlock()
stopCancel()
if err != nil {
cancel()
return zero, nil, err
}
return connection, cancel, nil
}
type valueContext struct {
context.Context
parent context.Context
}
func (v valueContext) Value(key any) any {
return v.parent.Value(key)
}
func (v valueContext) Deadline() (time.Time, bool) {
return v.parent.Deadline()
}
func (c *Connector[T]) Close() error {
c.access.Lock()
defer c.access.Unlock()
if c.closed {
return nil
}
c.closed = true
if c.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
if c.hasConnection {
c.callbacks.Close(c.connection)
c.hasConnection = false
}
return nil
}
func (c *Connector[T]) Reset() {
c.access.Lock()
defer c.access.Unlock()
if c.connectionCancel != nil {
c.connectionCancel()
c.connectionCancel = nil
}
if c.hasConnection {
c.callbacks.Reset(c.connection)
c.hasConnection = false
}
}
type Connection struct {
net.Conn
closeOnce sync.Once
done chan struct{}
closeError error
}
func WrapConnection(conn net.Conn) *Connection {
return &Connection{
Conn: conn,
done: make(chan struct{}),
}
}
func (c *Connection) Done() <-chan struct{} {
return c.done
}
func (c *Connection) IsClosed() bool {
select {
case <-c.done:
return true
default:
return false
}
}
func (c *Connection) CloseError() error {
select {
case <-c.done:
if c.closeError != nil {
return c.closeError
}
return ErrTransportClosed
default:
return nil
}
}
func (c *Connection) Close() error {
return c.CloseWithError(ErrTransportClosed)
}
func (c *Connection) CloseWithError(err error) error {
var returnError error
c.closeOnce.Do(func() {
c.closeError = err
returnError = c.Conn.Close()
close(c.done)
})
return returnError
}

View File

@@ -0,0 +1,263 @@
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")
}
}

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport" "github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -39,6 +40,13 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr,
results := make(chan queryResult) results := make(chan queryResult)
startRacer := func(ctx context.Context, fqdn string) { startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, servers, fqdn, message) response, err := t.tryOneName(ctx, servers, fqdn, message)
if err == nil {
if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 {
err = dns.RcodeSuccess
}
}
select { select {
case results <- queryResult{response, err}: case results <- queryResult{response, err}:
case <-returned: case <-returned:

View File

@@ -23,25 +23,16 @@ var _ adapter.FakeIPTransport = (*Transport)(nil)
type Transport struct { type Transport struct {
dns.TransportAdapter dns.TransportAdapter
logger logger.ContextLogger logger logger.ContextLogger
store adapter.FakeIPStore store adapter.FakeIPStore
inet4Enabled bool
inet6Enabled bool
} }
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) { func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) {
inet4Range := options.Inet4Range.Build(netip.Prefix{}) store := NewStore(ctx, logger, options.Inet4Range.Build(netip.Prefix{}), options.Inet6Range.Build(netip.Prefix{}))
inet6Range := options.Inet6Range.Build(netip.Prefix{})
if !inet4Range.IsValid() && !inet6Range.IsValid() {
return nil, E.New("at least one of inet4_range or inet6_range must be set")
}
store := NewStore(ctx, logger, inet4Range, inet6Range)
return &Transport{ return &Transport{
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil), TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil),
logger: logger, logger: logger,
store: store, store: store,
inet4Enabled: inet4Range.IsValid(),
inet6Enabled: inet6Range.IsValid(),
}, nil }, nil
} }
@@ -64,9 +55,6 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA { if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA {
return nil, E.New("only IP queries are supported by fakeip") return nil, E.New("only IP queries are supported by fakeip")
} }
if question.Qtype == mDNS.TypeA && !t.inet4Enabled || question.Qtype == mDNS.TypeAAAA && !t.inet6Enabled {
return dns.FixedResponseStatus(message, mDNS.RcodeSuccess), nil
}
address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA) address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -81,7 +81,10 @@ func (t *Transport) Reset() {
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if t.resolved != nil { if t.resolved != nil {
return t.resolved.Exchange(ctx, message) resolverObject := t.resolved.Object()
if resolverObject != nil {
return t.resolved.Exchange(resolverObject, ctx, message)
}
} }
question := message.Question[0] question := message.Question[0]
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {

View File

@@ -9,5 +9,6 @@ import (
type ResolvedResolver interface { type ResolvedResolver interface {
Start() error Start() error
Close() 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)
} }

View File

@@ -4,26 +4,19 @@ import (
"bufio" "bufio"
"context" "context"
"errors" "errors"
"net/netip"
"os" "os"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
dnsTransport "github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/resolved" "github.com/sagernet/sing-box/service/resolved"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
@@ -56,23 +49,13 @@ type DBusResolvedResolver struct {
interfaceMonitor tun.DefaultInterfaceMonitor interfaceMonitor tun.DefaultInterfaceMonitor
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
systemBus *dbus.Conn systemBus *dbus.Conn
savedServerSet atomic.Pointer[resolvedServerSet] resoledObject atomic.Pointer[ResolvedObject]
closeOnce sync.Once closeOnce sync.Once
} }
type resolvedServerSet struct { type ResolvedObject struct {
servers []resolvedServer dbus.BusObject
} InterfaceIndex int32
type resolvedServer struct {
primaryTransport adapter.DNSTransport
fallbackTransport adapter.DNSTransport
}
type resolvedServerSpecification struct {
address netip.Addr
port uint16
serverName string
} }
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) { func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
@@ -99,31 +82,17 @@ func (t *DBusResolvedResolver) Start() error {
"org.freedesktop.DBus", "org.freedesktop.DBus",
"NameOwnerChanged", "NameOwnerChanged",
dbus.WithMatchSender("org.freedesktop.DBus"), dbus.WithMatchSender("org.freedesktop.DBus"),
dbus.WithMatchArg(0, "org.freedesktop.resolve1"),
).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"), dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"),
).Err ).Err
if err != nil { if err != nil {
return E.Cause(err, "configure resolved properties listener") return E.Cause(err, "configure resolved restart listener")
} }
go t.loopUpdateStatus() go t.loopUpdateStatus()
return nil return nil
} }
func (t *DBusResolvedResolver) Close() error { func (t *DBusResolvedResolver) Close() error {
var closeErr error
t.closeOnce.Do(func() { t.closeOnce.Do(func() {
serverSet := t.savedServerSet.Swap(nil)
if serverSet != nil {
closeErr = serverSet.Close()
}
if t.interfaceCallback != nil { if t.interfaceCallback != nil {
t.interfaceMonitor.UnregisterCallback(t.interfaceCallback) t.interfaceMonitor.UnregisterCallback(t.interfaceCallback)
} }
@@ -131,97 +100,99 @@ func (t *DBusResolvedResolver) Close() error {
_ = t.systemBus.Close() _ = t.systemBus.Close()
} }
}) })
return closeErr return nil
} }
func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *DBusResolvedResolver) Object() any {
serverSet := t.savedServerSet.Load() return common.PtrOrNil(t.resoledObject.Load())
if serverSet == nil { }
var err error
serverSet, err = t.checkResolved(context.Background()) func (t *DBusResolvedResolver) Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if err != nil { question := message.Question[0]
return nil, err resolvedObject := object.(*ResolvedObject)
} call := resolvedObject.CallWithContext(
previousServerSet := t.savedServerSet.Swap(serverSet) ctx,
if previousServerSet != nil { "org.freedesktop.resolve1.Manager.ResolveRecord",
_ = previousServerSet.Close() 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) var (
if err == nil { records []resolved.ResourceRecord
return response, nil outflags uint64
} )
t.updateStatus() err := call.Store(&records, &outflags)
refreshedServerSet := t.savedServerSet.Load() if err != nil {
if refreshedServerSet == nil || refreshedServerSet == serverSet {
return nil, err 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() { func (t *DBusResolvedResolver) loopUpdateStatus() {
signalChan := make(chan *dbus.Signal, 1) signalChan := make(chan *dbus.Signal, 1)
t.systemBus.Signal(signalChan) t.systemBus.Signal(signalChan)
for signal := range signalChan { for signal := range signalChan {
switch signal.Name { var restarted bool
case "org.freedesktop.DBus.NameOwnerChanged": if signal.Name == "org.freedesktop.DBus.NameOwnerChanged" {
if len(signal.Body) != 3 { if len(signal.Body) != 3 || signal.Body[2].(string) == "" {
continue
}
newOwner, loaded := signal.Body[2].(string)
if !loaded || newOwner == "" {
continue
}
t.updateStatus()
case "org.freedesktop.DBus.Properties.PropertiesChanged":
if !shouldUpdateResolvedServerSet(signal) {
continue continue
} else {
restarted = true
} }
}
if restarted {
t.updateStatus() t.updateStatus()
} }
} }
} }
func (t *DBusResolvedResolver) updateStatus() { func (t *DBusResolvedResolver) updateStatus() {
serverSet, err := t.checkResolved(context.Background()) dbusObject, err := t.checkResolved(context.Background())
oldServerSet := t.savedServerSet.Swap(serverSet) oldValue := t.resoledObject.Swap(dbusObject)
if oldServerSet != nil {
_ = oldServerSet.Close()
}
if err != nil { if err != nil {
var dbusErr dbus.Error var dbusErr dbus.Error
if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" { if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwnerCould" {
t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable")) t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable"))
} }
if oldServerSet != nil { if oldValue != nil {
t.logger.Debug("systemd-resolved service is gone") t.logger.Debug("systemd-resolved service is gone")
} }
return return
} else if oldServerSet == nil { } else if oldValue == nil {
t.logger.Debug("using systemd-resolved service as resolver") t.logger.Debug("using systemd-resolved service as resolver")
} }
} }
func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet) (*mDNS.Msg, error) { func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObject, error) {
if serverSet == nil || len(serverSet.servers) == 0 {
return nil, E.New("link has no DNS servers configured")
}
var lastError error
for _, server := range serverSet.servers {
response, err := server.primaryTransport.Exchange(ctx, message)
if err != nil && server.fallbackTransport != nil {
response, err = server.fallbackTransport.Exchange(ctx, message)
}
if err != nil {
lastError = err
continue
}
return response, nil
}
return nil, lastError
}
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) {
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1") dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err
if err != nil { if err != nil {
@@ -249,19 +220,16 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ
if linkObject == nil { if linkObject == nil {
return nil, E.New("missing link object for default interface") return nil, E.New("missing link object for default interface")
} }
dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject) dnsProp, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
if err != nil { if err != nil {
return nil, err return nil, err
} }
linkDNSEx, err := loadResolvedLinkDNSEx(linkObject) var linkDNS []resolved.LinkDNS
err = dnsProp.Store(&linkDNS)
if err != nil { if err != nil {
return nil, err return nil, err
} }
linkDNS, err := loadResolvedLinkDNS(linkObject) if len(linkDNS) == 0 {
if err != nil {
return nil, err
}
if len(linkDNSEx) == 0 && len(linkDNS) == 0 {
for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() { for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() {
if inbound.Type() == C.TypeTun { if inbound.Type() == C.TypeTun {
return nil, E.New("No appropriate name servers or networks for name found") return nil, E.New("No appropriate name servers or networks for name found")
@@ -269,233 +237,12 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ
} }
return nil, E.New("link has no DNS servers configured") return nil, E.New("link has no DNS servers configured")
} }
serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{ return &ResolvedObject{
BindInterface: defaultInterface.Name, BusObject: dbusObject,
UDPFragmentDefault: true, InterfaceIndex: int32(defaultInterface.Index),
})
if err != nil {
return nil, err
}
var serverSpecifications []resolvedServerSpecification
if len(linkDNSEx) > 0 {
for _, entry := range linkDNSEx {
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, entry.Port, entry.Name)
if !loaded {
continue
}
serverSpecifications = append(serverSpecifications, serverSpecification)
}
} else {
for _, entry := range linkDNS {
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, 0, "")
if !loaded {
continue
}
serverSpecifications = append(serverSpecifications, serverSpecification)
}
}
if len(serverSpecifications) == 0 {
return nil, E.New("no valid DNS servers on link")
}
serverSet := &resolvedServerSet{
servers: make([]resolvedServer, 0, len(serverSpecifications)),
}
for _, serverSpecification := range serverSpecifications {
server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification)
if createErr != nil {
_ = serverSet.Close()
return nil, createErr
}
serverSet.servers = append(serverSet.servers, server)
}
return serverSet, nil
}
func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) {
if dnsOverTLSMode == "yes" {
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
if err != nil {
return resolvedServer{}, err
}
return resolvedServer{
primaryTransport: primaryTransport,
}, nil
}
if dnsOverTLSMode == "opportunistic" {
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
if err != nil {
return resolvedServer{}, err
}
fallbackTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
if err != nil {
_ = primaryTransport.Close()
return resolvedServer{}, err
}
return resolvedServer{
primaryTransport: primaryTransport,
fallbackTransport: fallbackTransport,
}, nil
}
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
if err != nil {
return resolvedServer{}, err
}
return resolvedServer{
primaryTransport: primaryTransport,
}, nil }, nil
} }
func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) {
serverAddress := M.SocksaddrFrom(serverSpecification.address, resolvedServerPort(serverSpecification.port, useTLS))
if useTLS {
tlsAddress := serverSpecification.address
if tlsAddress.Zone() != "" {
tlsAddress = tlsAddress.WithZone("")
}
serverName := serverSpecification.serverName
if serverName == "" {
serverName = tlsAddress.String()
}
tlsConfig, err := tls.NewClient(t.ctx, t.logger, tlsAddress.String(), option.OutboundTLSOptions{
Enabled: true,
ServerName: serverName,
})
if err != nil {
return nil, err
}
serverTransport := dnsTransport.NewTLSRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeTLS, "", nil), serverDialer, serverAddress, tlsConfig)
err = serverTransport.Start(adapter.StartStateStart)
if err != nil {
_ = serverTransport.Close()
return nil, err
}
return serverTransport, nil
}
serverTransport := dnsTransport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), serverDialer, serverAddress)
err := serverTransport.Start(adapter.StartStateStart)
if err != nil {
_ = serverTransport.Close()
return nil, err
}
return serverTransport, nil
}
func (s *resolvedServerSet) Close() error {
var errors []error
for _, server := range s.servers {
errors = append(errors, server.primaryTransport.Close())
if server.fallbackTransport != nil {
errors = append(errors, server.fallbackTransport.Close())
}
}
return E.Errors(errors...)
}
func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, port uint16, serverName string) (resolvedServerSpecification, bool) {
address, loaded := netip.AddrFromSlice(rawAddress)
if !loaded {
return resolvedServerSpecification{}, false
}
if address.Is6() && address.IsLinkLocalUnicast() && address.Zone() == "" {
address = address.WithZone(interfaceName)
}
return resolvedServerSpecification{
address: address,
port: port,
serverName: serverName,
}, true
}
func resolvedServerPort(port uint16, useTLS bool) uint16 {
if port > 0 {
return port
}
if useTLS {
return 853
}
return 53
}
func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) {
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
if err != nil {
if isResolvedUnknownPropertyError(err) {
return nil, nil
}
return nil, err
}
var linkDNS []resolved.LinkDNS
err = dnsProperty.Store(&linkDNS)
if err != nil {
return nil, err
}
return linkDNS, nil
}
func loadResolvedLinkDNSEx(linkObject dbus.BusObject) ([]resolved.LinkDNSEx, error) {
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSEx")
if err != nil {
if isResolvedUnknownPropertyError(err) {
return nil, nil
}
return nil, err
}
var linkDNSEx []resolved.LinkDNSEx
err = dnsProperty.Store(&linkDNSEx)
if err != nil {
return nil, err
}
return linkDNSEx, nil
}
func loadResolvedLinkDNSOverTLS(linkObject dbus.BusObject) (string, error) {
dnsOverTLSProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSOverTLS")
if err != nil {
if isResolvedUnknownPropertyError(err) {
return "", nil
}
return "", err
}
var dnsOverTLSMode string
err = dnsOverTLSProperty.Store(&dnsOverTLSMode)
if err != nil {
return "", err
}
return dnsOverTLSMode, nil
}
func isResolvedUnknownPropertyError(err error) bool {
var dbusError dbus.Error
return errors.As(err, &dbusError) && dbusError.Name == "org.freedesktop.DBus.Error.UnknownProperty"
}
func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool {
if len(signal.Body) != 3 {
return true
}
changedProperties, loaded := signal.Body[1].(map[string]dbus.Variant)
if !loaded {
return true
}
for propertyName := range changedProperties {
switch propertyName {
case "DNS", "DNSEx", "DNSOverTLS":
return true
}
}
invalidatedProperties, loaded := signal.Body[2].([]string)
if !loaded {
return true
}
for _, propertyName := range invalidatedProperties {
switch propertyName {
case "DNS", "DNSEx", "DNSOverTLS":
return true
}
}
return false
}
func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) { func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) {
t.updateStatus() t.updateStatus()
} }

View File

@@ -7,6 +7,7 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport" "github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -48,6 +49,13 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
results := make(chan queryResult) results := make(chan queryResult)
startRacer := func(ctx context.Context, fqdn string) { startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, systemConfig, fqdn, message) response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
if err == nil {
if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 {
err = E.New(fqdn, ": empty result")
}
}
select { select {
case results <- queryResult{response, err}: case results <- queryResult{response, err}:
case <-returned: case <-returned:

View File

@@ -31,13 +31,14 @@ func RegisterTransport(registry *dns.TransportRegistry) {
} }
type Transport struct { type Transport struct {
dns.TransportAdapter *transport.BaseTransport
ctx context.Context
dialer N.Dialer dialer N.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
tlsConfig tls.Config tlsConfig tls.Config
connection *transport.ConnPool[*quic.Conn] connector *transport.Connector[*quic.Conn]
} }
func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) { func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
@@ -62,76 +63,93 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
return nil, E.New("invalid server address: ", serverAddr) return nil, E.New("invalid server address: ", serverAddr)
} }
return &Transport{ t := &Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions), BaseTransport: transport.NewBaseTransport(
dialer: transportDialer, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
serverAddr: serverAddr, logger,
tlsConfig: tlsConfig, ),
connection: transport.NewConnPool(transport.ConnPoolOptions[*quic.Conn]{ ctx: ctx,
Mode: transport.ConnPoolSingle, dialer: transportDialer,
IsAlive: func(conn *quic.Conn) bool { serverAddr: serverAddr,
return conn != nil && !common.Done(conn.Context()) tlsConfig: tlsConfig,
}, }
Close: func(conn *quic.Conn, _ error) {
conn.CloseWithError(0, "") t.connector = transport.NewConnector(t.CloseContext(), t.dial, transport.ConnectorCallbacks[*quic.Conn]{
}, IsClosed: func(connection *quic.Conn) bool {
}), return common.Done(connection.Context())
}, nil },
Close: func(connection *quic.Conn) {
connection.CloseWithError(0, "")
},
Reset: func(connection *quic.Conn) {
connection.CloseWithError(0, "")
},
})
return t, nil
}
func (t *Transport) dial(ctx context.Context) (*quic.Conn, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
earlyConnection, err := sQUIC.DialEarly(
ctx,
bufio.NewUnbindPacketConn(conn),
t.serverAddr.UDPAddr(),
t.tlsConfig,
nil,
)
if err != nil {
conn.Close()
return nil, E.Cause(err, "establish QUIC connection")
}
return earlyConnection, nil
} }
func (t *Transport) Start(stage adapter.StartStage) error { func (t *Transport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer) return dialer.InitializeDetour(t.dialer)
} }
func (t *Transport) Close() error { func (t *Transport) Close() error {
return t.connection.Close() return E.Errors(t.BaseTransport.Close(), t.connector.Close())
} }
func (t *Transport) Reset() { func (t *Transport) Reset() {
t.connection.Reset() t.connector.Reset()
} }
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !t.BeginQuery() {
return nil, transport.ErrTransportClosed
}
defer t.EndQuery()
var ( var (
conn *quic.Conn conn *quic.Conn
err error err error
response *mDNS.Msg response *mDNS.Msg
) )
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
conn, _, err = t.connection.Acquire(ctx, func(ctx context.Context) (*quic.Conn, error) { conn, err = t.connector.Get(ctx)
rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
earlyConnection, err := sQUIC.DialEarly(
ctx,
bufio.NewUnbindPacketConn(rawConn),
t.serverAddr.UDPAddr(),
t.tlsConfig,
nil,
)
if err != nil {
rawConn.Close()
return nil, E.Cause(err, "establish QUIC connection")
}
return earlyConnection, nil
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
response, err = t.exchange(ctx, message, conn) response, err = t.exchange(ctx, message, conn)
if err == nil { if err == nil {
t.connection.Release(conn, true)
return response, nil return response, nil
} else if !isQUICRetryError(err) { } else if !isQUICRetryError(err) {
t.connection.Release(conn, true)
return nil, err return nil, err
} else { } else {
t.connection.Release(conn, true) t.connector.Reset()
t.Reset()
continue continue
} }
} }

View File

@@ -2,6 +2,7 @@ package transport
import ( import (
"context" "context"
"sync"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -16,6 +17,7 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
) )
@@ -27,13 +29,13 @@ func RegisterTLS(registry *dns.TransportRegistry) {
} }
type TLSTransport struct { type TLSTransport struct {
dns.TransportAdapter *BaseTransport
logger logger.ContextLogger
dialer tls.Dialer dialer tls.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
tlsConfig tls.Config tlsConfig tls.Config
connections *ConnPool[*tlsDNSConn] access sync.Mutex
connections list.List[*tlsDNSConn]
} }
type tlsDNSConn struct { type tlsDNSConn struct {
@@ -64,20 +66,10 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport { func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport {
return &TLSTransport{ return &TLSTransport{
TransportAdapter: adapter, BaseTransport: NewBaseTransport(adapter, logger),
logger: logger, dialer: tls.NewDialer(dialer, tlsConfig),
dialer: tls.NewDialer(dialer, tlsConfig), serverAddr: serverAddr,
serverAddr: serverAddr, tlsConfig: tlsConfig,
tlsConfig: tlsConfig,
connections: NewConnPool(ConnPoolOptions[*tlsDNSConn]{
Mode: ConnPoolOrdered,
IsAlive: func(conn *tlsDNSConn) bool {
return conn != nil
},
Close: func(conn *tlsDNSConn, _ error) {
conn.Close()
},
}),
} }
} }
@@ -85,43 +77,53 @@ func (t *TLSTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer) return dialer.InitializeDetour(t.dialer)
} }
func (t *TLSTransport) Close() error { func (t *TLSTransport) Close() error {
return t.connections.Close() t.access.Lock()
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
connection.Value.Close()
}
t.connections.Init()
t.access.Unlock()
return t.BaseTransport.Close()
} }
func (t *TLSTransport) Reset() { func (t *TLSTransport) Reset() {
t.connections.Reset() t.access.Lock()
defer t.access.Unlock()
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
connection.Value.Close()
}
t.connections.Init()
} }
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
var lastErr error if !t.BeginQuery() {
for attempt := 0; attempt < 2; attempt++ { return nil, ErrTransportClosed
conn, created, err := t.connections.Acquire(ctx, func(ctx context.Context) (*tlsDNSConn, error) { }
tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr) defer t.EndQuery()
if err != nil {
return nil, E.Cause(err, "dial TLS connection") t.access.Lock()
} conn := t.connections.PopFront()
return &tlsDNSConn{Conn: tlsConn}, nil t.access.Unlock()
}) if conn != nil {
if err != nil {
return nil, err
}
response, err := t.exchange(ctx, message, conn) response, err := t.exchange(ctx, message, conn)
if err == nil { if err == nil {
t.connections.Release(conn, true)
return response, nil return response, nil
} }
lastErr = err t.Logger.DebugContext(ctx, "discarded pooled connection: ", err)
t.logger.DebugContext(ctx, "discarded pooled connection: ", err)
t.connections.Release(conn, false)
if created {
return nil, err
}
} }
return nil, lastErr tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial TLS connection")
}
return t.exchange(ctx, message, &tlsDNSConn{Conn: tlsConn})
} }
func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) { func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) {
@@ -131,12 +133,22 @@ func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tl
conn.queryId++ conn.queryId++
err := WriteMessage(conn, conn.queryId, message) err := WriteMessage(conn, conn.queryId, message)
if err != nil { if err != nil {
conn.Close()
return nil, E.Cause(err, "write request") return nil, E.Cause(err, "write request")
} }
response, err := ReadMessage(conn) response, err := ReadMessage(conn)
if err != nil { if err != nil {
conn.Close()
return nil, E.Cause(err, "read response") return nil, E.Cause(err, "read response")
} }
t.access.Lock()
if t.State() >= StateClosing {
t.access.Unlock()
conn.Close()
return response, nil
}
conn.SetDeadline(time.Time{}) conn.SetDeadline(time.Time{})
t.connections.PushBack(conn)
t.access.Unlock()
return response, nil return response, nil
} }

View File

@@ -2,7 +2,6 @@ package transport
import ( import (
"context" "context"
"net"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -28,14 +27,13 @@ func RegisterUDP(registry *dns.TransportRegistry) {
} }
type UDPTransport struct { type UDPTransport struct {
dns.TransportAdapter *BaseTransport
logger logger.ContextLogger
dialer N.Dialer dialer N.Dialer
serverAddr M.Socksaddr serverAddr M.Socksaddr
udpSize atomic.Int32 udpSize atomic.Int32
connection *ConnPool[net.Conn] connector *Connector[*Connection]
callbackAccess sync.RWMutex callbackAccess sync.RWMutex
queryId uint16 queryId uint16
@@ -65,38 +63,43 @@ func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options o
func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialerInstance N.Dialer, serverAddr M.Socksaddr) *UDPTransport { func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialerInstance N.Dialer, serverAddr M.Socksaddr) *UDPTransport {
t := &UDPTransport{ t := &UDPTransport{
TransportAdapter: adapter, BaseTransport: NewBaseTransport(adapter, logger),
logger: logger, dialer: dialerInstance,
dialer: dialerInstance, serverAddr: serverAddr,
serverAddr: serverAddr, callbacks: make(map[uint16]*udpCallback),
callbacks: make(map[uint16]*udpCallback),
connection: NewConnPool(ConnPoolOptions[net.Conn]{
Mode: ConnPoolSingle,
IsAlive: func(conn net.Conn) bool {
return conn != nil
},
Close: func(conn net.Conn, cause error) {
conn.Close()
},
}),
} }
t.udpSize.Store(2048) t.udpSize.Store(2048)
t.connector = NewSingleflightConnector(t.CloseContext(), t.dial)
return t return t
} }
func (t *UDPTransport) dial(ctx context.Context) (*Connection, error) {
rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
conn := WrapConnection(rawConn)
go t.recvLoop(conn)
return conn, nil
}
func (t *UDPTransport) Start(stage adapter.StartStage) error { func (t *UDPTransport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.SetStarted()
if err != nil {
return err
}
return dialer.InitializeDetour(t.dialer) return dialer.InitializeDetour(t.dialer)
} }
func (t *UDPTransport) Close() error { func (t *UDPTransport) Close() error {
return t.connection.Close() return E.Errors(t.BaseTransport.Close(), t.connector.Close())
} }
func (t *UDPTransport) Reset() { func (t *UDPTransport) Reset() {
t.connection.Reset() t.connector.Reset()
} }
func (t *UDPTransport) nextAvailableQueryId() (uint16, error) { func (t *UDPTransport) nextAvailableQueryId() (uint16, error) {
@@ -113,12 +116,17 @@ func (t *UDPTransport) nextAvailableQueryId() (uint16, error) {
} }
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
if !t.BeginQuery() {
return nil, ErrTransportClosed
}
defer t.EndQuery()
response, err := t.exchange(ctx, message) response, err := t.exchange(ctx, message)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if response.Truncated { if response.Truncated {
t.logger.InfoContext(ctx, "response truncated, retrying with TCP") t.Logger.InfoContext(ctx, "response truncated, retrying with TCP")
return t.exchangeTCP(ctx, message) return t.exchangeTCP(ctx, message)
} }
return response, nil return response, nil
@@ -150,25 +158,16 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
break break
} }
if t.udpSize.CompareAndSwap(current, udpSize) { if t.udpSize.CompareAndSwap(current, udpSize) {
t.Reset() t.connector.Reset()
break break
} }
} }
} }
conn, connCtx, created, err := t.connection.AcquireShared(ctx, func(ctx context.Context) (net.Conn, error) { conn, err := t.connector.Get(ctx)
rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
if err != nil {
return nil, E.Cause(err, "dial UDP connection")
}
return rawConn, nil
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if created {
go t.recvLoop(conn)
}
callback := &udpCallback{ callback := &udpCallback{
done: make(chan struct{}), done: make(chan struct{}),
@@ -178,7 +177,6 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
queryId, err := t.nextAvailableQueryId() queryId, err := t.nextAvailableQueryId()
if err != nil { if err != nil {
t.callbackAccess.Unlock() t.callbackAccess.Unlock()
t.connection.Release(conn, true)
return nil, err return nil, err
} }
t.callbacks[queryId] = callback t.callbacks[queryId] = callback
@@ -205,30 +203,30 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
_, err = conn.Write(rawMessage) _, err = conn.Write(rawMessage)
if err != nil { if err != nil {
t.connection.Invalidate(conn, err) conn.CloseWithError(err)
return nil, E.Cause(err, "write request") return nil, E.Cause(err, "write request")
} }
select { select {
case <-callback.done: case <-callback.done:
t.connection.Release(conn, true)
callback.response.Id = originalId callback.response.Id = originalId
return callback.response, nil return callback.response, nil
case <-connCtx.Done(): case <-conn.Done():
return nil, context.Cause(connCtx) return nil, conn.CloseError()
case <-t.CloseContext().Done():
return nil, ErrTransportClosed
case <-ctx.Done(): case <-ctx.Done():
t.connection.Release(conn, true)
return nil, ctx.Err() return nil, ctx.Err()
} }
} }
func (t *UDPTransport) recvLoop(conn net.Conn) { func (t *UDPTransport) recvLoop(conn *Connection) {
for { for {
buffer := buf.NewSize(int(t.udpSize.Load())) buffer := buf.NewSize(int(t.udpSize.Load()))
_, err := buffer.ReadOnceFrom(conn) _, err := buffer.ReadOnceFrom(conn)
if err != nil { if err != nil {
buffer.Release() buffer.Release()
t.connection.Invalidate(conn, err) conn.CloseWithError(err)
return return
} }
@@ -236,7 +234,7 @@ func (t *UDPTransport) recvLoop(conn net.Conn) {
err = message.Unpack(buffer.Bytes()) err = message.Unpack(buffer.Bytes())
buffer.Release() buffer.Release()
if err != nil { if err != nil {
t.logger.Debug("discarded malformed UDP response: ", err) t.Logger.Debug("discarded malformed UDP response: ", err)
continue continue
} }

View File

@@ -2,68 +2,6 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
<<<<<<< HEAD
=======
#### 1.13.11
* Fix process searcher failure introduced in 1.13.9
* Fixes and improvements
#### 1.13.10
* Fix process searcher failure introduced in 1.13.9
#### 1.13.9
* Fixes and improvements
#### 1.13.8
* Update naiveproxy to v147.0.7727.49-1
* Fix fake-ip DNS server should return SUCCESS when address type is not configured
* Fixes and improvements
#### 1.13.7
* Fixes and improvements
#### 1.13.6
* Fixes and improvements
#### 1.13.5
* Fixes and improvements
#### 1.13.4
* Fixes and improvements
#### 1.13.3
* Add OpenWrt and Alpine APK packages to release **1**
* Backport to macOS 10.13 High Sierra **2**
* OCM service: Add WebSocket support for Responses API **3**
* Fixes and improvements
**1**:
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
**2**:
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
macOS 10.13 High Sierra, built using Go 1.25 with patches
from [SagerNet/go](https://github.com/SagerNet/go).
**3**:
See [OCM](/configuration/service/ocm).
>>>>>>> v1.13.11
#### 1.13.2 #### 1.13.2
* Fixes and improvements * Fixes and improvements

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "已在 sing-box 1.12.0 废弃" !!! failure "已在 sing-box 1.12.0 废弃"
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
### 结构 ### 结构

View File

@@ -209,7 +209,7 @@ icon: material/alert-decagram
(`source_port` || `source_port_range`) && (`source_port` || `source_port_range`) &&
`other fields` `other fields`
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics. Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
#### inbound #### inbound
@@ -546,4 +546,4 @@ Match any IP with query response.
#### rules #### rules
Included rules. Included rules.

View File

@@ -208,7 +208,7 @@ icon: material/alert-decagram
(`source_port` || `source_port_range`) && (`source_port` || `source_port_range`) &&
`other fields` `other fields`
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义 另外,引用规则集可视为被合并,而不是作为一个单独的规则子项
#### inbound #### inbound
@@ -256,7 +256,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! failure "已在 sing-box 1.12.0 中被移除" !!! failure "已在 sing-box 1.12.0 中被移除"
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。 GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。
匹配 Geosite。 匹配 Geosite。
@@ -264,7 +264,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
!!! failure "已在 sing-box 1.12.0 中被移除" !!! failure "已在 sing-box 1.12.0 中被移除"
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
匹配源 GeoIP。 匹配源 GeoIP。
@@ -453,7 +453,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! failure "已在 sing-box 1.12.0 废弃" !!! failure "已在 sing-box 1.12.0 废弃"
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项)。 `outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver)。
匹配出站。 匹配出站。
@@ -505,7 +505,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! failure "已在 sing-box 1.12.0 中被移除" !!! failure "已在 sing-box 1.12.0 中被移除"
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
与查询响应匹配 GeoIP。 与查询响应匹配 GeoIP。
@@ -550,4 +550,4 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
==必填== ==必填==
包括的规则。 包括的规则。

View File

@@ -64,7 +64,7 @@ DNS 服务器的路径。
#### tls #### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段 ### 拨号字段

View File

@@ -64,7 +64,7 @@ DNS 服务器的路径。
#### tls #### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段 ### 拨号字段

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "Deprecated in sing-box 1.12.0" !!! failure "Deprecated in sing-box 1.12.0"
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
!!! quote "sing-box 1.9.0 中的更改" !!! quote "sing-box 1.9.0 中的更改"

View File

@@ -51,7 +51,7 @@ DNS 服务器的端口。
#### tls #### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段 ### 拨号字段

View File

@@ -51,7 +51,7 @@ DNS 服务器的端口。
#### tls #### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段 ### 拨号字段

View File

@@ -42,7 +42,7 @@
将拒绝的 DNS 响应缓存存储在缓存文件中。 将拒绝的 DNS 响应缓存存储在缓存文件中。
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。 [地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。
#### rdrc_timeout #### rdrc_timeout

View File

@@ -1,6 +1,6 @@
!!! quote "" !!! quote ""
默认安装不包含 V2Ray API参阅 [安装](/zh/installation/build-from-source/#构建标记)。 默认安装不包含 V2Ray API参阅 [安装](/zh/installation/build-from-source/#_5)。
### 结构 ### 结构

View File

@@ -58,4 +58,4 @@ AnyTLS 填充方案行数组。
#### tls #### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@@ -26,7 +26,7 @@
#### tls #### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### users #### users

View File

@@ -104,4 +104,4 @@ base64 编码的认证密码。
==必填== ==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@@ -38,7 +38,7 @@ icon: material/alert-decagram
!!! warning "与官方 Hysteria2 的区别" !!! warning "与官方 Hysteria2 的区别"
官方程序支持一种名为 **userpass** 的验证方式, 官方程序支持一种名为 **userpass** 的验证方式,
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。 本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。 要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。
### 监听字段 ### 监听字段
@@ -85,7 +85,7 @@ Hysteria 用户
==必填== ==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### masquerade #### masquerade

View File

@@ -60,4 +60,4 @@ QUIC 拥塞控制算法。
#### tls #### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@@ -93,4 +93,4 @@
#### multiplex #### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。

View File

@@ -43,7 +43,7 @@ Trojan 用户。
#### tls #### tls
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### fallback #### fallback
@@ -61,7 +61,7 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
#### multiplex #### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
#### transport #### transport

View File

@@ -75,4 +75,4 @@ QUIC 拥塞控制算法
==必填== ==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@@ -2,15 +2,6 @@
icon: material/new-box 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" !!! quote "Changes in sing-box 1.13.0"
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark) :material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
@@ -357,9 +348,6 @@ Enforce strict routing rules when `auto_route` is enabled:
* Let unsupported network unreachable * Let unsupported network unreachable
* For legacy reasons, when neither `strict_route` nor `auto_redirect` are enabled, all ICMP traffic will not go through TUN. * 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*: *In Windows*:

View File

@@ -2,10 +2,6 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "sing-box 1.13.3 中的更改"
:material-alert: [strict_route](#strict_route)
!!! quote "sing-box 1.13.0 中的更改" !!! quote "sing-box 1.13.0 中的更改"
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark) :material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
@@ -351,9 +347,6 @@ tun 接口的 IPv6 前缀。
* 使不支持的网络不可达。 * 使不支持的网络不可达。
* 出于历史遗留原因,当未启用 `strict_route``auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。 * 出于历史遗留原因,当未启用 `strict_route``auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。
* 当启用 `auto_redirect` 时,`strict_route` 也影响 `SO_BINDTODEVICE` 流量:
* 启用:`SO_BINDTODEVICE` 流量被重定向通过 sing-box。
* 禁用:`SO_BINDTODEVICE` 流量绕过 sing-box。
*在 Windows 中* *在 Windows 中*

View File

@@ -48,11 +48,11 @@ VLESS 子协议。
#### tls #### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### multiplex #### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
#### transport #### transport

View File

@@ -43,11 +43,11 @@ VMess 用户。
#### tls #### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### multiplex #### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
#### transport #### transport

View File

@@ -59,7 +59,7 @@ AnyTLS 密码。
==必填== ==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段 ### 拨号字段

View File

@@ -4,8 +4,8 @@ icon: material/alert-decagram
!!! quote "sing-box 1.11.0 中的更改" !!! quote "sing-box 1.11.0 中的更改"
:material-delete-clock: [override_address](#override_address) :material-alert-decagram: [override_address](#override_address)
:material-delete-clock: [override_port](#override_port) :material-alert-decagram: [override_port](#override_port)
`direct` 出站直接发送请求。 `direct` 出站直接发送请求。
@@ -29,7 +29,7 @@ icon: material/alert-decagram
!!! failure "已在 sing-box 1.11.0 废弃" !!! failure "已在 sing-box 1.11.0 废弃"
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
覆盖连接目标地址。 覆盖连接目标地址。
@@ -37,7 +37,7 @@ icon: material/alert-decagram
!!! failure "已在 sing-box 1.11.0 废弃" !!! failure "已在 sing-box 1.11.0 废弃"
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
覆盖连接目标端口。 覆盖连接目标端口。

View File

@@ -4,7 +4,7 @@ icon: material/delete-clock
!!! failure "已在 sing-box 1.11.0 废弃" !!! failure "已在 sing-box 1.11.0 废弃"
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作). 旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions).
`dns` 出站是一个内部 DNS 服务器。 `dns` 出站是一个内部 DNS 服务器。

View File

@@ -51,7 +51,7 @@ HTTP 请求的额外标头。
#### tls #### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段 ### 拨号字段

View File

@@ -134,7 +134,7 @@ base64 编码的认证密码。
==必填== ==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段 ### 拨号字段

View File

@@ -38,7 +38,7 @@
!!! warning "与官方 Hysteria2 的区别" !!! warning "与官方 Hysteria2 的区别"
官方程序支持一种名为 **userpass** 的验证方式, 官方程序支持一种名为 **userpass** 的验证方式,
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。 本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。 要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。
### 字段 ### 字段
@@ -105,7 +105,7 @@ QUIC 流量混淆器密码.
==必填== ==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
#### brutal_debug #### brutal_debug

View File

@@ -34,12 +34,10 @@ icon: material/new-box
| Build Variant | Platforms | Description | | Build Variant | Platforms | Description |
|---------------|-----------|-------------| |---------------|-----------|-------------|
| (no suffix) | Linux amd64/arm64 | purego build, `libcronet.so` included | | (default) | Linux amd64/arm64 | purego build with `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) | | `-glibc` | Linux 386/amd64/arm/arm64 | CGO build dynamically linked with glibc, requires glibc >= 2.31 |
| `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO build, statically linked with musl | | `-musl` | Linux 386/amd64/arm/arm64 | CGO build statically linked with musl, no system requirements |
| (no suffix) | Windows amd64/arm64 | purego build, `libcronet.dll` included | | (default) | Windows amd64/arm64 | purego build with `libcronet.dll` included |
For Linux, choose the glibc or musl variant based on your distribution's libc type.
**Runtime Requirements:** **Runtime Requirements:**

View File

@@ -32,14 +32,12 @@ icon: material/new-box
**官方发布版本区别:** **官方发布版本区别:**
| 构建变体 | 平台 | 说明 | | 构建变体 | 平台 | 说明 |
|---|---|---| |-----------|------------------------|------------------------------------------|
| (无后缀) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` | | (默认) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` |
| `-glibc` | Linux 386/amd64/arm/arm64/mipsle/mips64le/riscv64/loong64 | CGO 构建,动态链接 glibc要求 glibc >= 2.31loong64: >= 2.36 | | `-glibc` | Linux 386/amd64/arm/arm64 | CGO 构建,动态链接 glibc要求 glibc >= 2.31 |
| `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO 构建,静态链接 musl | | `-musl` | Linux 386/amd64/arm/arm64 | CGO 构建,静态链接 musl,无系统要求 |
| (无后缀) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` | | (默认) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` |
对于 Linux请根据发行版的 libc 类型选择 glibc 或 musl 变体。
**运行时要求:** **运行时要求:**
@@ -105,7 +103,7 @@ QUIC 拥塞控制算法。
==必填== ==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
只有 `server_name``certificate``certificate_path``ech` 是被支持的。 只有 `server_name``certificate``certificate_path``ech` 是被支持的。

Some files were not shown because too many files have changed in this diff Show More