From 0e27312eda377c4057e996d2492fa8da2c86c524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 6 Mar 2026 17:19:06 +0800 Subject: [PATCH 01/99] Update Go to 1.25.8 --- .github/setup_go_for_windows7.sh | 39 ++++++++++++++++++++++++-------- .github/workflows/build.yml | 14 +++++++----- .github/workflows/docker.yml | 2 +- .github/workflows/linux.yml | 4 ++-- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/.github/setup_go_for_windows7.sh b/.github/setup_go_for_windows7.sh index fe31b944..777d78b0 100755 --- a/.github/setup_go_for_windows7.sh +++ b/.github/setup_go_for_windows7.sh @@ -1,16 +1,35 @@ #!/usr/bin/env bash -VERSION="1.25.7" +set -euo pipefail -mkdir -p $HOME/go -cd $HOME/go +VERSION="1.25.8" +PATCH_COMMITS=( + "466f6c7a29bc098b0d4c987b803c779222894a11" + "1bdabae205052afe1dadb2ad6f1ba612cdbc532a" + "a90777dcf692dd2168577853ba743b4338721b06" + "f6bddda4e8ff58a957462a1a09562924d5f3d05c" + "bed309eff415bcb3c77dd4bc3277b682b89a388d" + "34b899c2fb39b092db4fa67c4417e41dc046be4b" +) +CURL_ARGS=( + -fL + --silent + --show-error +) + +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +fi + +mkdir -p "$HOME/go" +cd "$HOME/go" wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz" tar -xzf "go${VERSION}.linux-amd64.tar.gz" mv go go_win7 cd go_win7 # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 -# this patch file only works on golang1.25.x +# these patch URLs only work on golang1.25.x # that means after golang1.26 release it must be changed # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/ # revert: @@ -18,10 +37,10 @@ cd go_win7 # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" +# fixes: +# bed309eff415bcb3c77dd4bc3277b682b89a388d: "Fix os.RemoveAll not working on Windows7" +# 34b899c2fb39b092db4fa67c4417e41dc046be4b: "Revert \"os: remove 5ms sleep on Windows in (*Process).Wait\"" -alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"' - -curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1 -curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1 -curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1 -curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1 +for patch_commit in "${PATCH_COMMITS[@]}"; do + curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1 +done diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ebb2ca9..f8121aa4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.7 + go-version: ~1.25.8 - name: Check input version if: github.event_name == 'workflow_dispatch' run: |- @@ -124,7 +124,7 @@ jobs: if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }} uses: actions/setup-go@v5 with: - go-version: ~1.25.7 + go-version: ~1.25.8 - name: Setup Go 1.24 if: matrix.legacy_go124 uses: actions/setup-go@v5 @@ -137,9 +137,11 @@ jobs: with: path: | ~/go/go_win7 - key: go_win7_1255 + key: go_win7_1258 - name: Setup Go for Windows 7 if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true' + env: + GITHUB_TOKEN: ${{ github.token }} run: |- .github/setup_go_for_windows7.sh - name: Setup Go for Windows 7 @@ -605,7 +607,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.7 + go-version: ~1.25.8 - name: Setup Android NDK id: setup-ndk uses: nttld/setup-ndk@v1 @@ -695,7 +697,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.7 + go-version: ~1.25.8 - name: Setup Android NDK id: setup-ndk uses: nttld/setup-ndk@v1 @@ -794,7 +796,7 @@ jobs: if: matrix.if uses: actions/setup-go@v5 with: - go-version: ~1.25.7 + go-version: ~1.25.8 - name: Set tag if: matrix.if run: |- diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 75e32583..feddcca8 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -56,7 +56,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.7 + go-version: ~1.25.8 - name: Clone cronet-go if: matrix.naive run: | diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index a029329c..0ab06e72 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -35,7 +35,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.7 + go-version: ~1.25.8 - name: Check input version if: github.event_name == 'workflow_dispatch' run: |- @@ -78,7 +78,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.7 + go-version: ~1.25.8 - name: Clone cronet-go if: matrix.naive run: | From 4b26ab16fb6d85c2fb35e6ac4e0d75ac69a15509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 7 Mar 2026 15:51:27 +0800 Subject: [PATCH 02/99] Bump version --- clients/android | 2 +- clients/apple | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/clients/android b/clients/android index 172199df..7777469b 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit 172199dfc39be91ba95394b0dab20735a88ef33f +Subproject commit 7777469b5d21bc0312ed38bede457ee3128260e2 diff --git a/clients/apple b/clients/apple index 16800708..c19945f6 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit 16800708dd375d2582eec2388b92c1be76fe8343 +Subproject commit c19945f65be76ae5d16fc684a166079877802641 diff --git a/docs/changelog.md b/docs/changelog.md index 00dc3167..29c48605 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.13.2 + +* Fixes and improvements + #### 1.13.1 * Fixes and improvements From d58efc5d013a775f5b97b6009dcd16069be20d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 8 Mar 2026 00:06:41 +0800 Subject: [PATCH 03/99] cronet-go: Fix library search path --- .github/CRONET_GO_VERSION | 2 +- go.mod | 62 +++++++++---------- go.sum | 124 +++++++++++++++++++------------------- 3 files changed, 94 insertions(+), 94 deletions(-) diff --git a/.github/CRONET_GO_VERSION b/.github/CRONET_GO_VERSION index 2838ee07..47b09f9b 100644 --- a/.github/CRONET_GO_VERSION +++ b/.github/CRONET_GO_VERSION @@ -1 +1 @@ -cba7b9ac0399055aa49fbdc57c03c374f58e1597 +2fef65f9dba90ddb89a87d00a6eb6165487c10c1 diff --git a/go.mod b/go.mod index c00a9a2d..99fdaff5 100644 --- a/go.mod +++ b/go.mod @@ -27,8 +27,8 @@ require ( github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cors v1.2.1 - github.com/sagernet/cronet-go v0.0.0-20260303101018-cba7b9ac0399 - github.com/sagernet/cronet-go/all v0.0.0-20260303101018-cba7b9ac0399 + github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9 + github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9 github.com/sagernet/fswatch v0.1.1 github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 @@ -105,35 +105,35 @@ require ( github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260303100323-125d0d93b3e6 // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260303100323-125d0d93b3e6 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/spf13/pflag v1.0.9 // indirect diff --git a/go.sum b/go.sum index 9348343a..09d4fb08 100644 --- a/go.sum +++ b/go.sum @@ -162,68 +162,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260303101018-cba7b9ac0399 h1:x3tVYQHdqqnKbEd9/H4KIGhtHTjA+KfiiaXedI3/w8Q= -github.com/sagernet/cronet-go v0.0.0-20260303101018-cba7b9ac0399/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= -github.com/sagernet/cronet-go/all v0.0.0-20260303101018-cba7b9ac0399 h1:mD3ehudpYf1IFgCTv25d/B6KnBc/lLFq1jmSQIK24y0= -github.com/sagernet/cronet-go/all v0.0.0-20260303101018-cba7b9ac0399/go.mod h1:MbYagcGGIaRo9tNrgafbCTO+Qc7eVEh32ZWMprSB8b0= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260303100323-125d0d93b3e6 h1:ghRKgSaswefPwQF8AYtUlNyumILOB0ptJWxgZ8MFrEE= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260303100323-125d0d93b3e6 h1:Behr7YCnQP2dsvzAJDIoMd5nTVU9/d6MMtk/S3MctwA= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260303100323-125d0d93b3e6 h1:6UL9XdGU/44oTHj36e+EBDJ0RonFoObmd299NG/qQCU= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:Q9apxjtkj6iMIBQlTo71QsOTrNlhHneaXQb1Q0IshU8= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260303100323-125d0d93b3e6 h1:0N+xlnMkFEeqgFe3X/PEvHt+/t+BPgxmbx7wzNcYppg= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:7f2vTXtePikBSV1bdD0zs5/WuZM+bRuej3mREpWL/qQ= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6 h1:HMlnhEYs+axOa0tAJ79se3QsYB8CpRCQo9mewWWFeeg= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:Ux/U6vF+1AoGLSJK3jVa9Kqkn64MX4Ivv7fy0ikDrpQ= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6 h1:5Dhuere2bQFzfGvKxA7TFgA5MoTtgcZMmJQuKwQKlyA= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260303100323-125d0d93b3e6 h1:aMRcLow4UpZWZ28fR9FjveTL/4okrigZySIkEVZnlgA= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260303100323-125d0d93b3e6 h1:y4g8oNtEfSdcKrBKsH5vMAjzGthvhHFNU80sanYDQEM= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260303100323-125d0d93b3e6 h1:CXN6OPILi5trwffmYiiJ9rqJL3XAWx1menLrBBwA0gU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260303100323-125d0d93b3e6 h1:ZphFHQeFOTpqCWPwFcQRnrePXajml8LbKlYFJ5n0isU= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260303100323-125d0d93b3e6 h1:nKzFK84oANHz7I6bab+25bBY+pdpAbO0b3NJroyLldo= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:HqqZUGRXcWvvwlbuvjk/efo8TKW1H/aHdqQTde+Xs9Q= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260303100323-125d0d93b3e6 h1:D2v9lZZG5sm4x/CkG7uqc6ZU3YlhFQ+GmJfvZMK0h/s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260303100323-125d0d93b3e6 h1:TWveNeXHrA5r8XOlf+vw7U2b2M0ip6GNF89jcUi1ogw= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260303100323-125d0d93b3e6 h1:DVCBoXOZI4PNG0cbCLg8lrphRXoLFcAIDLNmzsCVg3I= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260303100323-125d0d93b3e6 h1:7s5xqNlBUWkIXdruPYi3/txXekQhGWxrYxbnB0cnARo= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260303100323-125d0d93b3e6 h1:eyEb+Q7VH4hpE1nV+EmEnN2XX5WilgBpIsfCw4C/7no= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260303100323-125d0d93b3e6 h1:9F1W7+z1hHST6GSzdpQ8Q0NCkneAL18dkRA1HfxH09A= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260303100323-125d0d93b3e6 h1:MmQIR3iJsdvw1ONBP3geK57i9c3+v9dXPMNdZYcYGKw= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260303100323-125d0d93b3e6 h1:j6Pk1Wsl+PCbKRXtp7a912D2D6zqX5Nk51wDQU9TEDc= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260303100323-125d0d93b3e6 h1:0DnFhbRfNqwguNCxiinA7BowQ/RaFt627sjW09JNp80= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6 h1:3CZmlEk2/WW5UHLFJZxXPJ9IJxX3td8U3PyqWSGMl3c= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:eHkVRptoZf3BuuskkjcclO2dwQrX4zluoVGODMrX7n0= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6 h1:UgFmE0cZo9euu8/7sTAhj1G8lldavwXBdcPNyTE29CQ= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260303100323-125d0d93b3e6 h1:xbg3ZB9tLMGDQe4+aewG0Z4bEP/2pLtYBcDzILv5eEc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260303100323-125d0d93b3e6 h1:M0bTSTSTnSMlPY2WaZT6fL5TFICqk8v4cm+QVf8Fcao= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260303100323-125d0d93b3e6/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9 h1:xq5Yr10jXEppD3cnGjE3WENaB6D0YsZu6KptZ8d3054= +github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= +github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9 h1:uxQyy6Y/boOuecVA66tf79JgtoRGfeDJcfYZZLKVA5E= +github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:Xm6cCvs0/twozC1JYNq0sVlOVmcSGzV7YON1XGcD97w= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Qi0IKBpoPP3qZqIXuOKMsT2dv+l/MLWMyBHDMLRw2EA= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:p+wCMjOhj46SpSD/AJeTGgkCcbyA76FyH631XZatyU8= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 h1:Y7lWrZwEhC/HX8Pb5C92CrQihuaE7hrHmWB2ykst3iQ= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:3Ggy5wiyjA6t+aVVPnXlSEIVj9zkxd4ybH3NsvsNefs= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:DuFTCnZloblY+7olXiZoRdueWfxi34EV5UheTFKM2rA= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:x/6T2gjpLw9yNdCVR6xBlzMUzED9fxNFNt6U6A6SOh8= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Lx9PExM70rg8aNxPm0JPeSr5SWC3yFiCz4wIq86ugx8= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:BTEpw7/vKR9BNBsHebfpiGHDCPpjVJ3vLIbHNU3VUfM= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:hdEph9nQXRnKwc/lIDwo15rmzbC6znXF5jJWHPN1Fiw= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Iq++oYV7dtRJHTpu8yclHJdn+1oj2t1e84/YpdXYWW8= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 h1:Y43fuLL8cgwRHpEKwxh0O3vYp7g/SZGvbkJj3cQ6USA= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:bX2GJmF0VCC+tBrVAa49YEsmJ4A9dLmwoA6DJUxRtCY= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:gQTR/2azUCInE0r3kmesZT9xu+x801+BmtDY0d0Tw9Y= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 h1:X4mP3jlYvxgrKpZLOKMmc/O8T5/zP83/23pgfQOc3tY= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:c6xj2nXr/65EDiRFddUKQIBQ/b/lAPoH8WFYlgadaPc= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:ahbl7yjOvGVVNUwk9TcQk+xejVfoYAYFRlhWnby0/YM= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 h1:JC5Zv5+J85da6g5G56VhdaK53fmo6Os2q/wWi5QlxOw= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 h1:4bt7Go588BoM4VjNYMxx0MrvbwlFQn3DdRDCM7BmkRo= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:E1z0BeLUh8EZfCjIyS9BrfCocZrt+0KPS0bzop3Sxf4= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 h1:d8ejxRHO7Vi9JqR/6DxR7RyI/swA2JfDWATR4T7otBw= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 h1:iUDVEVu3RxL5ArPIY72BesbuX5zQ1la/ZFwKpQcGc5c= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 h1:xB6ikOC/R3n3hjy68EJ0sbZhH4vwEhd6JM9jZ1U2SVY= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 h1:mBOuLCPOOMMq8N1+dUM5FqZclqga1+u6fAbPqQcbIhc= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:cwPyDfj+ZNFE7kvcWbayQJyeC/KQA16HTXOxgHphL0w= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Zk9zG8kt3mXAboclUXQlvvxKQuhnI8u5NdDEl8uotNY= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:Lu05srGqddQRMnl1MZtGAReln2yJljeGx9b1IadlMJ8= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Tk9bDywUmOtc0iMjjCVIwMlAQNsxCy+bK+bTNA0OaBE= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:tQqDQw3tEHdQpt7NTdAwF3UvZ3CjNIj/IJKMRFmm388= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:biUIbI2YxUrcQikEfS/bwPA8NsHp/WO+VZUG4morUmE= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= From e343cec4d5fc75e1fba262fea5faca205f021e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 8 Mar 2026 16:00:54 +0800 Subject: [PATCH 04/99] Fix legacy DNS defaults on final transport --- dns/router.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/dns/router.go b/dns/router.go index 567f3225..4f18959b 100644 --- a/dns/router.go +++ b/dns/router.go @@ -195,7 +195,16 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, } } } - return r.transport.Default(), nil, -1 + transport := r.transport.Default() + if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { + if options.Strategy == C.DomainStrategyAsIS { + options.Strategy = legacyTransport.LegacyStrategy() + } + if !options.ClientSubnet.IsValid() { + options.ClientSubnet = legacyTransport.LegacyClientSubnet() + } + } + return transport, nil, -1 } func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) { @@ -345,7 +354,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ transport := options.Transport if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy { if options.Strategy == C.DomainStrategyAsIS { - options.Strategy = r.defaultDomainStrategy + options.Strategy = legacyTransport.LegacyStrategy() } if !options.ClientSubnet.IsValid() { options.ClientSubnet = legacyTransport.LegacyClientSubnet() From 1d388547ee7ad30bdb04a43bca4926dfce33951b Mon Sep 17 00:00:00 2001 From: Oleg Artyomov <47035805+ArtyomovOleg@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:18:27 +0300 Subject: [PATCH 05/99] service/ccm: strip Accept-Encoding before forwarding to avoid untracked usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When clients (e.g. Node.js Anthropic SDK) explicitly set Accept-Encoding: gzip, Go's http.Transport does not transparently decompress the response body, because it only does so when it added the header itself. This causes CCM's json.Unmarshal to receive raw gzip bytes, silently failing to parse usage data and leaving the usage counter unchanged. Fix: remove Accept-Encoding from the outgoing proxy request. Transport adds it automatically and transparently decompresses response.Body before CCM reads it. Wire compression (CCM→Anthropic) is preserved — Transport still negotiates gzip. Only CCM→localhost path is affected; compression on loopback has no practical benefit. --- service/ccm/service.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/service/ccm/service.go b/service/ccm/service.go index ba428060..944bedae 100644 --- a/service/ccm/service.go +++ b/service/ccm/service.go @@ -362,6 +362,13 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } + serviceOverridesAcceptEncoding := len(s.httpHeaders.Values("Accept-Encoding")) > 0 + if s.usageTracker != nil && !serviceOverridesAcceptEncoding { + // Strip Accept-Encoding so Go Transport adds it automatically + // and transparently decompresses the response for correct usage counting. + proxyRequest.Header.Del("Accept-Encoding") + } + anthropicBetaHeader := proxyRequest.Header.Get("anthropic-beta") if anthropicBetaHeader != "" { proxyRequest.Header.Set("anthropic-beta", anthropicBetaOAuthValue+","+anthropicBetaHeader) From 2ba896c5acd4b615a2d1f54e2e9b0fa417ce28c4 Mon Sep 17 00:00:00 2001 From: Heng lu Date: Sun, 8 Mar 2026 23:57:15 -0400 Subject: [PATCH 06/99] Fix netns fd leak in ListenNetworkNamespace --- common/listener/listener.go | 1 + 1 file changed, 1 insertion(+) diff --git a/common/listener/listener.go b/common/listener/listener.go index 7d49d664..cc27a62e 100644 --- a/common/listener/listener.go +++ b/common/listener/listener.go @@ -151,6 +151,7 @@ func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) ( if err != nil { return common.DefaultValue[T](), E.Cause(err, "get current netns") } + defer currentNs.Close() defer netns.Set(currentNs) var targetNs netns.NsHandle if strings.HasPrefix(nameOrPath, "/") { From 9cd60c28c0608b193b6f978eee30c75c8b99f0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 9 Mar 2026 12:01:36 +0800 Subject: [PATCH 07/99] tailscale: Fix inbound UDP packet connection --- protocol/tailscale/endpoint.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index ff82ef86..6ccaa01e 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -144,7 +144,7 @@ func (t *Endpoint) registerNetstackHandlers() { ctx := log.ContextWithNewID(t.ctx) source := M.SocksaddrFrom(src.Addr(), src.Port()) destination := M.SocksaddrFrom(dst.Addr(), dst.Port()) - packetConn := bufio.NewPacketConn(conn) + packetConn := bufio.NewUnbindPacketConnWithAddr(conn, destination) t.NewPacketConnectionEx(ctx, packetConn, source, destination, nil) }, true } From aa495fce384aed449844870a5316ed5b3fbc7634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 9 Mar 2026 12:26:12 +0800 Subject: [PATCH 08/99] Fix local DNS transport CNAME chain broken with systemd-resolved Replace D-Bus ResolveRecord API with direct raw DNS queries to upstream servers obtained from systemd-resolved's per-interface link properties. --- dns/transport/local/local.go | 5 +- dns/transport/local/local_resolved.go | 3 +- dns/transport/local/local_resolved_linux.go | 403 ++++++++++++++++---- 3 files changed, 330 insertions(+), 81 deletions(-) diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go index a42abc76..a3909acc 100644 --- a/dns/transport/local/local.go +++ b/dns/transport/local/local.go @@ -81,10 +81,7 @@ func (t *Transport) Reset() { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { if t.resolved != nil { - resolverObject := t.resolved.Object() - if resolverObject != nil { - return t.resolved.Exchange(resolverObject, ctx, message) - } + return t.resolved.Exchange(ctx, message) } question := message.Question[0] if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { diff --git a/dns/transport/local/local_resolved.go b/dns/transport/local/local_resolved.go index 2a1a190f..e0128d6d 100644 --- a/dns/transport/local/local_resolved.go +++ b/dns/transport/local/local_resolved.go @@ -9,6 +9,5 @@ import ( type ResolvedResolver interface { Start() error Close() error - Object() any - Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) + Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) } diff --git a/dns/transport/local/local_resolved_linux.go b/dns/transport/local/local_resolved_linux.go index bac34c02..fc3ca2b7 100644 --- a/dns/transport/local/local_resolved_linux.go +++ b/dns/transport/local/local_resolved_linux.go @@ -4,19 +4,26 @@ import ( "bufio" "context" "errors" + "net/netip" "os" "strings" "sync" "sync/atomic" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/dns" + dnsTransport "github.com/sagernet/sing-box/dns/transport" + "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/service/resolved" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" @@ -49,13 +56,23 @@ type DBusResolvedResolver struct { interfaceMonitor tun.DefaultInterfaceMonitor interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] systemBus *dbus.Conn - resoledObject atomic.Pointer[ResolvedObject] + savedServerSet atomic.Pointer[resolvedServerSet] closeOnce sync.Once } -type ResolvedObject struct { - dbus.BusObject - InterfaceIndex int32 +type resolvedServerSet struct { + servers []resolvedServer +} + +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) { @@ -82,17 +99,31 @@ func (t *DBusResolvedResolver) Start() error { "org.freedesktop.DBus", "NameOwnerChanged", dbus.WithMatchSender("org.freedesktop.DBus"), - dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"), + dbus.WithMatchArg(0, "org.freedesktop.resolve1"), ).Err if err != nil { return E.Cause(err, "configure resolved restart listener") } + err = t.systemBus.BusObject().AddMatchSignal( + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + dbus.WithMatchSender("org.freedesktop.resolve1"), + dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"), + ).Err + if err != nil { + return E.Cause(err, "configure resolved properties listener") + } go t.loopUpdateStatus() return nil } func (t *DBusResolvedResolver) Close() error { + var closeErr error t.closeOnce.Do(func() { + serverSet := t.savedServerSet.Swap(nil) + if serverSet != nil { + closeErr = serverSet.Close() + } if t.interfaceCallback != nil { t.interfaceMonitor.UnregisterCallback(t.interfaceCallback) } @@ -100,99 +131,97 @@ func (t *DBusResolvedResolver) Close() error { _ = t.systemBus.Close() } }) - return nil + return closeErr } -func (t *DBusResolvedResolver) Object() any { - return common.PtrOrNil(t.resoledObject.Load()) -} - -func (t *DBusResolvedResolver) Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { - question := message.Question[0] - resolvedObject := object.(*ResolvedObject) - call := resolvedObject.CallWithContext( - ctx, - "org.freedesktop.resolve1.Manager.ResolveRecord", - 0, - resolvedObject.InterfaceIndex, - question.Name, - question.Qclass, - question.Qtype, - uint64(0), - ) - if call.Err != nil { - var dbusError dbus.Error - if errors.As(call.Err, &dbusError) && dbusError.Name == "org.freedesktop.resolve1.NoNameServers" { - t.updateStatus() +func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { + serverSet := t.savedServerSet.Load() + if serverSet == nil { + var err error + serverSet, err = t.checkResolved(context.Background()) + if err != nil { + return nil, err + } + previousServerSet := t.savedServerSet.Swap(serverSet) + if previousServerSet != nil { + _ = previousServerSet.Close() } - return nil, E.Cause(call.Err, " resolve record via resolved") } - var ( - records []resolved.ResourceRecord - outflags uint64 - ) - err := call.Store(&records, &outflags) - if err != nil { + response, err := t.exchangeServerSet(ctx, message, serverSet) + if err == nil { + return response, nil + } + t.updateStatus() + refreshedServerSet := t.savedServerSet.Load() + if refreshedServerSet == nil || refreshedServerSet == serverSet { return nil, err } - 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 + return t.exchangeServerSet(ctx, message, refreshedServerSet) } func (t *DBusResolvedResolver) loopUpdateStatus() { signalChan := make(chan *dbus.Signal, 1) t.systemBus.Signal(signalChan) for signal := range signalChan { - var restarted bool - if signal.Name == "org.freedesktop.DBus.NameOwnerChanged" { - if len(signal.Body) != 3 || signal.Body[2].(string) == "" { + switch signal.Name { + case "org.freedesktop.DBus.NameOwnerChanged": + if len(signal.Body) != 3 { + continue + } + newOwner, loaded := signal.Body[2].(string) + if !loaded || newOwner == "" { + continue + } + t.updateStatus() + case "org.freedesktop.DBus.Properties.PropertiesChanged": + if !shouldUpdateResolvedServerSet(signal) { continue - } else { - restarted = true } - } - if restarted { t.updateStatus() } } } func (t *DBusResolvedResolver) updateStatus() { - dbusObject, err := t.checkResolved(context.Background()) - oldValue := t.resoledObject.Swap(dbusObject) + serverSet, err := t.checkResolved(context.Background()) + oldServerSet := t.savedServerSet.Swap(serverSet) + if oldServerSet != nil { + _ = oldServerSet.Close() + } if err != nil { var dbusErr dbus.Error - if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwnerCould" { + if !errors.As(err, &dbusErr) || dbusErr.Name != "org.freedesktop.DBus.Error.NameHasNoOwner" { t.logger.Debug(E.Cause(err, "systemd-resolved service unavailable")) } - if oldValue != nil { + if oldServerSet != nil { t.logger.Debug("systemd-resolved service is gone") } return - } else if oldValue == nil { + } else if oldServerSet == nil { t.logger.Debug("using systemd-resolved service as resolver") } } -func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObject, error) { +func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet) (*mDNS.Msg, error) { + if serverSet == nil || len(serverSet.servers) == 0 { + return nil, E.New("link has no DNS servers configured") + } + var lastError error + for _, server := range serverSet.servers { + response, err := server.primaryTransport.Exchange(ctx, message) + if err != nil && server.fallbackTransport != nil { + response, err = server.fallbackTransport.Exchange(ctx, message) + } + if err != nil { + lastError = err + continue + } + return response, nil + } + return nil, lastError +} + +func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) { dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1") err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err if err != nil { @@ -220,16 +249,19 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje if linkObject == nil { return nil, E.New("missing link object for default interface") } - dnsProp, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS") + dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject) if err != nil { return nil, err } - var linkDNS []resolved.LinkDNS - err = dnsProp.Store(&linkDNS) + linkDNSEx, err := loadResolvedLinkDNSEx(linkObject) if err != nil { return nil, err } - if len(linkDNS) == 0 { + linkDNS, err := loadResolvedLinkDNS(linkObject) + if err != nil { + return nil, err + } + if len(linkDNSEx) == 0 && len(linkDNS) == 0 { for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() { if inbound.Type() == C.TypeTun { return nil, E.New("No appropriate name servers or networks for name found") @@ -237,12 +269,233 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*ResolvedObje } return nil, E.New("link has no DNS servers configured") } - return &ResolvedObject{ - BusObject: dbusObject, - InterfaceIndex: int32(defaultInterface.Index), + serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{ + BindInterface: defaultInterface.Name, + UDPFragmentDefault: true, + }) + if err != nil { + return nil, err + } + var serverSpecifications []resolvedServerSpecification + if len(linkDNSEx) > 0 { + for _, entry := range linkDNSEx { + serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, entry.Port, entry.Name) + if !loaded { + continue + } + serverSpecifications = append(serverSpecifications, serverSpecification) + } + } else { + for _, entry := range linkDNS { + serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, 0, "") + if !loaded { + continue + } + serverSpecifications = append(serverSpecifications, serverSpecification) + } + } + if len(serverSpecifications) == 0 { + return nil, E.New("no valid DNS servers on link") + } + serverSet := &resolvedServerSet{ + servers: make([]resolvedServer, 0, len(serverSpecifications)), + } + for _, serverSpecification := range serverSpecifications { + server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification) + if createErr != nil { + _ = serverSet.Close() + return nil, createErr + } + serverSet.servers = append(serverSet.servers, server) + } + return serverSet, nil +} + +func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) { + if dnsOverTLSMode == "yes" { + primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true) + if err != nil { + return resolvedServer{}, err + } + return resolvedServer{ + primaryTransport: primaryTransport, + }, nil + } + if dnsOverTLSMode == "opportunistic" { + primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true) + if err != nil { + return resolvedServer{}, err + } + fallbackTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false) + if err != nil { + _ = primaryTransport.Close() + return resolvedServer{}, err + } + return resolvedServer{ + primaryTransport: primaryTransport, + fallbackTransport: fallbackTransport, + }, nil + } + primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false) + if err != nil { + return resolvedServer{}, err + } + return resolvedServer{ + primaryTransport: primaryTransport, }, nil } +func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) { + serverAddress := M.SocksaddrFrom(serverSpecification.address, resolvedServerPort(serverSpecification.port, useTLS)) + if useTLS { + tlsAddress := serverSpecification.address + if tlsAddress.Zone() != "" { + tlsAddress = tlsAddress.WithZone("") + } + serverName := serverSpecification.serverName + if serverName == "" { + serverName = tlsAddress.String() + } + tlsConfig, err := tls.NewClient(t.ctx, t.logger, tlsAddress.String(), option.OutboundTLSOptions{ + Enabled: true, + ServerName: serverName, + }) + if err != nil { + return nil, err + } + serverTransport := dnsTransport.NewTLSRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeTLS, "", nil), serverDialer, serverAddress, tlsConfig) + err = serverTransport.Start(adapter.StartStateStart) + if err != nil { + _ = serverTransport.Close() + return nil, err + } + return serverTransport, nil + } + serverTransport := dnsTransport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), serverDialer, serverAddress) + err := serverTransport.Start(adapter.StartStateStart) + if err != nil { + _ = serverTransport.Close() + return nil, err + } + return serverTransport, nil +} + +func (s *resolvedServerSet) Close() error { + var errors []error + for _, server := range s.servers { + errors = append(errors, server.primaryTransport.Close()) + if server.fallbackTransport != nil { + errors = append(errors, server.fallbackTransport.Close()) + } + } + return E.Errors(errors...) +} + +func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, port uint16, serverName string) (resolvedServerSpecification, bool) { + address, loaded := netip.AddrFromSlice(rawAddress) + if !loaded { + return resolvedServerSpecification{}, false + } + if address.Is6() && address.IsLinkLocalUnicast() && address.Zone() == "" { + address = address.WithZone(interfaceName) + } + return resolvedServerSpecification{ + address: address, + port: port, + serverName: serverName, + }, true +} + +func resolvedServerPort(port uint16, useTLS bool) uint16 { + if port > 0 { + return port + } + if useTLS { + return 853 + } + return 53 +} + +func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) { + dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS") + if err != nil { + if isResolvedUnknownPropertyError(err) { + return nil, nil + } + return nil, err + } + var linkDNS []resolved.LinkDNS + err = dnsProperty.Store(&linkDNS) + if err != nil { + return nil, err + } + return linkDNS, nil +} + +func loadResolvedLinkDNSEx(linkObject dbus.BusObject) ([]resolved.LinkDNSEx, error) { + dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSEx") + if err != nil { + if isResolvedUnknownPropertyError(err) { + return nil, nil + } + return nil, err + } + var linkDNSEx []resolved.LinkDNSEx + err = dnsProperty.Store(&linkDNSEx) + if err != nil { + return nil, err + } + return linkDNSEx, nil +} + +func loadResolvedLinkDNSOverTLS(linkObject dbus.BusObject) (string, error) { + dnsOverTLSProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSOverTLS") + if err != nil { + if isResolvedUnknownPropertyError(err) { + return "", nil + } + return "", err + } + var dnsOverTLSMode string + err = dnsOverTLSProperty.Store(&dnsOverTLSMode) + if err != nil { + return "", err + } + return dnsOverTLSMode, nil +} + +func isResolvedUnknownPropertyError(err error) bool { + var dbusError dbus.Error + return errors.As(err, &dbusError) && dbusError.Name == "org.freedesktop.DBus.Error.UnknownProperty" +} + +func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool { + if len(signal.Body) != 3 { + return true + } + changedProperties, loaded := signal.Body[1].(map[string]dbus.Variant) + if !loaded { + return true + } + for propertyName := range changedProperties { + switch propertyName { + case "DNS", "DNSEx", "DNSOverTLS": + return true + } + } + invalidatedProperties, loaded := signal.Body[2].([]string) + if !loaded { + return true + } + for _, propertyName := range invalidatedProperties { + switch propertyName { + case "DNS", "DNSEx", "DNSOverTLS": + return true + } + } + return false +} + func (t *DBusResolvedResolver) updateDefaultInterface(defaultInterface *control.Interface, flags int) { t.updateStatus() } From e1477bd06532dc5cfbacfc3d97dabbce42d39e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 9 Mar 2026 15:04:44 +0800 Subject: [PATCH 09/99] documentation: Update cronet-go descriptions --- docs/configuration/outbound/naive.md | 10 ++++++---- docs/configuration/outbound/naive.zh.md | 14 ++++++++------ docs/installation/build-from-source.md | 16 ++++++++-------- docs/installation/build-from-source.zh.md | 16 ++++++++-------- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/docs/configuration/outbound/naive.md b/docs/configuration/outbound/naive.md index d9af4fb1..5ed9d2b8 100644 --- a/docs/configuration/outbound/naive.md +++ b/docs/configuration/outbound/naive.md @@ -34,10 +34,12 @@ icon: material/new-box | Build Variant | Platforms | Description | |---------------|-----------|-------------| - | (default) | Linux amd64/arm64 | purego build with `libcronet.so` included | - | `-glibc` | Linux 386/amd64/arm/arm64 | CGO build dynamically linked with glibc, requires glibc >= 2.31 | - | `-musl` | Linux 386/amd64/arm/arm64 | CGO build statically linked with musl, no system requirements | - | (default) | Windows amd64/arm64 | purego build with `libcronet.dll` included | + | (no suffix) | Linux amd64/arm64 | purego build, `libcronet.so` included | + | `-glibc` | Linux 386/amd64/arm/arm64/mipsle/mips64le/riscv64/loong64 | CGO build, dynamically linked with glibc, requires glibc >= 2.31 (loong64: >= 2.36) | + | `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO build, statically linked with musl | + | (no suffix) | Windows amd64/arm64 | purego build, `libcronet.dll` included | + + For Linux, choose the glibc or musl variant based on your distribution's libc type. **Runtime Requirements:** diff --git a/docs/configuration/outbound/naive.zh.md b/docs/configuration/outbound/naive.zh.md index 07896407..9bae64c0 100644 --- a/docs/configuration/outbound/naive.zh.md +++ b/docs/configuration/outbound/naive.zh.md @@ -32,12 +32,14 @@ icon: material/new-box **官方发布版本区别:** - | 构建变体 | 平台 | 说明 | - |-----------|------------------------|------------------------------------------| - | (默认) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` | - | `-glibc` | Linux 386/amd64/arm/arm64 | CGO 构建,动态链接 glibc,要求 glibc >= 2.31 | - | `-musl` | Linux 386/amd64/arm/arm64 | CGO 构建,静态链接 musl,无系统要求 | - | (默认) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` | + | 构建变体 | 平台 | 说明 | + |---|---|---| + | (无后缀) | Linux amd64/arm64 | purego 构建,包含 `libcronet.so` | + | `-glibc` | Linux 386/amd64/arm/arm64/mipsle/mips64le/riscv64/loong64 | CGO 构建,动态链接 glibc,要求 glibc >= 2.31(loong64: >= 2.36) | + | `-musl` | Linux 386/amd64/arm/arm64/mipsle/riscv64/loong64 | CGO 构建,静态链接 musl | + | (无后缀) | Windows amd64/arm64 | purego 构建,包含 `libcronet.dll` | + + 对于 Linux,请根据发行版的 libc 类型选择 glibc 或 musl 变体。 **运行时要求:** diff --git a/docs/installation/build-from-source.md b/docs/installation/build-from-source.md index 552ec3fe..8152a89e 100644 --- a/docs/installation/build-from-source.md +++ b/docs/installation/build-from-source.md @@ -92,14 +92,14 @@ NaiveProxy outbound requires special build configurations depending on your targ ### Supported Platforms -| Platform | Architectures | Mode | Requirements | -|-----------------|------------------------|--------|---------------------------------------------------| -| Linux | amd64, arm64 | purego | None (library included in official releases) | -| Linux | 386, amd64, arm, arm64 | CGO | Chromium toolchain, glibc >= 2.31 at runtime | -| Linux (musl) | 386, amd64, arm, arm64 | CGO | Chromium toolchain | -| Windows | amd64, arm64 | purego | None (library included in official releases) | -| Apple platforms | * | CGO | Xcode | -| Android | * | CGO | Android NDK | +| Platform | Architectures | Mode | Requirements | +|-----------------|--------------------------------------------------------|--------|-----------------------------------------------------------------| +| Linux | amd64, arm64 | purego | None (library included in official releases) | +| Linux | 386, amd64, arm, arm64, mipsle, mips64le, riscv64, loong64 | CGO | Chromium toolchain, glibc >= 2.31 (loong64: >= 2.36) at runtime | +| Linux (musl) | 386, amd64, arm, arm64, mipsle, riscv64, loong64 | CGO | Chromium toolchain | +| Windows | amd64, arm64 | purego | None (library included in official releases) | +| Apple platforms | * | CGO | Xcode | +| Android | * | CGO | Android NDK | ### Windows diff --git a/docs/installation/build-from-source.zh.md b/docs/installation/build-from-source.zh.md index 0baf63c3..3972762c 100644 --- a/docs/installation/build-from-source.zh.md +++ b/docs/installation/build-from-source.zh.md @@ -96,14 +96,14 @@ NaiveProxy 出站需要根据目标平台进行特殊的构建配置。 ### 支持的平台 -| 平台 | 架构 | 模式 | 要求 | -|---------------|------------------------|--------|--------------------------------| -| Linux | amd64, arm64 | purego | 无(官方发布版本已包含库文件) | -| Linux | 386, amd64, arm, arm64 | CGO | Chromium 工具链,运行时需要 glibc >= 2.31 | -| Linux (musl) | 386, amd64, arm, arm64 | CGO | Chromium 工具链 | -| Windows | amd64, arm64 | purego | 无(官方发布版本已包含库文件) | -| Apple 平台 | * | CGO | Xcode | -| Android | * | CGO | Android NDK | +| 平台 | 架构 | 模式 | 要求 | +|--------------|----------------------------------------------------------|--------|-----------------------------------------------------| +| Linux | amd64, arm64 | purego | 无(官方发布版本已包含库文件) | +| Linux | 386, amd64, arm, arm64, mipsle, mips64le, riscv64, loong64 | CGO | Chromium 工具链,运行时需要 glibc >= 2.31(loong64: >= 2.36) | +| Linux (musl) | 386, amd64, arm, arm64, mipsle, riscv64, loong64 | CGO | Chromium 工具链 | +| Windows | amd64, arm64 | purego | 无(官方发布版本已包含库文件) | +| Apple 平台 | * | CGO | Xcode | +| Android | * | CGO | Android NDK | ### Windows From e21a72fcd13c2a33c5fec0a9b47052783a8bed5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 9 Mar 2026 15:38:25 +0800 Subject: [PATCH 10/99] Fix websocket connection and goroutine leaks in Clash API Co-authored-by: traitman <112139837+traitman@users.noreply.github.com> --- experimental/clashapi/api_meta.go | 13 ++++++++++--- experimental/clashapi/connections.go | 1 + experimental/clashapi/server.go | 6 ++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/experimental/clashapi/api_meta.go b/experimental/clashapi/api_meta.go index 77dad797..8b31d8a4 100644 --- a/experimental/clashapi/api_meta.go +++ b/experimental/clashapi/api_meta.go @@ -2,6 +2,7 @@ package clashapi import ( "bytes" + "context" "net" "net/http" "runtime/debug" @@ -27,7 +28,7 @@ func (s *Server) setupMetaAPI(r chi.Router) { }) r.Mount("/", middleware.Profiler()) } - r.Get("/memory", memory(s.trafficManager)) + r.Get("/memory", memory(s.ctx, s.trafficManager)) r.Mount("/group", groupRouter(s)) r.Mount("/upgrade", upgradeRouter(s)) } @@ -37,7 +38,7 @@ type Memory struct { OSLimit uint64 `json:"oslimit"` // maybe we need it in the future } -func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { +func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var conn net.Conn if r.Header.Get("Upgrade") == "websocket" { @@ -46,6 +47,7 @@ func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r if err != nil { return } + defer conn.Close() } if conn == nil { @@ -58,7 +60,12 @@ func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r buf := &bytes.Buffer{} var err error first := true - for range tick.C { + for { + select { + case <-ctx.Done(): + return + case <-tick.C: + } buf.Reset() inuse := trafficManager.Snapshot().Memory diff --git a/experimental/clashapi/connections.go b/experimental/clashapi/connections.go index 5074adf7..14274b31 100644 --- a/experimental/clashapi/connections.go +++ b/experimental/clashapi/connections.go @@ -38,6 +38,7 @@ func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager) if err != nil { return } + defer conn.Close() intervalStr := r.URL.Query().Get("interval") interval := 1000 diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index c3661182..ec40a95f 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -115,7 +115,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op chiRouter.Group(func(r chi.Router) { r.Use(authentication(options.Secret)) r.Get("/", hello(options.ExternalUI != "")) - r.Get("/logs", getLogs(logFactory)) + r.Get("/logs", getLogs(s.ctx, logFactory)) r.Get("/traffic", traffic(s.ctx, trafficManager)) r.Get("/version", version) r.Mount("/configs", configRouter(s, logFactory)) @@ -360,7 +360,7 @@ type Log struct { Payload string `json:"payload"` } -func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) { +func getLogs(ctx context.Context, logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { levelText := r.URL.Query().Get("level") if levelText == "" { @@ -399,6 +399,8 @@ func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *ht var logEntry log.Entry for { select { + case <-ctx.Done(): + return case <-done: return case logEntry = <-subscription: From efe20ea51ceba14d523276d54f77a52f11887866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 9 Mar 2026 19:41:01 +0800 Subject: [PATCH 11/99] release: Backport Go 1.25 to macOS 10.13 --- .github/setup_go_for_macos1013.sh | 45 +++++++++++++++++++++++++++++++ .github/workflows/build.yml | 36 ++++++++++++++++--------- 2 files changed, 68 insertions(+), 13 deletions(-) create mode 100755 .github/setup_go_for_macos1013.sh diff --git a/.github/setup_go_for_macos1013.sh b/.github/setup_go_for_macos1013.sh new file mode 100755 index 00000000..49eac606 --- /dev/null +++ b/.github/setup_go_for_macos1013.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -euo pipefail + +VERSION="1.25.8" +PATCH_COMMITS=( + "afe69d3cec1c6dcf0f1797b20546795730850070" + "1ed289b0cf87dc5aae9c6fe1aa5f200a83412938" +) +CURL_ARGS=( + -fL + --silent + --show-error +) + +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + CURL_ARGS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") +fi + +mkdir -p "$HOME/go" +cd "$HOME/go" +wget "https://dl.google.com/go/go${VERSION}.darwin-arm64.tar.gz" +tar -xzf "go${VERSION}.darwin-arm64.tar.gz" +#cp -a go go_bootstrap +mv go go_osx +cd go_osx + +# these patch URLs only work on golang1.25.x +# that means after golang1.26 release it must be changed +# see: https://github.com/SagerNet/go/commits/release-branch.go1.25/ +# revert: +# 33d3f603c1: "cmd/link/internal/ld: use 12.0.0 OS/SDK versions for macOS linking" +# 937368f84e: "crypto/x509: change how we retrieve chains on darwin" + +for patch_commit in "${PATCH_COMMITS[@]}"; do + curl "${CURL_ARGS[@]}" "https://github.com/SagerNet/go/commit/${patch_commit}.diff" | patch --verbose -p 1 +done + +# Rebuild is not needed: we build with CGO_ENABLED=1, so Apple's external +# linker handles LC_BUILD_VERSION via MACOSX_DEPLOYMENT_TARGET, and the +# stdlib (crypto/x509) is compiled from patched src automatically. +#cd src +#GOROOT_BOOTSTRAP="$HOME/go/go_bootstrap" ./make.bash +#cd ../.. +#rm -rf go_bootstrap "go${VERSION}.darwin-arm64.tar.gz" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8121aa4..2b3f702a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -121,15 +121,10 @@ jobs: with: fetch-depth: 0 - name: Setup Go - if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }} + if: ${{ ! matrix.legacy_win7 }} uses: actions/setup-go@v5 with: go-version: ~1.25.8 - - name: Setup Go 1.24 - if: matrix.legacy_go124 - uses: actions/setup-go@v5 - with: - go-version: ~1.24.10 - name: Cache Go for Windows 7 if: matrix.legacy_win7 id: cache-go-for-windows7 @@ -436,22 +431,36 @@ jobs: include: - { arch: amd64 } - { arch: arm64 } - - { arch: amd64, legacy_go124: true, legacy_name: "macos-11" } + - { arch: amd64, legacy_osx: true, legacy_name: "macos-10.13" } steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 with: fetch-depth: 0 - name: Setup Go - if: ${{ ! matrix.legacy_go124 }} + if: ${{ ! matrix.legacy_osx }} uses: actions/setup-go@v5 with: go-version: ^1.25.3 - - name: Setup Go 1.24 - if: matrix.legacy_go124 - uses: actions/setup-go@v5 + - name: Cache Go for macOS 10.13 + if: matrix.legacy_osx + id: cache-go-for-macos1013 + uses: actions/cache@v4 with: - go-version: ~1.24.6 + path: | + ~/go/go_osx + key: go_osx_1258 + - name: Setup Go for macOS 10.13 + if: matrix.legacy_osx && steps.cache-go-for-macos1013.outputs.cache-hit != 'true' + env: + GITHUB_TOKEN: ${{ github.token }} + run: |- + .github/setup_go_for_macos1013.sh + - name: Setup Go for macOS 10.13 + if: matrix.legacy_osx + run: |- + echo "PATH=$HOME/go/go_osx/bin:$PATH" >> $GITHUB_ENV + echo "GOROOT=$HOME/go/go_osx" >> $GITHUB_ENV - name: Set tag run: |- git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV" @@ -459,7 +468,7 @@ jobs: - name: Set build tags run: | set -xeuo pipefail - if [[ "${{ matrix.legacy_go124 }}" != "true" ]]; then + if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then TAGS=$(cat release/DEFAULT_BUILD_TAGS) else TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) @@ -479,6 +488,7 @@ jobs: CGO_ENABLED: "1" GOOS: darwin GOARCH: ${{ matrix.arch }} + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.legacy_osx && '10.13' || '' }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set name run: |- From df0bf927e4909574061fcd136d9854f37599da68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 7 Mar 2026 20:54:39 +0800 Subject: [PATCH 12/99] Fix missing `with_gvisor` build tag for tailscale --- protocol/tailscale/dns_transport.go | 2 ++ protocol/tailscale/endpoint.go | 2 ++ protocol/tailscale/protect_android.go | 2 ++ protocol/tailscale/protect_nonandroid.go | 2 +- protocol/tailscale/tun_device_unix.go | 2 +- protocol/tailscale/tun_device_windows.go | 2 +- service/derp/service.go | 2 ++ 7 files changed, 11 insertions(+), 3 deletions(-) diff --git a/protocol/tailscale/dns_transport.go b/protocol/tailscale/dns_transport.go index 521bb551..1c227db7 100644 --- a/protocol/tailscale/dns_transport.go +++ b/protocol/tailscale/dns_transport.go @@ -1,3 +1,5 @@ +//go:build with_gvisor + package tailscale import ( diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 6ccaa01e..46d36395 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -1,3 +1,5 @@ +//go:build with_gvisor + package tailscale import ( diff --git a/protocol/tailscale/protect_android.go b/protocol/tailscale/protect_android.go index 37dd33bd..63be868d 100644 --- a/protocol/tailscale/protect_android.go +++ b/protocol/tailscale/protect_android.go @@ -1,3 +1,5 @@ +//go:build with_gvisor + package tailscale import ( diff --git a/protocol/tailscale/protect_nonandroid.go b/protocol/tailscale/protect_nonandroid.go index f315c2ea..c2f39f1f 100644 --- a/protocol/tailscale/protect_nonandroid.go +++ b/protocol/tailscale/protect_nonandroid.go @@ -1,4 +1,4 @@ -//go:build !android +//go:build with_gvisor && !android package tailscale diff --git a/protocol/tailscale/tun_device_unix.go b/protocol/tailscale/tun_device_unix.go index 77f2955b..a8d237ab 100644 --- a/protocol/tailscale/tun_device_unix.go +++ b/protocol/tailscale/tun_device_unix.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build with_gvisor && !windows package tailscale diff --git a/protocol/tailscale/tun_device_windows.go b/protocol/tailscale/tun_device_windows.go index 3b0e3440..8c9e87ce 100644 --- a/protocol/tailscale/tun_device_windows.go +++ b/protocol/tailscale/tun_device_windows.go @@ -1,4 +1,4 @@ -//go:build windows +//go:build with_gvisor && windows package tailscale diff --git a/service/derp/service.go b/service/derp/service.go index 6cc1b9b6..02dac60b 100644 --- a/service/derp/service.go +++ b/service/derp/service.go @@ -1,3 +1,5 @@ +//go:build with_gvisor + package derp import ( From bc3884ca919d55a505f67c43d85f0d3375b788a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 6 Mar 2026 21:59:01 +0800 Subject: [PATCH 13/99] release: Add openwrt apk build --- .github/build_openwrt_apk.sh | 80 ++++++++++++++++++++++++++++++ .github/workflows/build.yml | 15 ++++++ docs/installation/tools/install.sh | 6 +++ 3 files changed, 101 insertions(+) create mode 100755 .github/build_openwrt_apk.sh diff --git a/.github/build_openwrt_apk.sh b/.github/build_openwrt_apk.sh new file mode 100755 index 00000000..49e1c131 --- /dev/null +++ b/.github/build_openwrt_apk.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +ARCHITECTURE="$1" +VERSION="$2" +BINARY_PATH="$3" +OUTPUT_PATH="$4" + +if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then + echo "Usage: $0 " + exit 1 +fi + +PROJECT=$(cd "$(dirname "$0")/.."; pwd) + +# Convert version to APK format: +# 1.13.0-beta.8 -> 1.13.0_beta8-r0 +# 1.13.0-rc.3 -> 1.13.0_rc3-r0 +# 1.13.0 -> 1.13.0-r0 +APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/') +APK_VERSION="${APK_VERSION}-r0" + +ROOT_DIR=$(mktemp -d) +trap 'rm -rf "$ROOT_DIR"' EXIT + +# Binary +install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box" + +# Config files +install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json" +install -Dm644 "$PROJECT/release/config/openwrt.conf" "$ROOT_DIR/etc/config/sing-box" +install -Dm755 "$PROJECT/release/config/openwrt.init" "$ROOT_DIR/etc/init.d/sing-box" +install -Dm644 "$PROJECT/release/config/openwrt.keep" "$ROOT_DIR/lib/upgrade/keep.d/sing-box" + +# Completions +install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash" +install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish" +install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box" + +# License +install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE" + +# APK metadata +PACKAGES_DIR="$ROOT_DIR/lib/apk/packages" +mkdir -p "$PACKAGES_DIR" + +# .conffiles +cat > "$PACKAGES_DIR/.conffiles" <<'EOF' +/etc/config/sing-box +/etc/sing-box/config.json +EOF + +# .conffiles_static (sha256 checksums) +while IFS= read -r conffile; do + sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1) + echo "$conffile $sha256" +done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static" + +# .list (all files, excluding lib/apk/packages/ metadata) +(cd "$ROOT_DIR" && find . -type f -o -type l) \ + | sed 's|^\./|/|' \ + | grep -v '^/lib/apk/packages/' \ + | sort > "$PACKAGES_DIR/.list" + +# Build APK +apk mkpkg \ + --info "name:sing-box" \ + --info "version:${APK_VERSION}" \ + --info "description:The universal proxy platform." \ + --info "arch:${ARCHITECTURE}" \ + --info "license:GPL-3.0-or-later" \ + --info "origin:sing-box" \ + --info "url:https://sing-box.sagernet.org/" \ + --info "maintainer:nekohasekai " \ + --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" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2b3f702a..6ce9a98a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -396,6 +396,21 @@ jobs: .github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk" done rm "dist/openwrt.deb" + - name: Install apk-tools + if: matrix.openwrt != '' + 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: Archive run: | set -xeuo pipefail diff --git a/docs/installation/tools/install.sh b/docs/installation/tools/install.sh index 74166f02..fc514767 100755 --- a/docs/installation/tools/install.sh +++ b/docs/installation/tools/install.sh @@ -47,6 +47,12 @@ elif command -v rpm >/dev/null 2>&1; then arch=$(uname -m) package_suffix=".rpm" package_install="rpm -i" +elif command -v apk >/dev/null 2>&1 && [ -f /etc/os-release ] && grep -q OPENWRT_ARCH /etc/os-release; then + os="openwrt" + . /etc/os-release + arch="$OPENWRT_ARCH" + package_suffix=".apk" + package_install="apk add --allow-untrusted" elif command -v opkg >/dev/null 2>&1; then os="openwrt" . /etc/os-release From 305b930d902a55320f6f84e8d8029dc3493d2b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 7 Mar 2026 23:30:34 +0800 Subject: [PATCH 14/99] release: Fix default config --- release/config/config.json | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/release/config/config.json b/release/config/config.json index bdc78d40..e5a2a663 100644 --- a/release/config/config.json +++ b/release/config/config.json @@ -5,7 +5,9 @@ "dns": { "servers": [ { - "address": "tls://8.8.8.8" + "type": "tls", + "tag": "google", + "server": "8.8.8.8" } ] }, @@ -26,17 +28,13 @@ "outbounds": [ { "type": "direct" - }, - { - "type": "dns", - "tag": "dns-out" } ], "route": { "rules": [ { "port": 53, - "outbound": "dns-out" + "action": "hijack-dns" } ] } From 4d6fb1d38d7089bf37a0c8ed31a5bbcac1d7eda3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 8 Mar 2026 20:04:23 +0800 Subject: [PATCH 15/99] Fix legacy DNS `client_subnet` options not working --- dns/client.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dns/client.go b/dns/client.go index ed4e8207..70b53c95 100644 --- a/dns/client.go +++ b/dns/client.go @@ -324,16 +324,20 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom } else { strategy = options.Strategy } + lookupOptions := options + if options.LookupStrategy != C.DomainStrategyAsIS { + lookupOptions.Strategy = strategy + } if strategy == C.DomainStrategyIPv4Only { - return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker) + return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker) } else if strategy == C.DomainStrategyIPv6Only { - return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker) + return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker) } var response4 []netip.Addr var response6 []netip.Addr var group task.Group group.Append("exchange4", func(ctx context.Context) error { - response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker) + response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, lookupOptions, responseChecker) if err != nil { return err } @@ -341,7 +345,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom return nil }) group.Append("exchange6", func(ctx context.Context) error { - response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker) + response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, lookupOptions, responseChecker) if err != nil { return err } From 65875e6dac1d6380b4dc4788ff22201958d213c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 10 Mar 2026 16:56:09 +0800 Subject: [PATCH 16/99] tailscale: Use system dialer for system interface * Revert "Fix netstack TCP connections with system interface --- go.mod | 2 +- go.sum | 4 +- protocol/tailscale/endpoint.go | 70 ++++++++++++++++++++++++++++------ 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 99fdaff5..a77d9d3d 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/sagernet/sing-tun v0.8.2 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 - github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260303140313-3bcf9a4b9349 + github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310072802-158edadd59bd github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index 09d4fb08..1d3eb585 100644 --- a/go.sum +++ b/go.sum @@ -254,8 +254,8 @@ github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkV github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260303140313-3bcf9a4b9349 h1:ju7aTbndj2sqK4NplE97ynLdhuCtel5OS4e0NrT71nk= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260303140313-3bcf9a4b9349/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310072802-158edadd59bd h1:WUVQsTUCr0OEWXoB6uPXaqup7SjMUFOkOHe0XBcpLn4= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310072802-158edadd59bd/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 46d36395..49d7428c 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -110,6 +110,7 @@ type Endpoint struct { systemInterfaceName string systemInterfaceMTU uint32 systemTun tun.Tun + systemDialer *dialer.DefaultDialer fallbackTCPCloser func() } @@ -324,8 +325,19 @@ func (t *Endpoint) Start(stage adapter.StartStage) error { _ = systemTun.Close() return err } + systemDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{ + BindInterface: tunName, + }) + if err != nil { + _ = systemTun.Close() + return err + } t.systemTun = systemTun + t.systemDialer = systemDialer t.server.TunDevice = wgTunDevice + t.server.RouterWrapper = func(inner router.Router) router.Router { + return &exitRouteFilteringRouter{Router: inner} + } } err := t.server.Start() if err != nil { @@ -459,6 +471,10 @@ func (t *Endpoint) Close() error { t.fallbackTCPCloser() t.fallbackTCPCloser = nil } + if t.systemTun != nil { + _ = t.systemTun.Close() + t.systemTun = nil + } return common.Close(common.PtrOrNil(t.server)) } @@ -476,6 +492,9 @@ func (t *Endpoint) DialContext(ctx context.Context, network string, destination } return N.DialSerial(ctx, t, network, destination, destinationAddresses) } + if t.systemDialer != nil { + return t.systemDialer.DialContext(ctx, network, destination) + } addr4, addr6 := t.server.TailscaleIPs() remoteAddr := tcpip.FullAddress{ NIC: 1, @@ -522,6 +541,9 @@ func (t *Endpoint) DialContext(ctx context.Context, network string, destination } func (t *Endpoint) listenPacketWithAddress(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if t.systemDialer != nil { + return t.systemDialer.ListenPacket(ctx, destination) + } addr4, addr6 := t.server.TailscaleIPs() bind := tcpip.FullAddress{ NIC: 1, @@ -679,19 +701,29 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, } func (t *Endpoint) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { - inet4Address, inet6Address := t.server.TailscaleIPs() - if metadata.Destination.Addr.Is4() && !inet4Address.IsValid() || metadata.Destination.Addr.Is6() && !inet6Address.IsValid() { - return nil, E.New("Tailscale is not ready yet") - } ctx := log.ContextWithNewID(t.ctx) - destination, err := ping.ConnectGVisor( - ctx, t.logger, - metadata.Source.Addr, metadata.Destination.Addr, - routeContext, - t.stack, - inet4Address, inet6Address, - timeout, - ) + var destination tun.DirectRouteDestination + var err error + if t.systemDialer != nil { + destination, err = ping.ConnectDestination( + ctx, t.logger, + t.systemDialer.DialerForICMPDestination(metadata.Destination.Addr).Control, + metadata.Destination.Addr, routeContext, timeout, + ) + } else { + inet4Address, inet6Address := t.server.TailscaleIPs() + if metadata.Destination.Addr.Is4() && !inet4Address.IsValid() || metadata.Destination.Addr.Is6() && !inet6Address.IsValid() { + return nil, E.New("Tailscale is not ready yet") + } + destination, err = ping.ConnectGVisor( + ctx, t.logger, + metadata.Source.Addr, metadata.Destination.Addr, + routeContext, + t.stack, + inet4Address, inet6Address, + timeout, + ) + } if err != nil { return nil, err } @@ -808,3 +840,17 @@ func (c *dnsConfigurtor) GetBaseConfig() (tsDNS.OSConfig, error) { func (c *dnsConfigurtor) Close() error { return nil } + +type exitRouteFilteringRouter struct { + router.Router +} + +func (r *exitRouteFilteringRouter) Set(config *router.Config) error { + if config != nil { + config = config.Clone() + config.Routes = common.Filter(config.Routes, func(prefix netip.Prefix) bool { + return !tsaddr.IsExitRoute(prefix) + }) + } + return r.Router.Set(config) +} From 0b045288033d970fe648548e724c7a3565521894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 10 Mar 2026 17:54:23 +0800 Subject: [PATCH 17/99] tailscaile: Fix using TUN auto redirect with tailscale system interface --- go.mod | 2 +- go.sum | 4 +-- protocol/tailscale/endpoint.go | 34 +++++++++++++++--------- protocol/tailscale/protect_android.go | 18 ------------- protocol/tailscale/protect_nonandroid.go | 8 ------ 5 files changed, 24 insertions(+), 42 deletions(-) delete mode 100644 protocol/tailscale/protect_android.go delete mode 100644 protocol/tailscale/protect_nonandroid.go diff --git a/go.mod b/go.mod index a77d9d3d..0f6661ab 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/sagernet/sing-tun v0.8.2 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 - github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310072802-158edadd59bd + github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310090001-e76c5dd4bd45 github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index 1d3eb585..5d5c3760 100644 --- a/go.sum +++ b/go.sum @@ -254,8 +254,8 @@ github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkV github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310072802-158edadd59bd h1:WUVQsTUCr0OEWXoB6uPXaqup7SjMUFOkOHe0XBcpLn4= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310072802-158edadd59bd/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310090001-e76c5dd4bd45 h1:J/Yn7XspzVcfSgKD30Tv3m6lqp64HwftBL6XnZMQiBI= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310090001-e76c5dd4bd45/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 49d7428c..730106c7 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -48,6 +48,7 @@ import ( "github.com/sagernet/tailscale/ipn" tsDNS "github.com/sagernet/tailscale/net/dns" "github.com/sagernet/tailscale/net/netmon" + "github.com/sagernet/tailscale/net/netns" "github.com/sagernet/tailscale/net/tsaddr" tsTUN "github.com/sagernet/tailscale/net/tstun" "github.com/sagernet/tailscale/tsnet" @@ -288,9 +289,6 @@ func (t *Endpoint) Start(stage adapter.StartStage) error { } }), nil }) - if runtime.GOOS == "android" { - setAndroidProtectFunc(t.platformInterface) - } } if t.systemInterface { mtu := t.systemInterfaceMTU @@ -336,9 +334,22 @@ func (t *Endpoint) Start(stage adapter.StartStage) error { t.systemDialer = systemDialer t.server.TunDevice = wgTunDevice t.server.RouterWrapper = func(inner router.Router) router.Router { - return &exitRouteFilteringRouter{Router: inner} + return &addressOnlyRouter{Router: inner} } } + if mark := t.network.AutoRedirectOutputMark(); mark > 0 { + controlFunc := t.network.AutoRedirectOutputMarkFunc() + if bindFunc := t.network.AutoDetectInterfaceFunc(); bindFunc != nil { + controlFunc = control.Append(controlFunc, bindFunc) + } + netns.SetControlFunc(controlFunc) + } else if runtime.GOOS == "android" && t.platformInterface != nil { + netns.SetControlFunc(func(network, address string, c syscall.RawConn) error { + return control.Raw(c, func(fd uintptr) error { + return t.platformInterface.AutoDetectInterfaceControl(int(fd)) + }) + }) + } err := t.server.Start() if err != nil { if t.systemTun != nil { @@ -464,9 +475,7 @@ func (t *Endpoint) watchState() { func (t *Endpoint) Close() error { netmon.RegisterInterfaceGetter(nil) - if runtime.GOOS == "android" { - setAndroidProtectFunc(nil) - } + netns.SetControlFunc(nil) if t.fallbackTCPCloser != nil { t.fallbackTCPCloser() t.fallbackTCPCloser = nil @@ -841,16 +850,15 @@ func (c *dnsConfigurtor) Close() error { return nil } -type exitRouteFilteringRouter struct { +type addressOnlyRouter struct { router.Router } -func (r *exitRouteFilteringRouter) Set(config *router.Config) error { +func (r *addressOnlyRouter) Set(config *router.Config) error { if config != nil { - config = config.Clone() - config.Routes = common.Filter(config.Routes, func(prefix netip.Prefix) bool { - return !tsaddr.IsExitRoute(prefix) - }) + config = &router.Config{ + LocalAddrs: config.LocalAddrs, + } } return r.Router.Set(config) } diff --git a/protocol/tailscale/protect_android.go b/protocol/tailscale/protect_android.go deleted file mode 100644 index 63be868d..00000000 --- a/protocol/tailscale/protect_android.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build with_gvisor - -package tailscale - -import ( - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/tailscale/net/netns" -) - -func setAndroidProtectFunc(platformInterface adapter.PlatformInterface) { - if platformInterface != nil { - netns.SetAndroidProtectFunc(func(fd int) error { - return platformInterface.AutoDetectInterfaceControl(fd) - }) - } else { - netns.SetAndroidProtectFunc(nil) - } -} diff --git a/protocol/tailscale/protect_nonandroid.go b/protocol/tailscale/protect_nonandroid.go deleted file mode 100644 index c2f39f1f..00000000 --- a/protocol/tailscale/protect_nonandroid.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build with_gvisor && !android - -package tailscale - -import "github.com/sagernet/sing-box/adapter" - -func setAndroidProtectFunc(platformInterface adapter.PlatformInterface) { -} From e0be8743f63723fbfedb4d2bbc658932e7b46709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 10 Mar 2026 20:56:07 +0800 Subject: [PATCH 18/99] ocm: Add Responses WebSocket API proxy and fix client config docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support the OpenAI Responses WebSocket API (`wss://.../v1/responses`) for bidirectional frame proxying with usage tracking. Fix Codex CLI client config examples to use profiles and correct flags. Update openai-go v3.24.0 → v3.26.0. --- docs/configuration/service/ocm.md | 20 ++- docs/configuration/service/ocm.zh.md | 20 ++- go.mod | 2 +- go.sum | 4 +- service/ocm/service.go | 7 + service/ocm/service_websocket.go | 255 +++++++++++++++++++++++++++ 6 files changed, 293 insertions(+), 15 deletions(-) create mode 100644 service/ocm/service_websocket.go diff --git a/docs/configuration/service/ocm.md b/docs/configuration/service/ocm.md index 59dba7da..ce6d50fc 100644 --- a/docs/configuration/service/ocm.md +++ b/docs/configuration/service/ocm.md @@ -114,14 +114,18 @@ Add to `~/.codex/config.toml`: [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" -wire_api = "responses" -requires_openai_auth = false +supports_websockets = true + +[profiles.ocm] +model_provider = "ocm" +# model = "gpt-5.4" # if the latest model is not yet publicly released +# model_reasoning_effort = "xhigh" ``` Then run: ```bash -codex --model-provider ocm +codex --profile ocm ``` ### Example with Authentication @@ -159,13 +163,17 @@ Add to `~/.codex/config.toml`: [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" -wire_api = "responses" -requires_openai_auth = false +supports_websockets = true experimental_bearer_token = "sk-alice-secret-token" + +[profiles.ocm] +model_provider = "ocm" +# model = "gpt-5.4" # if the latest model is not yet publicly released +# model_reasoning_effort = "xhigh" ``` Then run: ```bash -codex --model-provider ocm +codex --profile ocm ``` diff --git a/docs/configuration/service/ocm.zh.md b/docs/configuration/service/ocm.zh.md index ee1d8510..016990ff 100644 --- a/docs/configuration/service/ocm.zh.md +++ b/docs/configuration/service/ocm.zh.md @@ -114,14 +114,18 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" -wire_api = "responses" -requires_openai_auth = false +supports_websockets = true + +[profiles.ocm] +model_provider = "ocm" +# model = "gpt-5.4" # 如果最新模型尚未公开发布 +# model_reasoning_effort = "xhigh" ``` 然后运行: ```bash -codex --model-provider ocm +codex --profile ocm ``` ### 带身份验证的示例 @@ -159,13 +163,17 @@ codex --model-provider ocm [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" -wire_api = "responses" -requires_openai_auth = false +supports_websockets = true experimental_bearer_token = "sk-alice-secret-token" + +[profiles.ocm] +model_provider = "ocm" +# model = "gpt-5.4" # 如果最新模型尚未公开发布 +# model_reasoning_effort = "xhigh" ``` 然后运行: ```bash -codex --model-provider ocm +codex --profile ocm ``` diff --git a/go.mod b/go.mod index 0f6661ab..43751f29 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/metacubex/utls v1.8.4 github.com/mholt/acmez/v3 v3.1.6 github.com/miekg/dns v1.1.72 - github.com/openai/openai-go/v3 v3.24.0 + github.com/openai/openai-go/v3 v3.26.0 github.com/oschwald/maxminddb-golang v1.13.1 github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a diff --git a/go.sum b/go.sum index 5d5c3760..7cd04d80 100644 --- a/go.sum +++ b/go.sum @@ -138,8 +138,8 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU= -github.com/openai/openai-go/v3 v3.24.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE= +github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= diff --git a/service/ocm/service.go b/service/ocm/service.go index 2354d159..e05c9547 100644 --- a/service/ocm/service.go +++ b/service/ocm/service.go @@ -130,6 +130,7 @@ type Service struct { credentialPath string credentials *oauthCredentials users []option.OCMUser + dialer N.Dialer httpClient *http.Client httpHeaders http.Header listener *listener.Listener @@ -187,6 +188,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio logger: logger, credentialPath: options.CredentialPath, users: options.Users, + dialer: serviceDialer, httpClient: httpClient, httpHeaders: options.Headers.Build(), listener: listener.New(listener.Options{ @@ -356,6 +358,11 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } + if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") && strings.HasPrefix(path, "/v1/responses") { + s.handleWebSocket(w, r, proxyPath, username) + return + } + var requestModel string if s.usageTracker != nil && r.Body != nil { diff --git a/service/ocm/service_websocket.go b/service/ocm/service_websocket.go new file mode 100644 index 00000000..5e8cb8bb --- /dev/null +++ b/service/ocm/service_websocket.go @@ -0,0 +1,255 @@ +package ocm + +import ( + "context" + stdTLS "crypto/tls" + "encoding/json" + "io" + "net" + "net/http" + "strings" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/ws" + "github.com/sagernet/ws/wsutil" + + "github.com/openai/openai-go/v3/responses" +) + +func buildUpstreamWebSocketURL(baseURL string, proxyPath string) string { + upstreamURL := baseURL + if strings.HasPrefix(upstreamURL, "https://") { + upstreamURL = "wss://" + upstreamURL[len("https://"):] + } else if strings.HasPrefix(upstreamURL, "http://") { + upstreamURL = "ws://" + upstreamURL[len("http://"):] + } + return upstreamURL + proxyPath +} + +func isForwardableResponseHeader(key string) bool { + lowerKey := strings.ToLower(key) + switch { + case strings.HasPrefix(lowerKey, "x-codex-"): + return true + case strings.HasPrefix(lowerKey, "x-reasoning"): + return true + case lowerKey == "openai-model": + return true + case strings.Contains(lowerKey, "-secondary-"): + return true + default: + return false + } +} + +func (s *Service) handleWebSocket(w http.ResponseWriter, r *http.Request, proxyPath string, username string) { + accessToken, err := s.getAccessToken() + if err != nil { + s.logger.Error("get access token for websocket: ", err) + writeJSONError(w, r, http.StatusUnauthorized, "authentication_error", "authentication failed") + return + } + + upstreamURL := buildUpstreamWebSocketURL(s.getBaseURL(), proxyPath) + if r.URL.RawQuery != "" { + upstreamURL += "?" + r.URL.RawQuery + } + + upstreamHeaders := make(http.Header) + forwardHeaders := []string{ + "OpenAI-Beta", + "X-Conversation-ID", + } + for _, headerKey := range forwardHeaders { + if value := r.Header.Get(headerKey); value != "" { + upstreamHeaders.Set(headerKey, value) + } + } + for key, values := range r.Header { + lowerKey := strings.ToLower(key) + if strings.HasPrefix(lowerKey, "x-codex-") || strings.HasPrefix(lowerKey, "x-responsesapi-") { + upstreamHeaders[key] = values + } + } + for key, values := range s.httpHeaders { + upstreamHeaders.Del(key) + upstreamHeaders[key] = values + } + upstreamHeaders.Set("Authorization", "Bearer "+accessToken) + if accountID := s.getAccountID(); accountID != "" { + upstreamHeaders.Set("ChatGPT-Account-Id", accountID) + } + + upstreamResponseHeaders := make(http.Header) + upstreamDialer := ws.Dialer{ + NetDial: func(_ context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(s.ctx, network, M.ParseSocksaddr(addr)) + }, + TLSConfig: &stdTLS.Config{ + RootCAs: adapter.RootPoolFromContext(s.ctx), + Time: ntp.TimeFuncFromContext(s.ctx), + }, + Header: ws.HandshakeHeaderHTTP(upstreamHeaders), + OnHeader: func(key, value []byte) error { + upstreamResponseHeaders.Add(string(key), string(value)) + return nil + }, + } + + upstreamConn, upstreamBufferedReader, _, err := upstreamDialer.Dial(r.Context(), upstreamURL) + if err != nil { + s.logger.Error("dial upstream websocket: ", err) + writeJSONError(w, r, http.StatusBadGateway, "api_error", "upstream websocket connection failed") + return + } + + weeklyCycleHint := extractWeeklyCycleHint(upstreamResponseHeaders) + + clientResponseHeaders := make(http.Header) + for key, values := range upstreamResponseHeaders { + if isForwardableResponseHeader(key) { + clientResponseHeaders[key] = values + } + } + + clientUpgrader := ws.HTTPUpgrader{ + Header: clientResponseHeaders, + } + clientConn, _, _, err := clientUpgrader.Upgrade(r, w) + if err != nil { + s.logger.Error("upgrade client websocket: ", err) + upstreamConn.Close() + return + } + + var upstreamReadWriter io.ReadWriter + if upstreamBufferedReader != nil { + upstreamReadWriter = struct { + io.Reader + io.Writer + }{upstreamBufferedReader, upstreamConn} + } else { + upstreamReadWriter = upstreamConn + } + + modelChannel := make(chan string, 1) + var waitGroup sync.WaitGroup + var once sync.Once + closeAll := func() { + clientConn.Close() + upstreamConn.Close() + } + + waitGroup.Add(2) + go func() { + defer waitGroup.Done() + defer once.Do(closeAll) + s.proxyWebSocketClientToUpstream(clientConn, upstreamConn, modelChannel) + }() + go func() { + defer waitGroup.Done() + defer once.Do(closeAll) + s.proxyWebSocketUpstreamToClient(upstreamReadWriter, clientConn, modelChannel, username, weeklyCycleHint) + }() + waitGroup.Wait() +} + +func (s *Service) proxyWebSocketClientToUpstream(clientConn net.Conn, upstreamConn net.Conn, modelChannel chan<- string) { + for { + data, opCode, err := wsutil.ReadClientData(clientConn) + if err != nil { + if !E.IsClosedOrCanceled(err) { + s.logger.Debug("read client websocket: ", err) + } + return + } + + if opCode == ws.OpText && s.usageTracker != nil { + var request struct { + Type string `json:"type"` + Model string `json:"model"` + } + if json.Unmarshal(data, &request) == nil && request.Type == "response.create" && request.Model != "" { + select { + case modelChannel <- request.Model: + default: + } + } + } + + err = wsutil.WriteClientMessage(upstreamConn, opCode, data) + if err != nil { + if !E.IsClosedOrCanceled(err) { + s.logger.Debug("write upstream websocket: ", err) + } + return + } + } +} + +func (s *Service) proxyWebSocketUpstreamToClient(upstreamReadWriter io.ReadWriter, clientConn net.Conn, modelChannel <-chan string, username string, weeklyCycleHint *WeeklyCycleHint) { + var requestModel string + for { + data, opCode, err := wsutil.ReadServerData(upstreamReadWriter) + if err != nil { + if !E.IsClosedOrCanceled(err) { + s.logger.Debug("read upstream websocket: ", err) + } + return + } + + if opCode == ws.OpText && s.usageTracker != nil { + select { + case model := <-modelChannel: + requestModel = model + default: + } + + var event struct { + Type string `json:"type"` + } + if json.Unmarshal(data, &event) == nil && event.Type == "response.completed" { + var streamEvent responses.ResponseStreamEventUnion + if json.Unmarshal(data, &streamEvent) == nil { + completedEvent := streamEvent.AsResponseCompleted() + responseModel := string(completedEvent.Response.Model) + serviceTier := string(completedEvent.Response.ServiceTier) + inputTokens := completedEvent.Response.Usage.InputTokens + outputTokens := completedEvent.Response.Usage.OutputTokens + cachedTokens := completedEvent.Response.Usage.InputTokensDetails.CachedTokens + + if inputTokens > 0 || outputTokens > 0 { + if responseModel == "" { + responseModel = requestModel + } + if responseModel != "" { + s.usageTracker.AddUsageWithCycleHint( + responseModel, + inputTokens, + outputTokens, + cachedTokens, + serviceTier, + username, + time.Now(), + weeklyCycleHint, + ) + } + } + } + } + } + + err = wsutil.WriteServerMessage(clientConn, opCode, data) + if err != nil { + if !E.IsClosedOrCanceled(err) { + s.logger.Debug("write client websocket: ", err) + } + return + } + } +} From a09ffe6a0f3e45014d34ba0b0e5f3cee485c5972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 10 Mar 2026 20:58:59 +0800 Subject: [PATCH 19/99] ccm/ocm: Add `by_user_and_week` cost summary --- service/ccm/service_usage.go | 37 +++++++++++++++++++++++++++++++++--- service/ocm/service_usage.go | 37 +++++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/service/ccm/service_usage.go b/service/ccm/service_usage.go index 7d776774..36e9ee65 100644 --- a/service/ccm/service_usage.go +++ b/service/ccm/service_usage.go @@ -65,9 +65,10 @@ type CostCombinationJSON struct { } type CostsSummaryJSON struct { - TotalUSD float64 `json:"total_usd"` - ByUser map[string]float64 `json:"by_user"` - ByWeek map[string]float64 `json:"by_week,omitempty"` + TotalUSD float64 `json:"total_usd"` + ByUser map[string]float64 `json:"by_user"` + ByWeek map[string]float64 `json:"by_week,omitempty"` + ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"` } type AggregatedUsageJSON struct { @@ -492,6 +493,31 @@ func buildByWeekCost(combinations []CostCombination) map[string]float64 { return byWeek } +func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 { + byUserAndWeek := make(map[string]map[string]float64) + for _, combination := range combinations { + if combination.WeekStartUnix <= 0 { + continue + } + weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() + weekKey := formatWeekStartKey(weekStartAt) + for user, userStats := range combination.ByUser { + userWeeks, exists := byUserAndWeek[user] + if !exists { + userWeeks = make(map[string]float64) + byUserAndWeek[user] = userWeeks + } + userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ContextWindow) + } + } + for _, weekCosts := range byUserAndWeek { + for weekKey, cost := range weekCosts { + weekCosts[weekKey] = roundCost(cost) + } + } + return byUserAndWeek +} + func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 { if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() { return 0 @@ -522,6 +548,11 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { result.Costs.ByWeek = nil } + result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations) + if len(result.Costs.ByUserAndWeek) == 0 { + result.Costs.ByUserAndWeek = nil + } + for user, cost := range result.Costs.ByUser { result.Costs.ByUser[user] = roundCost(cost) } diff --git a/service/ocm/service_usage.go b/service/ocm/service_usage.go index a4c1d1c8..95d401a4 100644 --- a/service/ocm/service_usage.go +++ b/service/ocm/service_usage.go @@ -80,9 +80,10 @@ type CostCombinationJSON struct { } type CostsSummaryJSON struct { - TotalUSD float64 `json:"total_usd"` - ByUser map[string]float64 `json:"by_user"` - ByWeek map[string]float64 `json:"by_week,omitempty"` + TotalUSD float64 `json:"total_usd"` + ByUser map[string]float64 `json:"by_user"` + ByWeek map[string]float64 `json:"by_week,omitempty"` + ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"` } type AggregatedUsageJSON struct { @@ -864,6 +865,31 @@ func buildByWeekCost(combinations []CostCombination) map[string]float64 { return byWeek } +func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 { + byUserAndWeek := make(map[string]map[string]float64) + for _, combination := range combinations { + if combination.WeekStartUnix <= 0 { + continue + } + weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() + weekKey := formatWeekStartKey(weekStartAt) + for user, userStats := range combination.ByUser { + userWeeks, exists := byUserAndWeek[user] + if !exists { + userWeeks = make(map[string]float64) + byUserAndWeek[user] = userWeeks + } + userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ServiceTier) + } + } + for _, weekCosts := range byUserAndWeek { + for weekKey, cost := range weekCosts { + weekCosts[weekKey] = roundCost(cost) + } + } + return byUserAndWeek +} + func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 { if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() { return 0 @@ -894,6 +920,11 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { result.Costs.ByWeek = nil } + result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations) + if len(result.Costs.ByUserAndWeek) == 0 { + result.Costs.ByUserAndWeek = nil + } + for user, cost := range result.Costs.ByUser { result.Costs.ByUser[user] = roundCost(cost) } From 67621ee6ba2e407bdc487dfa2cb15b690de0b68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 10 Mar 2026 21:17:35 +0800 Subject: [PATCH 20/99] Fix OCM websocket proxy lifecycle and headers --- service/ocm/service.go | 60 ++++++++++++++++++++++++++-- service/ocm/service_websocket.go | 68 ++++++++++++++++++++++---------- 2 files changed, 105 insertions(+), 23 deletions(-) diff --git a/service/ocm/service.go b/service/ocm/service.go index e05c9547..0c2e3430 100644 --- a/service/ocm/service.go +++ b/service/ocm/service.go @@ -139,7 +139,9 @@ type Service struct { userManager *UserManager accessMutex sync.RWMutex usageTracker *AggregatedUsage - trackingGroup sync.WaitGroup + webSocketMutex sync.Mutex + webSocketGroup sync.WaitGroup + webSocketConns map[*webSocketSession]struct{} shuttingDown bool } @@ -197,8 +199,9 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio Network: []string{N.NetworkTCP}, Listen: options.ListenOptions, }), - userManager: userManager, - usageTracker: usageTracker, + userManager: userManager, + usageTracker: usageTracker, + webSocketConns: make(map[*webSocketSession]struct{}), } if options.TLS != nil { @@ -631,11 +634,17 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons } func (s *Service) Close() error { + webSocketSessions := s.startWebSocketShutdown() + err := common.Close( common.PtrOrNil(s.httpServer), common.PtrOrNil(s.listener), s.tlsConfig, ) + for _, session := range webSocketSessions { + session.Close() + } + s.webSocketGroup.Wait() if s.usageTracker != nil { s.usageTracker.cancelPendingSave() @@ -647,3 +656,48 @@ func (s *Service) Close() error { return err } + +func (s *Service) registerWebSocketSession(session *webSocketSession) bool { + s.webSocketMutex.Lock() + defer s.webSocketMutex.Unlock() + + if s.shuttingDown { + return false + } + + s.webSocketConns[session] = struct{}{} + s.webSocketGroup.Add(1) + return true +} + +func (s *Service) unregisterWebSocketSession(session *webSocketSession) { + s.webSocketMutex.Lock() + _, loaded := s.webSocketConns[session] + if loaded { + delete(s.webSocketConns, session) + } + s.webSocketMutex.Unlock() + + if loaded { + s.webSocketGroup.Done() + } +} + +func (s *Service) isShuttingDown() bool { + s.webSocketMutex.Lock() + defer s.webSocketMutex.Unlock() + return s.shuttingDown +} + +func (s *Service) startWebSocketShutdown() []*webSocketSession { + s.webSocketMutex.Lock() + defer s.webSocketMutex.Unlock() + + s.shuttingDown = true + + webSocketSessions := make([]*webSocketSession, 0, len(s.webSocketConns)) + for session := range s.webSocketConns { + webSocketSessions = append(webSocketSessions, session) + } + return webSocketSessions +} diff --git a/service/ocm/service_websocket.go b/service/ocm/service_websocket.go index 5e8cb8bb..c2e6148d 100644 --- a/service/ocm/service_websocket.go +++ b/service/ocm/service_websocket.go @@ -21,6 +21,19 @@ import ( "github.com/openai/openai-go/v3/responses" ) +type webSocketSession struct { + clientConn net.Conn + upstreamConn net.Conn + closeOnce sync.Once +} + +func (s *webSocketSession) Close() { + s.closeOnce.Do(func() { + s.clientConn.Close() + s.upstreamConn.Close() + }) +} + func buildUpstreamWebSocketURL(baseURL string, proxyPath string) string { upstreamURL := baseURL if strings.HasPrefix(upstreamURL, "https://") { @@ -47,6 +60,22 @@ func isForwardableResponseHeader(key string) bool { } } +func isForwardableWebSocketRequestHeader(key string) bool { + if isHopByHopHeader(key) { + return false + } + + lowerKey := strings.ToLower(key) + switch { + case lowerKey == "authorization": + return false + case strings.HasPrefix(lowerKey, "sec-websocket-"): + return false + default: + return true + } +} + func (s *Service) handleWebSocket(w http.ResponseWriter, r *http.Request, proxyPath string, username string) { accessToken, err := s.getAccessToken() if err != nil { @@ -61,18 +90,8 @@ func (s *Service) handleWebSocket(w http.ResponseWriter, r *http.Request, proxyP } upstreamHeaders := make(http.Header) - forwardHeaders := []string{ - "OpenAI-Beta", - "X-Conversation-ID", - } - for _, headerKey := range forwardHeaders { - if value := r.Header.Get(headerKey); value != "" { - upstreamHeaders.Set(headerKey, value) - } - } for key, values := range r.Header { - lowerKey := strings.ToLower(key) - if strings.HasPrefix(lowerKey, "x-codex-") || strings.HasPrefix(lowerKey, "x-responsesapi-") { + if isForwardableWebSocketRequestHeader(key) { upstreamHeaders[key] = values } } @@ -87,8 +106,8 @@ func (s *Service) handleWebSocket(w http.ResponseWriter, r *http.Request, proxyP upstreamResponseHeaders := make(http.Header) upstreamDialer := ws.Dialer{ - NetDial: func(_ context.Context, network, addr string) (net.Conn, error) { - return s.dialer.DialContext(s.ctx, network, M.ParseSocksaddr(addr)) + NetDial: func(ctx context.Context, network, addr string) (net.Conn, error) { + return s.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) }, TLSConfig: &stdTLS.Config{ RootCAs: adapter.RootPoolFromContext(s.ctx), @@ -120,12 +139,26 @@ func (s *Service) handleWebSocket(w http.ResponseWriter, r *http.Request, proxyP clientUpgrader := ws.HTTPUpgrader{ Header: clientResponseHeaders, } + if s.isShuttingDown() { + upstreamConn.Close() + writeJSONError(w, r, http.StatusServiceUnavailable, "api_error", "service is shutting down") + return + } clientConn, _, _, err := clientUpgrader.Upgrade(r, w) if err != nil { s.logger.Error("upgrade client websocket: ", err) upstreamConn.Close() return } + session := &webSocketSession{ + clientConn: clientConn, + upstreamConn: upstreamConn, + } + if !s.registerWebSocketSession(session) { + session.Close() + return + } + defer s.unregisterWebSocketSession(session) var upstreamReadWriter io.ReadWriter if upstreamBufferedReader != nil { @@ -139,21 +172,16 @@ func (s *Service) handleWebSocket(w http.ResponseWriter, r *http.Request, proxyP modelChannel := make(chan string, 1) var waitGroup sync.WaitGroup - var once sync.Once - closeAll := func() { - clientConn.Close() - upstreamConn.Close() - } waitGroup.Add(2) go func() { defer waitGroup.Done() - defer once.Do(closeAll) + defer session.Close() s.proxyWebSocketClientToUpstream(clientConn, upstreamConn, modelChannel) }() go func() { defer waitGroup.Done() - defer once.Do(closeAll) + defer session.Close() s.proxyWebSocketUpstreamToClient(upstreamReadWriter, clientConn, modelChannel, username, weeklyCycleHint) }() waitGroup.Wait() From 8bb4c4dd32490dd8d2602b1bdbfd7c2a42e32349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 10 Mar 2026 21:49:18 +0800 Subject: [PATCH 21/99] documentation: Update ocm/ccm examples --- docs/configuration/service/ccm.md | 35 ++++++++++++++++++++++++---- docs/configuration/service/ccm.zh.md | 35 ++++++++++++++++++++++++---- docs/configuration/service/ocm.md | 14 +++++++---- docs/configuration/service/ocm.zh.md | 15 ++++++++---- 4 files changed, 81 insertions(+), 18 deletions(-) diff --git a/docs/configuration/service/ccm.md b/docs/configuration/service/ccm.md index 28b82710..337cacb1 100644 --- a/docs/configuration/service/ccm.md +++ b/docs/configuration/service/ccm.md @@ -66,7 +66,19 @@ List of authorized users for token authentication. If empty, no authentication is required. -Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value. +Object format: + +```json +{ + "name": "", + "token": "" +} +``` + +Object fields: + +- `name`: Username identifier for tracking purposes. +- `token`: Bearer token for authentication. Claude Code authenticates by setting the `ANTHROPIC_AUTH_TOKEN` environment variable to their token value. #### headers @@ -84,23 +96,36 @@ TLS configuration, see [TLS](/configuration/shared/tls/#inbound). ### Example +#### Server + ```json { "services": [ { "type": "ccm", - "listen": "127.0.0.1", - "listen_port": 8080 + "listen": "0.0.0.0", + "listen_port": 8080, + "usages_path": "./claude-usages.json", + "users": [ + { + "name": "alice", + "token": "ak-ccm-hello-world" + }, + { + "name": "bob", + "token": "ak-ccm-hello-bob" + } + ] } ] } ``` -Connect to the CCM service: +#### Client ```bash export ANTHROPIC_BASE_URL="http://127.0.0.1:8080" -export ANTHROPIC_AUTH_TOKEN="sk-ant-ccm-auth-token-not-required-in-this-context" +export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world" claude ``` diff --git a/docs/configuration/service/ccm.zh.md b/docs/configuration/service/ccm.zh.md index cd5d3471..7bba322c 100644 --- a/docs/configuration/service/ccm.zh.md +++ b/docs/configuration/service/ccm.zh.md @@ -66,7 +66,19 @@ Claude Code OAuth 凭据文件的路径。 如果为空,则不需要身份验证。 -Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。 +对象格式: + +```json +{ + "name": "", + "token": "" +} +``` + +对象字段: + +- `name`:用于跟踪的用户名标识符。 +- `token`:用于身份验证的 Bearer 令牌。Claude Code 通过设置 `ANTHROPIC_AUTH_TOKEN` 环境变量为其令牌值进行身份验证。 #### headers @@ -84,23 +96,36 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 ### 示例 +#### 服务端 + ```json { "services": [ { "type": "ccm", - "listen": "127.0.0.1", - "listen_port": 8080 + "listen": "0.0.0.0", + "listen_port": 8080, + "usages_path": "./claude-usages.json", + "users": [ + { + "name": "alice", + "token": "ak-ccm-hello-world" + }, + { + "name": "bob", + "token": "ak-ccm-hello-bob" + } + ] } ] } ``` -连接到 CCM 服务: +#### 客户端 ```bash export ANTHROPIC_BASE_URL="http://127.0.0.1:8080" -export ANTHROPIC_AUTH_TOKEN="sk-ant-ccm-auth-token-not-required-in-this-context" +export ANTHROPIC_AUTH_TOKEN="ak-ccm-hello-world" claude ``` diff --git a/docs/configuration/service/ocm.md b/docs/configuration/service/ocm.md index ce6d50fc..5fdf2b6b 100644 --- a/docs/configuration/service/ocm.md +++ b/docs/configuration/service/ocm.md @@ -37,7 +37,9 @@ See [Listen Fields](/configuration/shared/listen/) for details. Path to the OpenAI OAuth credentials file. -If not specified, defaults to `~/.codex/auth.json`. +If not specified, defaults to: +- `$CODEX_HOME/auth.json` if `CODEX_HOME` environment variable is set +- `~/.codex/auth.json` otherwise Refreshed tokens are automatically written back to the same location. @@ -111,6 +113,8 @@ TLS configuration, see [TLS](/configuration/shared/tls/#inbound). Add to `~/.codex/config.toml`: ```toml +# profile = "ocm" # set as default profile + [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" @@ -143,11 +147,11 @@ codex --profile ocm "users": [ { "name": "alice", - "token": "sk-alice-secret-token" + "token": "sk-ocm-hello-world" }, { "name": "bob", - "token": "sk-bob-secret-token" + "token": "sk-ocm-hello-bob" } ] } @@ -160,11 +164,13 @@ codex --profile ocm Add to `~/.codex/config.toml`: ```toml +# profile = "ocm" # set as default profile + [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" supports_websockets = true -experimental_bearer_token = "sk-alice-secret-token" +experimental_bearer_token = "sk-ocm-hello-world" [profiles.ocm] model_provider = "ocm" diff --git a/docs/configuration/service/ocm.zh.md b/docs/configuration/service/ocm.zh.md index 016990ff..2e02dc55 100644 --- a/docs/configuration/service/ocm.zh.md +++ b/docs/configuration/service/ocm.zh.md @@ -37,7 +37,9 @@ OCM(OpenAI Codex 多路复用器)服务是一个多路复用服务,允许 OpenAI OAuth 凭据文件的路径。 -如果未指定,默认值为 `~/.codex/auth.json`。 +如果未指定,默认值为: +- 如果设置了 `CODEX_HOME` 环境变量,则使用 `$CODEX_HOME/auth.json` +- 否则使用 `~/.codex/auth.json` 刷新的令牌会自动写回相同位置。 @@ -111,6 +113,9 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 在 `~/.codex/config.toml` 中添加: ```toml +# profile = "ocm" # 设为默认配置 + + [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" @@ -143,11 +148,11 @@ codex --profile ocm "users": [ { "name": "alice", - "token": "sk-alice-secret-token" + "token": "sk-ocm-hello-world" }, { "name": "bob", - "token": "sk-bob-secret-token" + "token": "sk-ocm-hello-bob" } ] } @@ -160,11 +165,13 @@ codex --profile ocm 在 `~/.codex/config.toml` 中添加: ```toml +# profile = "ocm" # 设为默认配置 + [model_providers.ocm] name = "OCM Proxy" base_url = "http://127.0.0.1:8080/v1" supports_websockets = true -experimental_bearer_token = "sk-alice-secret-token" +experimental_bearer_token = "sk-ocm-hello-world" [profiles.ocm] model_provider = "ocm" From a7ee94321681519af0eb28d898c7d579f9f16d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 11 Mar 2026 00:27:09 +0800 Subject: [PATCH 22/99] Fix tailscale connections --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 43751f29..0ee67edc 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/sagernet/sing-tun v0.8.2 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 - github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310090001-e76c5dd4bd45 + github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310162543-0c2de366d4de github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index 7cd04d80..0cb3d35e 100644 --- a/go.sum +++ b/go.sum @@ -254,8 +254,8 @@ github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkV github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310090001-e76c5dd4bd45 h1:J/Yn7XspzVcfSgKD30Tv3m6lqp64HwftBL6XnZMQiBI= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310090001-e76c5dd4bd45/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310162543-0c2de366d4de h1:wsJ0COxUOIvBE+hUho0C/DbMeUe9jtwfh6dECAiTk94= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310162543-0c2de366d4de/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= From 49c450d9428c0c10f7a005b69675d172f154e69b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 11 Mar 2026 17:19:52 +0800 Subject: [PATCH 23/99] ccm/ocm: Fix missing metering for 1M context and /fast mode CCM: Fix 1M context detection - use prefix match for versioned beta strings (e.g. "context-1m-2025-08-07") and include cache tokens in the 200K threshold check per Anthropic billing docs. OCM: Add GPT-5.4 family pricing (standard/priority/flex) with extended context (>272K) premium pricing support. Add context window tracking to usage combinations, mirroring CCM's pattern. Update normalizeGPT5Model defaults to latest known models. --- service/ccm/service.go | 12 +- service/ocm/service.go | 4 + service/ocm/service_usage.go | 185 +++++++++++++++++++++++++++---- service/ocm/service_websocket.go | 2 + 4 files changed, 175 insertions(+), 28 deletions(-) diff --git a/service/ccm/service.go b/service/ccm/service.go index 944bedae..34c38824 100644 --- a/service/ccm/service.go +++ b/service/ccm/service.go @@ -281,11 +281,11 @@ func (s *Service) getAccessToken() (string, error) { return newCredentials.AccessToken, nil } -func detectContextWindow(betaHeader string, inputTokens int64) int { - if inputTokens > premiumContextThreshold { +func detectContextWindow(betaHeader string, totalInputTokens int64) int { + if totalInputTokens > premiumContextThreshold { features := strings.Split(betaHeader, ",") for _, feature := range features { - if strings.TrimSpace(feature) == "context-1m" { + if strings.HasPrefix(strings.TrimSpace(feature), "context-1m") { return contextWindowPremium } } @@ -454,7 +454,8 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons if usage.InputTokens > 0 || usage.OutputTokens > 0 { if responseModel != "" { - contextWindow := detectContextWindow(anthropicBetaHeader, usage.InputTokens) + totalInputTokens := usage.InputTokens + usage.CacheCreationInputTokens + usage.CacheReadInputTokens + contextWindow := detectContextWindow(anthropicBetaHeader, totalInputTokens) s.usageTracker.AddUsageWithCycleHint( responseModel, contextWindow, @@ -554,7 +555,8 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons if accumulatedUsage.InputTokens > 0 || accumulatedUsage.OutputTokens > 0 { if responseModel != "" { - contextWindow := detectContextWindow(anthropicBetaHeader, accumulatedUsage.InputTokens) + totalInputTokens := accumulatedUsage.InputTokens + accumulatedUsage.CacheCreationInputTokens + accumulatedUsage.CacheReadInputTokens + contextWindow := detectContextWindow(anthropicBetaHeader, totalInputTokens) s.usageTracker.AddUsageWithCycleHint( responseModel, contextWindow, diff --git a/service/ocm/service.go b/service/ocm/service.go index 0c2e3430..8b66964a 100644 --- a/service/ocm/service.go +++ b/service/ocm/service.go @@ -507,8 +507,10 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons responseModel = requestModel } if responseModel != "" { + contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens) s.usageTracker.AddUsageWithCycleHint( responseModel, + contextWindow, inputTokens, outputTokens, cachedTokens, @@ -616,8 +618,10 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons if inputTokens > 0 || outputTokens > 0 { if responseModel != "" { + contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens) s.usageTracker.AddUsageWithCycleHint( responseModel, + contextWindow, inputTokens, outputTokens, cachedTokens, diff --git a/service/ocm/service_usage.go b/service/ocm/service_usage.go index 95d401a4..589fd093 100644 --- a/service/ocm/service_usage.go +++ b/service/ocm/service_usage.go @@ -46,6 +46,7 @@ func (u *UsageStats) UnmarshalJSON(data []byte) error { type CostCombination struct { Model string `json:"model"` ServiceTier string `json:"service_tier,omitempty"` + ContextWindow int `json:"context_window"` WeekStartUnix int64 `json:"week_start_unix,omitempty"` Total UsageStats `json:"total"` ByUser map[string]UsageStats `json:"by_user"` @@ -74,6 +75,7 @@ type UsageStatsJSON struct { type CostCombinationJSON struct { Model string `json:"model"` ServiceTier string `json:"service_tier,omitempty"` + ContextWindow int `json:"context_window"` WeekStartUnix int64 `json:"week_start_unix,omitempty"` Total UsageStatsJSON `json:"total"` ByUser map[string]UsageStatsJSON `json:"by_user"` @@ -104,8 +106,9 @@ type ModelPricing struct { } type modelFamily struct { - pattern *regexp.Regexp - pricing ModelPricing + pattern *regexp.Regexp + pricing ModelPricing + premiumPricing *ModelPricing } const ( @@ -116,6 +119,12 @@ const ( serviceTierScale = "scale" ) +const ( + contextWindowStandard = 272000 + contextWindowPremium = 1050000 + premiumContextThreshold = 272000 +) + var ( gpt52Pricing = ModelPricing{ InputPrice: 1.75, @@ -159,6 +168,30 @@ var ( CachedInputPrice: 0.025, } + gpt54StandardPricing = ModelPricing{ + InputPrice: 2.5, + OutputPrice: 15.0, + CachedInputPrice: 0.25, + } + + gpt54PremiumPricing = ModelPricing{ + InputPrice: 5.0, + OutputPrice: 22.5, + CachedInputPrice: 0.5, + } + + gpt54ProPricing = ModelPricing{ + InputPrice: 30.0, + OutputPrice: 180.0, + CachedInputPrice: 30.0, + } + + gpt54ProPremiumPricing = ModelPricing{ + InputPrice: 60.0, + OutputPrice: 270.0, + CachedInputPrice: 60.0, + } + gpt52ProPricing = ModelPricing{ InputPrice: 21.0, OutputPrice: 168.0, @@ -171,6 +204,30 @@ var ( CachedInputPrice: 15.0, } + gpt54FlexPricing = ModelPricing{ + InputPrice: 1.25, + OutputPrice: 7.5, + CachedInputPrice: 0.125, + } + + gpt54PremiumFlexPricing = ModelPricing{ + InputPrice: 2.5, + OutputPrice: 11.25, + CachedInputPrice: 0.25, + } + + gpt54ProFlexPricing = ModelPricing{ + InputPrice: 15.0, + OutputPrice: 90.0, + CachedInputPrice: 15.0, + } + + gpt54ProPremiumFlexPricing = ModelPricing{ + InputPrice: 30.0, + OutputPrice: 135.0, + CachedInputPrice: 30.0, + } + gpt52FlexPricing = ModelPricing{ InputPrice: 0.875, OutputPrice: 7.0, @@ -195,6 +252,18 @@ var ( CachedInputPrice: 0.0025, } + gpt54PriorityPricing = ModelPricing{ + InputPrice: 5.0, + OutputPrice: 30.0, + CachedInputPrice: 0.5, + } + + gpt54PremiumPriorityPricing = ModelPricing{ + InputPrice: 10.0, + OutputPrice: 45.0, + CachedInputPrice: 1.0, + } + gpt52PriorityPricing = ModelPricing{ InputPrice: 3.5, OutputPrice: 28.0, @@ -382,6 +451,16 @@ var ( } standardModelFamilies = []modelFamily{ + { + pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`), + pricing: gpt54ProPricing, + premiumPricing: &gpt54ProPremiumPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`), + pricing: gpt54StandardPricing, + premiumPricing: &gpt54PremiumPricing, + }, { pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`), pricing: gpt52CodexPricing, @@ -525,6 +604,16 @@ var ( } flexModelFamilies = []modelFamily{ + { + pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`), + pricing: gpt54ProFlexPricing, + premiumPricing: &gpt54ProPremiumFlexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`), + pricing: gpt54FlexPricing, + premiumPricing: &gpt54PremiumFlexPricing, + }, { pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`), pricing: gpt5MiniFlexPricing, @@ -556,6 +645,11 @@ var ( } priorityModelFamilies = []modelFamily{ + { + pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`), + pricing: gpt54PriorityPricing, + premiumPricing: &gpt54PremiumPriorityPricing, + }, { pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`), pricing: gpt52CodexPriorityPricing, @@ -638,15 +732,28 @@ func modelFamiliesForTier(serviceTier string) []modelFamily { } } -func findPricingInFamilies(model string, modelFamilies []modelFamily) (ModelPricing, bool) { +func findPricingInFamilies(model string, contextWindow int, modelFamilies []modelFamily) (ModelPricing, bool) { + isPremium := contextWindow >= contextWindowPremium for _, family := range modelFamilies { if family.pattern.MatchString(model) { + if isPremium && family.premiumPricing != nil { + return *family.premiumPricing, true + } return family.pricing, true } } return ModelPricing{}, false } +func hasPremiumPricingInFamilies(model string, modelFamilies []modelFamily) bool { + for _, family := range modelFamilies { + if family.pattern.MatchString(model) { + return family.premiumPricing != nil + } + } + return false +} + func normalizeServiceTier(serviceTier string) string { switch strings.ToLower(strings.TrimSpace(serviceTier)) { case "", serviceTierAuto, serviceTierDefault: @@ -663,27 +770,27 @@ func normalizeServiceTier(serviceTier string) string { } } -func getPricing(model string, serviceTier string) ModelPricing { +func getPricing(model string, serviceTier string, contextWindow int) ModelPricing { normalizedServiceTier := normalizeServiceTier(serviceTier) - modelFamilies := modelFamiliesForTier(normalizedServiceTier) + families := modelFamiliesForTier(normalizedServiceTier) - if pricing, found := findPricingInFamilies(model, modelFamilies); found { + if pricing, found := findPricingInFamilies(model, contextWindow, families); found { return pricing } normalizedModel := normalizeGPT5Model(model) if normalizedModel != model { - if pricing, found := findPricingInFamilies(normalizedModel, modelFamilies); found { + if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, families); found { return pricing } } if normalizedServiceTier != serviceTierDefault { - if pricing, found := findPricingInFamilies(model, standardModelFamilies); found { + if pricing, found := findPricingInFamilies(model, contextWindow, standardModelFamilies); found { return pricing } if normalizedModel != model { - if pricing, found := findPricingInFamilies(normalizedModel, standardModelFamilies); found { + if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, standardModelFamilies); found { return pricing } } @@ -692,6 +799,30 @@ func getPricing(model string, serviceTier string) ModelPricing { return gpt4oPricing } +func detectContextWindow(model string, serviceTier string, inputTokens int64) int { + if inputTokens <= premiumContextThreshold { + return contextWindowStandard + } + normalizedServiceTier := normalizeServiceTier(serviceTier) + families := modelFamiliesForTier(normalizedServiceTier) + if hasPremiumPricingInFamilies(model, families) { + return contextWindowPremium + } + normalizedModel := normalizeGPT5Model(model) + if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, families) { + return contextWindowPremium + } + if normalizedServiceTier != serviceTierDefault { + if hasPremiumPricingInFamilies(model, standardModelFamilies) { + return contextWindowPremium + } + if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, standardModelFamilies) { + return contextWindowPremium + } + } + return contextWindowStandard +} + func normalizeGPT5Model(model string) string { if !strings.HasPrefix(model, "gpt-5.") { return model @@ -707,18 +838,18 @@ func normalizeGPT5Model(model string) string { case strings.Contains(model, "-chat-latest"): return "gpt-5.2-chat-latest" case strings.Contains(model, "-pro"): - return "gpt-5.2-pro" + return "gpt-5.4-pro" case strings.Contains(model, "-mini"): return "gpt-5-mini" case strings.Contains(model, "-nano"): return "gpt-5-nano" default: - return "gpt-5.2" + return "gpt-5.4" } } -func calculateCost(stats UsageStats, model string, serviceTier string) float64 { - pricing := getPricing(model, serviceTier) +func calculateCost(stats UsageStats, model string, serviceTier string, contextWindow int) float64 { + pricing := getPricing(model, serviceTier, contextWindow) regularInputTokens := stats.InputTokens - stats.CachedTokens if regularInputTokens < 0 { @@ -739,13 +870,16 @@ func roundCost(cost float64) float64 { func normalizeCombinations(combinations []CostCombination) { for index := range combinations { combinations[index].ServiceTier = normalizeServiceTier(combinations[index].ServiceTier) + if combinations[index].ContextWindow <= 0 { + combinations[index].ContextWindow = contextWindowStandard + } if combinations[index].ByUser == nil { combinations[index].ByUser = make(map[string]UsageStats) } } } -func addUsageToCombinations(combinations *[]CostCombination, model string, serviceTier string, weekStartUnix int64, user string, inputTokens, outputTokens, cachedTokens int64) { +func addUsageToCombinations(combinations *[]CostCombination, model string, serviceTier string, contextWindow int, weekStartUnix int64, user string, inputTokens, outputTokens, cachedTokens int64) { var matchedCombination *CostCombination for index := range *combinations { combination := &(*combinations)[index] @@ -753,7 +887,7 @@ func addUsageToCombinations(combinations *[]CostCombination, model string, servi if combination.ServiceTier != combinationServiceTier { combination.ServiceTier = combinationServiceTier } - if combination.Model == model && combinationServiceTier == serviceTier && combination.WeekStartUnix == weekStartUnix { + if combination.Model == model && combinationServiceTier == serviceTier && combination.ContextWindow == contextWindow && combination.WeekStartUnix == weekStartUnix { matchedCombination = combination break } @@ -763,6 +897,7 @@ func addUsageToCombinations(combinations *[]CostCombination, model string, servi newCombination := CostCombination{ Model: model, ServiceTier: serviceTier, + ContextWindow: contextWindow, WeekStartUnix: weekStartUnix, Total: UsageStats{}, ByUser: make(map[string]UsageStats), @@ -791,12 +926,13 @@ func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map var totalCost float64 for index, combination := range combinations { - combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ServiceTier) + combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow) totalCost += combinationTotalCost combinationJSON := CostCombinationJSON{ Model: combination.Model, ServiceTier: combination.ServiceTier, + ContextWindow: combination.ContextWindow, WeekStartUnix: combination.WeekStartUnix, Total: UsageStatsJSON{ RequestCount: combination.Total.RequestCount, @@ -809,7 +945,7 @@ func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map } for user, userStats := range combination.ByUser { - userCost := calculateCost(userStats, combination.Model, combination.ServiceTier) + userCost := calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow) if aggregateUserCosts != nil { aggregateUserCosts[user] += userCost } @@ -857,7 +993,7 @@ func buildByWeekCost(combinations []CostCombination) map[string]float64 { } weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() weekKey := formatWeekStartKey(weekStartAt) - byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ServiceTier) + byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow) } for weekKey, weekCost := range byWeek { byWeek[weekKey] = roundCost(weekCost) @@ -879,7 +1015,7 @@ func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[strin userWeeks = make(map[string]float64) byUserAndWeek[user] = userWeeks } - userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ServiceTier) + userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow) } } for _, weekCosts := range byUserAndWeek { @@ -987,14 +1123,17 @@ func (u *AggregatedUsage) Save() error { return err } -func (u *AggregatedUsage) AddUsage(model string, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string) error { - return u.AddUsageWithCycleHint(model, inputTokens, outputTokens, cachedTokens, serviceTier, user, time.Now(), nil) +func (u *AggregatedUsage) AddUsage(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string) error { + return u.AddUsageWithCycleHint(model, contextWindow, inputTokens, outputTokens, cachedTokens, serviceTier, user, time.Now(), nil) } -func (u *AggregatedUsage) AddUsageWithCycleHint(model string, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string, observedAt time.Time, cycleHint *WeeklyCycleHint) error { +func (u *AggregatedUsage) AddUsageWithCycleHint(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string, observedAt time.Time, cycleHint *WeeklyCycleHint) error { if model == "" { return E.New("model cannot be empty") } + if contextWindow <= 0 { + return E.New("contextWindow must be positive") + } normalizedServiceTier := normalizeServiceTier(serviceTier) if observedAt.IsZero() { @@ -1007,7 +1146,7 @@ func (u *AggregatedUsage) AddUsageWithCycleHint(model string, inputTokens, outpu u.LastUpdated = observedAt weekStartUnix := deriveWeekStartUnix(cycleHint) - addUsageToCombinations(&u.Combinations, model, normalizedServiceTier, weekStartUnix, user, inputTokens, outputTokens, cachedTokens) + addUsageToCombinations(&u.Combinations, model, normalizedServiceTier, contextWindow, weekStartUnix, user, inputTokens, outputTokens, cachedTokens) go u.scheduleSave() diff --git a/service/ocm/service_websocket.go b/service/ocm/service_websocket.go index c2e6148d..d19f2df8 100644 --- a/service/ocm/service_websocket.go +++ b/service/ocm/service_websocket.go @@ -256,8 +256,10 @@ func (s *Service) proxyWebSocketUpstreamToClient(upstreamReadWriter io.ReadWrite responseModel = requestModel } if responseModel != "" { + contextWindow := detectContextWindow(responseModel, serviceTier, inputTokens) s.usageTracker.AddUsageWithCycleHint( responseModel, + contextWindow, inputTokens, outputTokens, cachedTokens, From 8289bbd846e04da8098acfd1554c17c057c1060f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 11 Mar 2026 16:32:50 +0800 Subject: [PATCH 24/99] Add Alpine APK packaging to CI build Add fpm-based Alpine APK packaging alongside existing DEB/RPM/Pacman packages. Alpine APKs use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix. --- .github/build_alpine_apk.sh | 81 ++++++++++++++++++++++++++++++ .github/workflows/build.yml | 23 ++++++--- docs/installation/tools/install.sh | 5 ++ release/config/sing-box.confd | 6 +++ release/config/sing-box.initd | 32 ++++++++++-- 5 files changed, 137 insertions(+), 10 deletions(-) create mode 100755 .github/build_alpine_apk.sh create mode 100644 release/config/sing-box.confd mode change 100644 => 100755 release/config/sing-box.initd diff --git a/.github/build_alpine_apk.sh b/.github/build_alpine_apk.sh new file mode 100755 index 00000000..aaaa04f9 --- /dev/null +++ b/.github/build_alpine_apk.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +ARCHITECTURE="$1" +VERSION="$2" +BINARY_PATH="$3" +OUTPUT_PATH="$4" + +if [ -z "$ARCHITECTURE" ] || [ -z "$VERSION" ] || [ -z "$BINARY_PATH" ] || [ -z "$OUTPUT_PATH" ]; then + echo "Usage: $0 " + exit 1 +fi + +PROJECT=$(cd "$(dirname "$0")/.."; pwd) + +# Convert version to APK format: +# 1.13.0-beta.8 -> 1.13.0_beta8-r0 +# 1.13.0-rc.3 -> 1.13.0_rc3-r0 +# 1.13.0 -> 1.13.0-r0 +APK_VERSION=$(echo "$VERSION" | sed -E 's/-([a-z]+)\.([0-9]+)/_\1\2/') +APK_VERSION="${APK_VERSION}-r0" + +ROOT_DIR=$(mktemp -d) +trap 'rm -rf "$ROOT_DIR"' EXIT + +# Binary +install -Dm755 "$BINARY_PATH" "$ROOT_DIR/usr/bin/sing-box" + +# Config files +install -Dm644 "$PROJECT/release/config/config.json" "$ROOT_DIR/etc/sing-box/config.json" +install -Dm755 "$PROJECT/release/config/sing-box.initd" "$ROOT_DIR/etc/init.d/sing-box" +install -Dm644 "$PROJECT/release/config/sing-box.confd" "$ROOT_DIR/etc/conf.d/sing-box" + +# Service files +install -Dm644 "$PROJECT/release/config/sing-box.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box.service" +install -Dm644 "$PROJECT/release/config/sing-box@.service" "$ROOT_DIR/usr/lib/systemd/system/sing-box@.service" + +# Completions +install -Dm644 "$PROJECT/release/completions/sing-box.bash" "$ROOT_DIR/usr/share/bash-completion/completions/sing-box.bash" +install -Dm644 "$PROJECT/release/completions/sing-box.fish" "$ROOT_DIR/usr/share/fish/vendor_completions.d/sing-box.fish" +install -Dm644 "$PROJECT/release/completions/sing-box.zsh" "$ROOT_DIR/usr/share/zsh/site-functions/_sing-box" + +# License +install -Dm644 "$PROJECT/LICENSE" "$ROOT_DIR/usr/share/licenses/sing-box/LICENSE" + +# APK metadata +PACKAGES_DIR="$ROOT_DIR/lib/apk/packages" +mkdir -p "$PACKAGES_DIR" + +# .conffiles +cat > "$PACKAGES_DIR/.conffiles" <<'EOF' +/etc/conf.d/sing-box +/etc/init.d/sing-box +/etc/sing-box/config.json +EOF + +# .conffiles_static (sha256 checksums) +while IFS= read -r conffile; do + sha256=$(sha256sum "$ROOT_DIR$conffile" | cut -d' ' -f1) + echo "$conffile $sha256" +done < "$PACKAGES_DIR/.conffiles" > "$PACKAGES_DIR/.conffiles_static" + +# .list (all files, excluding lib/apk/packages/ metadata) +(cd "$ROOT_DIR" && find . -type f -o -type l) \ + | sed 's|^\./|/|' \ + | grep -v '^/lib/apk/packages/' \ + | sort > "$PACKAGES_DIR/.list" + +# Build APK +apk mkpkg \ + --info "name:sing-box" \ + --info "version:${APK_VERSION}" \ + --info "description:The universal proxy platform." \ + --info "arch:${ARCHITECTURE}" \ + --info "license:GPL-3.0-or-later with name use or association addition" \ + --info "origin:sing-box" \ + --info "url:https://sing-box.sagernet.org/" \ + --info "maintainer:nekohasekai " \ + --files "$ROOT_DIR" \ + --output "$OUTPUT_PATH" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ce9a98a..2cf9e62d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,27 +72,27 @@ jobs: include: - { os: linux, arch: amd64, variant: purego, naive: true } - { os: linux, arch: amd64, variant: glibc, naive: true } - - { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" } + - { os: linux, arch: amd64, variant: musl, naive: true, debian: amd64, rpm: x86_64, pacman: x86_64, alpine: x86_64, openwrt: "x86_64" } - { os: linux, arch: arm64, variant: purego, naive: true } - { os: linux, arch: arm64, variant: glibc, naive: true } - - { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" } + - { os: linux, arch: arm64, variant: musl, naive: true, debian: arm64, rpm: aarch64, pacman: aarch64, alpine: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" } - { os: linux, arch: "386", go386: sse2 } - { os: linux, arch: "386", variant: glibc, naive: true, go386: sse2 } - - { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" } + - { os: linux, arch: "386", variant: musl, naive: true, go386: sse2, debian: i386, rpm: i386, alpine: x86, openwrt: "i386_pentium4" } - { os: linux, arch: arm, goarm: "7" } - { os: linux, arch: arm, variant: glibc, naive: true, goarm: "7" } - - { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" } + - { os: linux, arch: arm, variant: musl, naive: true, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, alpine: armv7, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" } - { os: linux, arch: mipsle, gomips: hardfloat, naive: true, variant: glibc } - { os: linux, arch: mipsle, gomips: softfloat, naive: true, variant: musl, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" } - { os: linux, arch: mips64le, gomips: hardfloat, naive: true, variant: glibc, debian: mips64el, rpm: mips64el } - { os: linux, arch: riscv64, naive: true, variant: glibc } - - { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" } + - { os: linux, arch: riscv64, naive: true, variant: musl, debian: riscv64, rpm: riscv64, alpine: riscv64, openwrt: "riscv64_generic" } - { os: linux, arch: loong64, naive: true, variant: glibc } - - { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" } + - { os: linux, arch: loong64, naive: true, variant: musl, debian: loongarch64, rpm: loongarch64, alpine: loongarch64, openwrt: "loongarch64_generic" } - { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" } - { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" } @@ -397,7 +397,7 @@ jobs: done rm "dist/openwrt.deb" - name: Install apk-tools - if: matrix.openwrt != '' + 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 @@ -411,6 +411,15 @@ jobs: "dist/sing-box" \ "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.apk" done + - name: Package Alpine APK + if: matrix.alpine != '' + run: |- + set -xeuo pipefail + .github/build_alpine_apk.sh \ + "${{ matrix.alpine }}" \ + "${{ needs.calculate_version.outputs.version }}" \ + "dist/sing-box" \ + "dist/sing-box_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.alpine }}.apk" - name: Archive run: | set -xeuo pipefail diff --git a/docs/installation/tools/install.sh b/docs/installation/tools/install.sh index fc514767..7bfbc365 100755 --- a/docs/installation/tools/install.sh +++ b/docs/installation/tools/install.sh @@ -53,6 +53,11 @@ elif command -v apk >/dev/null 2>&1 && [ -f /etc/os-release ] && grep -q OPENWRT arch="$OPENWRT_ARCH" package_suffix=".apk" package_install="apk add --allow-untrusted" +elif command -v apk >/dev/null 2>&1; then + os="linux" + arch=$(apk --print-arch) + package_suffix=".apk" + package_install="apk add --allow-untrusted" elif command -v opkg >/dev/null 2>&1; then os="openwrt" . /etc/os-release diff --git a/release/config/sing-box.confd b/release/config/sing-box.confd new file mode 100644 index 00000000..506caa32 --- /dev/null +++ b/release/config/sing-box.confd @@ -0,0 +1,6 @@ +# /etc/conf.d/sing-box: config file for /etc/init.d/sing-box + +# sing-box configuration path, could be file or directory +# SINGBOX_CONFIG=/etc/sing-box + +# SINGBOX_WORKDIR=/var/lib/sing-box diff --git a/release/config/sing-box.initd b/release/config/sing-box.initd old mode 100644 new mode 100755 index db96e478..1541518a --- a/release/config/sing-box.initd +++ b/release/config/sing-box.initd @@ -4,15 +4,41 @@ name=$RC_SVCNAME description="sing-box service" supervisor="supervise-daemon" command="/usr/bin/sing-box" -command_args="-D /var/lib/sing-box -C /etc/sing-box run" +extra_commands="checkconfig" extra_started_commands="reload" +: ${SINGBOX_CONFIG:=${config:-"/etc/sing-box"}} + +if [ -d "$SINGBOX_CONFIG" ]; then + _config_opt="-C $SINGBOX_CONFIG" +elif [ -z "$SINGBOX_CONFIG" ]; then + _config_opt="" +else + _config_opt="-c $SINGBOX_CONFIG" +fi + +_workdir=${SINGBOX_WORKDIR:-${workdir:-"/var/lib/sing-box"}} + +command_args="run --disable-color + -D $_workdir + $_config_opt" + depend() { after net dns } +checkconfig() { + ebegin "Checking $RC_SVCNAME configuration" + sing-box check -D "$_workdir" $_config_opt + eend $? +} + +start_pre() { + checkconfig +} + reload() { ebegin "Reloading $RC_SVCNAME" - $supervisor "$RC_SVCNAME" --signal HUP + checkconfig && $supervisor "$RC_SVCNAME" --signal HUP eend $? -} \ No newline at end of file +} From 54468a1a2a983372eb5663f395097f06b57cc1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 8 Mar 2026 20:21:29 +0800 Subject: [PATCH 25/99] platform: Add f-droid update helpers --- experimental/libbox/fdroid.go | 493 ++++++++++++++++++++++++++ experimental/libbox/fdroid_mirrors.go | 92 +++++ 2 files changed, 585 insertions(+) create mode 100644 experimental/libbox/fdroid.go create mode 100644 experimental/libbox/fdroid_mirrors.go diff --git a/experimental/libbox/fdroid.go b/experimental/libbox/fdroid.go new file mode 100644 index 00000000..d574ffd8 --- /dev/null +++ b/experimental/libbox/fdroid.go @@ -0,0 +1,493 @@ +package libbox + +import ( + "archive/zip" + "bytes" + "crypto/tls" + "encoding/json" + "io" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + E "github.com/sagernet/sing/common/exceptions" +) + +const fdroidUserAgent = "F-Droid 1.21.1" + +type FDroidUpdateInfo struct { + VersionCode int32 + VersionName string + DownloadURL string + FileSize int64 + FileSHA256 string +} + +type FDroidPingResult struct { + URL string + LatencyMs int32 + Error string +} + +type FDroidPingResultIterator interface { + Len() int32 + HasNext() bool + Next() *FDroidPingResult +} + +type fdroidAPIResponse struct { + PackageName string `json:"packageName"` + SuggestedVersionCode int32 `json:"suggestedVersionCode"` + Packages []fdroidAPIPackage `json:"packages"` +} + +type fdroidAPIPackage struct { + VersionName string `json:"versionName"` + VersionCode int32 `json:"versionCode"` +} + +type fdroidEntry struct { + Timestamp int64 `json:"timestamp"` + Version int `json:"version"` + Index fdroidEntryFile `json:"index"` + Diffs map[string]fdroidEntryFile `json:"diffs"` +} + +type fdroidEntryFile struct { + Name string `json:"name"` + SHA256 string `json:"sha256"` + Size int64 `json:"size"` + NumPackages int `json:"numPackages"` +} + +type fdroidIndexV2 struct { + Packages map[string]fdroidV2Package `json:"packages"` +} + +type fdroidV2Package struct { + Versions map[string]fdroidV2Version `json:"versions"` +} + +type fdroidV2Version struct { + Manifest fdroidV2Manifest `json:"manifest"` + File fdroidV2File `json:"file"` +} + +type fdroidV2Manifest struct { + VersionCode int32 `json:"versionCode"` + VersionName string `json:"versionName"` +} + +type fdroidV2File struct { + Name string `json:"name"` + SHA256 string `json:"sha256"` + Size int64 `json:"size"` +} + +type fdroidIndexV1 struct { + Packages map[string][]fdroidV1Package `json:"packages"` +} + +type fdroidV1Package struct { + VersionCode int32 `json:"versionCode"` + VersionName string `json:"versionName"` + ApkName string `json:"apkName"` + Size int64 `json:"size"` + Hash string `json:"hash"` + HashType string `json:"hashType"` +} + +type fdroidCache struct { + MirrorURL string `json:"mirrorURL"` + Timestamp int64 `json:"timestamp"` + ETag string `json:"etag"` + IsV1 bool `json:"isV1,omitempty"` +} + +func CheckFDroidUpdate(mirrorURL, packageName string, currentVersionCode int32, cachePath string) (*FDroidUpdateInfo, error) { + mirrorURL = strings.TrimRight(mirrorURL, "/") + if strings.Contains(mirrorURL, "f-droid.org") { + return checkFDroidAPI(mirrorURL, packageName, currentVersionCode) + } + client := newFDroidHTTPClient() + defer client.CloseIdleConnections() + cache := loadFDroidCache(cachePath, mirrorURL) + if cache != nil && cache.IsV1 { + return checkFDroidV1(client, mirrorURL, packageName, currentVersionCode, cachePath, cache) + } + return checkFDroidV2(client, mirrorURL, packageName, currentVersionCode, cachePath, cache) +} + +func PingFDroidMirrors(mirrorURLs string) (FDroidPingResultIterator, error) { + urls := strings.Split(mirrorURLs, ",") + results := make([]*FDroidPingResult, len(urls)) + var waitGroup sync.WaitGroup + for i, rawURL := range urls { + waitGroup.Add(1) + go func(index int, target string) { + defer waitGroup.Done() + target = strings.TrimSpace(target) + result := &FDroidPingResult{URL: target} + latency, err := pingTLS(target) + if err != nil { + result.LatencyMs = -1 + result.Error = err.Error() + } else { + result.LatencyMs = int32(latency.Milliseconds()) + } + results[index] = result + }(i, rawURL) + } + waitGroup.Wait() + sort.Slice(results, func(i, j int) bool { + if results[i].LatencyMs < 0 { + return false + } + if results[j].LatencyMs < 0 { + return true + } + return results[i].LatencyMs < results[j].LatencyMs + }) + return newIterator(results), nil +} + +func PingFDroidMirror(mirrorURL string) *FDroidPingResult { + mirrorURL = strings.TrimSpace(mirrorURL) + result := &FDroidPingResult{URL: mirrorURL} + latency, err := pingTLS(mirrorURL) + if err != nil { + result.LatencyMs = -1 + result.Error = err.Error() + } else { + result.LatencyMs = int32(latency.Milliseconds()) + } + return result +} + +func newFDroidHTTPClient() *http.Client { + return &http.Client{ + Timeout: 30 * time.Second, + } +} + +func newFDroidRequest(requestURL string) (*http.Request, error) { + request, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + return nil, err + } + request.Header.Set("User-Agent", fdroidUserAgent) + return request, nil +} + +func checkFDroidAPI(mirrorURL, packageName string, currentVersionCode int32) (*FDroidUpdateInfo, error) { + client := newFDroidHTTPClient() + defer client.CloseIdleConnections() + + apiURL := "https://f-droid.org/api/v1/packages/" + packageName + request, err := newFDroidRequest(apiURL) + if err != nil { + return nil, err + } + + response, err := client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return nil, E.New("HTTP ", response.Status) + } + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + var apiResponse fdroidAPIResponse + err = json.Unmarshal(body, &apiResponse) + if err != nil { + return nil, err + } + + var bestCode int32 + var bestName string + for _, pkg := range apiResponse.Packages { + if pkg.VersionCode > currentVersionCode && pkg.VersionCode > bestCode { + bestCode = pkg.VersionCode + bestName = pkg.VersionName + } + } + + if bestCode == 0 { + return nil, nil + } + + return &FDroidUpdateInfo{ + VersionCode: bestCode, + VersionName: bestName, + DownloadURL: "https://f-droid.org/repo/" + packageName + "_" + strconv.FormatInt(int64(bestCode), 10) + ".apk", + }, nil +} + +func checkFDroidV2(client *http.Client, mirrorURL, packageName string, currentVersionCode int32, cachePath string, cache *fdroidCache) (*FDroidUpdateInfo, error) { + entryURL := mirrorURL + "/entry.jar" + request, err := newFDroidRequest(entryURL) + if err != nil { + return nil, err + } + if cache != nil && cache.ETag != "" { + request.Header.Set("If-None-Match", cache.ETag) + } + + response, err := client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode == http.StatusNotModified { + return nil, nil + } + if response.StatusCode == http.StatusNotFound { + writeFDroidCache(cachePath, mirrorURL, 0, "", true) + return checkFDroidV1(client, mirrorURL, packageName, currentVersionCode, cachePath, nil) + } + if response.StatusCode != http.StatusOK { + return nil, E.New("HTTP ", response.Status, ": ", entryURL) + } + + jarData, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + etag := response.Header.Get("ETag") + + var entry fdroidEntry + err = readJSONFromJar(jarData, "entry.json", &entry) + if err != nil { + return nil, E.Cause(err, "read entry.jar") + } + + if entry.Timestamp == 0 { + return nil, E.New("entry.json not found in entry.jar") + } + + if cache != nil && cache.Timestamp == entry.Timestamp { + writeFDroidCache(cachePath, mirrorURL, entry.Timestamp, etag, false) + return nil, nil + } + + var indexURL string + if cache != nil { + cachedTimestamp := strconv.FormatInt(cache.Timestamp, 10) + if diff, ok := entry.Diffs[cachedTimestamp]; ok { + indexURL = mirrorURL + "/" + diff.Name + } + } + if indexURL == "" { + indexURL = mirrorURL + "/" + entry.Index.Name + } + + indexRequest, err := newFDroidRequest(indexURL) + if err != nil { + return nil, err + } + + indexResponse, err := client.Do(indexRequest) + if err != nil { + return nil, err + } + defer indexResponse.Body.Close() + + if indexResponse.StatusCode != http.StatusOK { + return nil, E.New("HTTP ", indexResponse.Status, ": ", indexURL) + } + + indexData, err := io.ReadAll(indexResponse.Body) + if err != nil { + return nil, err + } + + var index fdroidIndexV2 + err = json.Unmarshal(indexData, &index) + if err != nil { + return nil, err + } + + writeFDroidCache(cachePath, mirrorURL, entry.Timestamp, etag, false) + + pkg, ok := index.Packages[packageName] + if !ok { + return nil, nil + } + + var bestCode int32 + var bestVersion fdroidV2Version + for _, version := range pkg.Versions { + if version.Manifest.VersionCode > currentVersionCode && version.Manifest.VersionCode > bestCode { + bestCode = version.Manifest.VersionCode + bestVersion = version + } + } + + if bestCode == 0 { + return nil, nil + } + + return &FDroidUpdateInfo{ + VersionCode: bestCode, + VersionName: bestVersion.Manifest.VersionName, + DownloadURL: mirrorURL + "/" + bestVersion.File.Name, + FileSize: bestVersion.File.Size, + FileSHA256: bestVersion.File.SHA256, + }, nil +} + +func checkFDroidV1(client *http.Client, mirrorURL, packageName string, currentVersionCode int32, cachePath string, cache *fdroidCache) (*FDroidUpdateInfo, error) { + indexURL := mirrorURL + "/index-v1.jar" + + request, err := newFDroidRequest(indexURL) + if err != nil { + return nil, err + } + if cache != nil && cache.ETag != "" { + request.Header.Set("If-None-Match", cache.ETag) + } + + response, err := client.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if response.StatusCode == http.StatusNotModified { + return nil, nil + } + if response.StatusCode != http.StatusOK { + return nil, E.New("HTTP ", response.Status, ": ", indexURL) + } + + jarData, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + etag := response.Header.Get("ETag") + + var index fdroidIndexV1 + err = readJSONFromJar(jarData, "index-v1.json", &index) + if err != nil { + return nil, E.Cause(err, "read index-v1.jar") + } + + writeFDroidCache(cachePath, mirrorURL, 0, etag, true) + + packages, ok := index.Packages[packageName] + if !ok { + return nil, nil + } + + var bestCode int32 + var bestPackage fdroidV1Package + for _, pkg := range packages { + if pkg.VersionCode > currentVersionCode && pkg.VersionCode > bestCode { + bestCode = pkg.VersionCode + bestPackage = pkg + } + } + + if bestCode == 0 { + return nil, nil + } + + return &FDroidUpdateInfo{ + VersionCode: bestCode, + VersionName: bestPackage.VersionName, + DownloadURL: mirrorURL + "/" + bestPackage.ApkName, + FileSize: bestPackage.Size, + FileSHA256: bestPackage.Hash, + }, nil +} + +func readJSONFromJar(jarData []byte, fileName string, destination any) error { + zipReader, err := zip.NewReader(bytes.NewReader(jarData), int64(len(jarData))) + if err != nil { + return err + } + for _, file := range zipReader.File { + if file.Name != fileName { + continue + } + reader, err := file.Open() + if err != nil { + return err + } + data, err := io.ReadAll(reader) + reader.Close() + if err != nil { + return err + } + return json.Unmarshal(data, destination) + } + return nil +} + +func pingTLS(mirrorURL string) (time.Duration, error) { + parsed, err := url.Parse(mirrorURL) + if err != nil { + return 0, err + } + host := parsed.Host + if !strings.Contains(host, ":") { + host = host + ":443" + } + + dialer := &net.Dialer{Timeout: 5 * time.Second} + start := time.Now() + conn, err := tls.DialWithDialer(dialer, "tcp", host, &tls.Config{}) + if err != nil { + return 0, err + } + latency := time.Since(start) + conn.Close() + return latency, nil +} + +func loadFDroidCache(cachePath, mirrorURL string) *fdroidCache { + cacheFile := filepath.Join(cachePath, "fdroid_cache.json") + data, err := os.ReadFile(cacheFile) + if err != nil { + return nil + } + var cache fdroidCache + err = json.Unmarshal(data, &cache) + if err != nil { + return nil + } + if cache.MirrorURL != mirrorURL { + return nil + } + return &cache +} + +func writeFDroidCache(cachePath, mirrorURL string, timestamp int64, etag string, isV1 bool) { + cache := fdroidCache{ + MirrorURL: mirrorURL, + Timestamp: timestamp, + ETag: etag, + IsV1: isV1, + } + data, err := json.Marshal(cache) + if err != nil { + return + } + os.MkdirAll(cachePath, 0o755) + os.WriteFile(filepath.Join(cachePath, "fdroid_cache.json"), data, 0o644) +} diff --git a/experimental/libbox/fdroid_mirrors.go b/experimental/libbox/fdroid_mirrors.go new file mode 100644 index 00000000..4ca82555 --- /dev/null +++ b/experimental/libbox/fdroid_mirrors.go @@ -0,0 +1,92 @@ +package libbox + +type FDroidMirror struct { + URL string + Country string + Name string +} + +type FDroidMirrorIterator interface { + Len() int32 + HasNext() bool + Next() *FDroidMirror +} + +var builtinFDroidMirrors = []FDroidMirror{ + // Official + {URL: "https://f-droid.org/repo", Country: "Official", Name: "f-droid.org"}, + {URL: "https://cloudflare.f-droid.org/repo", Country: "Official", Name: "Cloudflare CDN"}, + + // China + {URL: "https://mirrors.tuna.tsinghua.edu.cn/fdroid/repo", Country: "China", Name: "Tsinghua TUNA"}, + {URL: "https://mirrors.nju.edu.cn/fdroid/repo", Country: "China", Name: "Nanjing University"}, + {URL: "https://mirror.iscas.ac.cn/fdroid/repo", Country: "China", Name: "ISCAS"}, + {URL: "https://mirror.nyist.edu.cn/fdroid/repo", Country: "China", Name: "NYIST"}, + {URL: "https://mirrors.cqupt.edu.cn/fdroid/repo", Country: "China", Name: "CQUPT"}, + {URL: "https://mirrors.shanghaitech.edu.cn/fdroid/repo", Country: "China", Name: "ShanghaiTech"}, + + // India + {URL: "https://mirror.hyd.albony.in/fdroid/repo", Country: "India", Name: "Albony Hyderabad"}, + {URL: "https://mirror.del2.albony.in/fdroid/repo", Country: "India", Name: "Albony Delhi"}, + + // Taiwan + {URL: "https://mirror.ossplanet.net/fdroid/repo", Country: "Taiwan", Name: "OSSPlanet"}, + + // France + {URL: "https://fdroid.tetaneutral.net/fdroid/repo", Country: "France", Name: "tetaneutral.net"}, + {URL: "https://mirror.freedif.org/fdroid/repo", Country: "France", Name: "FreeDif"}, + + // Germany + {URL: "https://ftp.fau.de/fdroid/repo", Country: "Germany", Name: "FAU Erlangen"}, + {URL: "https://ftp.agdsn.de/fdroid/repo", Country: "Germany", Name: "AGDSN Dresden"}, + {URL: "https://ftp.gwdg.de/pub/android/fdroid/repo", Country: "Germany", Name: "GWDG"}, + {URL: "https://mirror.level66.network/fdroid/repo", Country: "Germany", Name: "Level66"}, + {URL: "https://mirror.mci-1.serverforge.org/fdroid/repo", Country: "Germany", Name: "ServerForge"}, + + // Netherlands + {URL: "https://ftp.snt.utwente.nl/pub/software/fdroid/repo", Country: "Netherlands", Name: "University of Twente"}, + + // Sweden + {URL: "https://ftp.lysator.liu.se/pub/fdroid/repo", Country: "Sweden", Name: "Lysator"}, + + // Denmark + {URL: "https://mirrors.dotsrc.org/fdroid/repo", Country: "Denmark", Name: "dotsrc.org"}, + + // Austria + {URL: "https://mirror.kumi.systems/fdroid/repo", Country: "Austria", Name: "Kumi Systems"}, + + // Switzerland + {URL: "https://mirror.init7.net/fdroid/repo", Country: "Switzerland", Name: "Init7"}, + + // Romania + {URL: "https://mirrors.hostico.ro/fdroid/repo", Country: "Romania", Name: "Hostico"}, + {URL: "https://mirrors.chroot.ro/fdroid/repo", Country: "Romania", Name: "Chroot"}, + {URL: "https://ftp.lug.ro/fdroid/repo", Country: "Romania", Name: "LUG Romania"}, + + // US + {URL: "https://plug-mirror.rcac.purdue.edu/fdroid/repo", Country: "US", Name: "Purdue"}, + {URL: "https://mirror.fcix.net/fdroid/repo", Country: "US", Name: "FCIX"}, + {URL: "https://opencolo.mm.fcix.net/fdroid/repo", Country: "US", Name: "OpenColo"}, + {URL: "https://forksystems.mm.fcix.net/fdroid/repo", Country: "US", Name: "Fork Systems"}, + {URL: "https://southfront.mm.fcix.net/fdroid/repo", Country: "US", Name: "South Front"}, + {URL: "https://ziply.mm.fcix.net/fdroid/repo", Country: "US", Name: "Ziply"}, + + // Canada + {URL: "https://mirror.quantum5.ca/fdroid/repo", Country: "Canada", Name: "Quantum5"}, + + // Australia + {URL: "https://mirror.aarnet.edu.au/fdroid/repo", Country: "Australia", Name: "AARNet"}, + + // Other + {URL: "https://mirror.cyberbits.eu/fdroid/repo", Country: "Europe", Name: "Cyberbits EU"}, + {URL: "https://mirror.eu.ossplanet.net/fdroid/repo", Country: "Europe", Name: "OSSPlanet EU"}, + {URL: "https://mirror.cyberbits.asia/fdroid/repo", Country: "Asia", Name: "Cyberbits Asia"}, + {URL: "https://mirrors.jevincanders.net/fdroid/repo", Country: "US", Name: "Jevincanders"}, + {URL: "https://mirrors.komogoto.com/fdroid/repo", Country: "US", Name: "Komogoto"}, + {URL: "https://fdroid.rasp.sh/fdroid/repo", Country: "Europe", Name: "rasp.sh"}, + {URL: "https://mirror.gofoss.xyz/fdroid/repo", Country: "Europe", Name: "GoFOSS"}, +} + +func GetFDroidMirrors() FDroidMirrorIterator { + return newPtrIterator(builtinFDroidMirrors) +} From eb0f38544ce1c2921890a11f9e592d5b83d7e271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 11 Mar 2026 20:12:18 +0800 Subject: [PATCH 26/99] tailscale: Fix system interface rules --- clients/android | 2 +- clients/apple | 2 +- go.mod | 2 +- go.sum | 4 ++-- protocol/tailscale/endpoint.go | 21 +++------------------ 5 files changed, 8 insertions(+), 23 deletions(-) diff --git a/clients/android b/clients/android index 7777469b..0d31ac46 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit 7777469b5d21bc0312ed38bede457ee3128260e2 +Subproject commit 0d31ac467f4e62f325dffbc818207df3cb51a9bd diff --git a/clients/apple b/clients/apple index c19945f6..22dcf646 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit c19945f65be76ae5d16fc684a166079877802641 +Subproject commit 22dcf646ceb4fec6751fd4d11e7003b02a5ebc57 diff --git a/go.mod b/go.mod index 0ee67edc..baebf734 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/sagernet/sing-tun v0.8.2 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 - github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310162543-0c2de366d4de + github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index 0cb3d35e..c77b8786 100644 --- a/go.sum +++ b/go.sum @@ -254,8 +254,8 @@ github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkV github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310162543-0c2de366d4de h1:wsJ0COxUOIvBE+hUho0C/DbMeUe9jtwfh6dECAiTk94= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310162543-0c2de366d4de/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e h1:Sv1qUhJIidjSTc24XEknovDZnbmVSlAXj8wNVgIfgGo= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 730106c7..b8f2003d 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -333,9 +333,6 @@ func (t *Endpoint) Start(stage adapter.StartStage) error { t.systemTun = systemTun t.systemDialer = systemDialer t.server.TunDevice = wgTunDevice - t.server.RouterWrapper = func(inner router.Router) router.Router { - return &addressOnlyRouter{Router: inner} - } } if mark := t.network.AutoRedirectOutputMark(); mark > 0 { controlFunc := t.network.AutoRedirectOutputMarkFunc() @@ -480,11 +477,12 @@ func (t *Endpoint) Close() error { t.fallbackTCPCloser() t.fallbackTCPCloser = nil } + err := common.Close(common.PtrOrNil(t.server)) if t.systemTun != nil { - _ = t.systemTun.Close() + t.systemTun.Close() t.systemTun = nil } - return common.Close(common.PtrOrNil(t.server)) + return err } func (t *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { @@ -849,16 +847,3 @@ func (c *dnsConfigurtor) GetBaseConfig() (tsDNS.OSConfig, error) { func (c *dnsConfigurtor) Close() error { return nil } - -type addressOnlyRouter struct { - router.Router -} - -func (r *addressOnlyRouter) Set(config *router.Config) error { - if config != nil { - config = &router.Config{ - LocalAddrs: config.LocalAddrs, - } - } - return r.Router.Set(config) -} From eed6a36e5dbd3d4bad883d9d1477f80be797ac84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 11 Mar 2026 21:26:58 +0800 Subject: [PATCH 27/99] =?UTF-8?q?tun=EF=BC=9AFix=20auto=5Fredirect=20dropp?= =?UTF-8?q?ing=20SO=5FBINDTODEVICE=20traffic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index baebf734..8e9600bc 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.2 + github.com/sagernet/sing-tun v0.8.3-0.20260311131511-caaf8469e09e github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e diff --git a/go.sum b/go.sum index c77b8786..00c5d294 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.2 h1:rQr/x3eQCHh3oleIaoJdPdJwqzZp4+QWcJLT0Wz2xKY= -github.com/sagernet/sing-tun v0.8.2/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-tun v0.8.3-0.20260311131511-caaf8469e09e h1:WpNy2r8zmINe+lw1x2qB7ioSnN0eYE2GpxIixR6C18k= +github.com/sagernet/sing-tun v0.8.3-0.20260311131511-caaf8469e09e/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= From fe585157d27961725729a868f84ad8f6a892201e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 11 Mar 2026 16:25:23 +0800 Subject: [PATCH 28/99] Bump version --- docs/changelog.md | 24 ++++++++++++++++++++++++ docs/configuration/inbound/tun.md | 7 +++++++ docs/configuration/inbound/tun.zh.md | 7 +++++++ 3 files changed, 38 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 29c48605..23ed65df 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,30 @@ icon: material/alert-decagram --- +#### 1.13.3-beta.1 + +* Add OpenWrt and Alpine APK packages to release **1** +* Backport to macOS 10.13 High Sierra **2** +* OCM service: Add WebSocket support for Responses API **3** +* Fixes and improvements + +**1**: + +Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix: + +- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk` +- Alpine: `sing-box_{version}_linux_{architecture}.apk` + +**2**: + +Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support +macOS 10.13 High Sierra, built using Go 1.25 with patches +from [SagerNet/go](https://github.com/SagerNet/go). + +**3**: + +See [OCM](/configuration/service/ocm). + #### 1.13.2 * Fixes and improvements diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 7e67e488..ed368a13 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.13.3" + + :material-alert: [strict_route](#strict_route) + !!! quote "Changes in sing-box 1.13.0" :material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark) @@ -348,6 +352,9 @@ Enforce strict routing rules when `auto_route` is enabled: * Let unsupported network unreachable * For legacy reasons, when neither `strict_route` nor `auto_redirect` are enabled, all ICMP traffic will not go through TUN. +* When `auto_redirect` is enabled, `strict_route` also affects `SO_BINDTODEVICE` traffic: + * Enabled: `SO_BINDTODEVICE` traffic is redirected through sing-box. + * Disabled: `SO_BINDTODEVICE` traffic bypasses sing-box. *In Windows*: diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index d8520aed..eaf5ff49 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -2,6 +2,10 @@ icon: material/new-box --- +!!! quote "sing-box 1.13.3 中的更改" + + :material-alert: [strict_route](#strict_route) + !!! quote "sing-box 1.13.0 中的更改" :material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark) @@ -347,6 +351,9 @@ tun 接口的 IPv6 前缀。 * 使不支持的网络不可达。 * 出于历史遗留原因,当未启用 `strict_route` 或 `auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。 +* 当启用 `auto_redirect` 时,`strict_route` 也影响 `SO_BINDTODEVICE` 流量: + * 启用:`SO_BINDTODEVICE` 流量被重定向通过 sing-box。 + * 禁用:`SO_BINDTODEVICE` 流量绕过 sing-box。 *在 Windows 中*: From b990de2e122bcf4b8a5ee1f1d81ea0a45c00bd10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 14 Mar 2026 20:59:31 +0800 Subject: [PATCH 29/99] tun: Fix "Fix auto_redirect dropping SO_BINDTODEVICE traffic" --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8e9600bc..ec75f35c 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.3-0.20260311131511-caaf8469e09e + github.com/sagernet/sing-tun v0.8.3-0.20260314125843-0329538ecd5e github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e diff --git a/go.sum b/go.sum index 00c5d294..0943a455 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.3-0.20260311131511-caaf8469e09e h1:WpNy2r8zmINe+lw1x2qB7ioSnN0eYE2GpxIixR6C18k= -github.com/sagernet/sing-tun v0.8.3-0.20260311131511-caaf8469e09e/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-tun v0.8.3-0.20260314125843-0329538ecd5e h1:jpH63IgobmmR4PNp1YKt9ZUt83poa22EO7KKDAc1q5A= +github.com/sagernet/sing-tun v0.8.3-0.20260314125843-0329538ecd5e/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= From 041646b7285e3bec837e8244dde4c1a62066d0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 14 Mar 2026 21:15:13 +0800 Subject: [PATCH 30/99] Fix kTLS crash --- common/ktls/ktls_read.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/ktls/ktls_read.go b/common/ktls/ktls_read.go index 7ffa1e18..5609bfb5 100644 --- a/common/ktls/ktls_read.go +++ b/common/ktls/ktls_read.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "net" + "unsafe" ) func (c *Conn) Read(b []byte) (int, error) { @@ -229,7 +230,7 @@ func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) { record := c.rawConn.RawInput.Next(recordHeaderLen + n) data, typ, err = c.rawConn.In.Decrypt(record) if err != nil { - err = c.rawConn.In.SetErrorLocked(c.sendAlert(uint8(err.(tls.AlertError)))) + err = c.rawConn.In.SetErrorLocked(c.sendAlert(*(*uint8)((*[2]unsafe.Pointer)(unsafe.Pointer(&err))[1]))) return } return From f2d15139f57eb87a7cd4747189f6305846d0824c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 15 Mar 2026 16:58:34 +0800 Subject: [PATCH 31/99] tun: Fix nftables single include_uid not working --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ec75f35c..a394028d 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.3-0.20260314125843-0329538ecd5e + github.com/sagernet/sing-tun v0.8.3 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e diff --git a/go.sum b/go.sum index 0943a455..76a680f8 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.3-0.20260314125843-0329538ecd5e h1:jpH63IgobmmR4PNp1YKt9ZUt83poa22EO7KKDAc1q5A= -github.com/sagernet/sing-tun v0.8.3-0.20260314125843-0329538ecd5e/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-tun v0.8.3 h1:mozxmuIoRhFdVHnheenLpBaammVj7bZPcnkApaYKDPY= +github.com/sagernet/sing-tun v0.8.3/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= From f46fbf188ab85450f4bc446ec9f08a79f395dad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B7=B1=E9=B8=A3?= <66902050+DeepChirp@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:54:32 +0800 Subject: [PATCH 32/99] documentation: Minor fixes --- docs/configuration/inbound/hysteria2.zh.md | 2 +- docs/configuration/outbound/hysteria2.zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index 5ad5d75d..809fbfea 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -38,7 +38,7 @@ icon: material/alert-decagram !!! warning "与官方 Hysteria2 的区别" 官方程序支持一种名为 **userpass** 的验证方式, - 本质上上是将用户名与密码的组合 `:` 作为实际上的密码,而 sing-box 不提供此别名。 + 本质上是将用户名与密码的组合 `:` 作为实际上的密码,而 sing-box 不提供此别名。 要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。 ### 监听字段 diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index d2a8598f..a9256cab 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -38,7 +38,7 @@ !!! warning "与官方 Hysteria2 的区别" 官方程序支持一种名为 **userpass** 的验证方式, - 本质上上是将用户名与密码的组合 `:` 作为实际上的密码,而 sing-box 不提供此别名。 + 本质上是将用户名与密码的组合 `:` 作为实际上的密码,而 sing-box 不提供此别名。 要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。 ### 字段 From 0889ddd001188e9112555a2f1701c0b35d5ad06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 15 Mar 2026 17:48:10 +0800 Subject: [PATCH 33/99] Fix connector canceled dial cleanup --- dns/transport/connector.go | 94 +++++++++++++------- dns/transport/connector_test.go | 146 +++++++++++++++++++++++++++++++- 2 files changed, 209 insertions(+), 31 deletions(-) diff --git a/dns/transport/connector.go b/dns/transport/connector.go index 769232f4..3a87456d 100644 --- a/dns/transport/connector.go +++ b/dns/transport/connector.go @@ -55,6 +55,12 @@ type contextKeyConnecting struct{} var errRecursiveConnectorDial = E.New("recursive connector dial") +type connectorDialResult[T any] struct { + connection T + cancel context.CancelFunc + err error +} + func (c *Connector[T]) Get(ctx context.Context) (T, error) { var zero T for { @@ -100,41 +106,37 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) { return zero, err } - c.connecting = make(chan struct{}) + connecting := make(chan struct{}) + c.connecting = connecting + dialContext := context.WithValue(ctx, contextKeyConnecting{}, c) + dialResult := make(chan connectorDialResult[T], 1) c.access.Unlock() - dialContext := context.WithValue(ctx, contextKeyConnecting{}, c) - connection, cancel, err := c.dialWithCancellation(dialContext) + go func() { + connection, cancel, err := c.dialWithCancellation(dialContext) + dialResult <- connectorDialResult[T]{ + connection: connection, + cancel: cancel, + err: err, + } + }() - 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() + select { + case result := <-dialResult: + return c.completeDial(ctx, connecting, result) + case <-ctx.Done(): + go func() { + result := <-dialResult + _, _ = c.completeDial(ctx, connecting, result) + }() + return zero, ctx.Err() + case <-c.closeCtx.Done(): + go func() { + result := <-dialResult + _, _ = c.completeDial(ctx, connecting, result) + }() 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 } } @@ -143,6 +145,38 @@ func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T return loaded && dialConnector == connector } +func (c *Connector[T]) completeDial(ctx context.Context, connecting chan struct{}, result connectorDialResult[T]) (T, error) { + var zero T + + c.access.Lock() + defer c.access.Unlock() + defer func() { + if c.connecting == connecting { + c.connecting = nil + } + close(connecting) + }() + + if result.err != nil { + return zero, result.err + } + if c.closed || c.closeCtx.Err() != nil { + result.cancel() + c.callbacks.Close(result.connection) + return zero, ErrTransportClosed + } + if err := ctx.Err(); err != nil { + result.cancel() + c.callbacks.Close(result.connection) + return zero, err + } + + c.connection = result.connection + c.hasConnection = true + c.connectionCancel = result.cancel + return c.connection, nil +} + func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) { var zero T if err := ctx.Err(); err != nil { diff --git a/dns/transport/connector_test.go b/dns/transport/connector_test.go index 280e5da6..309b28c8 100644 --- a/dns/transport/connector_test.go +++ b/dns/transport/connector_test.go @@ -188,13 +188,157 @@ func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) { err := <-result require.ErrorIs(t, err, context.Canceled) require.EqualValues(t, 1, dialCount.Load()) - require.EqualValues(t, 1, closeCount.Load()) + require.Eventually(t, func() bool { + return closeCount.Load() == 1 + }, time.Second, 10*time.Millisecond) _, err = connector.Get(context.Background()) require.NoError(t, err) require.EqualValues(t, 2, dialCount.Load()) } +func TestConnectorCanceledRequestReturnsBeforeIgnoredDialCompletes(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() + + select { + case err := <-result: + require.ErrorIs(t, err, context.Canceled) + case <-time.After(time.Second): + t.Fatal("Get did not return after request cancel") + } + + require.EqualValues(t, 1, dialCount.Load()) + require.EqualValues(t, 0, closeCount.Load()) + + close(releaseDial) + + require.Eventually(t, func() bool { + return closeCount.Load() == 1 + }, time.Second, 10*time.Millisecond) + + _, err := connector.Get(context.Background()) + require.NoError(t, err) + require.EqualValues(t, 2, dialCount.Load()) +} + +func TestConnectorWaiterDoesNotStartNewDialBeforeCanceledDialCompletes(t *testing.T) { + t.Parallel() + + var ( + dialCount atomic.Int32 + closeCount atomic.Int32 + ) + firstDialStarted := make(chan struct{}, 1) + secondDialStarted := make(chan struct{}, 1) + releaseFirstDial := make(chan struct{}) + + connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { + attempt := dialCount.Add(1) + switch attempt { + case 1: + select { + case firstDialStarted <- struct{}{}: + default: + } + <-releaseFirstDial + case 2: + select { + case secondDialStarted <- struct{}{}: + default: + } + } + 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()) + firstResult := make(chan error, 1) + go func() { + _, err := connector.Get(requestContext) + firstResult <- err + }() + + <-firstDialStarted + cancel() + + secondResult := make(chan error, 1) + go func() { + _, err := connector.Get(context.Background()) + secondResult <- err + }() + + select { + case <-secondDialStarted: + t.Fatal("second dial started before first dial completed") + case <-time.After(100 * time.Millisecond): + } + + select { + case err := <-firstResult: + require.ErrorIs(t, err, context.Canceled) + case <-time.After(time.Second): + t.Fatal("first Get did not return after request cancel") + } + + close(releaseFirstDial) + + require.Eventually(t, func() bool { + return closeCount.Load() == 1 + }, time.Second, 10*time.Millisecond) + + select { + case <-secondDialStarted: + case <-time.After(time.Second): + t.Fatal("second dial did not start after first dial completed") + } + + err := <-secondResult + require.NoError(t, err) + require.EqualValues(t, 2, dialCount.Load()) +} + func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) { t.Parallel() From d3768cca3602c2949417fe767c1acd88d7f67698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 15 Mar 2026 16:59:23 +0800 Subject: [PATCH 34/99] Bump version --- clients/android | 2 +- clients/apple | 2 +- docs/changelog.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/android b/clients/android index 0d31ac46..6f09892c 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit 0d31ac467f4e62f325dffbc818207df3cb51a9bd +Subproject commit 6f09892c7193c696b9fc182123db8051562cdf74 diff --git a/clients/apple b/clients/apple index 22dcf646..f3b4b223 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit 22dcf646ceb4fec6751fd4d11e7003b02a5ebc57 +Subproject commit f3b4b2238efd238fb1ec6ef2da88017b60a6cfa1 diff --git a/docs/changelog.md b/docs/changelog.md index 23ed65df..9aaba894 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,7 @@ icon: material/alert-decagram --- -#### 1.13.3-beta.1 +#### 1.13.3 * Add OpenWrt and Alpine APK packages to release **1** * Backport to macOS 10.13 High Sierra **2** From d2fa21d07b52fae2c0d7bbd9ac98909c6f210a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 16 Mar 2026 09:36:24 +0800 Subject: [PATCH 35/99] Deprecate Socksaddr.IsFqdn: do not reject potentially valid domain names --- common/dialer/default.go | 2 +- common/dialer/resolve.go | 8 ++++---- go.mod | 2 +- go.sum | 4 ++-- option/dns.go | 2 +- option/outbound.go | 2 +- option/tailscale.go | 2 +- protocol/socks/outbound.go | 4 ++-- protocol/tailscale/dns_transport.go | 4 ++-- protocol/tailscale/endpoint.go | 6 +++--- protocol/vless/outbound.go | 4 ++-- protocol/vmess/outbound.go | 2 +- protocol/wireguard/endpoint.go | 4 ++-- route/route.go | 4 ++-- transport/trojan/protocol.go | 2 +- transport/wireguard/endpoint.go | 6 +++--- 16 files changed, 29 insertions(+), 29 deletions(-) diff --git a/common/dialer/default.go b/common/dialer/default.go index 6b2379f4..39b96dfe 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -239,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) { if !address.IsValid() { return nil, E.New("invalid address") - } else if address.IsFqdn() { + } else if address.IsDomain() { return nil, E.New("domain not resolved") } if d.networkStrategy == nil { diff --git a/common/dialer/resolve.go b/common/dialer/resolve.go index 49ed0703..21fe38d5 100644 --- a/common/dialer/resolve.go +++ b/common/dialer/resolve.go @@ -96,7 +96,7 @@ func (d *resolveDialer) DialContext(ctx context.Context, network string, destina if err != nil { return nil, err } - if !destination.IsFqdn() { + if !destination.IsDomain() { return d.dialer.DialContext(ctx, network, destination) } ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) @@ -116,7 +116,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd if err != nil { return nil, err } - if !destination.IsFqdn() { + if !destination.IsDomain() { return d.dialer.ListenPacket(ctx, destination) } ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) @@ -144,7 +144,7 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context if err != nil { return nil, err } - if !destination.IsFqdn() { + if !destination.IsDomain() { return d.dialer.DialContext(ctx, network, destination) } ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) @@ -167,7 +167,7 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C if err != nil { return nil, err } - if !destination.IsFqdn() { + if !destination.IsDomain() { return d.dialer.ListenPacket(ctx, destination) } ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) diff --git a/go.mod b/go.mod index a394028d..8b1c752b 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.2 + github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.0 github.com/sagernet/sing-shadowsocks v0.2.8 diff --git a/go.sum b/go.sum index 76a680f8..222aace8 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.2 h1:kX1IH9SWJv4S0T9M8O+HNahWgbOuY1VauxbF7NU5lOg= -github.com/sagernet/sing v0.8.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde h1:RNQzlpnsXIuu1HGts/fIzJ1PR7RhrzaNlU52MDyiX1c= +github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.0 h1:dhrFnP45wgVKEOT1EvtsToxdzRnHIDIAgj6WHV9pLyM= diff --git a/option/dns.go b/option/dns.go index 4c1ac208..b5ccf208 100644 --- a/option/dns.go +++ b/option/dns.go @@ -339,7 +339,7 @@ func (o DNSServerAddressOptions) Build() M.Socksaddr { } func (o DNSServerAddressOptions) ServerIsDomain() bool { - return M.IsDomainName(o.Server) + return o.Build().IsDomain() } func (o *DNSServerAddressOptions) TakeServerOptions() ServerOptions { diff --git a/option/outbound.go b/option/outbound.go index cb388c44..6676a3e9 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -155,7 +155,7 @@ func (o ServerOptions) Build() M.Socksaddr { } func (o ServerOptions) ServerIsDomain() bool { - return M.IsDomainName(o.Server) + return o.Build().IsDomain() } func (o *ServerOptions) TakeServerOptions() ServerOptions { diff --git a/option/tailscale.go b/option/tailscale.go index dac8e866..68a14369 100644 --- a/option/tailscale.go +++ b/option/tailscale.go @@ -61,7 +61,7 @@ func (d DERPVerifyClientURLOptions) ServerIsDomain() bool { if err != nil { return false } - return M.IsDomainName(verifyURL.Host) + return M.ParseSocksaddr(verifyURL.Hostname()).IsDomain() } func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) { diff --git a/protocol/socks/outbound.go b/protocol/socks/outbound.go index 851412ff..344c7988 100644 --- a/protocol/socks/outbound.go +++ b/protocol/socks/outbound.go @@ -83,7 +83,7 @@ func (h *Outbound) DialContext(ctx context.Context, network string, destination default: return nil, E.Extend(N.ErrUnknownNetwork, network) } - if h.resolve && destination.IsFqdn() { + if h.resolve && destination.IsDomain() { destinationAddresses, err := h.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { return nil, err @@ -101,7 +101,7 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) return h.uotClient.ListenPacket(ctx, destination) } - if h.resolve && destination.IsFqdn() { + if h.resolve && destination.IsDomain() { destinationAddresses, err := h.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { return nil, err diff --git a/protocol/tailscale/dns_transport.go b/protocol/tailscale/dns_transport.go index 1c227db7..3a92a66b 100644 --- a/protocol/tailscale/dns_transport.go +++ b/protocol/tailscale/dns_transport.go @@ -287,7 +287,7 @@ type DNSDialer struct { } func (d *DNSDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - if destination.IsFqdn() { + if destination.IsDomain() { panic("invalid request here") } for _, prefix := range d.transport.routePrefixes { @@ -299,7 +299,7 @@ func (d *DNSDialer) DialContext(ctx context.Context, network string, destination } func (d *DNSDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - if destination.IsFqdn() { + if destination.IsDomain() { panic("invalid request here") } for _, prefix := range d.transport.routePrefixes { diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index b8f2003d..d1c22aed 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -190,7 +190,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL if err != nil { return nil, E.Cause(err, "parse control URL") } - remoteIsDomain = M.IsDomainName(controlURL.Hostname()) + remoteIsDomain = M.ParseSocksaddr(controlURL.Hostname()).IsDomain() } else { // controlplane.tailscale.com remoteIsDomain = true @@ -492,7 +492,7 @@ func (t *Endpoint) DialContext(ctx context.Context, network string, destination case N.NetworkUDP: t.logger.InfoContext(ctx, "outbound packet connection to ", destination) } - if destination.IsFqdn() { + if destination.IsDomain() { destinationAddresses, err := t.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { return nil, err @@ -578,7 +578,7 @@ func (t *Endpoint) listenPacketWithAddress(ctx context.Context, destination M.So func (t *Endpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { t.logger.InfoContext(ctx, "outbound packet connection to ", destination) - if destination.IsFqdn() { + if destination.IsDomain() { destinationAddresses, err := t.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { return nil, netip.Addr{}, err diff --git a/protocol/vless/outbound.go b/protocol/vless/outbound.go index ab774760..d8132cf9 100644 --- a/protocol/vless/outbound.go +++ b/protocol/vless/outbound.go @@ -167,7 +167,7 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati if h.xudp { return h.client.DialEarlyXUDPPacketConn(conn, destination) } else if h.packetAddr { - if destination.IsFqdn() { + if destination.IsDomain() { return nil, E.New("packetaddr: domain destination is not supported") } packetConn, err := h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}) @@ -204,7 +204,7 @@ func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) if h.xudp { return h.client.DialEarlyXUDPPacketConn(conn, destination) } else if h.packetAddr { - if destination.IsFqdn() { + if destination.IsDomain() { return nil, E.New("packetaddr: domain destination is not supported") } conn, err := h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}) diff --git a/protocol/vmess/outbound.go b/protocol/vmess/outbound.go index f0b41ae0..703f06b1 100644 --- a/protocol/vmess/outbound.go +++ b/protocol/vmess/outbound.go @@ -194,7 +194,7 @@ func (h *vmessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) return nil, err } if h.packetAddr { - if destination.IsFqdn() { + if destination.IsDomain() { return nil, E.New("packetaddr: domain destination is not supported") } return packetaddr.NewConn(h.client.DialEarlyPacketConn(conn, M.Socksaddr{Fqdn: packetaddr.SeqPacketMagicAddress}), destination), nil diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index bcf2078e..9fdc4814 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -210,7 +210,7 @@ func (w *Endpoint) DialContext(ctx context.Context, network string, destination case N.NetworkUDP: w.logger.InfoContext(ctx, "outbound packet connection to ", destination) } - if destination.IsFqdn() { + if destination.IsDomain() { destinationAddresses, err := w.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { return nil, err @@ -224,7 +224,7 @@ func (w *Endpoint) DialContext(ctx context.Context, network string, destination func (w *Endpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) { w.logger.InfoContext(ctx, "outbound packet connection to ", destination) - if destination.IsFqdn() { + if destination.IsDomain() { destinationAddresses, err := w.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{}) if err != nil { return nil, netip.Addr{}, err diff --git a/route/route.go b/route/route.go index cdd7ba25..40a90e7d 100644 --- a/route/route.go +++ b/route/route.go @@ -349,7 +349,7 @@ func (r *Router) PreMatch(metadata adapter.InboundContext, routeContext tun.Dire } directRouteOutbound = defaultOutbound.(adapter.DirectRouteOutbound) } - if metadata.Destination.IsFqdn() { + if metadata.Destination.IsDomain() { if len(metadata.DestinationAddresses) == 0 { var strategy C.DomainStrategy if metadata.Source.IsIPv4() { @@ -790,7 +790,7 @@ func (r *Router) actionSniff( } func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *R.RuleActionResolve) error { - if metadata.Destination.IsFqdn() { + if metadata.Destination.IsDomain() { var transport adapter.DNSTransport if action.Server != "" { var loaded bool diff --git a/transport/trojan/protocol.go b/transport/trojan/protocol.go index 6369d86d..0456b6b9 100644 --- a/transport/trojan/protocol.go +++ b/transport/trojan/protocol.go @@ -136,7 +136,7 @@ func (c *ClientPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) return } n = buffer.Len() - if destination.IsFqdn() { + if destination.IsDomain() { addr = destination } else { addr = destination.UDPAddr() diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go index dac07c85..f9f4628a 100644 --- a/transport/wireguard/endpoint.go +++ b/transport/wireguard/endpoint.go @@ -63,7 +63,7 @@ func NewEndpoint(options EndpointOptions) (*Endpoint, error) { } if rawPeer.Endpoint.Addr.IsValid() { peer.endpoint = rawPeer.Endpoint.AddrPort() - } else if rawPeer.Endpoint.IsFqdn() { + } else if rawPeer.Endpoint.IsDomain() { peer.destination = rawPeer.Endpoint } publicKeyBytes, err := base64.StdEncoding.DecodeString(rawPeer.PublicKey) @@ -135,13 +135,13 @@ func NewEndpoint(options EndpointOptions) (*Endpoint, error) { func (e *Endpoint) Start(resolve bool) error { if common.Any(e.peers, func(peer peerConfig) bool { - return !peer.endpoint.IsValid() && peer.destination.IsFqdn() + return !peer.endpoint.IsValid() && peer.destination.IsDomain() }) { if !resolve { return nil } for peerIndex, peer := range e.peers { - if peer.endpoint.IsValid() || !peer.destination.IsFqdn() { + if peer.endpoint.IsValid() || !peer.destination.IsDomain() { continue } destinationAddress, err := e.options.ResolvePeer(peer.destination.Fqdn) From 9fbfb877236ba4c60f56e8100fb5d043dd03083e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 16 Mar 2026 12:10:32 +0800 Subject: [PATCH 36/99] documentation: Fix unicode heading anchors --- mkdocs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 081ba3aa..e2959266 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -182,6 +182,10 @@ nav: - CCM: configuration/service/ccm.md - OCM: configuration/service/ocm.md markdown_extensions: + - toc: + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower - pymdownx.inlinehilite - pymdownx.snippets - pymdownx.superfences From 686cf1f304b9984c890762a7953629fec7bfca64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 16 Mar 2026 12:24:10 +0800 Subject: [PATCH 37/99] documentation: Fix Chinese link anchors --- docs/configuration/dns/fakeip.zh.md | 2 +- docs/configuration/dns/rule.zh.md | 8 +++---- docs/configuration/dns/server/http3.zh.md | 2 +- docs/configuration/dns/server/https.zh.md | 2 +- docs/configuration/dns/server/legacy.zh.md | 2 +- docs/configuration/dns/server/quic.zh.md | 2 +- docs/configuration/dns/server/tls.zh.md | 2 +- .../experimental/cache-file.zh.md | 2 +- .../experimental/v2ray-api.zh.md | 2 +- docs/configuration/inbound/anytls.zh.md | 2 +- docs/configuration/inbound/http.zh.md | 2 +- docs/configuration/inbound/hysteria.zh.md | 2 +- docs/configuration/inbound/hysteria2.zh.md | 2 +- docs/configuration/inbound/naive.zh.md | 2 +- docs/configuration/inbound/shadowsocks.zh.md | 2 +- docs/configuration/inbound/trojan.zh.md | 4 ++-- docs/configuration/inbound/tuic.zh.md | 2 +- docs/configuration/inbound/tun.md | 5 ++++ docs/configuration/inbound/vless.zh.md | 4 ++-- docs/configuration/inbound/vmess.zh.md | 4 ++-- docs/configuration/outbound/anytls.zh.md | 2 +- docs/configuration/outbound/direct.zh.md | 8 +++---- docs/configuration/outbound/dns.zh.md | 2 +- docs/configuration/outbound/http.zh.md | 2 +- docs/configuration/outbound/hysteria.zh.md | 2 +- docs/configuration/outbound/hysteria2.zh.md | 2 +- docs/configuration/outbound/naive.zh.md | 2 +- docs/configuration/outbound/selector.zh.md | 2 +- docs/configuration/outbound/shadowsocks.zh.md | 2 +- docs/configuration/outbound/shadowtls.zh.md | 2 +- docs/configuration/outbound/tor.zh.md | 2 +- docs/configuration/outbound/trojan.zh.md | 4 ++-- docs/configuration/outbound/tuic.zh.md | 2 +- docs/configuration/outbound/vless.zh.md | 4 ++-- docs/configuration/outbound/vmess.zh.md | 4 ++-- docs/configuration/outbound/wireguard.zh.md | 2 +- docs/configuration/route/geoip.zh.md | 2 +- docs/configuration/route/geosite.zh.md | 2 +- docs/configuration/route/index.zh.md | 12 +++++----- docs/configuration/route/rule.zh.md | 7 +++--- docs/configuration/route/rule_action.zh.md | 10 ++++---- .../rule-set/headless-rule.zh.md | 4 ++-- docs/configuration/service/ccm.zh.md | 2 +- docs/configuration/service/derp.zh.md | 4 ++-- docs/configuration/service/ocm.zh.md | 2 +- docs/configuration/service/ssm-api.zh.md | 2 +- docs/configuration/shared/dial.zh.md | 4 ++-- docs/configuration/shared/listen.zh.md | 10 ++++---- docs/configuration/shared/pre-match.zh.md | 6 ++--- docs/configuration/shared/tls.zh.md | 2 +- .../shared/v2ray-transport.zh.md | 2 +- docs/configuration/shared/wifi-state.zh.md | 2 +- docs/deprecated.zh.md | 18 +++++++------- docs/installation/build-from-source.zh.md | 24 +++++++++---------- docs/migration.zh.md | 8 +++---- 55 files changed, 114 insertions(+), 108 deletions(-) diff --git a/docs/configuration/dns/fakeip.zh.md b/docs/configuration/dns/fakeip.zh.md index e4d77b35..c8d5dfe3 100644 --- a/docs/configuration/dns/fakeip.zh.md +++ b/docs/configuration/dns/fakeip.zh.md @@ -4,7 +4,7 @@ icon: material/delete-clock !!! failure "已在 sing-box 1.12.0 废弃" - 旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。 + 旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 ### 结构 diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 588e0736..c46bc475 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -256,7 +256,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! 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。 @@ -264,7 +264,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 !!! 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。 @@ -453,7 +453,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. !!! failure "已在 sing-box 1.12.0 废弃" - `outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver)。 + `outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项)。 匹配出站。 @@ -505,7 +505,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. !!! failure "已在 sing-box 1.12.0 中被移除" - 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。 diff --git a/docs/configuration/dns/server/http3.zh.md b/docs/configuration/dns/server/http3.zh.md index 70e13b10..1032fedb 100644 --- a/docs/configuration/dns/server/http3.zh.md +++ b/docs/configuration/dns/server/http3.zh.md @@ -64,7 +64,7 @@ DNS 服务器的路径。 #### tls -TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 diff --git a/docs/configuration/dns/server/https.zh.md b/docs/configuration/dns/server/https.zh.md index 691d5eb5..7aa73c3f 100644 --- a/docs/configuration/dns/server/https.zh.md +++ b/docs/configuration/dns/server/https.zh.md @@ -64,7 +64,7 @@ DNS 服务器的路径。 #### tls -TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 diff --git a/docs/configuration/dns/server/legacy.zh.md b/docs/configuration/dns/server/legacy.zh.md index 4365749e..906db47c 100644 --- a/docs/configuration/dns/server/legacy.zh.md +++ b/docs/configuration/dns/server/legacy.zh.md @@ -4,7 +4,7 @@ icon: material/delete-clock !!! failure "Deprecated in sing-box 1.12.0" - 旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。 + 旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。 !!! quote "sing-box 1.9.0 中的更改" diff --git a/docs/configuration/dns/server/quic.zh.md b/docs/configuration/dns/server/quic.zh.md index 03b3002c..c18c18ed 100644 --- a/docs/configuration/dns/server/quic.zh.md +++ b/docs/configuration/dns/server/quic.zh.md @@ -51,7 +51,7 @@ DNS 服务器的端口。 #### tls -TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 diff --git a/docs/configuration/dns/server/tls.zh.md b/docs/configuration/dns/server/tls.zh.md index 7402e521..afd1111a 100644 --- a/docs/configuration/dns/server/tls.zh.md +++ b/docs/configuration/dns/server/tls.zh.md @@ -51,7 +51,7 @@ DNS 服务器的端口。 #### tls -TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md index db2ae205..309e13a1 100644 --- a/docs/configuration/experimental/cache-file.zh.md +++ b/docs/configuration/experimental/cache-file.zh.md @@ -42,7 +42,7 @@ 将拒绝的 DNS 响应缓存存储在缓存文件中。 -[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。 +[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。 #### rdrc_timeout diff --git a/docs/configuration/experimental/v2ray-api.zh.md b/docs/configuration/experimental/v2ray-api.zh.md index 81fc8427..87d5c95d 100644 --- a/docs/configuration/experimental/v2ray-api.zh.md +++ b/docs/configuration/experimental/v2ray-api.zh.md @@ -1,6 +1,6 @@ !!! quote "" - 默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#_5)。 + 默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#构建标记)。 ### 结构 diff --git a/docs/configuration/inbound/anytls.zh.md b/docs/configuration/inbound/anytls.zh.md index 55b6749e..8c3d1daf 100644 --- a/docs/configuration/inbound/anytls.zh.md +++ b/docs/configuration/inbound/anytls.zh.md @@ -58,4 +58,4 @@ AnyTLS 填充方案行数组。 #### tls -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 diff --git a/docs/configuration/inbound/http.zh.md b/docs/configuration/inbound/http.zh.md index 2f3d44f5..e1dd876b 100644 --- a/docs/configuration/inbound/http.zh.md +++ b/docs/configuration/inbound/http.zh.md @@ -26,7 +26,7 @@ #### tls -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 #### users diff --git a/docs/configuration/inbound/hysteria.zh.md b/docs/configuration/inbound/hysteria.zh.md index b7566052..561d7102 100644 --- a/docs/configuration/inbound/hysteria.zh.md +++ b/docs/configuration/inbound/hysteria.zh.md @@ -104,4 +104,4 @@ base64 编码的认证密码。 ==必填== -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 \ No newline at end of file +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 \ No newline at end of file diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index 809fbfea..35a3c25b 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -85,7 +85,7 @@ Hysteria 用户 ==必填== -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 #### masquerade diff --git a/docs/configuration/inbound/naive.zh.md b/docs/configuration/inbound/naive.zh.md index c9bfc917..0984d310 100644 --- a/docs/configuration/inbound/naive.zh.md +++ b/docs/configuration/inbound/naive.zh.md @@ -60,4 +60,4 @@ QUIC 拥塞控制算法。 #### tls -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 \ No newline at end of file +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 \ No newline at end of file diff --git a/docs/configuration/inbound/shadowsocks.zh.md b/docs/configuration/inbound/shadowsocks.zh.md index c97e9bef..a991b0c3 100644 --- a/docs/configuration/inbound/shadowsocks.zh.md +++ b/docs/configuration/inbound/shadowsocks.zh.md @@ -93,4 +93,4 @@ #### multiplex -参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。 +参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 diff --git a/docs/configuration/inbound/trojan.zh.md b/docs/configuration/inbound/trojan.zh.md index fa86d613..d81b4c1d 100644 --- a/docs/configuration/inbound/trojan.zh.md +++ b/docs/configuration/inbound/trojan.zh.md @@ -43,7 +43,7 @@ Trojan 用户。 #### tls -TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 #### fallback @@ -61,7 +61,7 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 #### multiplex -参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。 +参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 #### transport diff --git a/docs/configuration/inbound/tuic.zh.md b/docs/configuration/inbound/tuic.zh.md index 99252056..ae531635 100644 --- a/docs/configuration/inbound/tuic.zh.md +++ b/docs/configuration/inbound/tuic.zh.md @@ -75,4 +75,4 @@ QUIC 拥塞控制算法 ==必填== -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 \ No newline at end of file +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 \ No newline at end of file diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index ed368a13..74d02dc9 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -2,6 +2,11 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [include_mac_address](#include_mac_address) + :material-plus: [exclude_mac_address](#exclude_mac_address) + !!! quote "Changes in sing-box 1.13.3" :material-alert: [strict_route](#strict_route) diff --git a/docs/configuration/inbound/vless.zh.md b/docs/configuration/inbound/vless.zh.md index 30b151da..2ce4785b 100644 --- a/docs/configuration/inbound/vless.zh.md +++ b/docs/configuration/inbound/vless.zh.md @@ -48,11 +48,11 @@ VLESS 子协议。 #### tls -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 #### multiplex -参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。 +参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 #### transport diff --git a/docs/configuration/inbound/vmess.zh.md b/docs/configuration/inbound/vmess.zh.md index 9aef44df..f741ed1b 100644 --- a/docs/configuration/inbound/vmess.zh.md +++ b/docs/configuration/inbound/vmess.zh.md @@ -43,11 +43,11 @@ VMess 用户。 #### tls -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。 #### multiplex -参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。 +参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。 #### transport diff --git a/docs/configuration/outbound/anytls.zh.md b/docs/configuration/outbound/anytls.zh.md index 1c888cfd..c1f8999e 100644 --- a/docs/configuration/outbound/anytls.zh.md +++ b/docs/configuration/outbound/anytls.zh.md @@ -59,7 +59,7 @@ AnyTLS 密码。 ==必填== -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 diff --git a/docs/configuration/outbound/direct.zh.md b/docs/configuration/outbound/direct.zh.md index 55d3bf8c..824a3529 100644 --- a/docs/configuration/outbound/direct.zh.md +++ b/docs/configuration/outbound/direct.zh.md @@ -4,8 +4,8 @@ icon: material/alert-decagram !!! quote "sing-box 1.11.0 中的更改" - :material-alert-decagram: [override_address](#override_address) - :material-alert-decagram: [override_port](#override_port) + :material-delete-clock: [override_address](#override_address) + :material-delete-clock: [override_port](#override_port) `direct` 出站直接发送请求。 @@ -29,7 +29,7 @@ icon: material/alert-decagram !!! failure "已在 sing-box 1.11.0 废弃" - 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。 + 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 覆盖连接目标地址。 @@ -37,7 +37,7 @@ icon: material/alert-decagram !!! failure "已在 sing-box 1.11.0 废弃" - 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。 + 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 覆盖连接目标端口。 diff --git a/docs/configuration/outbound/dns.zh.md b/docs/configuration/outbound/dns.zh.md index 3db2fefb..592075b3 100644 --- a/docs/configuration/outbound/dns.zh.md +++ b/docs/configuration/outbound/dns.zh.md @@ -4,7 +4,7 @@ icon: material/delete-clock !!! failure "已在 sing-box 1.11.0 废弃" - 旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions). + 旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作). `dns` 出站是一个内部 DNS 服务器。 diff --git a/docs/configuration/outbound/http.zh.md b/docs/configuration/outbound/http.zh.md index 53dd1b6a..55387a63 100644 --- a/docs/configuration/outbound/http.zh.md +++ b/docs/configuration/outbound/http.zh.md @@ -51,7 +51,7 @@ HTTP 请求的额外标头。 #### tls -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 diff --git a/docs/configuration/outbound/hysteria.zh.md b/docs/configuration/outbound/hysteria.zh.md index 4f4c00bb..ae1d3590 100644 --- a/docs/configuration/outbound/hysteria.zh.md +++ b/docs/configuration/outbound/hysteria.zh.md @@ -134,7 +134,7 @@ base64 编码的认证密码。 ==必填== -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index a9256cab..bc77f4ec 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -105,7 +105,7 @@ QUIC 流量混淆器密码. ==必填== -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 #### brutal_debug diff --git a/docs/configuration/outbound/naive.zh.md b/docs/configuration/outbound/naive.zh.md index 9bae64c0..dbfd7fbf 100644 --- a/docs/configuration/outbound/naive.zh.md +++ b/docs/configuration/outbound/naive.zh.md @@ -105,7 +105,7 @@ QUIC 拥塞控制算法。 ==必填== -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 只有 `server_name`、`certificate`、`certificate_path` 和 `ech` 是被支持的。 diff --git a/docs/configuration/outbound/selector.zh.md b/docs/configuration/outbound/selector.zh.md index ffe2d70a..520fb15c 100644 --- a/docs/configuration/outbound/selector.zh.md +++ b/docs/configuration/outbound/selector.zh.md @@ -17,7 +17,7 @@ !!! quote "" - 选择器目前只能通过 [Clash API](/zh/configuration/experimental#clash-api) 来控制。 + 选择器目前只能通过 [Clash API](/zh/configuration/experimental/clash-api/) 来控制。 ### 字段 diff --git a/docs/configuration/outbound/shadowsocks.zh.md b/docs/configuration/outbound/shadowsocks.zh.md index 818a4fa9..7b4ff560 100644 --- a/docs/configuration/outbound/shadowsocks.zh.md +++ b/docs/configuration/outbound/shadowsocks.zh.md @@ -95,7 +95,7 @@ UDP over TCP 配置。 #### multiplex -参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。 +参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 ### 拨号字段 diff --git a/docs/configuration/outbound/shadowtls.zh.md b/docs/configuration/outbound/shadowtls.zh.md index bb8d0e87..72a73d7d 100644 --- a/docs/configuration/outbound/shadowtls.zh.md +++ b/docs/configuration/outbound/shadowtls.zh.md @@ -49,7 +49,7 @@ ShadowTLS 协议版本。 ==必填== -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 diff --git a/docs/configuration/outbound/tor.zh.md b/docs/configuration/outbound/tor.zh.md index be505964..a49eb323 100644 --- a/docs/configuration/outbound/tor.zh.md +++ b/docs/configuration/outbound/tor.zh.md @@ -18,7 +18,7 @@ !!! info "" - 默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#_5)。 + 默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#构建标记)。 ### 字段 diff --git a/docs/configuration/outbound/trojan.zh.md b/docs/configuration/outbound/trojan.zh.md index 2248c739..8a78ca2d 100644 --- a/docs/configuration/outbound/trojan.zh.md +++ b/docs/configuration/outbound/trojan.zh.md @@ -47,11 +47,11 @@ Trojan 密码。 #### tls -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 #### multiplex -参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。 +参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 #### transport diff --git a/docs/configuration/outbound/tuic.zh.md b/docs/configuration/outbound/tuic.zh.md index 4511711a..6d31d7bc 100644 --- a/docs/configuration/outbound/tuic.zh.md +++ b/docs/configuration/outbound/tuic.zh.md @@ -97,7 +97,7 @@ UDP 包中继模式 ==必填== -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 ### 拨号字段 diff --git a/docs/configuration/outbound/vless.zh.md b/docs/configuration/outbound/vless.zh.md index 8978a6ac..f3bc9a08 100644 --- a/docs/configuration/outbound/vless.zh.md +++ b/docs/configuration/outbound/vless.zh.md @@ -57,7 +57,7 @@ VLESS 子协议。 #### tls -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 #### packet_encoding @@ -71,7 +71,7 @@ UDP 包编码,默认使用 xudp。 #### multiplex -参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。 +参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 #### transport diff --git a/docs/configuration/outbound/vmess.zh.md b/docs/configuration/outbound/vmess.zh.md index 295b8dde..cbc8bdee 100644 --- a/docs/configuration/outbound/vmess.zh.md +++ b/docs/configuration/outbound/vmess.zh.md @@ -82,7 +82,7 @@ VMess 用户 ID。 #### tls -TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。 +TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。 #### packet_encoding @@ -96,7 +96,7 @@ UDP 包编码。 #### multiplex -参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。 +参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。 #### transport diff --git a/docs/configuration/outbound/wireguard.zh.md b/docs/configuration/outbound/wireguard.zh.md index 3b22affd..2b6d4a0a 100644 --- a/docs/configuration/outbound/wireguard.zh.md +++ b/docs/configuration/outbound/wireguard.zh.md @@ -4,7 +4,7 @@ icon: material/delete-clock !!! failure "已在 sing-box 1.11.0 废弃" - WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。 + WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。 !!! quote "sing-box 1.11.0 中的更改" diff --git a/docs/configuration/route/geoip.zh.md b/docs/configuration/route/geoip.zh.md index 3d63a3b7..17559a46 100644 --- a/docs/configuration/route/geoip.zh.md +++ b/docs/configuration/route/geoip.zh.md @@ -4,7 +4,7 @@ icon: material/note-remove !!! 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-到规则集)。 ### 结构 diff --git a/docs/configuration/route/geosite.zh.md b/docs/configuration/route/geosite.zh.md index 9afea3d7..1ea0752a 100644 --- a/docs/configuration/route/geosite.zh.md +++ b/docs/configuration/route/geosite.zh.md @@ -4,7 +4,7 @@ icon: material/note-remove !!! 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-到规则集)。 ### 结构 diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index fa50bfe7..1a50d3e3 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -12,7 +12,7 @@ icon: material/alert-decagram !!! quote "sing-box 1.11.0 中的更改" - :material-plus: [network_strategy](#network_strategy) + :material-plus: [default_network_strategy](#default_network_strategy) :material-plus: [default_network_type](#default_network_type) :material-plus: [default_fallback_network_type](#default_fallback_network_type) :material-plus: [default_fallback_delay](#default_fallback_delay) @@ -110,7 +110,7 @@ icon: material/alert-decagram !!! question "自 sing-box 1.12.0 起" -详情参阅 [拨号字段](/configuration/shared/dial/#domain_resolver)。 +详情参阅 [拨号字段](/zh/configuration/shared/dial/#domain_resolver)。 可以被 `outbound.domain_resolver` 覆盖。 @@ -118,7 +118,7 @@ icon: material/alert-decagram !!! question "自 sing-box 1.11.0 起" -详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。 +详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。 当 `outbound.bind_interface`, `outbound.inet4_bind_address` 或 `outbound.inet6_bind_address` 已设置时不生效。 @@ -130,16 +130,16 @@ icon: material/alert-decagram !!! question "自 sing-box 1.11.0 起" -详情参阅 [拨号字段](/configuration/shared/dial/#default_network_type)。 +详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_network_type)。 #### default_fallback_network_type !!! question "自 sing-box 1.11.0 起" -详情参阅 [拨号字段](/configuration/shared/dial/#default_fallback_network_type)。 +详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_fallback_network_type)。 #### default_fallback_delay !!! question "自 sing-box 1.11.0 起" -详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。 +详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。 diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 1ffe57d6..c8838018 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -22,6 +22,7 @@ icon: material/new-box :material-plus: [client](#client) :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) + :material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source) :material-plus: [process_path_regex](#process_path_regex) !!! quote "sing-box 1.8.0 中的更改" @@ -254,7 +255,7 @@ icon: material/new-box !!! failure "已在 sing-box 1.8.0 废弃" - Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。 + Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。 匹配 Geosite。 @@ -262,7 +263,7 @@ icon: material/new-box !!! failure "已在 sing-box 1.8.0 废弃" - GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。 + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 匹配源 GeoIP。 @@ -270,7 +271,7 @@ icon: material/new-box !!! failure "已在 sing-box 1.8.0 废弃" - GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。 + GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 匹配 GeoIP。 diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md index 16efb53a..16e16180 100644 --- a/docs/configuration/route/rule_action.zh.md +++ b/docs/configuration/route/rule_action.zh.md @@ -66,7 +66,7 @@ icon: material/new-box 目标出站的标签。 -如果未指定,规则仅在来自 auto redirect 的[预匹配](/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。 +如果未指定,规则仅在来自 auto redirect 的[预匹配](/zh/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。 #### route-options 字段 @@ -154,22 +154,22 @@ icon: material/new-box #### network_strategy -详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。 +详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。 仅当出站为 `direct` 且 `outbound.bind_interface`, `outbound.inet4_bind_address` 且 `outbound.inet6_bind_address` 未设置时生效。 #### network_type -详情参阅 [拨号字段](/configuration/shared/dial/#network_type)。 +详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_type)。 #### fallback_network_type -详情参阅 [拨号字段](/configuration/shared/dial/#fallback_network_type)。 +详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_network_type)。 #### fallback_delay -详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。 +详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。 #### udp_disable_domain_unmapping diff --git a/docs/configuration/rule-set/headless-rule.zh.md b/docs/configuration/rule-set/headless-rule.zh.md index d539d710..f2b88631 100644 --- a/docs/configuration/rule-set/headless-rule.zh.md +++ b/docs/configuration/rule-set/headless-rule.zh.md @@ -10,8 +10,8 @@ icon: material/new-box !!! quote "sing-box 1.11.0 中的更改" :material-plus: [network_type](#network_type) - :material-alert: [network_is_expensive](#network_is_expensive) - :material-alert: [network_is_constrained](#network_is_constrained) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) ### 结构 diff --git a/docs/configuration/service/ccm.zh.md b/docs/configuration/service/ccm.zh.md index 7bba322c..f6490b5e 100644 --- a/docs/configuration/service/ccm.zh.md +++ b/docs/configuration/service/ccm.zh.md @@ -92,7 +92,7 @@ Claude Code OAuth 凭据文件的路径。 #### tls -TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 ### 示例 diff --git a/docs/configuration/service/derp.zh.md b/docs/configuration/service/derp.zh.md index ab89ac08..b22ff413 100644 --- a/docs/configuration/service/derp.zh.md +++ b/docs/configuration/service/derp.zh.md @@ -36,7 +36,7 @@ DERP 服务是一个 Tailscale DERP 服务器,类似于 [derper](https://pkg.g #### tls -TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 #### config_path @@ -96,7 +96,7 @@ Derper 配置文件路径。 - `server`:**必填** DERP 服务器地址。 - `server_port`:**必填** DERP 服务器端口。 - `host`:自定义 DERP 主机名。 -- `tls`:[TLS](/zh/configuration/shared/tls/#outbound) +- `tls`:[TLS](/zh/configuration/shared/tls/#出站) - `拨号字段`:[拨号字段](/zh/configuration/shared/dial/) #### mesh_psk diff --git a/docs/configuration/service/ocm.zh.md b/docs/configuration/service/ocm.zh.md index 2e02dc55..90394006 100644 --- a/docs/configuration/service/ocm.zh.md +++ b/docs/configuration/service/ocm.zh.md @@ -90,7 +90,7 @@ OpenAI OAuth 凭据文件的路径。 #### tls -TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 ### 示例 diff --git a/docs/configuration/service/ssm-api.zh.md b/docs/configuration/service/ssm-api.zh.md index 66e3e922..fbe45ebb 100644 --- a/docs/configuration/service/ssm-api.zh.md +++ b/docs/configuration/service/ssm-api.zh.md @@ -55,4 +55,4 @@ SSM API 服务是一个用于管理 Shadowsocks 服务器的 RESTful API 服务 #### tls -TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 \ No newline at end of file +TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。 \ No newline at end of file diff --git a/docs/configuration/shared/dial.zh.md b/docs/configuration/shared/dial.zh.md index 49309351..daf7f8e0 100644 --- a/docs/configuration/shared/dial.zh.md +++ b/docs/configuration/shared/dial.zh.md @@ -173,7 +173,7 @@ TCP keep alive 间隔。 用于设置解析域名的域名解析器。 -此选项的格式与 [路由 DNS 规则动作](/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。 +此选项的格式与 [路由 DNS 规则动作](/zh/configuration/dns/rule_action/#route) 相同,但不包含 `action` 字段。 若直接将此选项设置为字符串,则等同于设置该选项的 `server` 字段。 @@ -246,7 +246,7 @@ TCP keep alive 间隔。 !!! failure "已在 sing-box 1.12.0 废弃" - `domain_strategy` 已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-domain-strategy-option-to-domain-resolver)。 + `domain_strategy` 已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移出站域名策略选项到域名解析器)。 可选值:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 diff --git a/docs/configuration/shared/listen.zh.md b/docs/configuration/shared/listen.zh.md index 905cea3c..0afcbc46 100644 --- a/docs/configuration/shared/listen.zh.md +++ b/docs/configuration/shared/listen.zh.md @@ -145,13 +145,13 @@ UDP NAT 过期时间。 如果设置,连接将被转发到指定的入站。 -需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#_3)。 +需要目标入站支持,参阅 [注入支持](/zh/configuration/inbound/#字段)。 #### sniff !!! failure "已在 sing-box 1.11.0 废弃" - 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). 启用协议探测。 @@ -171,7 +171,7 @@ UDP NAT 过期时间。 !!! failure "已在 sing-box 1.11.0 废弃" - 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). 探测超时时间。 @@ -181,7 +181,7 @@ UDP NAT 过期时间。 !!! failure "已在 sing-box 1.11.0 废弃" - 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). 可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 @@ -193,7 +193,7 @@ UDP NAT 过期时间。 !!! failure "已在 sing-box 1.11.0 废弃" - 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作). 如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。 diff --git a/docs/configuration/shared/pre-match.zh.md b/docs/configuration/shared/pre-match.zh.md index 615400b0..06d78f10 100644 --- a/docs/configuration/shared/pre-match.zh.md +++ b/docs/configuration/shared/pre-match.zh.md @@ -22,13 +22,13 @@ icon: material/new-box 以 TCP RST / ICMP 不可达拒绝。 -详情参阅 [reject](/configuration/route/rule_action/#reject)。 +详情参阅 [reject](/zh/configuration/route/rule_action/#reject)。 #### route 将 ICMP 连接路由到指定出站以直接回复。 -详情参阅 [route](/configuration/route/rule_action/#route)。 +详情参阅 [route](/zh/configuration/route/rule_action/#route)。 #### bypass @@ -44,4 +44,4 @@ icon: material/new-box 对于其他所有场景,指定了 `outbound` 的 bypass 行为与 `route` 相同。 -详情参阅 [bypass](/configuration/route/rule_action/#bypass)。 +详情参阅 [bypass](/zh/configuration/route/rule_action/#bypass)。 diff --git a/docs/configuration/shared/tls.zh.md b/docs/configuration/shared/tls.zh.md index e0460983..0b47189b 100644 --- a/docs/configuration/shared/tls.zh.md +++ b/docs/configuration/shared/tls.zh.md @@ -426,7 +426,7 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/ 其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。 此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。 - 如需 TLS 指纹抵抗,请改用 [NaiveProxy](/configuration/inbound/naive/)。 + 如需 TLS 指纹抵抗,请改用 [NaiveProxy](/zh/configuration/inbound/naive/)。 uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。 diff --git a/docs/configuration/shared/v2ray-transport.zh.md b/docs/configuration/shared/v2ray-transport.zh.md index e5bd7de7..e5ea3ed6 100644 --- a/docs/configuration/shared/v2ray-transport.zh.md +++ b/docs/configuration/shared/v2ray-transport.zh.md @@ -144,7 +144,7 @@ HTTP 请求的额外标头 !!! note "" - 默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#_5)。 + 默认安装不包含标准 gRPC (兼容性好,但性能较差), 参阅 [安装](/zh/installation/build-from-source/#构建标记)。 ```json { diff --git a/docs/configuration/shared/wifi-state.zh.md b/docs/configuration/shared/wifi-state.zh.md index 02e8b6c9..c7d4db5f 100644 --- a/docs/configuration/shared/wifi-state.zh.md +++ b/docs/configuration/shared/wifi-state.zh.md @@ -4,7 +4,7 @@ icon: material/new-box # Wi-Fi 状态 -!!! quote "sing-box 1.13.0 的变更" +!!! quote "sing-box 1.13.0 中的更改" :material-plus: Linux 支持 :material-plus: Windows 支持 diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 78c46053..82b6db04 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -7,7 +7,7 @@ icon: material/delete-alert #### 旧的 DNS 服务器格式 DNS 服务器已重构, -参阅 [迁移指南](/migration/#migrate-to-new-dns-servers). +参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式). 对旧格式的兼容性将在 sing-box 1.14.0 中被移除。 @@ -15,7 +15,7 @@ DNS 服务器已重构, 旧的 `outbound` DNS 规则已废弃, 且可被拨号字段代替, -参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver). +参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项). #### 旧的 ECH 字段 @@ -31,28 +31,28 @@ ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支 #### 旧的特殊出站 旧的特殊出站(`block` / `dns`)已废弃且可以通过规则动作替代, -参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions)。 +参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作)。 旧字段将在 sing-box 1.13.0 中被移除。 #### 旧的入站字段 旧的入站字段(`inbound.`)已废弃且可以通过规则动作替代, -参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions)。 +参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作)。 旧字段将在 sing-box 1.13.0 中被移除。 #### direct 出站中的目标地址覆盖字段 direct 出站中的目标地址覆盖字段(`override_address` / `override_port`)已废弃且可以通过规则动作替代, -参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。 +参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 旧字段将在 sing-box 1.13.0 中被移除。 #### WireGuard 出站 WireGuard 出站已废弃且可以通过端点替代, -参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。 +参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。 旧出站将在 sing-box 1.13.0 中被移除。 @@ -86,7 +86,7 @@ GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用 #### Clash API 中的 Cache file 及相关功能 Clash API 中的 `cache_file` 及相关功能已废弃且已迁移到独立的 `cache_file` 设置, -参阅 [迁移指南](/zh/migration/#clash-api)。 +参阅 [迁移指南](/zh/migration/#将缓存文件从-clash-api-迁移到独立选项)。 #### GeoIP @@ -96,7 +96,7 @@ maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量 且现有的实现均存在内存使用大与管理困难的问题。 sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/), -可以完全替代 GeoIP, 参阅 [迁移指南](/zh/migration/#geoip)。 +可以完全替代 GeoIP, 参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。 #### Geosite @@ -106,7 +106,7 @@ Geosite,即由 V2Ray 维护的 domain-list-community 项目,作为早期流 存在着包括缺少维护、规则不准确和管理困难内的大量问题。 sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/), -可以完全替代 Geosite,参阅 [迁移指南](/zh/migration/#geosite)。 +可以完全替代 Geosite,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。 ## 1.6.0 diff --git a/docs/installation/build-from-source.zh.md b/docs/installation/build-from-source.zh.md index 3972762c..d6cd03b5 100644 --- a/docs/installation/build-from-source.zh.md +++ b/docs/installation/build-from-source.zh.md @@ -51,20 +51,20 @@ go build -tags "tag_a tag_b" ./cmd/sing-box | 构建标记 | 默认启动 | 说明 | |------------------------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/configuration/dns/server/), [Naive inbound](/configuration/inbound/naive/), [Hysteria Inbound](/configuration/inbound/hysteria/), [Hysteria Outbound](/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/configuration/shared/v2ray-transport#quic). | -| `with_grpc` | :material-close:️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/configuration/shared/v2ray-transport#grpc). | -| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). | -| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). | -| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). | -| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | -| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | -| `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | -| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/configuration/inbound/tun#stack) and [WireGuard outbound](/configuration/outbound/wireguard#system_interface). | -| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/configuration/outbound/tor/). | -| `with_tailscale` | :material-check: | 构建 Tailscale 支持,参阅 [Tailscale 端点](/configuration/endpoint/tailscale)。 | +| `with_quic` | :material-check: | Build with QUIC support, see [QUIC and HTTP3 DNS transports](/zh/configuration/dns/server/), [Naive inbound](/zh/configuration/inbound/naive/), [Hysteria Inbound](/zh/configuration/inbound/hysteria/), [Hysteria Outbound](/zh/configuration/outbound/hysteria/) and [V2Ray Transport#QUIC](/zh/configuration/shared/v2ray-transport#quic). | +| `with_grpc` | :material-close:️ | Build with standard gRPC support, see [V2Ray Transport#gRPC](/zh/configuration/shared/v2ray-transport#grpc). | +| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/zh/configuration/dns/server/). | +| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/zh/configuration/outbound/wireguard/). | +| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/zh/configuration/shared/tls#utls). | +| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/zh/configuration/shared/tls/). | +| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/zh/configuration/experimental#clash-api-fields). | +| `with_v2ray_api` | :material-close:️ | Build with V2Ray API support, see [Experimental](/zh/configuration/experimental#v2ray-api-fields). | +| `with_gvisor` | :material-check: | Build with gVisor support, see [Tun inbound](/zh/configuration/inbound/tun#stack) and [WireGuard outbound](/zh/configuration/outbound/wireguard#system_interface). | +| `with_embedded_tor` (CGO required) | :material-close:️ | Build with embedded Tor support, see [Tor outbound](/zh/configuration/outbound/tor/). | +| `with_tailscale` | :material-check: | 构建 Tailscale 支持,参阅 [Tailscale 端点](/zh/configuration/endpoint/tailscale)。 | | `with_ccm` | :material-check: | 构建 Claude Code Multiplexer 服务支持。 | | `with_ocm` | :material-check: | 构建 OpenAI Codex Multiplexer 服务支持。 | -| `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/configuration/outbound/naive/)。 | +| `with_naive_outbound` | :material-check: | 构建 NaiveProxy 出站支持,参阅 [NaiveProxy 出站](/zh/configuration/outbound/naive/)。 | | `badlinkname` | :material-check: | 启用 `go:linkname` 以访问标准库内部函数。Go 标准库未提供本项目需要的许多底层 API,且在外部重新实现不切实际。用于 kTLS(内核 TLS 卸载)和原始 TLS 记录操作。 | | `tfogo_checklinkname0` | :material-check: | `badlinkname` 的伴随标记。Go 1.23+ 链接器强制限制 `go:linkname` 使用;此标记表示构建使用 `-checklinkname=0` 以绕过该限制。 | diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 6f8ba62a..c08be78f 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -518,9 +518,9 @@ DNS 服务器已经重构。 !!! info "参考" - [DNS 规则](/configuration/dns/rule/#outbound) / - [拨号字段](/configuration/shared/dial/#domain_resolver) / - [路由](/configuration/route/#default_domain_resolver) + [DNS 规则](/zh/configuration/dns/rule/#outbound) / + [拨号字段](/zh/configuration/shared/dial/#domain_resolver) / + [路由](/zh/configuration/route/#default_domain_resolver) === ":material-card-remove: 废弃的" @@ -596,7 +596,7 @@ DNS 服务器已经重构。 !!! info "参考" - [拨号字段](/configuration/shared/dial/#domain_strategy) + [拨号字段](/zh/configuration/shared/dial/#domain_strategy) === ":material-card-remove: 弃用的" From a8e3cd325653e4f3fb5e3dfb8bfe7a85e31d7951 Mon Sep 17 00:00:00 2001 From: Andrew Novikov <68810358+npokc123@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:05:40 +0800 Subject: [PATCH 38/99] tun: Fix nfqueue not working in prerouting --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8b1c752b..f9d0042e 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.3 + github.com/sagernet/sing-tun v0.8.4 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e diff --git a/go.sum b/go.sum index 222aace8..b4bc4c27 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.3 h1:mozxmuIoRhFdVHnheenLpBaammVj7bZPcnkApaYKDPY= -github.com/sagernet/sing-tun v0.8.3/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-tun v0.8.4 h1:pZ/ZoBQTeVks75iS1w7Qe8brBEsPVT0ENiVvtbsFBGo= +github.com/sagernet/sing-tun v0.8.4/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= From ea464cef8d3c5b6028b04cd8737ce19396d52513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 21 Mar 2026 12:50:42 +0800 Subject: [PATCH 39/99] daemon: Fix CloseService leaving instance non-nil on close error --- daemon/started_service.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/daemon/started_service.go b/daemon/started_service.go index 7ebdac1e..b9795859 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -226,13 +226,14 @@ func (s *StartedService) CloseService() error { return os.ErrInvalid } s.updateStatus(ServiceStatus_STOPPING) - if s.instance != nil { - err := s.instance.Close() + instance := s.instance + s.instance = nil + if instance != nil { + err := instance.Close() if err != nil { return s.updateStatusError(err) } } - s.instance = nil s.startedAt = time.Time{} s.updateStatus(ServiceStatus_IDLE) s.serviceAccess.Unlock() From 1e57c06295160f19150f2cf8b455744c04f02bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 21 Mar 2026 13:37:14 +0800 Subject: [PATCH 40/99] daemon: Allow StartOrReloadService to recover from FATAL state --- daemon/started_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/started_service.go b/daemon/started_service.go index b9795859..e6e07511 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -168,7 +168,7 @@ func (s *StartedService) waitForStarted(ctx context.Context) error { func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error { s.serviceAccess.Lock() switch s.serviceStatus.Status { - case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING: + case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING, ServiceStatus_FATAL: default: s.serviceAccess.Unlock() return os.ErrInvalid From 6913b11e0a95b06c904ccc099f9d744e5cb87fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 21 Mar 2026 17:09:34 +0800 Subject: [PATCH 41/99] Reject removed legacy inbound fields instead of silently ignoring --- option/inbound.go | 6 ++++++ protocol/tun/inbound.go | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/option/inbound.go b/option/inbound.go index 4fb6081d..21497a3f 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -44,6 +44,12 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro if err != nil { return err } + if listenWrapper, isListen := options.(ListenOptionsWrapper); isListen { + //nolint:staticcheck + if listenWrapper.TakeListenOptions().InboundOptions != (InboundOptions{}) { + return E.New("legacy inbound fields are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions") + } + } h.Options = options return nil } diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index df9344b8..6820831a 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -67,6 +67,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if options.GSO { return nil, E.New("GSO option in tun is deprecated in sing-box 1.11.0 and removed in sing-box 1.12.0") } + //nolint:staticcheck + if options.InboundOptions != (option.InboundOptions{}) { + return nil, E.New("legacy inbound fields are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions") + } address := options.Address inet4Address := common.Filter(address, func(it netip.Prefix) bool { From 795d1c28927499a404588651ecf3b1f11534b811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 12:26:19 +0800 Subject: [PATCH 42/99] Fix nested rule-set match cache isolation --- adapter/inbound.go | 4 ++++ route/rule/rule_item_rule_set.go | 8 +++++--- route/rule/rule_set_local.go | 4 +++- route/rule/rule_set_remote.go | 4 +++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/adapter/inbound.go b/adapter/inbound.go index b32e9f82..f047199e 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -101,6 +101,10 @@ type InboundContext struct { func (c *InboundContext) ResetRuleCache() { c.IPCIDRMatchSource = false c.IPCIDRAcceptEmpty = false + c.ResetRuleMatchCache() +} + +func (c *InboundContext) ResetRuleMatchCache() { c.SourceAddressMatch = false c.SourcePortMatch = false c.DestinationAddressMatch = false diff --git a/route/rule/rule_item_rule_set.go b/route/rule/rule_item_rule_set.go index a0115a04..858bb877 100644 --- a/route/rule/rule_item_rule_set.go +++ b/route/rule/rule_item_rule_set.go @@ -41,10 +41,12 @@ func (r *RuleSetItem) Start() error { } func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { - metadata.IPCIDRMatchSource = r.ipCidrMatchSource - metadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty for _, ruleSet := range r.setList { - if ruleSet.Match(metadata) { + nestedMetadata := *metadata + nestedMetadata.ResetRuleMatchCache() + nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource + nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty + if ruleSet.Match(&nestedMetadata) { return true } } diff --git a/route/rule/rule_set_local.go b/route/rule/rule_set_local.go index b09915ed..8409831b 100644 --- a/route/rule/rule_set_local.go +++ b/route/rule/rule_set_local.go @@ -203,7 +203,9 @@ func (s *LocalRuleSet) Close() error { func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { for _, rule := range s.rules { - if rule.Match(metadata) { + nestedMetadata := *metadata + nestedMetadata.ResetRuleMatchCache() + if rule.Match(&nestedMetadata) { return true } } diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index 3aba76ba..81a8d3fc 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -323,7 +323,9 @@ func (s *RemoteRuleSet) Close() error { func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { for _, rule := range s.rules { - if rule.Match(metadata) { + nestedMetadata := *metadata + nestedMetadata.ResetRuleMatchCache() + if rule.Match(&nestedMetadata) { return true } } From 7623bcd19e749b87566f900a8389fe3ef8a50f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 13:58:55 +0800 Subject: [PATCH 43/99] Fix DialerForICMPDestination --- common/dialer/default.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/dialer/default.go b/common/dialer/default.go index 39b96dfe..6b843ed0 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -329,9 +329,9 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer { if !destination.Is6() { - return d.dialer6.Dialer - } else { return d.dialer4.Dialer + } else { + return d.dialer6.Dialer } } From c13faa8e3c65e860cfaa3c98b2155087299fcaf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 14:15:02 +0800 Subject: [PATCH 44/99] tailscale: Only set ProcessLocalIPs/ProcessSubnets for fake TUN --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f9d0042e..2155b956 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/sagernet/sing-tun v0.8.4 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 - github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e + github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index b4bc4c27..502e5964 100644 --- a/go.sum +++ b/go.sum @@ -254,8 +254,8 @@ github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkV github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e h1:Sv1qUhJIidjSTc24XEknovDZnbmVSlAXj8wNVgIfgGo= -github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 h1:8zc1Aph1+ElqF9/7aSPkO0o4vTd+AfQC+CO324mLWGg= +github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg= github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= From b8e5a71450461fc1dcbbfe68ff0d1afa8afcf7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 14:26:45 +0800 Subject: [PATCH 45/99] Add process information cache to avoid duplicate lookups PreMatch and full match phases each created a fresh InboundContext, causing process search (expensive OS syscalls) to run twice per connection. Use a freelru ShardedLRU cache with 200ms TTL to serve the second lookup from cache. --- route/process_cache.go | 34 ++++++++++++++++++++++++++++++++++ route/route.go | 3 +-- route/router.go | 10 ++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 route/process_cache.go diff --git a/route/process_cache.go b/route/process_cache.go new file mode 100644 index 00000000..691a4e8e --- /dev/null +++ b/route/process_cache.go @@ -0,0 +1,34 @@ +package route + +import ( + "context" + "net/netip" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/process" +) + +type processCacheKey struct { + Network string + Source netip.AddrPort + Destination netip.AddrPort +} + +type processCacheEntry struct { + result *adapter.ConnectionOwner + err error +} + +func (r *Router) findProcessInfoCached(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + key := processCacheKey{ + Network: network, + Source: source, + Destination: destination, + } + if entry, ok := r.processCache.Get(key); ok { + return entry.result, entry.err + } + result, err := process.FindProcessInfo(r.processSearcher, ctx, network, source, destination) + r.processCache.Add(key, processCacheEntry{result: result, err: err}) + return result, err +} diff --git a/route/route.go b/route/route.go index 40a90e7d..7773ff3c 100644 --- a/route/route.go +++ b/route/route.go @@ -9,7 +9,6 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" R "github.com/sagernet/sing-box/route/rule" @@ -415,7 +414,7 @@ func (r *Router) matchRule( } else if metadata.Destination.IsIP() { originDestination = metadata.Destination.AddrPort() } - processInfo, fErr := process.FindProcessInfo(r.processSearcher, ctx, metadata.Network, metadata.Source.AddrPort(), originDestination) + processInfo, fErr := r.findProcessInfoCached(ctx, metadata.Network, metadata.Source.AddrPort(), originDestination) if fErr != nil { r.logger.InfoContext(ctx, "failed to search process: ", fErr) } else { diff --git a/route/router.go b/route/router.go index 5c73cb1c..9f8c343c 100644 --- a/route/router.go +++ b/route/router.go @@ -4,6 +4,7 @@ import ( "context" "os" "runtime" + "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" @@ -12,8 +13,11 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" R "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/task" + "github.com/sagernet/sing/contrab/freelru" + "github.com/sagernet/sing/contrab/maphash" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" ) @@ -34,6 +38,7 @@ type Router struct { ruleSets []adapter.RuleSet ruleSetMap map[string]adapter.RuleSet processSearcher process.Searcher + processCache freelru.Cache[processCacheKey, processCacheEntry] pauseManager pause.Manager trackers []adapter.ConnectionTracker platformInterface adapter.PlatformInterface @@ -141,6 +146,11 @@ func (r *Router) Start(stage adapter.StartStage) error { } } } + if r.processSearcher != nil { + processCache := common.Must1(freelru.NewSharded[processCacheKey, processCacheEntry](256, maphash.NewHasher[processCacheKey]().Hash32)) + processCache.SetLifetime(200 * time.Millisecond) + r.processCache = processCache + } case adapter.StartStatePostStart: for i, rule := range r.rules { monitor.Start("initialize rule[", i, "]") From 3f05a37f65318a155e5795bf6f6f758519409f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 15:54:29 +0800 Subject: [PATCH 46/99] Optimize Linux process finder --- common/process/searcher.go | 1 + common/process/searcher_android.go | 10 +- common/process/searcher_darwin.go | 4 + common/process/searcher_linux.go | 65 ++- common/process/searcher_linux_shared.go | 451 +++++++++++++------ common/process/searcher_linux_shared_test.go | 60 +++ common/process/searcher_windows.go | 4 + route/platform_searcher.go | 4 + route/router.go | 7 + 9 files changed, 462 insertions(+), 144 deletions(-) create mode 100644 common/process/searcher_linux_shared_test.go diff --git a/common/process/searcher.go b/common/process/searcher.go index 1af2c2bd..743a8037 100644 --- a/common/process/searcher.go +++ b/common/process/searcher.go @@ -14,6 +14,7 @@ import ( type Searcher interface { FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) + Close() error } var ErrNotFound = E.New("process not found") diff --git a/common/process/searcher_android.go b/common/process/searcher_android.go index ac9550ce..48d853cf 100644 --- a/common/process/searcher_android.go +++ b/common/process/searcher_android.go @@ -18,8 +18,16 @@ func NewSearcher(config Config) (Searcher, error) { 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) { - _, uid, err := resolveSocketByNetlink(network, source, destination) + family, protocol, err := socketDiagSettings(network, source) + if err != nil { + return nil, err + } + _, uid, err := querySocketDiagOnce(family, protocol, source) if err != nil { return nil, err } diff --git a/common/process/searcher_darwin.go b/common/process/searcher_darwin.go index 03428cc8..7cc3083e 100644 --- a/common/process/searcher_darwin.go +++ b/common/process/searcher_darwin.go @@ -24,6 +24,10 @@ func NewSearcher(_ Config) (Searcher, error) { 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) { processName, err := findProcessName(network, source.Addr(), int(source.Port())) if err != nil { diff --git a/common/process/searcher_linux.go b/common/process/searcher_linux.go index 86d37d7c..9b1a9160 100644 --- a/common/process/searcher_linux.go +++ b/common/process/searcher_linux.go @@ -4,33 +4,82 @@ package process import ( "context" + "errors" "net/netip" + "syscall" + "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" ) var _ Searcher = (*linuxSearcher)(nil) type linuxSearcher struct { - logger log.ContextLogger + logger log.ContextLogger + diagConns [4]*socketDiagConn + processPathCache *uidProcessPathCache } func NewSearcher(config Config) (Searcher, error) { - return &linuxSearcher{config.Logger}, nil + searcher := &linuxSearcher{ + logger: config.Logger, + processPathCache: newUIDProcessPathCache(time.Second), + } + for _, family := range []uint8{syscall.AF_INET, syscall.AF_INET6} { + for _, protocol := range []uint8{syscall.IPPROTO_TCP, syscall.IPPROTO_UDP} { + searcher.diagConns[socketDiagConnIndex(family, protocol)] = newSocketDiagConn(family, protocol) + } + } + return searcher, nil +} + +func (s *linuxSearcher) Close() error { + var errs []error + for _, conn := range s.diagConns { + if conn == nil { + continue + } + errs = append(errs, conn.Close()) + } + return E.Errors(errs...) } func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { - inode, uid, err := resolveSocketByNetlink(network, source, destination) + inode, uid, err := s.resolveSocketByNetlink(network, source, destination) if err != nil { return nil, err } - processPath, err := resolveProcessNameByProcSearch(inode, uid) + processInfo := &adapter.ConnectionOwner{ + UserId: int32(uid), + } + processPath, err := s.processPathCache.findProcessPath(inode, uid) if err != nil { s.logger.DebugContext(ctx, "find process path: ", err) + } else { + processInfo.ProcessPath = processPath } - return &adapter.ConnectionOwner{ - UserId: int32(uid), - ProcessPath: processPath, - }, nil + return processInfo, nil +} + +func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { + family, protocol, err := socketDiagSettings(network, source) + if err != nil { + return 0, 0, err + } + conn := s.diagConns[socketDiagConnIndex(family, protocol)] + if conn == nil { + return 0, 0, E.New("missing socket diag connection for family=", family, " protocol=", protocol) + } + if destination.IsValid() && source.Addr().BitLen() == destination.Addr().BitLen() { + inode, uid, err = conn.query(source, destination) + if err == nil { + return inode, uid, nil + } + if !errors.Is(err, ErrNotFound) { + return 0, 0, err + } + } + return querySocketDiagOnce(family, protocol, source) } diff --git a/common/process/searcher_linux_shared.go b/common/process/searcher_linux_shared.go index e75b0b4f..cd0601bc 100644 --- a/common/process/searcher_linux_shared.go +++ b/common/process/searcher_linux_shared.go @@ -3,43 +3,67 @@ package process import ( - "bytes" "encoding/binary" - "fmt" - "net" + "errors" "net/netip" "os" - "path" + "path/filepath" "strings" + "sync" "syscall" + "time" "unicode" - "unsafe" - "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" 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 ( - sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48 - socketDiagByFamily = 20 - pathProc = "/proc" + sizeOfSocketDiagRequestData = 56 + sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData + socketDiagResponseMinSize = 72 + socketDiagByFamily = 20 + pathProc = "/proc" ) -func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) { - var family uint8 - var protocol uint8 +type socketDiagConn struct { + access sync.Mutex + family uint8 + protocol uint8 + fd int +} +type uidProcessPathCache struct { + cache freelru.Cache[uint32, *uidProcessPaths] +} + +type uidProcessPaths struct { + entries map[uint32]string +} + +func newSocketDiagConn(family, protocol uint8) *socketDiagConn { + return &socketDiagConn{ + family: family, + protocol: protocol, + fd: -1, + } +} + +func socketDiagConnIndex(family, protocol uint8) int { + index := 0 + if protocol == syscall.IPPROTO_UDP { + index += 2 + } + if family == syscall.AF_INET6 { + index++ + } + return index +} + +func socketDiagSettings(network string, source netip.AddrPort) (family, protocol uint8, err error) { switch network { case N.NetworkTCP: protocol = syscall.IPPROTO_TCP @@ -48,151 +72,308 @@ func resolveSocketByNetlink(network string, source netip.AddrPort, destination n default: return 0, 0, os.ErrInvalid } - - if source.Addr().Is4() { + switch { + case source.Addr().Is4(): family = syscall.AF_INET - } else { + case source.Addr().Is6(): family = syscall.AF_INET6 + default: + return 0, 0, os.ErrInvalid } - - req := packSocketDiagRequest(family, protocol, source) - - socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG) - if err != nil { - return 0, 0, E.Cause(err, "dial netlink") - } - defer syscall.Close(socket) - - syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100}) - syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100}) - - err = syscall.Connect(socket, &syscall.SockaddrNetlink{ - Family: syscall.AF_NETLINK, - Pad: 0, - Pid: 0, - Groups: 0, - }) - if err != nil { - return - } - - _, err = syscall.Write(socket, req) - if err != nil { - return 0, 0, E.Cause(err, "write netlink request") - } - - buffer := buf.New() - defer buffer.Release() - - n, err := syscall.Read(socket, buffer.FreeBytes()) - if err != nil { - return 0, 0, E.Cause(err, "read netlink response") - } - - buffer.Truncate(n) - - messages, err := syscall.ParseNetlinkMessage(buffer.Bytes()) - if err != nil { - return 0, 0, E.Cause(err, "parse netlink message") - } else if len(messages) == 0 { - return 0, 0, E.New("unexcepted netlink response") - } - - message := messages[0] - if message.Header.Type&syscall.NLMSG_ERROR != 0 { - return 0, 0, E.New("netlink message: NLMSG_ERROR") - } - - inode, uid = unpackSocketDiagResponse(&messages[0]) - return + return family, protocol, nil } -func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte { - s := make([]byte, 16) - copy(s, source.Addr().AsSlice()) - - buf := make([]byte, sizeOfSocketDiagRequest) - - nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest) - nativeEndian.PutUint16(buf[4:6], socketDiagByFamily) - nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP) - nativeEndian.PutUint32(buf[8:12], 0) - nativeEndian.PutUint32(buf[12:16], 0) - - buf[16] = family - buf[17] = protocol - buf[18] = 0 - buf[19] = 0 - nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF) - - binary.BigEndian.PutUint16(buf[24:26], source.Port()) - binary.BigEndian.PutUint16(buf[26:28], 0) - - copy(buf[28:44], s) - copy(buf[44:60], net.IPv6zero) - - nativeEndian.PutUint32(buf[60:64], 0) - nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF) - - return buf +func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache { + cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32)) + cache.SetLifetime(ttl) + return &uidProcessPathCache{cache: cache} } -func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) { - if len(msg.Data) < 72 { - return 0, 0 +func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) { + if cached, ok := c.cache.Get(uid); ok { + if processPath, found := cached.entries[targetInode]; found { + return processPath, nil + } } - - data := msg.Data - - uid = nativeEndian.Uint32(data[64:68]) - inode = nativeEndian.Uint32(data[68:72]) - - return -} - -func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) { - files, err := os.ReadDir(pathProc) + 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 { + return 0, 0, E.Cause(err, "dial netlink") + } + defer syscall.Close(fd) + return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true)) +} + +func (c *socketDiagConn) ensureOpenLocked() error { + if c.fd != -1 { + return nil + } + fd, err := openSocketDiag() + if err != nil { + return err + } + c.fd = fd + return nil +} + +func openSocketDiag() (int, error) { + fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM|syscall.SOCK_CLOEXEC, syscall.NETLINK_INET_DIAG) + if err != nil { + return -1, err + } + timeout := &syscall.Timeval{Usec: 100} + if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil { + syscall.Close(fd) + return -1, err + } + if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil { + syscall.Close(fd) + return -1, err + } + if err = syscall.Connect(fd, &syscall.SockaddrNetlink{ + Family: syscall.AF_NETLINK, + Pid: 0, + Groups: 0, + }); err != nil { + syscall.Close(fd) + return -1, err + } + return fd, nil +} + +func (c *socketDiagConn) closeLocked() error { + if c.fd == -1 { + return nil + } + err := syscall.Close(c.fd) + c.fd = -1 + return err +} + +func packSocketDiagRequest(family, protocol byte, source netip.AddrPort, destination netip.AddrPort, dump bool) []byte { + request := make([]byte, sizeOfSocketDiagRequest) + + binary.NativeEndian.PutUint32(request[0:4], sizeOfSocketDiagRequest) + binary.NativeEndian.PutUint16(request[4:6], socketDiagByFamily) + flags := uint16(syscall.NLM_F_REQUEST) + if dump { + flags |= syscall.NLM_F_DUMP + } + binary.NativeEndian.PutUint16(request[6:8], flags) + binary.NativeEndian.PutUint32(request[8:12], 0) + binary.NativeEndian.PutUint32(request[12:16], 0) + + request[16] = family + request[17] = protocol + request[18] = 0 + request[19] = 0 + if dump { + binary.NativeEndian.PutUint32(request[20:24], 0xFFFFFFFF) + } + requestSource := source + requestDestination := destination + if protocol == syscall.IPPROTO_UDP && !dump && destination.IsValid() { + // udp_dump_one expects the exact-match endpoints reversed for historical reasons. + requestSource, requestDestination = destination, source + } + binary.BigEndian.PutUint16(request[24:26], requestSource.Port()) + binary.BigEndian.PutUint16(request[26:28], requestDestination.Port()) + if family == syscall.AF_INET6 { + copy(request[28:44], requestSource.Addr().AsSlice()) + if requestDestination.IsValid() { + copy(request[44:60], requestDestination.Addr().AsSlice()) + } + } else { + copy(request[28:32], requestSource.Addr().AsSlice()) + if requestDestination.IsValid() { + copy(request[44:48], requestDestination.Addr().AsSlice()) + } + } + binary.NativeEndian.PutUint32(request[60:64], 0) + binary.NativeEndian.PutUint64(request[64:72], 0xFFFFFFFFFFFFFFFF) + return request +} + +func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) { + _, err = syscall.Write(fd, request) + if err != nil { + return 0, 0, E.Cause(err, "write netlink request") + } + buffer := make([]byte, 64<<10) + n, err := syscall.Read(fd, buffer) + if err != nil { + return 0, 0, E.Cause(err, "read netlink response") + } + messages, err := syscall.ParseNetlinkMessage(buffer[:n]) + if err != nil { + return 0, 0, E.Cause(err, "parse netlink message") + } + return unpackSocketDiagMessages(messages) +} + +func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) { + for _, message := range messages { + switch message.Header.Type { + case syscall.NLMSG_DONE: + continue + case syscall.NLMSG_ERROR: + err = unpackSocketDiagError(&message) + if err != nil { + return 0, 0, err + } + case socketDiagByFamily: + inode, uid = unpackSocketDiagResponse(&message) + if inode != 0 || uid != 0 { + return inode, uid, nil + } + } + } + return 0, 0, ErrNotFound +} + +func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) { + if len(msg.Data) < socketDiagResponseMinSize { + return 0, 0 + } + uid = binary.NativeEndian.Uint32(msg.Data[64:68]) + inode = binary.NativeEndian.Uint32(msg.Data[68:72]) + return inode, uid +} + +func unpackSocketDiagError(msg *syscall.NetlinkMessage) error { + if len(msg.Data) < 4 { + return E.New("netlink message: NLMSG_ERROR") + } + errno := int32(binary.NativeEndian.Uint32(msg.Data[:4])) + if errno == 0 { + return nil + } + if errno < 0 { + errno = -errno + } + sysErr := syscall.Errno(errno) + switch sysErr { + case syscall.ENOENT, syscall.ESRCH: + return ErrNotFound + default: + return E.New("netlink message: ", sysErr) + } +} + +func shouldRetrySocketDiag(err error) bool { + return err != nil && !errors.Is(err, ErrNotFound) +} + +func buildProcessPathByUIDCache(uid uint32) (map[uint32]string, error) { + files, err := os.ReadDir(pathProc) + if err != nil { + return nil, err + } buffer := make([]byte, syscall.PathMax) - socket := []byte(fmt.Sprintf("socket:[%d]", inode)) - - for _, f := range files { - if !f.IsDir() || !isPid(f.Name()) { + processPaths := make(map[uint32]string) + for _, file := range files { + if !file.IsDir() || !isPid(file.Name()) { continue } - - info, err := f.Info() + info, err := file.Info() if err != nil { - return "", err + if isIgnorableProcError(err) { + continue + } + return nil, err } if info.Sys().(*syscall.Stat_t).Uid != uid { continue } - - processPath := path.Join(pathProc, f.Name()) - fdPath := path.Join(processPath, "fd") - + processPath := filepath.Join(pathProc, file.Name()) + fdPath := filepath.Join(processPath, "fd") + exePath, err := os.Readlink(filepath.Join(processPath, "exe")) + if err != nil { + if isIgnorableProcError(err) { + continue + } + return nil, err + } fds, err := os.ReadDir(fdPath) if err != nil { continue } - for _, fd := range fds { - n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer) + n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer) if err != nil { continue } - - if bytes.Equal(buffer[:n], socket) { - return os.Readlink(path.Join(processPath, "exe")) + inode, ok := parseSocketInode(buffer[:n]) + if !ok { + continue + } + if _, loaded := processPaths[inode]; !loaded { + processPaths[inode] = exePath } } } + return processPaths, nil +} - return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode) +func isIgnorableProcError(err error) bool { + return os.IsNotExist(err) || os.IsPermission(err) +} + +func parseSocketInode(link []byte) (uint32, bool) { + const socketPrefix = "socket:[" + if len(link) <= len(socketPrefix) || string(link[:len(socketPrefix)]) != socketPrefix || link[len(link)-1] != ']' { + return 0, false + } + var inode uint64 + for _, char := range link[len(socketPrefix) : len(link)-1] { + if char < '0' || char > '9' { + return 0, false + } + inode = inode*10 + uint64(char-'0') + if inode > uint64(^uint32(0)) { + return 0, false + } + } + return uint32(inode), true } func isPid(s string) bool { diff --git a/common/process/searcher_linux_shared_test.go b/common/process/searcher_linux_shared_test.go new file mode 100644 index 00000000..1befff4e --- /dev/null +++ b/common/process/searcher_linux_shared_test.go @@ -0,0 +1,60 @@ +//go:build linux + +package process + +import ( + "net" + "net/netip" + "os" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestQuerySocketDiagUDPExact(t *testing.T) { + t.Parallel() + server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}) + require.NoError(t, err) + defer server.Close() + + client, err := net.DialUDP("udp4", nil, server.LocalAddr().(*net.UDPAddr)) + require.NoError(t, err) + defer client.Close() + + err = client.SetDeadline(time.Now().Add(time.Second)) + require.NoError(t, err) + _, err = client.Write([]byte{0}) + require.NoError(t, err) + + err = server.SetReadDeadline(time.Now().Add(time.Second)) + require.NoError(t, err) + buffer := make([]byte, 1) + _, _, err = server.ReadFromUDP(buffer) + require.NoError(t, err) + + source := addrPortFromUDPAddr(t, client.LocalAddr()) + destination := addrPortFromUDPAddr(t, client.RemoteAddr()) + + fd, err := openSocketDiag() + require.NoError(t, err) + defer syscall.Close(fd) + + inode, uid, err := querySocketDiag(fd, packSocketDiagRequest(syscall.AF_INET, syscall.IPPROTO_UDP, source, destination, false)) + require.NoError(t, err) + require.NotZero(t, inode) + require.EqualValues(t, os.Getuid(), uid) +} + +func addrPortFromUDPAddr(t *testing.T, addr net.Addr) netip.AddrPort { + t.Helper() + + udpAddr, ok := addr.(*net.UDPAddr) + require.True(t, ok) + + ip, ok := netip.AddrFromSlice(udpAddr.IP) + require.True(t, ok) + + return netip.AddrPortFrom(ip.Unmap(), uint16(udpAddr.Port)) +} diff --git a/common/process/searcher_windows.go b/common/process/searcher_windows.go index ac95e0ce..39695355 100644 --- a/common/process/searcher_windows.go +++ b/common/process/searcher_windows.go @@ -28,6 +28,10 @@ func initWin32API() error { 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) { pid, err := winiphlpapi.FindPid(network, source) if err != nil { diff --git a/route/platform_searcher.go b/route/platform_searcher.go index f6a4e764..20fbda3f 100644 --- a/route/platform_searcher.go +++ b/route/platform_searcher.go @@ -43,3 +43,7 @@ func (s *platformSearcher) FindProcessInfo(ctx context.Context, network string, return s.platform.FindConnectionOwner(request) } + +func (s *platformSearcher) Close() error { + return nil +} diff --git a/route/router.go b/route/router.go index 9f8c343c..bc19b5d3 100644 --- a/route/router.go +++ b/route/router.go @@ -196,6 +196,13 @@ func (r *Router) Close() error { }) monitor.Finish() } + if r.processSearcher != nil { + monitor.Start("close process searcher") + err = E.Append(err, r.processSearcher.Close(), func(err error) error { + return E.Cause(err, "close process searcher") + }) + monitor.Finish() + } return err } From d2a933784cb31ae15521e8e71dca5c1fee77a74e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 16:49:01 +0800 Subject: [PATCH 47/99] Optimize Darwin process finder --- common/process/searcher.go | 2 +- common/process/searcher_darwin.go | 118 +------- common/process/searcher_darwin_shared.go | 269 ++++++++++++++++++ .../libbox/connection_owner_darwin.go | 57 ++++ 4 files changed, 330 insertions(+), 116 deletions(-) create mode 100644 common/process/searcher_darwin_shared.go create mode 100644 experimental/libbox/connection_owner_darwin.go diff --git a/common/process/searcher.go b/common/process/searcher.go index 743a8037..64305237 100644 --- a/common/process/searcher.go +++ b/common/process/searcher.go @@ -29,7 +29,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou if err != nil { return nil, err } - if info.UserId != -1 { + if info.UserId != -1 && info.UserName == "" { osUser, _ := user.LookupId(F.ToString(info.UserId)) if osUser != nil { info.UserName = osUser.Username diff --git a/common/process/searcher_darwin.go b/common/process/searcher_darwin.go index 7cc3083e..1b5c0dd6 100644 --- a/common/process/searcher_darwin.go +++ b/common/process/searcher_darwin.go @@ -1,19 +1,15 @@ +//go:build darwin + package process import ( "context" - "encoding/binary" "net/netip" - "os" "strconv" "strings" "syscall" - "unsafe" "github.com/sagernet/sing-box/adapter" - N "github.com/sagernet/sing/common/network" - - "golang.org/x/sys/unix" ) var _ Searcher = (*darwinSearcher)(nil) @@ -29,11 +25,7 @@ func (d *darwinSearcher) Close() error { } func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { - processName, err := findProcessName(network, source.Addr(), int(source.Port())) - if err != nil { - return nil, err - } - return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil + return FindDarwinConnectionOwner(network, source, destination) } var structSize = func() int { @@ -51,107 +43,3 @@ var structSize = func() int { 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])) -} diff --git a/common/process/searcher_darwin_shared.go b/common/process/searcher_darwin_shared.go new file mode 100644 index 00000000..05925530 --- /dev/null +++ b/common/process/searcher_darwin_shared.go @@ -0,0 +1,269 @@ +//go:build darwin + +package process + +import ( + "encoding/binary" + "net/netip" + "os" + "sync" + "syscall" + "time" + "unsafe" + + "github.com/sagernet/sing-box/adapter" + N "github.com/sagernet/sing/common/network" + + "golang.org/x/sys/unix" +) + +const ( + darwinSnapshotTTL = 200 * time.Millisecond + + darwinXinpgenSize = 24 + darwinXsocketOffset = 104 + darwinXinpcbForeignPort = 16 + darwinXinpcbLocalPort = 18 + darwinXinpcbVFlag = 44 + darwinXinpcbForeignAddr = 48 + darwinXinpcbLocalAddr = 64 + darwinXinpcbIPv4Addr = 12 + darwinXsocketUID = 64 + darwinXsocketLastPID = 68 + darwinTCPExtraStructSize = 208 +) + +type darwinConnectionEntry struct { + localAddr netip.Addr + remoteAddr netip.Addr + localPort uint16 + remotePort uint16 + pid uint32 + uid int32 +} + +type darwinConnectionMatchKind uint8 + +const ( + darwinConnectionMatchExact darwinConnectionMatchKind = iota + darwinConnectionMatchLocalFallback + darwinConnectionMatchWildcardFallback +) + +type darwinSnapshot struct { + createdAt time.Time + entries []darwinConnectionEntry +} + +type darwinConnectionFinder struct { + access sync.Mutex + ttl time.Duration + snapshots map[string]darwinSnapshot + builder func(string) (darwinSnapshot, error) +} + +var sharedDarwinConnectionFinder = newDarwinConnectionFinder(darwinSnapshotTTL) + +func newDarwinConnectionFinder(ttl time.Duration) *darwinConnectionFinder { + return &darwinConnectionFinder{ + ttl: ttl, + snapshots: make(map[string]darwinSnapshot), + builder: buildDarwinSnapshot, + } +} + +func FindDarwinConnectionOwner(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + return sharedDarwinConnectionFinder.find(network, source, destination) +} + +func (f *darwinConnectionFinder) find(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) { + networkName := N.NetworkName(network) + source = normalizeDarwinAddrPort(source) + destination = normalizeDarwinAddrPort(destination) + var lastOwner *adapter.ConnectionOwner + for attempt := 0; attempt < 2; attempt++ { + snapshot, fromCache, err := f.loadSnapshot(networkName, attempt > 0) + if err != nil { + return nil, err + } + entry, matchKind, err := matchDarwinConnectionEntry(snapshot.entries, networkName, source, destination) + if err != nil { + if err == ErrNotFound && fromCache { + continue + } + return nil, err + } + if fromCache && matchKind != darwinConnectionMatchExact { + continue + } + owner := &adapter.ConnectionOwner{ + UserId: entry.uid, + } + lastOwner = owner + if entry.pid == 0 { + return owner, nil + } + processPath, err := getExecPathFromPID(entry.pid) + if err == nil { + owner.ProcessPath = processPath + return owner, nil + } + if fromCache { + continue + } + return owner, nil + } + if lastOwner != nil { + return lastOwner, nil + } + return nil, ErrNotFound +} + +func (f *darwinConnectionFinder) loadSnapshot(network string, forceRefresh bool) (darwinSnapshot, bool, error) { + f.access.Lock() + defer f.access.Unlock() + if !forceRefresh { + if snapshot, loaded := f.snapshots[network]; loaded && time.Since(snapshot.createdAt) < f.ttl { + return snapshot, true, nil + } + } + snapshot, err := f.builder(network) + if err != nil { + return darwinSnapshot{}, false, err + } + f.snapshots[network] = snapshot + return snapshot, false, nil +} + +func buildDarwinSnapshot(network string) (darwinSnapshot, error) { + spath, itemSize, err := darwinSnapshotSettings(network) + if err != nil { + return darwinSnapshot{}, err + } + value, err := unix.SysctlRaw(spath) + if err != nil { + return darwinSnapshot{}, err + } + return darwinSnapshot{ + createdAt: time.Now(), + entries: parseDarwinSnapshot(value, itemSize), + }, nil +} + +func darwinSnapshotSettings(network string) (string, int, error) { + itemSize := structSize + switch network { + case N.NetworkTCP: + return "net.inet.tcp.pcblist_n", itemSize + darwinTCPExtraStructSize, nil + case N.NetworkUDP: + return "net.inet.udp.pcblist_n", itemSize, nil + default: + return "", 0, os.ErrInvalid + } +} + +func parseDarwinSnapshot(buf []byte, itemSize int) []darwinConnectionEntry { + entries := make([]darwinConnectionEntry, 0, (len(buf)-darwinXinpgenSize)/itemSize) + for i := darwinXinpgenSize; i+itemSize <= len(buf); i += itemSize { + inp := i + so := i + darwinXsocketOffset + entry, ok := parseDarwinConnectionEntry(buf[inp:so], buf[so:so+structSize-darwinXsocketOffset]) + if ok { + entries = append(entries, entry) + } + } + return entries +} + +func parseDarwinConnectionEntry(inp []byte, so []byte) (darwinConnectionEntry, bool) { + if len(inp) < darwinXsocketOffset || len(so) < structSize-darwinXsocketOffset { + return darwinConnectionEntry{}, false + } + entry := darwinConnectionEntry{ + remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]), + localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]), + pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]), + uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])), + } + flag := inp[darwinXinpcbVFlag] + switch { + case flag&0x1 != 0: + entry.remoteAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr : darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr+4])) + entry.localAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr : darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr+4])) + return entry, true + case flag&0x2 != 0: + entry.remoteAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16])) + entry.localAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16])) + return entry, true + default: + return darwinConnectionEntry{}, false + } +} + +func matchDarwinConnectionEntry(entries []darwinConnectionEntry, network string, source netip.AddrPort, destination netip.AddrPort) (darwinConnectionEntry, darwinConnectionMatchKind, error) { + sourceAddr := source.Addr() + if !sourceAddr.IsValid() { + return darwinConnectionEntry{}, darwinConnectionMatchExact, os.ErrInvalid + } + var localFallback darwinConnectionEntry + var hasLocalFallback bool + var wildcardFallback darwinConnectionEntry + var hasWildcardFallback bool + for _, entry := range entries { + if entry.localPort != source.Port() || sourceAddr.BitLen() != entry.localAddr.BitLen() { + continue + } + if entry.localAddr == sourceAddr && destination.IsValid() && entry.remotePort == destination.Port() && entry.remoteAddr == destination.Addr() { + return entry, darwinConnectionMatchExact, nil + } + if !destination.IsValid() && entry.localAddr == sourceAddr { + return entry, darwinConnectionMatchExact, nil + } + if network != N.NetworkUDP { + continue + } + if !hasLocalFallback && entry.localAddr == sourceAddr { + hasLocalFallback = true + localFallback = entry + } + if !hasWildcardFallback && entry.localAddr.IsUnspecified() { + hasWildcardFallback = true + wildcardFallback = entry + } + } + if hasLocalFallback { + return localFallback, darwinConnectionMatchLocalFallback, nil + } + if hasWildcardFallback { + return wildcardFallback, darwinConnectionMatchWildcardFallback, nil + } + return darwinConnectionEntry{}, darwinConnectionMatchExact, ErrNotFound +} + +func normalizeDarwinAddrPort(addrPort netip.AddrPort) netip.AddrPort { + if !addrPort.IsValid() { + return addrPort + } + return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()) +} + +func getExecPathFromPID(pid uint32) (string, error) { + const ( + procpidpathinfo = 0xb + procpidpathinfosize = 1024 + proccallnumpidinfo = 0x2 + ) + buf := make([]byte, procpidpathinfosize) + _, _, errno := syscall.Syscall6( + syscall.SYS_PROC_INFO, + proccallnumpidinfo, + uintptr(pid), + procpidpathinfo, + 0, + uintptr(unsafe.Pointer(&buf[0])), + procpidpathinfosize) + if errno != 0 { + return "", errno + } + return unix.ByteSliceToString(buf), nil +} diff --git a/experimental/libbox/connection_owner_darwin.go b/experimental/libbox/connection_owner_darwin.go new file mode 100644 index 00000000..20220106 --- /dev/null +++ b/experimental/libbox/connection_owner_darwin.go @@ -0,0 +1,57 @@ +package libbox + +import ( + "net/netip" + "os/user" + "syscall" + + "github.com/sagernet/sing-box/common/process" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +func FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (*ConnectionOwner, error) { + source, err := parseConnectionOwnerAddrPort(sourceAddress, sourcePort) + if err != nil { + return nil, E.Cause(err, "parse source") + } + destination, err := parseConnectionOwnerAddrPort(destinationAddress, destinationPort) + if err != nil { + return nil, E.Cause(err, "parse destination") + } + var network string + switch ipProtocol { + case syscall.IPPROTO_TCP: + network = "tcp" + case syscall.IPPROTO_UDP: + network = "udp" + default: + return nil, E.New("unknown protocol: ", ipProtocol) + } + owner, err := process.FindDarwinConnectionOwner(network, source, destination) + if err != nil { + return nil, err + } + result := &ConnectionOwner{ + UserId: owner.UserId, + ProcessPath: owner.ProcessPath, + } + if owner.UserId != -1 && owner.UserName == "" { + osUser, _ := user.LookupId(F.ToString(owner.UserId)) + if osUser != nil { + result.UserName = osUser.Username + } + } + return result, nil +} + +func parseConnectionOwnerAddrPort(address string, port int32) (netip.AddrPort, error) { + if port < 0 || port > 65535 { + return netip.AddrPort{}, E.New("invalid port: ", port) + } + addr, err := netip.ParseAddr(address) + if err != nil { + return netip.AddrPort{}, err + } + return netip.AddrPortFrom(addr.Unmap(), uint16(port)), nil +} From 0045103d1423f24cbdecc871d3feca94b6bfb7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 18:33:03 +0800 Subject: [PATCH 48/99] Fix `package_name` shared uid matching --- adapter/platform.go | 10 ++++---- common/process/searcher_android.go | 23 +++++++++--------- daemon/started_service.go | 10 ++++---- daemon/started_service.pb.go | 14 +++++------ daemon/started_service.proto | 2 +- .../clashapi/trafficontrol/tracker.go | 4 ++-- experimental/libbox/command_types.go | 24 +++++++++++-------- experimental/libbox/platform.go | 16 +++++++++---- experimental/libbox/service.go | 8 +++---- go.mod | 2 +- go.sum | 4 ++-- route/route.go | 4 ++-- route/rule/rule_item_package_name.go | 9 +++++-- 13 files changed, 74 insertions(+), 56 deletions(-) diff --git a/adapter/platform.go b/adapter/platform.go index 95db93c6..fa4cbc2e 100644 --- a/adapter/platform.go +++ b/adapter/platform.go @@ -47,11 +47,11 @@ type FindConnectionOwnerRequest struct { } type ConnectionOwner struct { - ProcessID uint32 - UserId int32 - UserName string - ProcessPath string - AndroidPackageName string + ProcessID uint32 + UserId int32 + UserName string + ProcessPath string + AndroidPackageNames []string } type Notification struct { diff --git a/common/process/searcher_android.go b/common/process/searcher_android.go index 48d853cf..287c7219 100644 --- a/common/process/searcher_android.go +++ b/common/process/searcher_android.go @@ -6,6 +6,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" ) var _ Searcher = (*androidSearcher)(nil) @@ -31,17 +32,17 @@ func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, s if err != nil { return nil, err } - if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded { - return &adapter.ConnectionOwner{ - UserId: int32(uid), - AndroidPackageName: sharedPackage, - }, nil + appID := uid % 100000 + var packageNames []string + if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded { + packageNames = append(packageNames, sharedPackage) } - if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded { - return &adapter.ConnectionOwner{ - UserId: int32(uid), - AndroidPackageName: packageName, - }, nil + if packages, loaded := s.packageManager.PackagesByID(appID); loaded { + packageNames = append(packageNames, packages...) } - return &adapter.ConnectionOwner{UserId: int32(uid)}, nil + packageNames = common.Uniq(packageNames) + return &adapter.ConnectionOwner{ + UserId: int32(uid), + AndroidPackageNames: packageNames, + }, nil } diff --git a/daemon/started_service.go b/daemon/started_service.go index e6e07511..c260e8cb 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -950,11 +950,11 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection { var processInfo *ProcessInfo if metadata.Metadata.ProcessInfo != nil { processInfo = &ProcessInfo{ - ProcessId: metadata.Metadata.ProcessInfo.ProcessID, - UserId: metadata.Metadata.ProcessInfo.UserId, - UserName: metadata.Metadata.ProcessInfo.UserName, - ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath, - PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName, + ProcessId: metadata.Metadata.ProcessInfo.ProcessID, + UserId: metadata.Metadata.ProcessInfo.UserId, + UserName: metadata.Metadata.ProcessInfo.UserName, + ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath, + PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames, } } return &Connection{ diff --git a/daemon/started_service.pb.go b/daemon/started_service.pb.go index ef9ea825..927fb514 100644 --- a/daemon/started_service.pb.go +++ b/daemon/started_service.pb.go @@ -1460,7 +1460,7 @@ type ProcessInfo struct { UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,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"` - PackageName string `protobuf:"bytes,5,opt,name=packageName,proto3" json:"packageName,omitempty"` + PackageNames []string `protobuf:"bytes,5,rep,name=packageNames,proto3" json:"packageNames,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1523,11 +1523,11 @@ func (x *ProcessInfo) GetProcessPath() string { return "" } -func (x *ProcessInfo) GetPackageName() string { +func (x *ProcessInfo) GetPackageNames() []string { if x != nil { - return x.PackageName + return x.PackageNames } - return "" + return nil } type CloseConnectionRequest struct { @@ -1884,13 +1884,13 @@ const file_daemon_started_service_proto_rawDesc = "" + "\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" + "\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" + "\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" + - "\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa3\x01\n" + + "\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa5\x01\n" + "\vProcessInfo\x12\x1c\n" + "\tprocessId\x18\x01 \x01(\rR\tprocessId\x12\x16\n" + "\x06userId\x18\x02 \x01(\x05R\x06userId\x12\x1a\n" + "\buserName\x18\x03 \x01(\tR\buserName\x12 \n" + - "\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12 \n" + - "\vpackageName\x18\x05 \x01(\tR\vpackageName\"(\n" + + "\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12\"\n" + + "\fpackageNames\x18\x05 \x03(\tR\fpackageNames\"(\n" + "\x16CloseConnectionRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\"K\n" + "\x12DeprecatedWarnings\x125\n" + diff --git a/daemon/started_service.proto b/daemon/started_service.proto index cc778f91..8a76081a 100644 --- a/daemon/started_service.proto +++ b/daemon/started_service.proto @@ -195,7 +195,7 @@ message ProcessInfo { int32 userId = 2; string userName = 3; string processPath = 4; - string packageName = 5; + repeated string packageNames = 5; } message CloseConnectionRequest { diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 23500cd0..f001b77b 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -45,8 +45,8 @@ func (t TrackerMetadata) MarshalJSON() ([]byte, error) { if t.Metadata.ProcessInfo != nil { if t.Metadata.ProcessInfo.ProcessPath != "" { processPath = t.Metadata.ProcessInfo.ProcessPath - } else if t.Metadata.ProcessInfo.AndroidPackageName != "" { - processPath = t.Metadata.ProcessInfo.AndroidPackageName + } else if len(t.Metadata.ProcessInfo.AndroidPackageNames) > 0 { + processPath = t.Metadata.ProcessInfo.AndroidPackageNames[0] } if processPath == "" { if t.Metadata.ProcessInfo.UserId != -1 { diff --git a/experimental/libbox/command_types.go b/experimental/libbox/command_types.go index 39027ac7..c330dd4b 100644 --- a/experimental/libbox/command_types.go +++ b/experimental/libbox/command_types.go @@ -239,11 +239,15 @@ func (c *Connections) Iterator() ConnectionIterator { } type ProcessInfo struct { - ProcessID int64 - UserID int32 - UserName string - ProcessPath string - PackageName string + ProcessID int64 + UserID int32 + UserName string + ProcessPath string + packageNames []string +} + +func (p *ProcessInfo) PackageNames() StringIterator { + return newIterator(p.packageNames) } type Connection struct { @@ -339,11 +343,11 @@ func connectionFromGRPC(conn *daemon.Connection) Connection { var processInfo *ProcessInfo if conn.ProcessInfo != nil { processInfo = &ProcessInfo{ - ProcessID: int64(conn.ProcessInfo.ProcessId), - UserID: conn.ProcessInfo.UserId, - UserName: conn.ProcessInfo.UserName, - ProcessPath: conn.ProcessInfo.ProcessPath, - PackageName: conn.ProcessInfo.PackageName, + ProcessID: int64(conn.ProcessInfo.ProcessId), + UserID: conn.ProcessInfo.UserId, + UserName: conn.ProcessInfo.UserName, + ProcessPath: conn.ProcessInfo.ProcessPath, + packageNames: conn.ProcessInfo.PackageNames, } } return Connection{ diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index 63c54ccf..4db32a22 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -24,10 +24,18 @@ type PlatformInterface interface { } type ConnectionOwner struct { - UserId int32 - UserName string - ProcessPath string - AndroidPackageName string + UserId int32 + UserName string + ProcessPath string + androidPackageNames []string +} + +func (c *ConnectionOwner) SetAndroidPackageNames(names StringIterator) { + c.androidPackageNames = iteratorToArray[string](names) +} + +func (c *ConnectionOwner) AndroidPackageNames() StringIterator { + return newIterator(c.androidPackageNames) } type InterfaceUpdateListener interface { diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 3a13f6d1..0a841a1b 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -201,10 +201,10 @@ func (w *platformInterfaceWrapper) FindConnectionOwner(request *adapter.FindConn return nil, err } return &adapter.ConnectionOwner{ - UserId: result.UserId, - UserName: result.UserName, - ProcessPath: result.ProcessPath, - AndroidPackageName: result.AndroidPackageName, + UserId: result.UserId, + UserName: result.UserName, + ProcessPath: result.ProcessPath, + AndroidPackageNames: result.androidPackageNames, }, nil } diff --git a/go.mod b/go.mod index 2155b956..4072171a 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.4 + github.com/sagernet/sing-tun v0.8.5 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 diff --git a/go.sum b/go.sum index 502e5964..9c4f2278 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.4 h1:pZ/ZoBQTeVks75iS1w7Qe8brBEsPVT0ENiVvtbsFBGo= -github.com/sagernet/sing-tun v0.8.4/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-tun v0.8.5 h1:PiQDXJB+btQiYV2x/gZ3TC6hhXErWsmnwufYHVuu6Z8= +github.com/sagernet/sing-tun v0.8.5/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= diff --git a/route/route.go b/route/route.go index 7773ff3c..77b66ea4 100644 --- a/route/route.go +++ b/route/route.go @@ -426,8 +426,8 @@ func (r *Router) matchRule( } else { r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath) } - } else if processInfo.AndroidPackageName != "" { - r.logger.InfoContext(ctx, "found package name: ", processInfo.AndroidPackageName) + } else if len(processInfo.AndroidPackageNames) > 0 { + r.logger.InfoContext(ctx, "found package name: ", strings.Join(processInfo.AndroidPackageNames, ", ")) } else if processInfo.UserId != -1 { if processInfo.UserName != "" { r.logger.InfoContext(ctx, "found user: ", processInfo.UserName) diff --git a/route/rule/rule_item_package_name.go b/route/rule/rule_item_package_name.go index fa227587..514768de 100644 --- a/route/rule/rule_item_package_name.go +++ b/route/rule/rule_item_package_name.go @@ -25,10 +25,15 @@ func NewPackageNameItem(packageNameList []string) *PackageNameItem { } func (r *PackageNameItem) Match(metadata *adapter.InboundContext) bool { - if metadata.ProcessInfo == nil || metadata.ProcessInfo.AndroidPackageName == "" { + if metadata.ProcessInfo == nil || len(metadata.ProcessInfo.AndroidPackageNames) == 0 { return false } - return r.packageMap[metadata.ProcessInfo.AndroidPackageName] + for _, packageName := range metadata.ProcessInfo.AndroidPackageNames { + if r.packageMap[packageName] { + return true + } + } + return false } func (r *PackageNameItem) String() string { From 9ac1e2ff32f2e2222289aaa1d59eef8df6e7b6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 18:47:19 +0800 Subject: [PATCH 49/99] Match `package_name` in `process_path` rule on Android --- route/rule/rule_item_process_path.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/route/rule/rule_item_process_path.go b/route/rule/rule_item_process_path.go index 75dee476..ac5c6a18 100644 --- a/route/rule/rule_item_process_path.go +++ b/route/rule/rule_item_process_path.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" ) var _ RuleItem = (*ProcessPathItem)(nil) @@ -25,10 +26,20 @@ func NewProcessPathItem(processNameList []string) *ProcessPathItem { } func (r *ProcessPathItem) Match(metadata *adapter.InboundContext) bool { - if metadata.ProcessInfo == nil || metadata.ProcessInfo.ProcessPath == "" { + if metadata.ProcessInfo == nil { return false } - return r.processMap[metadata.ProcessInfo.ProcessPath] + if metadata.ProcessInfo.ProcessPath != "" && r.processMap[metadata.ProcessInfo.ProcessPath] { + return true + } + if C.IsAndroid { + for _, packageName := range metadata.ProcessInfo.AndroidPackageNames { + if r.processMap[packageName] { + return true + } + } + } + return false } func (r *ProcessPathItem) String() string { From 72bc4c1f879d36600afda03974e0a47fabac2f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 19:21:55 +0800 Subject: [PATCH 50/99] Fix DNS transport returning error for empty AAAA response Closes #3925 --- dns/transport/dhcp/dhcp_shared.go | 8 -------- dns/transport/local/local_shared.go | 8 -------- 2 files changed, 16 deletions(-) diff --git a/dns/transport/dhcp/dhcp_shared.go b/dns/transport/dhcp/dhcp_shared.go index 6aa83361..20cd50c5 100644 --- a/dns/transport/dhcp/dhcp_shared.go +++ b/dns/transport/dhcp/dhcp_shared.go @@ -7,7 +7,6 @@ import ( "strings" "syscall" - "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns/transport" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" @@ -40,13 +39,6 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr, results := make(chan queryResult) startRacer := func(ctx context.Context, fqdn string) { 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 { case results <- queryResult{response, err}: case <-returned: diff --git a/dns/transport/local/local_shared.go b/dns/transport/local/local_shared.go index 3b05dac6..77635458 100644 --- a/dns/transport/local/local_shared.go +++ b/dns/transport/local/local_shared.go @@ -7,7 +7,6 @@ import ( "syscall" "time" - "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns/transport" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" @@ -49,13 +48,6 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi results := make(chan queryResult) startRacer := func(ctx context.Context, fqdn string) { 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 { case results <- queryResult{response, err}: case <-returned: From a3623eb41adc9cc40eb99927fd744f0c926506a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 19:38:55 +0800 Subject: [PATCH 51/99] tun: Fix system stack rewriting TUN subnet destinations to loopback --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4072171a..e13f5627 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.5 + github.com/sagernet/sing-tun v0.8.6 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 diff --git a/go.sum b/go.sum index 9c4f2278..e095de83 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.5 h1:PiQDXJB+btQiYV2x/gZ3TC6hhXErWsmnwufYHVuu6Z8= -github.com/sagernet/sing-tun v0.8.5/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-tun v0.8.6 h1:NydXFikSXhiKqhahHKtuZ90HQPZFzlOFVRONmkr4C7I= +github.com/sagernet/sing-tun v0.8.6/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= From d454aa0fdfcce427cbbbd2348eb3a5295f26e986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 24 Mar 2026 12:50:37 +0800 Subject: [PATCH 52/99] route: formalize nested rule_set group-state semantics Before 795d1c289, nested rule-set evaluation reused the parent rule match cache. In practice, this meant these fields leaked across nested evaluation: - SourceAddressMatch - SourcePortMatch - DestinationAddressMatch - DestinationPortMatch - DidMatch That leak had two opposite effects. First, it made included rule-sets partially behave like the docs' "merged" semantics. For example, if an outer route rule had: rule_set = ["geosite-additional-!cn"] ip_cidr = 104.26.10.0/24 and the inline rule-set matched `domain_suffix = speedtest.net`, the inner match could set `DestinationAddressMatch = true` and the outer rule would then pass its destination-address group check. This is why some `rule_set + ip_cidr` combinations used to work. But the same leak also polluted sibling rules and sibling rule-sets. A branch could partially match one group, then fail later, and still leave that group cache set for the next branch. This broke cases such as gh-3485: with `rule_set = [test1, test2]`, `test1` could touch destination-address cache before an AdGuard `@@` exclusion made the whole branch fail, and `test2` would then run against dirty state. 795d1c289 fixed that by cloning metadata for nested rule-set/rule evaluation and resetting the rule match cache for each branch. That stopped sibling pollution, but it also removed the only mechanism by which a successful nested branch could affect the parent rule's grouped matching state. As a result, nested rule-sets became pure boolean sub-items against the outer rule. The previous example stopped working: the inner `domain_suffix = speedtest.net` still matched, but the outer rule no longer observed any destination-address-group success, so it fell through to `final`. This change makes the semantics explicit instead of relying on cache side effects: - `rule_set: ["a", "b"]` is OR - rules inside one rule-set are OR - each nested branch is evaluated in isolation - failed branches contribute no grouped match state - a successful branch contributes its grouped match state back to the parent rule - grouped state from different rule-sets must not be combined together to satisfy one outer rule In other words, rule-sets now behave as "OR branches whose successful group matches merge into the outer rule", which matches the documented intent without reintroducing cross-branch cache leakage. --- route/rule/match_state.go | 108 +++++ route/rule/rule_abstract.go | 199 ++++++--- route/rule/rule_abstract_test.go | 6 +- route/rule/rule_default.go | 10 +- route/rule/rule_dns.go | 51 +-- route/rule/rule_headless.go | 8 + route/rule/rule_item_rule_set.go | 11 +- route/rule/rule_set_local.go | 11 +- route/rule/rule_set_remote.go | 11 +- route/rule/rule_set_semantics_test.go | 620 ++++++++++++++++++++++++++ 10 files changed, 921 insertions(+), 114 deletions(-) create mode 100644 route/rule/match_state.go create mode 100644 route/rule/rule_set_semantics_test.go diff --git a/route/rule/match_state.go b/route/rule/match_state.go new file mode 100644 index 00000000..f537d5de --- /dev/null +++ b/route/rule/match_state.go @@ -0,0 +1,108 @@ +package rule + +import "github.com/sagernet/sing-box/adapter" + +type ruleMatchState uint8 + +const ( + ruleMatchSourceAddress ruleMatchState = 1 << iota + ruleMatchSourcePort + ruleMatchDestinationAddress + ruleMatchDestinationPort +) + +type ruleMatchStateSet uint16 + +func singleRuleMatchState(state ruleMatchState) ruleMatchStateSet { + return 1 << state +} + +func emptyRuleMatchState() ruleMatchStateSet { + return singleRuleMatchState(0) +} + +func (s ruleMatchStateSet) isEmpty() bool { + return s == 0 +} + +func (s ruleMatchStateSet) contains(state ruleMatchState) bool { + return s&(1< 0 +} + +func (r *abstractDefaultRule) destinationIPCIDRMatchesDestination(metadata *adapter.InboundContext) bool { + return !metadata.IgnoreDestinationIPCIDRMatch && !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0 +} + +func (r *abstractDefaultRule) requiresSourceAddressMatch(metadata *adapter.InboundContext) bool { + return len(r.sourceAddressItems) > 0 || r.destinationIPCIDRMatchesSource(metadata) +} + +func (r *abstractDefaultRule) requiresDestinationAddressMatch(metadata *adapter.InboundContext) bool { + return len(r.destinationAddressItems) > 0 || r.destinationIPCIDRMatchesDestination(metadata) +} + +func (r *abstractDefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { if len(r.allItems) == 0 { - return true + return emptyRuleMatchState() } - - if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch { + var baseState ruleMatchState + if len(r.sourceAddressItems) > 0 { metadata.DidMatch = true - for _, item := range r.sourceAddressItems { - if item.Match(metadata) { - metadata.SourceAddressMatch = true - break - } + if matchAnyItem(r.sourceAddressItems, metadata) { + baseState |= ruleMatchSourceAddress } } - - if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch { + if r.destinationIPCIDRMatchesSource(metadata) && !baseState.has(ruleMatchSourceAddress) { metadata.DidMatch = true - for _, item := range r.sourcePortItems { - if item.Match(metadata) { - metadata.SourcePortMatch = true - break - } + if matchAnyItem(r.destinationIPCIDRItems, metadata) { + baseState |= ruleMatchSourceAddress + } + } else if r.destinationIPCIDRMatchesSource(metadata) { + metadata.DidMatch = true + } + if len(r.sourcePortItems) > 0 { + metadata.DidMatch = true + if matchAnyItem(r.sourcePortItems, metadata) { + baseState |= ruleMatchSourcePort } } - - if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch { + if len(r.destinationAddressItems) > 0 { metadata.DidMatch = true - for _, item := range r.destinationAddressItems { - if item.Match(metadata) { - metadata.DestinationAddressMatch = true - break - } + if matchAnyItem(r.destinationAddressItems, metadata) { + baseState |= ruleMatchDestinationAddress } } - - if !metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0 && !metadata.DestinationAddressMatch { + if r.destinationIPCIDRMatchesDestination(metadata) && !baseState.has(ruleMatchDestinationAddress) { metadata.DidMatch = true - for _, item := range r.destinationIPCIDRItems { - if item.Match(metadata) { - metadata.DestinationAddressMatch = true - break - } + if matchAnyItem(r.destinationIPCIDRItems, metadata) { + baseState |= ruleMatchDestinationAddress + } + } else if r.destinationIPCIDRMatchesDestination(metadata) { + metadata.DidMatch = true + } + if len(r.destinationPortItems) > 0 { + metadata.DidMatch = true + if matchAnyItem(r.destinationPortItems, metadata) { + baseState |= ruleMatchDestinationPort } } - - if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch { - metadata.DidMatch = true - for _, item := range r.destinationPortItems { - if item.Match(metadata) { - metadata.DestinationPortMatch = true - break - } - } - } - for _, item := range r.items { metadata.DidMatch = true if !item.Match(metadata) { - return r.invert + return r.invertedFailure() } } - - if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch { - return r.invert + stateSet := singleRuleMatchState(baseState) + if r.ruleSetItem != nil { + metadata.DidMatch = true + ruleSetStates := matchRuleItemStates(r.ruleSetItem, metadata) + if ruleSetStates.isEmpty() { + return r.invertedFailure() + } + stateSet = ruleSetStates.withBase(baseState) } - - if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch { - return r.invert - } - - if ((!metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0) || len(r.destinationAddressItems) > 0) && !metadata.DestinationAddressMatch { - return r.invert - } - - if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch { - return r.invert - } - - if !metadata.DidMatch { + stateSet = stateSet.filter(func(state ruleMatchState) bool { + if r.requiresSourceAddressMatch(metadata) && !state.has(ruleMatchSourceAddress) { + return false + } + if len(r.sourcePortItems) > 0 && !state.has(ruleMatchSourcePort) { + return false + } + if r.requiresDestinationAddressMatch(metadata) && !state.has(ruleMatchDestinationAddress) { + return false + } + if len(r.destinationPortItems) > 0 && !state.has(ruleMatchDestinationPort) { + return false + } return true + }) + if stateSet.isEmpty() { + return r.invertedFailure() } + if r.invert { + // DNS pre-lookup defers destination address-limit checks until the response phase. + if metadata.IgnoreDestinationIPCIDRMatch && stateSet == emptyRuleMatchState() && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 { + return emptyRuleMatchState() + } + return 0 + } + return stateSet +} - return !r.invert +func (r *abstractDefaultRule) invertedFailure() ruleMatchStateSet { + if r.invert { + return emptyRuleMatchState() + } + return 0 } func (r *abstractDefaultRule) Action() adapter.RuleAction { @@ -191,17 +221,42 @@ func (r *abstractLogicalRule) Close() error { } func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { + return !r.matchStates(metadata).isEmpty() +} + +func (r *abstractLogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + var stateSet ruleMatchStateSet if r.mode == C.LogicalTypeAnd { - return common.All(r.rules, func(it adapter.HeadlessRule) bool { - metadata.ResetRuleCache() - return it.Match(metadata) - }) != r.invert + stateSet = emptyRuleMatchState() + for _, rule := range r.rules { + nestedMetadata := *metadata + nestedMetadata.ResetRuleCache() + nestedStateSet := matchHeadlessRuleStates(rule, &nestedMetadata) + if nestedStateSet.isEmpty() { + if r.invert { + return emptyRuleMatchState() + } + return 0 + } + stateSet = stateSet.combine(nestedStateSet) + } } else { - return common.Any(r.rules, func(it adapter.HeadlessRule) bool { - metadata.ResetRuleCache() - return it.Match(metadata) - }) != r.invert + for _, rule := range r.rules { + nestedMetadata := *metadata + nestedMetadata.ResetRuleCache() + stateSet = stateSet.merge(matchHeadlessRuleStates(rule, &nestedMetadata)) + } + if stateSet.isEmpty() { + if r.invert { + return emptyRuleMatchState() + } + return 0 + } } + if r.invert { + return 0 + } + return stateSet } func (r *abstractLogicalRule) Action() adapter.RuleAction { @@ -222,3 +277,13 @@ func (r *abstractLogicalRule) String() string { return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")" } } + +func matchAnyItem(items []RuleItem, metadata *adapter.InboundContext) bool { + return common.Any(items, func(it RuleItem) bool { + return it.Match(metadata) + }) +} + +func (s ruleMatchState) has(target ruleMatchState) bool { + return s&target != 0 +} diff --git a/route/rule/rule_abstract_test.go b/route/rule/rule_abstract_test.go index 2d2e8ba8..ace3dec6 100644 --- a/route/rule/rule_abstract_test.go +++ b/route/rule/rule_abstract_test.go @@ -78,9 +78,9 @@ func newRuleSetOnlyRule(ruleSetMatched bool, invert bool) *DefaultRule { } return &DefaultRule{ abstractDefaultRule: abstractDefaultRule{ - items: []RuleItem{ruleSetItem}, - allItems: []RuleItem{ruleSetItem}, - invert: invert, + ruleSetItem: ruleSetItem, + allItems: []RuleItem{ruleSetItem}, + invert: invert, }, } } diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index 202fb3b3..b921c8b2 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -47,6 +47,10 @@ type DefaultRule struct { abstractDefaultRule } +func (r *DefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractDefaultRule.matchStates(metadata) +} + type RuleItem interface { Match(metadata *adapter.InboundContext) bool String() string @@ -275,7 +279,7 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio matchSource = true } item := NewRuleSetItem(router, options.RuleSet, matchSource, false) - rule.items = append(rule.items, item) + rule.ruleSetItem = item rule.allItems = append(rule.allItems, item) } return rule, nil @@ -287,6 +291,10 @@ type LogicalRule struct { abstractLogicalRule } +func (r *LogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractLogicalRule.matchStates(metadata) +} + func NewLogicalRule(ctx context.Context, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { action, err := NewRuleAction(ctx, logger, options.RuleAction) if err != nil { diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 9235dd6f..04f0f236 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -47,6 +47,10 @@ type DefaultDNSRule struct { abstractDefaultRule } +func (r *DefaultDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractDefaultRule.matchStates(metadata) +} + func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) { rule := &DefaultDNSRule{ abstractDefaultRule: abstractDefaultRule{ @@ -271,7 +275,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op matchSource = true } item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty) - rule.items = append(rule.items, item) + rule.ruleSetItem = item rule.allItems = append(rule.allItems, item) } return rule, nil @@ -285,12 +289,9 @@ func (r *DefaultDNSRule) WithAddressLimit() bool { if len(r.destinationIPCIDRItems) > 0 { return true } - for _, rawRule := range r.items { - ruleSet, isRuleSet := rawRule.(*RuleSetItem) - if !isRuleSet { - continue - } - if ruleSet.ContainsDestinationIPCIDRRule() { + if r.ruleSetItem != nil { + ruleSet, isRuleSet := r.ruleSetItem.(*RuleSetItem) + if isRuleSet && ruleSet.ContainsDestinationIPCIDRRule() { return true } } @@ -302,11 +303,11 @@ func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool { defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }() - return r.abstractDefaultRule.Match(metadata) + return !r.matchStates(metadata).isEmpty() } func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool { - return r.abstractDefaultRule.Match(metadata) + return !r.matchStates(metadata).isEmpty() } var _ adapter.DNSRule = (*LogicalDNSRule)(nil) @@ -315,6 +316,10 @@ type LogicalDNSRule struct { abstractLogicalRule } +func (r *LogicalDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractLogicalRule.matchStates(metadata) +} + func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ @@ -362,29 +367,13 @@ func (r *LogicalDNSRule) WithAddressLimit() bool { } func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool { - if r.mode == C.LogicalTypeAnd { - return common.All(r.rules, func(it adapter.HeadlessRule) bool { - metadata.ResetRuleCache() - return it.(adapter.DNSRule).Match(metadata) - }) != r.invert - } else { - return common.Any(r.rules, func(it adapter.HeadlessRule) bool { - metadata.ResetRuleCache() - return it.(adapter.DNSRule).Match(metadata) - }) != r.invert - } + metadata.IgnoreDestinationIPCIDRMatch = true + defer func() { + metadata.IgnoreDestinationIPCIDRMatch = false + }() + return !r.matchStates(metadata).isEmpty() } func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool { - if r.mode == C.LogicalTypeAnd { - return common.All(r.rules, func(it adapter.HeadlessRule) bool { - metadata.ResetRuleCache() - return it.(adapter.DNSRule).MatchAddressLimit(metadata) - }) != r.invert - } else { - return common.Any(r.rules, func(it adapter.HeadlessRule) bool { - metadata.ResetRuleCache() - return it.(adapter.DNSRule).MatchAddressLimit(metadata) - }) != r.invert - } + return !r.matchStates(metadata).isEmpty() } diff --git a/route/rule/rule_headless.go b/route/rule/rule_headless.go index 689e6e3e..f180bacc 100644 --- a/route/rule/rule_headless.go +++ b/route/rule/rule_headless.go @@ -34,6 +34,10 @@ type DefaultHeadlessRule struct { abstractDefaultRule } +func (r *DefaultHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractDefaultRule.matchStates(metadata) +} + func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { networkManager := service.FromContext[adapter.NetworkManager](ctx) rule := &DefaultHeadlessRule{ @@ -199,6 +203,10 @@ type LogicalHeadlessRule struct { abstractLogicalRule } +func (r *LogicalHeadlessRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.abstractLogicalRule.matchStates(metadata) +} + func NewLogicalHeadlessRule(ctx context.Context, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { r := &LogicalHeadlessRule{ abstractLogicalRule{ diff --git a/route/rule/rule_item_rule_set.go b/route/rule/rule_item_rule_set.go index 858bb877..0916279d 100644 --- a/route/rule/rule_item_rule_set.go +++ b/route/rule/rule_item_rule_set.go @@ -41,16 +41,19 @@ func (r *RuleSetItem) Start() error { } func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { + return !r.matchStates(metadata).isEmpty() +} + +func (r *RuleSetItem) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + var stateSet ruleMatchStateSet for _, ruleSet := range r.setList { nestedMetadata := *metadata nestedMetadata.ResetRuleMatchCache() nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty - if ruleSet.Match(&nestedMetadata) { - return true - } + stateSet = stateSet.merge(matchHeadlessRuleStates(ruleSet, &nestedMetadata)) } - return false + return stateSet } func (r *RuleSetItem) ContainsDestinationIPCIDRRule() bool { diff --git a/route/rule/rule_set_local.go b/route/rule/rule_set_local.go index 8409831b..ec0f91b2 100644 --- a/route/rule/rule_set_local.go +++ b/route/rule/rule_set_local.go @@ -202,12 +202,15 @@ func (s *LocalRuleSet) Close() error { } func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { + return !s.matchStates(metadata).isEmpty() +} + +func (s *LocalRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + var stateSet ruleMatchStateSet for _, rule := range s.rules { nestedMetadata := *metadata nestedMetadata.ResetRuleMatchCache() - if rule.Match(&nestedMetadata) { - return true - } + stateSet = stateSet.merge(matchHeadlessRuleStates(rule, &nestedMetadata)) } - return false + return stateSet } diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index 81a8d3fc..c85dc859 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -322,12 +322,15 @@ func (s *RemoteRuleSet) Close() error { } func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { + return !s.matchStates(metadata).isEmpty() +} + +func (s *RemoteRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + var stateSet ruleMatchStateSet for _, rule := range s.rules { nestedMetadata := *metadata nestedMetadata.ResetRuleMatchCache() - if rule.Match(&nestedMetadata) { - return true - } + stateSet = stateSet.merge(matchHeadlessRuleStates(rule, &nestedMetadata)) } - return false + return stateSet } diff --git a/route/rule/rule_set_semantics_test.go b/route/rule/rule_set_semantics_test.go new file mode 100644 index 00000000..27461ce6 --- /dev/null +++ b/route/rule/rule_set_semantics_test.go @@ -0,0 +1,620 @@ +package rule + +import ( + "context" + "net/netip" + "strings" + "testing" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/convertor/adguard" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + slogger "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/stretchr/testify/require" +) + +func TestRouteRuleSetMergeDestinationAddressGroup(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + metadata adapter.InboundContext + inner adapter.HeadlessRule + }{ + { + name: "domain", + metadata: testMetadata("www.example.com"), + inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, []string{"www.example.com"}, nil) }), + }, + { + name: "domain_suffix", + metadata: testMetadata("www.example.com"), + inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationAddressItem(t, rule, nil, []string{"example.com"}) }), + }, + { + name: "domain_keyword", + metadata: testMetadata("www.example.com"), + inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationKeywordItem(rule, []string{"example"}) }), + }, + { + name: "domain_regex", + metadata: testMetadata("www.example.com"), + inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { addDestinationRegexItem(t, rule, []string{`^www\.example\.com$`}) }), + }, + { + name: "ip_cidr", + metadata: func() adapter.InboundContext { + metadata := testMetadata("lookup.example") + metadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("8.8.8.8")} + return metadata + }(), + inner: headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"8.8.8.0/24"}) + }), + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + ruleSet := newLocalRuleSetForTest("merge-destination", testCase.inner) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.Match(&testCase.metadata)) + }) + } +} + +func TestRouteRuleSetMergeSourceAndPortGroups(t *testing.T) { + t.Parallel() + t.Run("source address", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-source-address", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addSourceAddressItem(t, rule, []string{"10.0.0.0/8"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addSourceAddressItem(t, rule, []string{"198.51.100.0/24"}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("source address via ruleset ipcidr match source", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-source-address-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"10.0.0.0/8"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{ + setList: []adapter.RuleSet{ruleSet}, + ipCidrMatchSource: true, + }) + addSourceAddressItem(t, rule, []string{"198.51.100.0/24"}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("destination port", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-destination-port", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationPortItem(rule, []uint16{443}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationPortItem(rule, []uint16{8443}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("destination port range", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-destination-port-range", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationPortRangeItem(t, rule, []string{"400:500"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationPortItem(rule, []uint16{8443}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("source port", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-source-port", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addSourcePortItem(rule, []uint16{1000}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addSourcePortItem(rule, []uint16{2000}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("source port range", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("merge-source-port-range", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addSourcePortRangeItem(t, rule, []string{"900:1100"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addSourcePortItem(rule, []uint16{2000}) + }) + require.True(t, rule.Match(&metadata)) + }) +} + +func TestRouteRuleSetOtherFieldsStayAnd(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("other-fields-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + }) + require.False(t, rule.Match(&metadata)) +} + +func TestRouteRuleSetOrSemantics(t *testing.T) { + t.Parallel() + t.Run("later ruleset can satisfy outer group", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + emptyStateSet := newLocalRuleSetForTest("network-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + })) + destinationStateSet := newLocalRuleSetForTest("domain-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptyStateSet, destinationStateSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("later rule in same set can satisfy outer group", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest( + "rule-set-or", + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }), + ) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("cross ruleset union is not allowed", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + sourceStateSet := newLocalRuleSetForTest("source-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addSourcePortItem(rule, []uint16{1000}) + })) + destinationStateSet := newLocalRuleSetForTest("destination-only", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{sourceStateSet, destinationStateSet}}) + addSourcePortItem(rule, []uint16{2000}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.False(t, rule.Match(&metadata)) + }) +} + +func TestRouteRuleSetLogicalSemantics(t *testing.T) { + t.Parallel() + t.Run("logical or keeps all successful branch states", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("logical-or", headlessLogicalRule( + C.LogicalTypeOr, + false, + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }), + )) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("logical and unions child states", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("logical-and", headlessLogicalRule( + C.LogicalTypeAnd, + false, + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addSourcePortItem(rule, []uint16{1000}) + }), + )) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + addSourcePortItem(rule, []uint16{2000}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("invert success does not contribute positive state", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("invert", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + rule.invert = true + addDestinationAddressItem(t, rule, nil, []string{"cn"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.False(t, rule.Match(&metadata)) + }) +} + +func TestRouteRuleSetNoLeakageRegressions(t *testing.T) { + t.Parallel() + t.Run("same ruleset failed branch does not leak", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest( + "same-set", + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addSourcePortItem(rule, []uint16{1}) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + addSourcePortItem(rule, []uint16{1000}) + }), + ) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) + t.Run("adguard exclusion remains isolated across rulesets", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("im.qq.com") + excludeSet := newLocalRuleSetForTest("adguard", mustAdGuardRule(t, "@@||im.qq.com^\n||whatever1.com^\n")) + otherSet := newLocalRuleSetForTest("other", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"whatever2.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{excludeSet, otherSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) +} + +func TestDefaultRuleDoesNotReuseGroupedMatchCacheAcrossEvaluations(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }) + require.True(t, rule.Match(&metadata)) + + metadata.Destination.Fqdn = "www.example.org" + require.False(t, rule.Match(&metadata)) +} + +func TestRouteRuleSetRemoteUsesSameSemantics(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newRemoteRuleSetForTest( + "remote", + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + }), + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }), + ) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.Match(&metadata)) +} + +func TestDNSRuleSetSemantics(t *testing.T) { + t.Parallel() + t.Run("match address limit merges destination group", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("dns-merge", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.MatchAddressLimit(&metadata)) + }) + t.Run("dns keeps ruleset or semantics", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + emptyStateSet := newLocalRuleSetForTest("dns-empty", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + })) + destinationStateSet := newLocalRuleSetForTest("dns-destination", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptyStateSet, destinationStateSet}}) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.True(t, rule.MatchAddressLimit(&metadata)) + }) + t.Run("ruleset ip cidr flags stay scoped", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("dns-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addRuleSetItem(rule, &RuleSetItem{ + setList: []adapter.RuleSet{ruleSet}, + ipCidrAcceptEmpty: true, + }) + }) + require.True(t, rule.MatchAddressLimit(&metadata)) + require.False(t, metadata.IPCIDRMatchSource) + require.False(t, metadata.IPCIDRAcceptEmpty) + }) +} + +func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + build func(*testing.T, *abstractDefaultRule) + matchedAddrs []netip.Addr + unmatchedAddrs []netip.Addr + }{ + { + name: "ip_cidr", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")}, + unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, + }, + { + name: "ip_is_private", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPIsPrivateItem(rule) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.1")}, + unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")}, + }, + { + name: "ip_accept_any", + build: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPAcceptAnyItem(rule) + }, + matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")}, + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + testCase.build(t, rule) + }) + + preLookupMetadata := testMetadata("lookup.example") + require.True(t, rule.Match(&preLookupMetadata)) + + matchedMetadata := testMetadata("lookup.example") + matchedMetadata.DestinationAddresses = testCase.matchedAddrs + require.False(t, rule.MatchAddressLimit(&matchedMetadata)) + + unmatchedMetadata := testMetadata("lookup.example") + unmatchedMetadata.DestinationAddresses = testCase.unmatchedAddrs + require.True(t, rule.MatchAddressLimit(&unmatchedMetadata)) + }) + } + t.Run("mixed resolved and deferred fields keep old pre lookup false", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("lookup.example") + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP})) + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }) + require.False(t, rule.Match(&metadata)) + }) + t.Run("ruleset only deferred fields keep old pre lookup false", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("lookup.example") + ruleSet := newLocalRuleSetForTest("dns-ruleset-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + rule.invert = true + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) +} + +func routeRuleForTest(build func(*abstractDefaultRule)) *DefaultRule { + rule := &DefaultRule{} + build(&rule.abstractDefaultRule) + return rule +} + +func dnsRuleForTest(build func(*abstractDefaultRule)) *DefaultDNSRule { + rule := &DefaultDNSRule{} + build(&rule.abstractDefaultRule) + return rule +} + +func headlessDefaultRule(t *testing.T, build func(*abstractDefaultRule)) *DefaultHeadlessRule { + t.Helper() + rule := &DefaultHeadlessRule{} + build(&rule.abstractDefaultRule) + return rule +} + +func headlessLogicalRule(mode string, invert bool, rules ...adapter.HeadlessRule) *LogicalHeadlessRule { + return &LogicalHeadlessRule{ + abstractLogicalRule: abstractLogicalRule{ + rules: rules, + mode: mode, + invert: invert, + }, + } +} + +func newLocalRuleSetForTest(tag string, rules ...adapter.HeadlessRule) *LocalRuleSet { + return &LocalRuleSet{ + tag: tag, + rules: rules, + } +} + +func newRemoteRuleSetForTest(tag string, rules ...adapter.HeadlessRule) *RemoteRuleSet { + return &RemoteRuleSet{ + options: option.RuleSet{Tag: tag}, + rules: rules, + } +} + +func mustAdGuardRule(t *testing.T, content string) adapter.HeadlessRule { + t.Helper() + rules, err := adguard.ToOptions(strings.NewReader(content), slogger.NOP()) + require.NoError(t, err) + require.Len(t, rules, 1) + rule, err := NewHeadlessRule(context.Background(), rules[0]) + require.NoError(t, err) + return rule +} + +func testMetadata(domain string) adapter.InboundContext { + return adapter.InboundContext{ + Network: N.NetworkTCP, + Source: M.Socksaddr{ + Addr: netip.MustParseAddr("10.0.0.1"), + Port: 1000, + }, + Destination: M.Socksaddr{ + Fqdn: domain, + Port: 443, + }, + } +} + +func addRuleSetItem(rule *abstractDefaultRule, item *RuleSetItem) { + rule.ruleSetItem = item + rule.allItems = append(rule.allItems, item) +} + +func addOtherItem(rule *abstractDefaultRule, item RuleItem) { + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) +} + +func addSourceAddressItem(t *testing.T, rule *abstractDefaultRule, cidrs []string) { + t.Helper() + item, err := NewIPCIDRItem(true, cidrs) + require.NoError(t, err) + rule.sourceAddressItems = append(rule.sourceAddressItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationAddressItem(t *testing.T, rule *abstractDefaultRule, domains []string, suffixes []string) { + t.Helper() + item, err := NewDomainItem(domains, suffixes) + require.NoError(t, err) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationKeywordItem(rule *abstractDefaultRule, keywords []string) { + item := NewDomainKeywordItem(keywords) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationRegexItem(t *testing.T, rule *abstractDefaultRule, regexes []string) { + t.Helper() + item, err := NewDomainRegexItem(regexes) + require.NoError(t, err) + rule.destinationAddressItems = append(rule.destinationAddressItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationIPCIDRItem(t *testing.T, rule *abstractDefaultRule, cidrs []string) { + t.Helper() + item, err := NewIPCIDRItem(false, cidrs) + require.NoError(t, err) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationIPIsPrivateItem(rule *abstractDefaultRule) { + item := NewIPIsPrivateItem(false) + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationIPAcceptAnyItem(rule *abstractDefaultRule) { + item := NewIPAcceptAnyItem() + rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addSourcePortItem(rule *abstractDefaultRule, ports []uint16) { + item := NewPortItem(true, ports) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addSourcePortRangeItem(t *testing.T, rule *abstractDefaultRule, ranges []string) { + t.Helper() + item, err := NewPortRangeItem(true, ranges) + require.NoError(t, err) + rule.sourcePortItems = append(rule.sourcePortItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationPortItem(rule *abstractDefaultRule, ports []uint16) { + item := NewPortItem(false, ports) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) +} + +func addDestinationPortRangeItem(t *testing.T, rule *abstractDefaultRule, ranges []string) { + t.Helper() + item, err := NewPortRangeItem(false, ranges) + require.NoError(t, err) + rule.destinationPortItems = append(rule.destinationPortItems, item) + rule.allItems = append(rule.allItems, item) +} From 7425100bac48e45bb4e21caf0c758d09a7491428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 24 Mar 2026 14:32:03 +0800 Subject: [PATCH 53/99] release: Refactor release tracks for Linux packages and Docker Support 4 release tracks instead of 2: - sing-box / latest (stable release) - sing-box-beta / latest-beta (stable pre-release) - sing-box-testing / latest-testing (testing branch) - sing-box-oldstable / latest-oldstable (oldstable branch) Track is detected via git branch --contains and git tag, replacing the old version-string hyphen check. --- .github/detect_track.sh | 33 +++++++++++++++++++++++++++++++++ .github/workflows/docker.yml | 19 +++++++++---------- .github/workflows/linux.yml | 16 ++-------------- 3 files changed, 44 insertions(+), 24 deletions(-) create mode 100755 .github/detect_track.sh diff --git a/.github/detect_track.sh b/.github/detect_track.sh new file mode 100755 index 00000000..124ca6e2 --- /dev/null +++ b/.github/detect_track.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +branches=$(git branch -r --contains HEAD) +if echo "$branches" | grep -q 'origin/stable'; then + track=stable +elif echo "$branches" | grep -q 'origin/testing'; then + track=testing +elif echo "$branches" | grep -q 'origin/oldstable'; then + track=oldstable +else + echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2 + exit 1 +fi + +if [[ "$track" == "stable" ]]; then + tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true) + if [[ -n "$tag" && "$tag" == *"-"* ]]; then + track=beta + fi +fi + +case "$track" in + stable) name=sing-box; docker_tag=latest ;; + beta) name=sing-box-beta; docker_tag=latest-beta ;; + testing) name=sing-box-testing; docker_tag=latest-testing ;; + oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;; +esac + +echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2 +echo "TRACK=${track}" >> "$GITHUB_ENV" +echo "NAME=${name}" >> "$GITHUB_ENV" +echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index feddcca8..99e8ee8a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -19,7 +19,6 @@ env: jobs: build_binary: name: Build binary - if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable' runs-on: ubuntu-latest strategy: fail-fast: true @@ -260,13 +259,13 @@ jobs: fi echo "ref=$ref" echo "ref=$ref" >> $GITHUB_OUTPUT - if [[ $ref == *"-"* ]]; then - latest=latest-beta - else - latest=latest - fi - echo "latest=$latest" - echo "latest=$latest" >> $GITHUB_OUTPUT + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + with: + ref: ${{ steps.ref.outputs.ref }} + fetch-depth: 0 + - name: Detect track + run: bash .github/detect_track.sh - name: Download digests uses: actions/download-artifact@v5 with: @@ -286,11 +285,11 @@ jobs: working-directory: /tmp/digests run: | docker buildx imagetools create \ - -t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \ + -t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \ -t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \ $(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *) - name: Inspect image if: github.event_name != 'push' run: | - docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }} + docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }} docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }} diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 0ab06e72..f3c60989 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -11,11 +11,6 @@ on: description: "Version name" required: true type: string - forceBeta: - description: "Force beta" - required: false - type: boolean - default: false release: types: - published @@ -23,7 +18,6 @@ on: jobs: calculate_version: name: Calculate version - if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable' runs-on: ubuntu-latest outputs: version: ${{ steps.outputs.outputs.version }} @@ -168,14 +162,8 @@ jobs: - name: Set mtime run: |- TZ=UTC touch -t '197001010000' dist/sing-box - - name: Set name - 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: Detect track + run: bash .github/detect_track.sh - name: Set version run: |- PKG_VERSION="${{ needs.calculate_version.outputs.version }}" From b0c6762bc15a7175a50cb719074736c8520939b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 25 Mar 2026 10:32:09 +0800 Subject: [PATCH 54/99] route: merge rule_set branches into outer rules Treat rule_set items as merged branches instead of standalone boolean sub-items. Evaluate each branch inside a referenced rule-set as if it were merged into the outer rule and keep OR semantics between branches. This lets outer grouped fields satisfy matching groups inside a branch without introducing a standalone outer fallback or cross-branch state union. Keep inherited grouped state outside inverted default and logical branches. Negated rule-set branches now evaluate !(...) against their own conditions and only reapply the outer grouped match after negation succeeds, so configs like outer-group && !inner-condition continue to work. Add regression tests for same-group merged matches, cross-group and extra-AND failures, DNS merged-branch behaviour, and inverted merged branches. Update the route and DNS rule docs to clarify that rule-set branches merge into the outer rule while keeping OR semantics between branches. --- docs/configuration/dns/rule.md | 4 +- docs/configuration/dns/rule.zh.md | 4 +- docs/configuration/route/rule.md | 2 +- docs/configuration/route/rule.zh.md | 4 +- route/rule/match_state.go | 26 ++- route/rule/rule_abstract.go | 50 ++++-- route/rule/rule_item_rule_set.go | 6 +- route/rule/rule_set_local.go | 6 +- route/rule/rule_set_remote.go | 6 +- route/rule/rule_set_semantics_test.go | 232 ++++++++++++++++++++++++++ 10 files changed, 308 insertions(+), 32 deletions(-) diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 6407e1bf..43486748 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -209,7 +209,7 @@ icon: material/alert-decagram (`source_port` || `source_port_range`) && `other fields` - Additionally, included rule-sets can be considered merged rather than as a single rule sub-item. + Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics. #### inbound @@ -546,4 +546,4 @@ Match any IP with query response. #### rules -Included rules. \ No newline at end of file +Included rules. diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index c46bc475..f35cfc7e 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -208,7 +208,7 @@ icon: material/alert-decagram (`source_port` || `source_port_range`) && `other fields` - 另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。 + 另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。 #### inbound @@ -550,4 +550,4 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. ==必填== -包括的规则。 \ No newline at end of file +包括的规则。 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 31f768fe..92518726 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -199,7 +199,7 @@ icon: material/new-box (`source_port` || `source_port_range`) && `other fields` - Additionally, included rule-sets can be considered merged rather than as a single rule sub-item. + Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics. #### inbound diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index c8838018..53da4475 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -197,7 +197,7 @@ icon: material/new-box (`source_port` || `source_port_range`) && `other fields` - 另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。 + 另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。 #### inbound @@ -501,4 +501,4 @@ icon: material/new-box ==必填== -包括的规则。 \ No newline at end of file +包括的规则。 diff --git a/route/rule/match_state.go b/route/rule/match_state.go index f537d5de..feac8418 100644 --- a/route/rule/match_state.go +++ b/route/rule/match_state.go @@ -87,22 +87,40 @@ type ruleStateMatcher interface { matchStates(metadata *adapter.InboundContext) ruleMatchStateSet } +type ruleStateMatcherWithBase interface { + matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet +} + func matchHeadlessRuleStates(rule adapter.HeadlessRule, metadata *adapter.InboundContext) ruleMatchStateSet { + return matchHeadlessRuleStatesWithBase(rule, metadata, 0) +} + +func matchHeadlessRuleStatesWithBase(rule adapter.HeadlessRule, metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { + if matcher, isStateMatcher := rule.(ruleStateMatcherWithBase); isStateMatcher { + return matcher.matchStatesWithBase(metadata, base) + } if matcher, isStateMatcher := rule.(ruleStateMatcher); isStateMatcher { - return matcher.matchStates(metadata) + return matcher.matchStates(metadata).withBase(base) } if rule.Match(metadata) { - return emptyRuleMatchState() + return emptyRuleMatchState().withBase(base) } return 0 } func matchRuleItemStates(item RuleItem, metadata *adapter.InboundContext) ruleMatchStateSet { + return matchRuleItemStatesWithBase(item, metadata, 0) +} + +func matchRuleItemStatesWithBase(item RuleItem, metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { + if matcher, isStateMatcher := item.(ruleStateMatcherWithBase); isStateMatcher { + return matcher.matchStatesWithBase(metadata, base) + } if matcher, isStateMatcher := item.(ruleStateMatcher); isStateMatcher { - return matcher.matchStates(metadata) + return matcher.matchStates(metadata).withBase(base) } if item.Match(metadata) { - return emptyRuleMatchState() + return emptyRuleMatchState().withBase(base) } return 0 } diff --git a/route/rule/rule_abstract.go b/route/rule/rule_abstract.go index 141f3d27..8a95fa6d 100644 --- a/route/rule/rule_abstract.go +++ b/route/rule/rule_abstract.go @@ -72,10 +72,18 @@ func (r *abstractDefaultRule) requiresDestinationAddressMatch(metadata *adapter. } func (r *abstractDefaultRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.matchStatesWithBase(metadata, 0) +} + +func (r *abstractDefaultRule) matchStatesWithBase(metadata *adapter.InboundContext, inheritedBase ruleMatchState) ruleMatchStateSet { if len(r.allItems) == 0 { - return emptyRuleMatchState() + return emptyRuleMatchState().withBase(inheritedBase) } - var baseState ruleMatchState + evaluationBase := inheritedBase + if r.invert { + evaluationBase = 0 + } + baseState := evaluationBase if len(r.sourceAddressItems) > 0 { metadata.DidMatch = true if matchAnyItem(r.sourceAddressItems, metadata) { @@ -119,17 +127,15 @@ func (r *abstractDefaultRule) matchStates(metadata *adapter.InboundContext) rule for _, item := range r.items { metadata.DidMatch = true if !item.Match(metadata) { - return r.invertedFailure() + return r.invertedFailure(inheritedBase) } } - stateSet := singleRuleMatchState(baseState) + var stateSet ruleMatchStateSet if r.ruleSetItem != nil { metadata.DidMatch = true - ruleSetStates := matchRuleItemStates(r.ruleSetItem, metadata) - if ruleSetStates.isEmpty() { - return r.invertedFailure() - } - stateSet = ruleSetStates.withBase(baseState) + stateSet = matchRuleItemStatesWithBase(r.ruleSetItem, metadata, baseState) + } else { + stateSet = singleRuleMatchState(baseState) } stateSet = stateSet.filter(func(state ruleMatchState) bool { if r.requiresSourceAddressMatch(metadata) && !state.has(ruleMatchSourceAddress) { @@ -147,21 +153,21 @@ func (r *abstractDefaultRule) matchStates(metadata *adapter.InboundContext) rule return true }) if stateSet.isEmpty() { - return r.invertedFailure() + return r.invertedFailure(inheritedBase) } if r.invert { // DNS pre-lookup defers destination address-limit checks until the response phase. if metadata.IgnoreDestinationIPCIDRMatch && stateSet == emptyRuleMatchState() && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 { - return emptyRuleMatchState() + return emptyRuleMatchState().withBase(inheritedBase) } return 0 } return stateSet } -func (r *abstractDefaultRule) invertedFailure() ruleMatchStateSet { +func (r *abstractDefaultRule) invertedFailure(base ruleMatchState) ruleMatchStateSet { if r.invert { - return emptyRuleMatchState() + return emptyRuleMatchState().withBase(base) } return 0 } @@ -225,16 +231,24 @@ func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { } func (r *abstractLogicalRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.matchStatesWithBase(metadata, 0) +} + +func (r *abstractLogicalRule) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { + evaluationBase := base + if r.invert { + evaluationBase = 0 + } var stateSet ruleMatchStateSet if r.mode == C.LogicalTypeAnd { - stateSet = emptyRuleMatchState() + stateSet = emptyRuleMatchState().withBase(evaluationBase) for _, rule := range r.rules { nestedMetadata := *metadata nestedMetadata.ResetRuleCache() - nestedStateSet := matchHeadlessRuleStates(rule, &nestedMetadata) + nestedStateSet := matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase) if nestedStateSet.isEmpty() { if r.invert { - return emptyRuleMatchState() + return emptyRuleMatchState().withBase(base) } return 0 } @@ -244,11 +258,11 @@ func (r *abstractLogicalRule) matchStates(metadata *adapter.InboundContext) rule for _, rule := range r.rules { nestedMetadata := *metadata nestedMetadata.ResetRuleCache() - stateSet = stateSet.merge(matchHeadlessRuleStates(rule, &nestedMetadata)) + stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase)) } if stateSet.isEmpty() { if r.invert { - return emptyRuleMatchState() + return emptyRuleMatchState().withBase(base) } return 0 } diff --git a/route/rule/rule_item_rule_set.go b/route/rule/rule_item_rule_set.go index 0916279d..3467843b 100644 --- a/route/rule/rule_item_rule_set.go +++ b/route/rule/rule_item_rule_set.go @@ -45,13 +45,17 @@ func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool { } func (r *RuleSetItem) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return r.matchStatesWithBase(metadata, 0) +} + +func (r *RuleSetItem) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { var stateSet ruleMatchStateSet for _, ruleSet := range r.setList { nestedMetadata := *metadata nestedMetadata.ResetRuleMatchCache() nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty - stateSet = stateSet.merge(matchHeadlessRuleStates(ruleSet, &nestedMetadata)) + stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(ruleSet, &nestedMetadata, base)) } return stateSet } diff --git a/route/rule/rule_set_local.go b/route/rule/rule_set_local.go index ec0f91b2..ed873d70 100644 --- a/route/rule/rule_set_local.go +++ b/route/rule/rule_set_local.go @@ -206,11 +206,15 @@ func (s *LocalRuleSet) Match(metadata *adapter.InboundContext) bool { } func (s *LocalRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return s.matchStatesWithBase(metadata, 0) +} + +func (s *LocalRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { var stateSet ruleMatchStateSet for _, rule := range s.rules { nestedMetadata := *metadata nestedMetadata.ResetRuleMatchCache() - stateSet = stateSet.merge(matchHeadlessRuleStates(rule, &nestedMetadata)) + stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base)) } return stateSet } diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index c85dc859..bda6e23f 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -326,11 +326,15 @@ func (s *RemoteRuleSet) Match(metadata *adapter.InboundContext) bool { } func (s *RemoteRuleSet) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet { + return s.matchStatesWithBase(metadata, 0) +} + +func (s *RemoteRuleSet) matchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) ruleMatchStateSet { var stateSet ruleMatchStateSet for _, rule := range s.rules { nestedMetadata := *metadata nestedMetadata.ResetRuleMatchCache() - stateSet = stateSet.merge(matchHeadlessRuleStates(rule, &nestedMetadata)) + stateSet = stateSet.merge(matchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base)) } return stateSet } diff --git a/route/rule/rule_set_semantics_test.go b/route/rule/rule_set_semantics_test.go index 27461ce6..a01defe6 100644 --- a/route/rule/rule_set_semantics_test.go +++ b/route/rule/rule_set_semantics_test.go @@ -149,6 +149,95 @@ func TestRouteRuleSetMergeSourceAndPortGroups(t *testing.T) { }) } +func TestRouteRuleSetOuterGroupedStateMergesIntoSameGroup(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + metadata adapter.InboundContext + buildOuter func(*testing.T, *abstractDefaultRule) + buildInner func(*testing.T, *abstractDefaultRule) + }{ + { + name: "destination address", + metadata: testMetadata("www.example.com"), + buildOuter: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + }, + buildInner: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + }, + }, + { + name: "source address", + metadata: testMetadata("www.example.com"), + buildOuter: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addSourceAddressItem(t, rule, []string{"10.0.0.0/8"}) + }, + buildInner: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addSourceAddressItem(t, rule, []string{"198.51.100.0/24"}) + }, + }, + { + name: "source port", + metadata: testMetadata("www.example.com"), + buildOuter: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addSourcePortItem(rule, []uint16{1000}) + }, + buildInner: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addSourcePortItem(rule, []uint16{2000}) + }, + }, + { + name: "destination port", + metadata: testMetadata("www.example.com"), + buildOuter: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationPortItem(rule, []uint16{443}) + }, + buildInner: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationPortItem(rule, []uint16{8443}) + }, + }, + { + name: "destination ip cidr", + metadata: func() adapter.InboundContext { + metadata := testMetadata("lookup.example") + metadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("203.0.113.1")} + return metadata + }(), + buildOuter: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"}) + }, + buildInner: func(t *testing.T, rule *abstractDefaultRule) { + t.Helper() + addDestinationIPCIDRItem(t, rule, []string{"198.51.100.0/24"}) + }, + }, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + ruleSet := newLocalRuleSetForTest("outer-merge-"+testCase.name, headlessDefaultRule(t, func(rule *abstractDefaultRule) { + testCase.buildInner(t, rule) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + testCase.buildOuter(t, rule) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&testCase.metadata)) + }) + } +} + func TestRouteRuleSetOtherFieldsStayAnd(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") @@ -162,6 +251,34 @@ func TestRouteRuleSetOtherFieldsStayAnd(t *testing.T) { require.False(t, rule.Match(&metadata)) } +func TestRouteRuleSetMergedBranchKeepsAndConstraints(t *testing.T) { + t.Parallel() + t.Run("outer group does not bypass inner non grouped condition", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("network-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) + t.Run("outer group does not satisfy different grouped branch", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("different-group", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addSourcePortItem(rule, []uint16{1000}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) +} + func TestRouteRuleSetOrSemantics(t *testing.T) { t.Parallel() t.Run("later ruleset can satisfy outer group", func(t *testing.T) { @@ -271,6 +388,68 @@ func TestRouteRuleSetLogicalSemantics(t *testing.T) { }) } +func TestRouteRuleSetInvertMergedBranchSemantics(t *testing.T) { + t.Parallel() + t.Run("default invert keeps inherited group outside grouped predicate", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("invert-grouped", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + rule.invert = true + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("default invert keeps inherited group after negation succeeds", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("invert-network", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + rule.invert = true + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + })) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("logical invert keeps inherited group outside grouped predicate", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("logical-invert-grouped", headlessLogicalRule( + C.LogicalTypeOr, + true, + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + }), + )) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("logical invert keeps inherited group after negation succeeds", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("logical-invert-network", headlessLogicalRule( + C.LogicalTypeOr, + true, + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + }), + )) + rule := routeRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) +} + func TestRouteRuleSetNoLeakageRegressions(t *testing.T) { t.Parallel() t.Run("same ruleset failed branch does not leak", func(t *testing.T) { @@ -339,6 +518,59 @@ func TestRouteRuleSetRemoteUsesSameSemantics(t *testing.T) { func TestDNSRuleSetSemantics(t *testing.T) { t.Parallel() + t.Run("outer destination group merges into matching ruleset branch", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.baidu.com") + ruleSet := newLocalRuleSetForTest("dns-merged-branch", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"baidu.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("outer destination group does not bypass ruleset non grouped condition", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("dns-network-and", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.False(t, rule.Match(&metadata)) + }) + t.Run("outer destination group stays outside inverted grouped branch", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.baidu.com") + ruleSet := newLocalRuleSetForTest("dns-invert-grouped", headlessDefaultRule(t, func(rule *abstractDefaultRule) { + rule.invert = true + addDestinationAddressItem(t, rule, nil, []string{"google.com"}) + })) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"baidu.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) + t.Run("outer destination group stays outside inverted logical branch", func(t *testing.T) { + t.Parallel() + metadata := testMetadata("www.example.com") + ruleSet := newLocalRuleSetForTest("dns-logical-invert-network", headlessLogicalRule( + C.LogicalTypeOr, + true, + headlessDefaultRule(t, func(rule *abstractDefaultRule) { + addOtherItem(rule, NewNetworkItem([]string{N.NetworkUDP})) + }), + )) + rule := dnsRuleForTest(func(rule *abstractDefaultRule) { + addDestinationAddressItem(t, rule, nil, []string{"example.com"}) + addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}}) + }) + require.True(t, rule.Match(&metadata)) + }) t.Run("match address limit merges destination group", func(t *testing.T) { t.Parallel() metadata := testMetadata("www.example.com") From 6381de7bab866ed28e40be6e0775a042d868902c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 26 Mar 2026 12:31:19 +0800 Subject: [PATCH 55/99] route: Fix query_type never matching in rule_set headless rules --- route/rule/rule_headless.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/route/rule/rule_headless.go b/route/rule/rule_headless.go index f180bacc..c5146318 100644 --- a/route/rule/rule_headless.go +++ b/route/rule/rule_headless.go @@ -45,6 +45,11 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR invert: options.Invert, }, } + if len(options.QueryType) > 0 { + item := NewQueryTypeItem(options.QueryType) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.Network) > 0 { item := NewNetworkItem(options.Network) rule.items = append(rule.items, item) From d09182614c7778c1e0f51d990635c5d4249c437e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Mar 2026 19:40:16 +0800 Subject: [PATCH 56/99] Bump version --- clients/android | 2 +- clients/apple | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/clients/android b/clients/android index 6f09892c..834a5f7d 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit 6f09892c7193c696b9fc182123db8051562cdf74 +Subproject commit 834a5f7df08bedb24dc361781959459e0550222f diff --git a/clients/apple b/clients/apple index f3b4b223..6b790c7a 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit f3b4b2238efd238fb1ec6ef2da88017b60a6cfa1 +Subproject commit 6b790c7a80c38d572c23ff7a075b5e18c744edcf diff --git a/docs/changelog.md b/docs/changelog.md index 9aaba894..ab11525f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.13.4 + +* Fixes and improvements + #### 1.13.3 * Add OpenWrt and Alpine APK packages to release **1** From e98b4ad449b099c58057bca84bec9c101f8bc37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 26 Mar 2026 16:32:46 +0800 Subject: [PATCH 57/99] Fix WireGuard shutdown race crashing Stop peer goroutines before closing the TUN device to prevent RoutineSequentialReceiver from calling Write on a nil dispatcher. --- transport/wireguard/endpoint.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go index f9f4628a..3a02e17a 100644 --- a/transport/wireguard/endpoint.go +++ b/transport/wireguard/endpoint.go @@ -229,12 +229,13 @@ func (e *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n } func (e *Endpoint) Close() error { - if e.device != nil { - e.device.Close() - } if e.pauseCallback != nil { e.pause.UnregisterCallback(e.pauseCallback) } + if e.device != nil { + e.device.Down() + e.device.Close() + } return nil } From 02ccde6c7126d793496e1948afe71067a20cc2f4 Mon Sep 17 00:00:00 2001 From: Zhengchao Ding Date: Sun, 29 Mar 2026 14:28:29 +0800 Subject: [PATCH 58/99] fix(rpm): add vendor field to fpm config to avoid (none) vendor Co-authored-by: Hyper --- .fpm_systemd | 1 + 1 file changed, 1 insertion(+) diff --git a/.fpm_systemd b/.fpm_systemd index 402ed429..9b455da9 100644 --- a/.fpm_systemd +++ b/.fpm_systemd @@ -4,6 +4,7 @@ --license GPL-3.0-or-later --description "The universal proxy platform." --url "https://sing-box.sagernet.org/" +--vendor SagerNet --maintainer "nekohasekai " --deb-field "Bug: https://github.com/SagerNet/sing-box/issues" --no-deb-generate-changes From 4fd2532b0a05676c1a9a2c8b3446691d30dfb6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 30 Mar 2026 22:27:12 +0800 Subject: [PATCH 59/99] Fix naive quic error message --- protocol/naive/inbound.go | 5 ++++- protocol/naive/inbound_conn.go | 20 ++++++++++++++------ protocol/naive/quic/inbound_init.go | 1 + 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/protocol/naive/inbound.go b/protocol/naive/inbound.go index 48c35926..5613f196 100644 --- a/protocol/naive/inbound.go +++ b/protocol/naive/inbound.go @@ -29,7 +29,10 @@ import ( "golang.org/x/net/http2/h2c" ) -var ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error) +var ( + ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error) + WrapError func(error) error +) func RegisterInbound(registry *inbound.Registry) { inbound.Register[option.NaiveInboundOptions](registry, C.TypeNaive, NewInbound) diff --git a/protocol/naive/inbound_conn.go b/protocol/naive/inbound_conn.go index 0711b637..d5986b40 100644 --- a/protocol/naive/inbound_conn.go +++ b/protocol/naive/inbound_conn.go @@ -179,18 +179,18 @@ type naiveConn struct { func (c *naiveConn) Read(p []byte) (n int, err error) { n, err = c.readWithPadding(c.Conn, p) - return n, baderror.WrapH2(err) + return n, wrapError(err) } func (c *naiveConn) Write(p []byte) (n int, err error) { n, err = c.writeChunked(c.Conn, p) - return n, baderror.WrapH2(err) + return n, wrapError(err) } func (c *naiveConn) WriteBuffer(buffer *buf.Buffer) error { defer buffer.Release() err := c.writeBufferWithPadding(c.Conn, buffer) - return baderror.WrapH2(err) + return wrapError(err) } func (c *naiveConn) FrontHeadroom() int { return c.frontHeadroom() } @@ -210,7 +210,7 @@ type naiveH2Conn struct { func (c *naiveH2Conn) Read(p []byte) (n int, err error) { n, err = c.readWithPadding(c.reader, p) - return n, baderror.WrapH2(err) + return n, wrapError(err) } func (c *naiveH2Conn) Write(p []byte) (n int, err error) { @@ -218,7 +218,7 @@ func (c *naiveH2Conn) Write(p []byte) (n int, err error) { if err == nil { c.flusher.Flush() } - return n, baderror.WrapH2(err) + return n, wrapError(err) } func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error { @@ -227,7 +227,15 @@ func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error { if err == nil { c.flusher.Flush() } - return baderror.WrapH2(err) + return wrapError(err) +} + +func wrapError(err error) error { + err = baderror.WrapH2(err) + if WrapError != nil { + err = WrapError(err) + } + return err } func (c *naiveH2Conn) Close() error { diff --git a/protocol/naive/quic/inbound_init.go b/protocol/naive/quic/inbound_init.go index a356cfae..1f868267 100644 --- a/protocol/naive/quic/inbound_init.go +++ b/protocol/naive/quic/inbound_init.go @@ -124,4 +124,5 @@ func init() { return quicListener, nil } + naive.WrapError = qtls.WrapError } From 84d2280960744117a4575240c2315947d4497b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 30 Mar 2026 22:27:58 +0800 Subject: [PATCH 60/99] quic: Fix protocol client close & Sync hysteria bbr fix --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e13f5627..43c984dc 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde github.com/sagernet/sing-mux v0.3.4 - github.com/sagernet/sing-quic v0.6.0 + github.com/sagernet/sing-quic v0.6.1 github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 diff --git a/go.sum b/go.sum index e095de83..8efd9d70 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde h1:RNQzlpnsXIuu1HG github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= -github.com/sagernet/sing-quic v0.6.0 h1:dhrFnP45wgVKEOT1EvtsToxdzRnHIDIAgj6WHV9pLyM= -github.com/sagernet/sing-quic v0.6.0/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8= +github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw= +github.com/sagernet/sing-quic v0.6.1/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= From e3bcb06c3eda1c2f43674fb367a536a80f43db9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 26 Mar 2026 16:49:28 +0800 Subject: [PATCH 61/99] platform: Add HTTPResponse.WriteToWithProgress --- experimental/libbox/http.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/experimental/libbox/http.go b/experimental/libbox/http.go index 9f4b2915..69a23d26 100644 --- a/experimental/libbox/http.go +++ b/experimental/libbox/http.go @@ -52,6 +52,11 @@ type HTTPRequest interface { type HTTPResponse interface { GetContent() (*StringBox, error) WriteTo(path string) error + WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error +} + +type HTTPResponseWriteToProgressHandler interface { + Update(progress int64, total int64) } var ( @@ -239,3 +244,31 @@ func (h *httpResponse) WriteTo(path string) error { defer file.Close() return common.Error(bufio.Copy(file, h.Body)) } + +func (h *httpResponse) WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error { + defer h.Body.Close() + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + return common.Error(bufio.Copy(&progressWriter{ + writer: file, + handler: handler, + total: h.ContentLength, + }, h.Body)) +} + +type progressWriter struct { + writer io.Writer + handler HTTPResponseWriteToProgressHandler + total int64 + written int64 +} + +func (w *progressWriter) Write(p []byte) (int, error) { + n, err := w.writer.Write(p) + w.written += int64(n) + w.handler.Update(w.written, w.total) + return n, err +} From e15bdf11eb4fafb5b39299d1e553e91736b98b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 30 Mar 2026 22:58:05 +0800 Subject: [PATCH 62/99] sing: Minor fixes --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 43c984dc..7af87ee3 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde + github.com/sagernet/sing v0.8.3 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.1 github.com/sagernet/sing-shadowsocks v0.2.8 diff --git a/go.sum b/go.sum index 8efd9d70..9fd6ec86 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde h1:RNQzlpnsXIuu1HGts/fIzJ1PR7RhrzaNlU52MDyiX1c= -github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.3 h1:zGMy9M1deBPEew9pCYIUHKeE+/lDQ5A2CBqjBjjzqkA= +github.com/sagernet/sing v0.8.3/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw= From 7ffdc48b49718d6aeee19ede523a31d1286724cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 30 Mar 2026 22:42:01 +0800 Subject: [PATCH 63/99] Bump version --- clients/android | 2 +- clients/apple | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/clients/android b/clients/android index 834a5f7d..cba1cc3c 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit 834a5f7df08bedb24dc361781959459e0550222f +Subproject commit cba1cc3ce028e7954342fffe2245cb5366138d10 diff --git a/clients/apple b/clients/apple index 6b790c7a..ffbf405b 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit 6b790c7a80c38d572c23ff7a075b5e18c744edcf +Subproject commit ffbf405b5223c7537ac70f52d8cd39258b67942c diff --git a/docs/changelog.md b/docs/changelog.md index ab11525f..0b152c95 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.13.5 + +* Fixes and improvements + #### 1.13.4 * Fixes and improvements From 354b4b040e163110e1464f05e628f8ab1e3dfe6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 1 Apr 2026 16:16:58 +0800 Subject: [PATCH 64/99] sing: Fix vectorised readv iovec length calculation This does not seem to affect any actual paths in the sing-box. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7af87ee3..721c05fb 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.3 + github.com/sagernet/sing v0.8.4 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.1 github.com/sagernet/sing-shadowsocks v0.2.8 diff --git a/go.sum b/go.sum index 9fd6ec86..458096a3 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.3 h1:zGMy9M1deBPEew9pCYIUHKeE+/lDQ5A2CBqjBjjzqkA= -github.com/sagernet/sing v0.8.3/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.4 h1:Fj+jlY3F8vhcRfz/G/P3Dwcs5wqnmyNPT7u1RVVmjFI= +github.com/sagernet/sing v0.8.4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw= From d9b435fb62cd15f3ed48d8f8920e569ca6469509 Mon Sep 17 00:00:00 2001 From: hdrover <185519579+hdrover@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:38:47 +0300 Subject: [PATCH 65/99] Fix naive inbound padding bytes --- protocol/naive/inbound_conn.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol/naive/inbound_conn.go b/protocol/naive/inbound_conn.go index d5986b40..8cc3ded2 100644 --- a/protocol/naive/inbound_conn.go +++ b/protocol/naive/inbound_conn.go @@ -95,7 +95,7 @@ func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, er binary.BigEndian.PutUint16(header, uint16(len(data))) header[2] = byte(paddingSize) common.Must1(buffer.Write(data)) - buffer.Extend(paddingSize) + common.Must(buffer.WriteZeroN(paddingSize)) _, err = writer.Write(buffer.Bytes()) if err == nil { n = len(data) @@ -117,7 +117,7 @@ func (p *paddingConn) writeBufferWithPadding(writer io.Writer, buffer *buf.Buffe header := buffer.ExtendHeader(3) binary.BigEndian.PutUint16(header, uint16(bufferLen)) header[2] = byte(paddingSize) - buffer.Extend(paddingSize) + common.Must(buffer.WriteZeroN(paddingSize)) p.writePadding++ } return common.Error(writer.Write(buffer.Bytes())) From 813b634d08d85cbd6f44c29118233096cbb84e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 6 Apr 2026 22:33:45 +0800 Subject: [PATCH 66/99] Bump version --- clients/android | 2 +- docs/changelog.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/clients/android b/clients/android index cba1cc3c..4f0826b9 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit cba1cc3ce028e7954342fffe2245cb5366138d10 +Subproject commit 4f0826b94d59e96a7a35d00891f2e0ad2c72123d diff --git a/docs/changelog.md b/docs/changelog.md index 0b152c95..f74fc24b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.13.6 + +* Fixes and improvements + #### 1.13.5 * Fixes and improvements From 7c3d8cf8db1db985443fd35dd8ad48080be568b8 Mon Sep 17 00:00:00 2001 From: TargetLocked <32962687+TargetLocked@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:29:15 +0800 Subject: [PATCH 67/99] Fix disable tcp keep alive --- common/dialer/default.go | 5 ++++- common/listener/listener_tcp.go | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/common/dialer/default.go b/common/dialer/default.go index 6b843ed0..4ffe00c1 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -149,7 +149,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial } else { dialer.Timeout = C.TCPConnectTimeout } - if !options.DisableTCPKeepAlive { + if options.DisableTCPKeepAlive { + dialer.KeepAlive = -1 + dialer.KeepAliveConfig.Enable = false + } else { keepIdle := time.Duration(options.TCPKeepAlive) if keepIdle == 0 { keepIdle = C.TCPKeepAliveInitial diff --git a/common/listener/listener_tcp.go b/common/listener/listener_tcp.go index 899d444f..54d84a6b 100644 --- a/common/listener/listener_tcp.go +++ b/common/listener/listener_tcp.go @@ -37,7 +37,10 @@ func (l *Listener) ListenTCP() (net.Listener, error) { if l.listenOptions.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) if keepIdle == 0 { keepIdle = C.TCPKeepAliveInitial From 5a957fd750b205f38cb3440522a279421f8b7bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berkay=20=C3=96zdemirci?= Date: Fri, 10 Apr 2026 09:05:07 +0300 Subject: [PATCH 68/99] Fix EDNS OPT record corruption in DNS cache The TTL computation and assignment loops treat OPT record's Hdr.Ttl as a regular TTL, but per RFC 6891 it encodes EDNS0 metadata (ExtRCode|Version|Flags). This corrupts cached responses causing systemd-resolved to reject them with EDNS version 255. Also fix pointer aliasing: storeCache() stored raw *dns.Msg pointer so subsequent mutations by Exchange() corrupted cached data. - Skip OPT records in all TTL loops (Exchange + loadResponse) - Use message.Copy() in storeCache() to isolate cache from mutations --- dns/client.go | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/dns/client.go b/dns/client.go index 70b53c95..1a2ee8f8 100644 --- a/dns/client.go +++ b/dns/client.go @@ -283,6 +283,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m if timeToLive == 0 { for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { + if record.Header().Rrtype == dns.TypeOPT { + continue + } if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive { timeToLive = record.Header().Ttl } @@ -294,6 +297,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m } for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { + if record.Header().Rrtype == dns.TypeOPT { + continue + } record.Header().Ttl = timeToLive } } @@ -381,21 +387,21 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio } if c.disableExpire { if !c.independentCache { - c.cache.Add(question, message) + c.cache.Add(question, message.Copy()) } else { c.transportCache.Add(transportCacheKey{ Question: question, transportTag: transport.Tag(), - }, message) + }, message.Copy()) } } else { if !c.independentCache { - c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive)) + c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive)) } else { c.transportCache.AddWithLifetime(transportCacheKey{ Question: question, transportTag: transport.Tag(), - }, message, time.Second*time.Duration(timeToLive)) + }, message.Copy(), time.Second*time.Duration(timeToLive)) } } } @@ -486,6 +492,9 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp var originTTL int for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { + if record.Header().Rrtype == dns.TypeOPT { + continue + } if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL { originTTL = int(record.Header().Ttl) } @@ -500,12 +509,18 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp duration := uint32(originTTL - nowTTL) for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { + if record.Header().Rrtype == dns.TypeOPT { + continue + } record.Header().Ttl = record.Header().Ttl - duration } } } else { for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { for _, record := range recordList { + if record.Header().Rrtype == dns.TypeOPT { + continue + } record.Header().Ttl = uint32(nowTTL) } } From 55ec8abf174c5aa63c44cc6832ea647369cfa7b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Apr 2026 20:54:32 +0800 Subject: [PATCH 69/99] Fix local DNS server for Android --- box.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/box.go b/box.go index fe116b31..a72884e0 100644 --- a/box.go +++ b/box.go @@ -19,7 +19,6 @@ import ( "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" - "github.com/sagernet/sing-box/dns/transport/local" "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/log" @@ -326,11 +325,12 @@ func New(options Options) (*Box, error) { ) }) dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) { - return local.NewTransport( + return dnsTransportRegistry.CreateDNSTransport( ctx, logFactory.NewLogger("dns/local"), "local", - option.LocalDNSServerOptions{}, + C.DNSTypeLocal, + &option.LocalDNSServerOptions{}, ) }) if platformInterface != nil { @@ -555,6 +555,10 @@ func (s *Box) Outbound() adapter.OutboundManager { return s.outbound } +func (s *Box) Endpoint() adapter.EndpointManager { + return s.endpoint +} + func (s *Box) LogFactory() log.Factory { return s.logFactory } From 53db1f178c3024615c55c0e0154c69f6ee318db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 9 Apr 2026 04:12:56 +0800 Subject: [PATCH 70/99] Fix tailscale crash --- protocol/tailscale/endpoint.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index d1c22aed..33b76930 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -262,9 +262,16 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL } func (t *Endpoint) Start(stage adapter.StartStage) error { - if stage != adapter.StartStateStart { - return nil + switch stage { + case adapter.StartStateStart: + return t.start() + case adapter.StartStatePostStart: + return t.postStart() } + return nil +} + +func (t *Endpoint) start() error { if t.platformInterface != nil { err := t.network.UpdateInterfaces() if err != nil { @@ -347,6 +354,10 @@ func (t *Endpoint) Start(stage adapter.StartStage) error { }) }) } + return nil +} + +func (t *Endpoint) postStart() error { err := t.server.Start() if err != nil { if t.systemTun != nil { @@ -471,13 +482,13 @@ func (t *Endpoint) watchState() { } func (t *Endpoint) Close() error { + err := common.Close(common.PtrOrNil(t.server)) netmon.RegisterInterfaceGetter(nil) netns.SetControlFunc(nil) if t.fallbackTCPCloser != nil { t.fallbackTCPCloser() t.fallbackTCPCloser = nil } - err := common.Close(common.PtrOrNil(t.server)) if t.systemTun != nil { t.systemTun.Close() t.systemTun = nil From 76fa3c2e5e210dca9da9d490a87d2dad8e2643c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 14:13:06 +0800 Subject: [PATCH 71/99] tun: Fixes --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 721c05fb..60e66ff3 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.6 + github.com/sagernet/sing-tun v0.8.7 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 diff --git a/go.sum b/go.sum index 458096a3..854463e6 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.6 h1:NydXFikSXhiKqhahHKtuZ90HQPZFzlOFVRONmkr4C7I= -github.com/sagernet/sing-tun v0.8.6/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-tun v0.8.7 h1:q49cI7Cbp+BcgzaJitQ9QdLO77BqnnaQRkSEMoGmF3g= +github.com/sagernet/sing-tun v0.8.7/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= From 025b947a24cf4cba30ca3e629b2a326d1d02e93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 10 Apr 2026 14:13:36 +0800 Subject: [PATCH 72/99] Bump version --- clients/android | 2 +- docs/changelog.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/clients/android b/clients/android index 4f0826b9..fea0f3a7 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit 4f0826b94d59e96a7a35d00891f2e0ad2c72123d +Subproject commit fea0f3a7ba5921d87ddd11902be3c148a61fc689 diff --git a/docs/changelog.md b/docs/changelog.md index f74fc24b..92bc7d9f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.13.7 + +* Fixes and improvements + #### 1.13.6 * Fixes and improvements From 8e3176b789053f2a25d36ebe46bb4f6dcb1a1ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 14 Apr 2026 14:15:20 +0800 Subject: [PATCH 73/99] Fix FakeIP returning error for unconfigured address family Return SUCCESS with empty answers instead of an error when the queried address family has no range configured. Reject configurations where neither inet4_range nor inet6_range is set. --- dns/transport/fakeip/fakeip.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/dns/transport/fakeip/fakeip.go b/dns/transport/fakeip/fakeip.go index 07f0fd09..9aa41e58 100644 --- a/dns/transport/fakeip/fakeip.go +++ b/dns/transport/fakeip/fakeip.go @@ -23,16 +23,25 @@ var _ adapter.FakeIPTransport = (*Transport)(nil) type Transport struct { dns.TransportAdapter - logger logger.ContextLogger - store adapter.FakeIPStore + logger logger.ContextLogger + store adapter.FakeIPStore + inet4Enabled bool + inet6Enabled bool } func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) { - store := NewStore(ctx, logger, options.Inet4Range.Build(netip.Prefix{}), options.Inet6Range.Build(netip.Prefix{})) + inet4Range := options.Inet4Range.Build(netip.Prefix{}) + inet6Range := options.Inet6Range.Build(netip.Prefix{}) + if !inet4Range.IsValid() && !inet6Range.IsValid() { + return nil, E.New("at least one of inet4_range or inet6_range must be set") + } + store := NewStore(ctx, logger, inet4Range, inet6Range) return &Transport{ TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil), logger: logger, store: store, + inet4Enabled: inet4Range.IsValid(), + inet6Enabled: inet6Range.IsValid(), }, nil } @@ -55,6 +64,9 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA { 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) if err != nil { return nil, err From f43fc797d459907bf79031955485af40bbd56a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 14 Apr 2026 14:24:21 +0800 Subject: [PATCH 74/99] Update naiveproxy to v147.0.7727.49-1 --- .github/CRONET_GO_VERSION | 2 +- go.mod | 62 +++++++++---------- go.sum | 124 +++++++++++++++++++------------------- 3 files changed, 94 insertions(+), 94 deletions(-) diff --git a/.github/CRONET_GO_VERSION b/.github/CRONET_GO_VERSION index 47b09f9b..f8f1198f 100644 --- a/.github/CRONET_GO_VERSION +++ b/.github/CRONET_GO_VERSION @@ -1 +1 @@ -2fef65f9dba90ddb89a87d00a6eb6165487c10c1 +e4926ba205fae5351e3d3eeafff7e7029654424a diff --git a/go.mod b/go.mod index 60e66ff3..5aec4dba 100644 --- a/go.mod +++ b/go.mod @@ -27,8 +27,8 @@ require ( github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cors v1.2.1 - github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9 - github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9 + github.com/sagernet/cronet-go v0.0.0-20260413093659-e4926ba205fa + github.com/sagernet/cronet-go/all v0.0.0-20260413093659-e4926ba205fa github.com/sagernet/fswatch v0.1.1 github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 @@ -105,35 +105,35 @@ require ( github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/spf13/pflag v1.0.9 // indirect diff --git a/go.sum b/go.sum index 854463e6..26f9d0e2 100644 --- a/go.sum +++ b/go.sum @@ -162,68 +162,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9 h1:xq5Yr10jXEppD3cnGjE3WENaB6D0YsZu6KptZ8d3054= -github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= -github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9 h1:uxQyy6Y/boOuecVA66tf79JgtoRGfeDJcfYZZLKVA5E= -github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:Xm6cCvs0/twozC1JYNq0sVlOVmcSGzV7YON1XGcD97w= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Qi0IKBpoPP3qZqIXuOKMsT2dv+l/MLWMyBHDMLRw2EA= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:p+wCMjOhj46SpSD/AJeTGgkCcbyA76FyH631XZatyU8= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 h1:Y7lWrZwEhC/HX8Pb5C92CrQihuaE7hrHmWB2ykst3iQ= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:3Ggy5wiyjA6t+aVVPnXlSEIVj9zkxd4ybH3NsvsNefs= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:DuFTCnZloblY+7olXiZoRdueWfxi34EV5UheTFKM2rA= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:x/6T2gjpLw9yNdCVR6xBlzMUzED9fxNFNt6U6A6SOh8= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Lx9PExM70rg8aNxPm0JPeSr5SWC3yFiCz4wIq86ugx8= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:BTEpw7/vKR9BNBsHebfpiGHDCPpjVJ3vLIbHNU3VUfM= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:hdEph9nQXRnKwc/lIDwo15rmzbC6znXF5jJWHPN1Fiw= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Iq++oYV7dtRJHTpu8yclHJdn+1oj2t1e84/YpdXYWW8= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 h1:Y43fuLL8cgwRHpEKwxh0O3vYp7g/SZGvbkJj3cQ6USA= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:bX2GJmF0VCC+tBrVAa49YEsmJ4A9dLmwoA6DJUxRtCY= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:gQTR/2azUCInE0r3kmesZT9xu+x801+BmtDY0d0Tw9Y= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 h1:X4mP3jlYvxgrKpZLOKMmc/O8T5/zP83/23pgfQOc3tY= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:c6xj2nXr/65EDiRFddUKQIBQ/b/lAPoH8WFYlgadaPc= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:ahbl7yjOvGVVNUwk9TcQk+xejVfoYAYFRlhWnby0/YM= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 h1:JC5Zv5+J85da6g5G56VhdaK53fmo6Os2q/wWi5QlxOw= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 h1:4bt7Go588BoM4VjNYMxx0MrvbwlFQn3DdRDCM7BmkRo= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:E1z0BeLUh8EZfCjIyS9BrfCocZrt+0KPS0bzop3Sxf4= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 h1:d8ejxRHO7Vi9JqR/6DxR7RyI/swA2JfDWATR4T7otBw= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 h1:iUDVEVu3RxL5ArPIY72BesbuX5zQ1la/ZFwKpQcGc5c= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 h1:xB6ikOC/R3n3hjy68EJ0sbZhH4vwEhd6JM9jZ1U2SVY= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 h1:mBOuLCPOOMMq8N1+dUM5FqZclqga1+u6fAbPqQcbIhc= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:cwPyDfj+ZNFE7kvcWbayQJyeC/KQA16HTXOxgHphL0w= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Zk9zG8kt3mXAboclUXQlvvxKQuhnI8u5NdDEl8uotNY= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:Lu05srGqddQRMnl1MZtGAReln2yJljeGx9b1IadlMJ8= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Tk9bDywUmOtc0iMjjCVIwMlAQNsxCy+bK+bTNA0OaBE= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:tQqDQw3tEHdQpt7NTdAwF3UvZ3CjNIj/IJKMRFmm388= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:biUIbI2YxUrcQikEfS/bwPA8NsHp/WO+VZUG4morUmE= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20260413093659-e4926ba205fa h1:7SehNSF1UHbLZa5dk+1rW1aperffJzl5r6TCJIXtAaY= +github.com/sagernet/cronet-go v0.0.0-20260413093659-e4926ba205fa/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= +github.com/sagernet/cronet-go/all v0.0.0-20260413093659-e4926ba205fa h1:ijk5v9N/akiMgqu734yMpv7Pk9F4Qmjh8Vfdcb4uJHE= +github.com/sagernet/cronet-go/all v0.0.0-20260413093659-e4926ba205fa/go.mod h1:+FENo4+0AOvH9e3oY6/iO7yy7USNt61dgbnI5W0TDZ0= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260413092954-cd09eb3e271b h1:O+PkYT88ayVWESX5tqxeMeS9OnzC3ZTic8gYiPJNXT8= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:o0MsgbsJwYkbqlbfaCvmAwb8/LAXeoSP8NE/aNvR/yY= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260413092954-cd09eb3e271b h1:JEQnc7cRMUahWJFtWY6n0hs1LE0KgyRv3pD0RWS8Yo8= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:69+AKzuUW9hzw2nU79c2DWfuzrIZ3PJm1KAwXh+7xr0= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:jp9FHUVTCJQ67Ecw3Inoct6/z1VTFXPtNYpXt47pa4E= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:WN3DZoECd2UbhmYQGpOA4jx4QBXiZuN1DvL/35NT61g= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:H4RKicwrIa4PwTXZOmXOg85hiCrpeFja4daOlX180pE= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:Rwi+Cu+Hgwj28F1lh837gGqSqn7oU8+r5i3UJyLPkKc= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:v2wcnPX3gt0PngFYXjXYAiarFckwx3pVAP6ETSpbSWE= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260413092954-cd09eb3e271b h1:Bl0zZ3QZq6pPJMbQlYHDhhaGngVefRlFzxWc0p48eHo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260413092954-cd09eb3e271b h1:vf+MbGv6RvvmXUNvganykBOnDIVXxy8XgtKOOqOcxtE= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:2IAc1bVFYF+B6hof34ChQKVhw7LElBxEEx7S0n+7o78= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260413092954-cd09eb3e271b h1:NrJaiOS0VLmWTbUHhXDsLTqelmCW4y3xJqptPs4Sx0s= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260413092954-cd09eb3e271b h1:A+ubSkca1nl2cT8pYUqCo1O7M41suNrKpWhZKCM/aIQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:WrhGH5FDXlCAoXwN6N44yCMvy6EbIurmTmptkz3mmms= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260413092954-cd09eb3e271b h1:kgwB5p5e0gdVX5iYRE7VbZS/On4qnb4UKonkGPwhkDI= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260413092954-cd09eb3e271b h1:Z3dOeFlRIOeQhSh+mCYDHui1yR3S/Uw8eupczzBvxqw= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260413092954-cd09eb3e271b h1:LPi6jz1k11Q67hm3Pw6aaPJ/Z6e3VtNhzrRjr5/5AQo= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260413092954-cd09eb3e271b h1:55sqihyfXWN7y7p7gOEgtUz9cm1mV3SDQ90/v6ROFaA= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260413092954-cd09eb3e271b h1:OTA1cbv5YIDVsYA8AAXHC4NgEc7b6pDiY+edujLWfJU= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260413092954-cd09eb3e271b h1:B/rdD/1A+RgqUYUZcoGhLeMqijnBd1mUt8+5LhOH7j8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260413092954-cd09eb3e271b h1:QFRWi6FucrODS4xQ8e9GYIzGSeMFO/DAMtTCVeJiCvM= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260413092954-cd09eb3e271b h1:2WJjPKZHLNIB4D17c3o9S+SP9kb3Qh0D26oWlun1+pE= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260413092954-cd09eb3e271b h1:cUNTe4gNncRpYL28jzQf6qcJej40zzGQsH0o6CLUGws= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:+sc1LJF0FjU2hVO5xBqqT+8qzoU08J2uHwxSle2m/Hw= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:+D/uhFxllI/KTLpeNEl8dwF3omPGmUFbrqt5tJkAyp0= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b h1:nSUzzTUAZdqjGGckayk64sz+F0TGJPHvauTiAn27UKk= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b h1:PE/fYBiHzB52gnQMg0soBfQyJCzmWHti48kCe2TBt9w= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:hy/3lPV11pKAAojDFnb95l9NpwOym6kME7FxS9p8sXs= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= From 1cfcea769faf7364ef683f44a9cfec38a3279959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 14 Apr 2026 14:26:49 +0800 Subject: [PATCH 75/99] Update Go to 1.25.9 --- .github/setup_go_for_macos1013.sh | 2 +- .github/setup_go_for_windows7.sh | 2 +- .github/workflows/build.yml | 10 +++++----- .github/workflows/docker.yml | 2 +- .github/workflows/linux.yml | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/setup_go_for_macos1013.sh b/.github/setup_go_for_macos1013.sh index 49eac606..9889d236 100755 --- a/.github/setup_go_for_macos1013.sh +++ b/.github/setup_go_for_macos1013.sh @@ -2,7 +2,7 @@ set -euo pipefail -VERSION="1.25.8" +VERSION="1.25.9" PATCH_COMMITS=( "afe69d3cec1c6dcf0f1797b20546795730850070" "1ed289b0cf87dc5aae9c6fe1aa5f200a83412938" diff --git a/.github/setup_go_for_windows7.sh b/.github/setup_go_for_windows7.sh index 777d78b0..e8c36596 100755 --- a/.github/setup_go_for_windows7.sh +++ b/.github/setup_go_for_windows7.sh @@ -2,7 +2,7 @@ set -euo pipefail -VERSION="1.25.8" +VERSION="1.25.9" PATCH_COMMITS=( "466f6c7a29bc098b0d4c987b803c779222894a11" "1bdabae205052afe1dadb2ad6f1ba612cdbc532a" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2cf9e62d..9d144536 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.8 + go-version: ~1.25.9 - name: Check input version if: github.event_name == 'workflow_dispatch' run: |- @@ -124,7 +124,7 @@ jobs: if: ${{ ! matrix.legacy_win7 }} uses: actions/setup-go@v5 with: - go-version: ~1.25.8 + go-version: ~1.25.9 - name: Cache Go for Windows 7 if: matrix.legacy_win7 id: cache-go-for-windows7 @@ -641,7 +641,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.8 + go-version: ~1.25.9 - name: Setup Android NDK id: setup-ndk uses: nttld/setup-ndk@v1 @@ -731,7 +731,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.8 + go-version: ~1.25.9 - name: Setup Android NDK id: setup-ndk uses: nttld/setup-ndk@v1 @@ -830,7 +830,7 @@ jobs: if: matrix.if uses: actions/setup-go@v5 with: - go-version: ~1.25.8 + go-version: ~1.25.9 - name: Set tag if: matrix.if run: |- diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 99e8ee8a..2ec65bda 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -55,7 +55,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.8 + go-version: ~1.25.9 - name: Clone cronet-go if: matrix.naive run: | diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index f3c60989..88c1a5fd 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -29,7 +29,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.8 + go-version: ~1.25.9 - name: Check input version if: github.event_name == 'workflow_dispatch' run: |- @@ -72,7 +72,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: ~1.25.8 + go-version: ~1.25.9 - name: Clone cronet-go if: matrix.naive run: | From d5adb54bc6c6b2c21ab6f748276c4ec62d9bb650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 14 Apr 2026 14:24:46 +0800 Subject: [PATCH 76/99] Bump version --- clients/android | 2 +- clients/apple | 2 +- docs/changelog.md | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/clients/android b/clients/android index fea0f3a7..ab099186 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit fea0f3a7ba5921d87ddd11902be3c148a61fc689 +Subproject commit ab09918615e3fbfb6f2f17a06f1b49448dcccbb1 diff --git a/clients/apple b/clients/apple index ffbf405b..ad7434d6 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit ffbf405b5223c7537ac70f52d8cd39258b67942c +Subproject commit ad7434d6769382a9b9e6919fb925840f5cabe43e diff --git a/docs/changelog.md b/docs/changelog.md index 92bc7d9f..8e500b75 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,12 @@ icon: material/alert-decagram --- +#### 1.13.8 + +* Update naiveproxy to v147.0.7727.49-1 +* Fix fake-ip DNS server should return SUCCESS when another address type is not configured +* Fixes and improvements + #### 1.13.7 * Fixes and improvements From bb3ad9c694004353cb32b320d4da9a5194eb42d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 14 Apr 2026 16:00:47 +0800 Subject: [PATCH 77/99] documentation: Fix typo --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 8e500b75..8f826099 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,7 +5,7 @@ icon: material/alert-decagram #### 1.13.8 * Update naiveproxy to v147.0.7727.49-1 -* Fix fake-ip DNS server should return SUCCESS when another address type is not configured +* Fix fake-ip DNS server should return SUCCESS when address type is not configured * Fixes and improvements #### 1.13.7 From 7ed5ef6da4d095616f869627163c311e00894f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 16 Apr 2026 00:27:14 +0800 Subject: [PATCH 78/99] sing: Fix udpnat2 timeout --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5aec4dba..736336e9 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.4 + github.com/sagernet/sing v0.8.6 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.1 github.com/sagernet/sing-shadowsocks v0.2.8 diff --git a/go.sum b/go.sum index 26f9d0e2..e56d9d70 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.4 h1:Fj+jlY3F8vhcRfz/G/P3Dwcs5wqnmyNPT7u1RVVmjFI= -github.com/sagernet/sing v0.8.4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.6 h1:9rAYgDzNkjHkdMnmyOVQr1SR+U8QvUojBcmAdAK0Mkw= +github.com/sagernet/sing v0.8.6/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw= From 9b155ba467945eb91cf42426b178abbad9949e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 16 Apr 2026 16:08:44 +0800 Subject: [PATCH 79/99] Fix rdrc cache --- experimental/cachefile/rdrc.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/cachefile/rdrc.go b/experimental/cachefile/rdrc.go index d27ea8b2..dde324c3 100644 --- a/experimental/cachefile/rdrc.go +++ b/experimental/cachefile/rdrc.go @@ -72,6 +72,9 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) ( } func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) error { + expiresAt := buf.Get(8) + defer buf.Put(expiresAt) + binary.BigEndian.PutUint64(expiresAt, uint64(time.Now().Add(c.rdrcTimeout).Unix())) return c.batch(func(tx *bbolt.Tx) error { bucket, err := c.createBucket(tx, bucketRDRC) if err != nil { @@ -85,9 +88,6 @@ func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) e binary.BigEndian.PutUint16(key, qType) copy(key[2:], qName) defer buf.Put(key) - expiresAt := buf.Get(8) - defer buf.Put(expiresAt) - binary.BigEndian.PutUint64(expiresAt, uint64(time.Now().Add(c.rdrcTimeout).Unix())) return bucket.Put(key, expiresAt) }) } From 0bd109d7bc75a92f1247e6728508f3ca62a2d04f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 19 Apr 2026 20:38:52 +0800 Subject: [PATCH 80/99] sing: Fix interface finder --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 736336e9..1be5d2d5 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.6 + github.com/sagernet/sing v0.8.7 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.1 github.com/sagernet/sing-shadowsocks v0.2.8 diff --git a/go.sum b/go.sum index e56d9d70..9c50a82f 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.6 h1:9rAYgDzNkjHkdMnmyOVQr1SR+U8QvUojBcmAdAK0Mkw= -github.com/sagernet/sing v0.8.6/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.7 h1:XQKFtGasfW9XmfRghZs7f6hLk/7vFCdqdY2ha8qGyDg= +github.com/sagernet/sing v0.8.7/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw= From ca76c5637762b5efa2c8f305d3ce6b5b2f82d250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 19 Apr 2026 20:39:38 +0800 Subject: [PATCH 81/99] daemon: Fix registry leak --- daemon/instance.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/instance.go b/daemon/instance.go index 4ed74182..3acf75cc 100644 --- a/daemon/instance.go +++ b/daemon/instance.go @@ -69,7 +69,7 @@ type OverrideOptions struct { } func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) { - ctx := s.ctx + ctx := service.ExtendContext(s.ctx) service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager)) ctx, cancel := context.WithCancel(include.Context(ctx)) options, err := parseConfig(ctx, profileContent) From 3a236d9c3c4530715c7b0fac0cb7f7ae2f9fb174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 19 Apr 2026 20:41:33 +0800 Subject: [PATCH 82/99] fswatch: Fix close --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 1be5d2d5..96f46a1a 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/sagernet/cors v1.2.1 github.com/sagernet/cronet-go v0.0.0-20260413093659-e4926ba205fa github.com/sagernet/cronet-go/all v0.0.0-20260413093659-e4926ba205fa - github.com/sagernet/fswatch v0.1.1 + github.com/sagernet/fswatch v0.1.2 github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 @@ -76,7 +76,7 @@ require ( github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect github.com/ebitengine/purego v0.9.1 // indirect github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gaissmai/bart v0.18.0 // indirect github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect diff --git a/go.sum b/go.sum index 9c50a82f..ebb4d843 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE= github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo= @@ -224,8 +224,8 @@ github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e27 github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b h1:hy/3lPV11pKAAojDFnb95l9NpwOym6kME7FxS9p8sXs= github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= -github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= -github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= +github.com/sagernet/fswatch v0.1.2 h1:/TT7k4mkce1qFPxamLO842WjqBgbTBiXP2mlUjp9PFk= +github.com/sagernet/fswatch v0.1.2/go.mod h1:5BpGmpUQVd3Mc5r313HRpvADHRg3/rKn5QbwFteB880= github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= github.com/sagernet/gomobile v0.1.12/go.mod h1:A8l3FlHi2D/+mfcd4HHvk5DGFPW/ShFb9jHP5VmSiDY= github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/GejpNh+z30d5H1hwaB0nD9eY3o= From a80ef94f090cb5024b5dd74304d003b462f4a9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 19 Apr 2026 21:15:54 +0800 Subject: [PATCH 83/99] Fix tailscale endpoint early-start close panic --- protocol/tailscale/endpoint.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 33b76930..30db4b6a 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -110,6 +110,7 @@ type Endpoint struct { systemInterface bool systemInterfaceName string systemInterfaceMTU uint32 + serverStarted bool systemTun tun.Tun systemDialer *dialer.DefaultDialer fallbackTCPCloser func() @@ -365,6 +366,7 @@ func (t *Endpoint) postStart() error { } return err } + t.serverStarted = true if t.fallbackTCPCloser == nil { t.fallbackTCPCloser = t.server.RegisterFallbackTCPHandler(func(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) { return func(conn net.Conn) { @@ -482,7 +484,11 @@ func (t *Endpoint) watchState() { } func (t *Endpoint) Close() error { - err := common.Close(common.PtrOrNil(t.server)) + var err error + if t.serverStarted { + err = common.Close(common.PtrOrNil(t.server)) + t.serverStarted = false + } netmon.RegisterInterfaceGetter(nil) netns.SetControlFunc(nil) if t.fallbackTCPCloser != nil { From fb61987d9384b391932e75cda2f20aee2db9b823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 19 Apr 2026 21:54:20 +0800 Subject: [PATCH 84/99] tun: memmod: be more resilient toward weird PE files --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 96f46a1a..2574e604 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.7 + github.com/sagernet/sing-tun v0.8.8 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 diff --git a/go.sum b/go.sum index ebb4d843..20270241 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.7 h1:q49cI7Cbp+BcgzaJitQ9QdLO77BqnnaQRkSEMoGmF3g= -github.com/sagernet/sing-tun v0.8.7/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-tun v0.8.8 h1:cnEjNKUPiE3F20l4umFBaWxEjL4insMiNFxwaXqooEc= +github.com/sagernet/sing-tun v0.8.8/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= From 9b72b352d59a05835315cf9c03c6874b4c339690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 20 Apr 2026 00:10:46 +0800 Subject: [PATCH 85/99] sing: Fix UoT connect race --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2574e604..099f3359 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.7 + github.com/sagernet/sing v0.8.8 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.1 github.com/sagernet/sing-shadowsocks v0.2.8 diff --git a/go.sum b/go.sum index 20270241..e83f7537 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.7 h1:XQKFtGasfW9XmfRghZs7f6hLk/7vFCdqdY2ha8qGyDg= -github.com/sagernet/sing v0.8.7/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.8 h1:1dRlGJ3wm4d2nwjKI1R/dr/7GKDKgUvXyD4OAWlQyt8= +github.com/sagernet/sing v0.8.8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw= From b3606e33a615acd88777ff415bb3d01b8587e3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 19 Apr 2026 22:45:54 +0800 Subject: [PATCH 86/99] release: fix apk package file ownership --- .github/build_alpine_apk.sh | 17 +++++++++++++++-- .github/build_openwrt_apk.sh | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/build_alpine_apk.sh b/.github/build_alpine_apk.sh index aaaa04f9..610103b5 100755 --- a/.github/build_alpine_apk.sh +++ b/.github/build_alpine_apk.sh @@ -2,6 +2,18 @@ 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" < "$APK_ROOT_DIR/etc/group" < "$PACKAGES_DIR/.conffiles_static" | sort > "$PACKAGES_DIR/.list" # Build APK -apk mkpkg \ +apk --root "$APK_ROOT_DIR" mkpkg \ --info "name:sing-box" \ --info "version:${APK_VERSION}" \ --info "description:The universal proxy platform." \ diff --git a/.github/build_openwrt_apk.sh b/.github/build_openwrt_apk.sh index 49e1c131..59f07fd8 100755 --- a/.github/build_openwrt_apk.sh +++ b/.github/build_openwrt_apk.sh @@ -2,6 +2,18 @@ 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" < "$APK_ROOT_DIR/etc/group" < "$PACKAGES_DIR/.conffiles_static" | sort > "$PACKAGES_DIR/.list" # Build APK -apk mkpkg \ +apk --root "$APK_ROOT_DIR" mkpkg \ --info "name:sing-box" \ --info "version:${APK_VERSION}" \ --info "description:The universal proxy platform." \ From 3124cdd6619d0b5d3fd223efbb8170bf404eb101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 19 Apr 2026 23:45:12 +0800 Subject: [PATCH 87/99] Fix windows bssid matching --- adapter/network.go | 21 +++++++++++++++++++++ route/network.go | 1 + route/rule/rule_item_wifi_bssid.go | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/adapter/network.go b/adapter/network.go index dd53b2b4..14fe46c8 100644 --- a/adapter/network.go +++ b/adapter/network.go @@ -1,6 +1,9 @@ package adapter import ( + "encoding/hex" + "net" + "strings" "time" C "github.com/sagernet/sing-box/constant" @@ -51,6 +54,24 @@ type WIFIState struct { 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 { control.Interface Type C.InterfaceType diff --git a/route/network.go b/route/network.go index b8eefdc0..03e94879 100644 --- a/route/network.go +++ b/route/network.go @@ -424,6 +424,7 @@ func (r *NetworkManager) WIFIState() adapter.WIFIState { } func (r *NetworkManager) onWIFIStateChanged(state adapter.WIFIState) { + state.BSSID = adapter.NormalizeWIFIBSSID(state.BSSID) r.wifiStateMutex.Lock() if state != r.wifiState { r.wifiState = state diff --git a/route/rule/rule_item_wifi_bssid.go b/route/rule/rule_item_wifi_bssid.go index 8f887322..703562ba 100644 --- a/route/rule/rule_item_wifi_bssid.go +++ b/route/rule/rule_item_wifi_bssid.go @@ -18,7 +18,7 @@ type WIFIBSSIDItem struct { func NewWIFIBSSIDItem(networkManager adapter.NetworkManager, bssidList []string) *WIFIBSSIDItem { bssidMap := make(map[string]bool) for _, bssid := range bssidList { - bssidMap[bssid] = true + bssidMap[adapter.NormalizeWIFIBSSID(bssid)] = true } return &WIFIBSSIDItem{ bssidList, From e4bc459975deb09e06c17e96df4c4c33760feb77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 19 Apr 2026 23:25:00 +0800 Subject: [PATCH 88/99] Skip process search for non-local source addresses --- route/process_cache.go | 58 ++++++++++++++++++++++++++++++++++++++++++ route/route.go | 32 +---------------------- 2 files changed, 59 insertions(+), 31 deletions(-) diff --git a/route/process_cache.go b/route/process_cache.go index 691a4e8e..01b477c4 100644 --- a/route/process_cache.go +++ b/route/process_cache.go @@ -3,6 +3,7 @@ package route import ( "context" "net/netip" + "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" @@ -32,3 +33,60 @@ func (r *Router) findProcessInfoCached(ctx context.Context, network string, sour r.processCache.Add(key, processCacheEntry{result: result, err: err}) return result, err } + +func (r *Router) searchProcessInfo(ctx context.Context, metadata *adapter.InboundContext) { + if r.processSearcher == nil || metadata.ProcessInfo != nil || !r.isLocalSource(metadata.Source.Addr) { + return + } + var originDestination netip.AddrPort + if metadata.OriginDestination.IsValid() { + originDestination = metadata.OriginDestination.AddrPort() + } else if metadata.Destination.IsIP() { + originDestination = metadata.Destination.AddrPort() + } + processInfo, err := r.findProcessInfoCached(ctx, metadata.Network, metadata.Source.AddrPort(), originDestination) + if err != nil { + r.logger.InfoContext(ctx, "failed to search process: ", err) + return + } + metadata.ProcessInfo = processInfo + if processInfo.ProcessPath != "" { + if processInfo.UserName != "" { + r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.UserName) + } else if processInfo.UserId != -1 { + r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId) + } else { + r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath) + } + return + } + if len(processInfo.AndroidPackageNames) > 0 { + r.logger.InfoContext(ctx, "found package name: ", strings.Join(processInfo.AndroidPackageNames, ", ")) + return + } + if processInfo.UserId != -1 { + if processInfo.UserName != "" { + r.logger.InfoContext(ctx, "found user: ", processInfo.UserName) + } else { + r.logger.InfoContext(ctx, "found user id: ", processInfo.UserId) + } + } +} + +func (r *Router) isLocalSource(source netip.Addr) bool { + if !source.IsValid() { + return false + } + source = source.Unmap() + if source.IsLoopback() { + return true + } + for _, netInterface := range r.network.InterfaceFinder().Interfaces() { + for _, prefix := range netInterface.Addresses { + if prefix.Addr().Unmap() == source { + return true + } + } + } + return false +} diff --git a/route/route.go b/route/route.go index 77b66ea4..7c24219e 100644 --- a/route/route.go +++ b/route/route.go @@ -407,37 +407,7 @@ func (r *Router) matchRule( selectedRule adapter.Rule, selectedRuleIndex int, buffers []*buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error, ) { - if r.processSearcher != nil && metadata.ProcessInfo == nil { - var originDestination netip.AddrPort - if metadata.OriginDestination.IsValid() { - originDestination = metadata.OriginDestination.AddrPort() - } else if metadata.Destination.IsIP() { - originDestination = metadata.Destination.AddrPort() - } - processInfo, fErr := r.findProcessInfoCached(ctx, metadata.Network, metadata.Source.AddrPort(), originDestination) - if fErr != nil { - r.logger.InfoContext(ctx, "failed to search process: ", fErr) - } else { - if processInfo.ProcessPath != "" { - if processInfo.UserName != "" { - r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.UserName) - } else if processInfo.UserId != -1 { - r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId) - } else { - r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath) - } - } else if len(processInfo.AndroidPackageNames) > 0 { - r.logger.InfoContext(ctx, "found package name: ", strings.Join(processInfo.AndroidPackageNames, ", ")) - } else if processInfo.UserId != -1 { - if processInfo.UserName != "" { - r.logger.InfoContext(ctx, "found user: ", processInfo.UserName) - } else { - r.logger.InfoContext(ctx, "found user id: ", processInfo.UserId) - } - } - metadata.ProcessInfo = processInfo - } - } + r.searchProcessInfo(ctx, metadata) if metadata.Destination.Addr.IsValid() && r.dnsTransport.FakeIP() != nil && r.dnsTransport.FakeIP().Store().Contains(metadata.Destination.Addr) { domain, loaded := r.dnsTransport.FakeIP().Store().Lookup(metadata.Destination.Addr) if !loaded { From 60dd7ea5c9f9503440861af913dce0f37d4a517d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 19 Apr 2026 23:25:00 +0800 Subject: [PATCH 89/99] Simplify lifecycle logs --- adapter/endpoint/manager.go | 7 +++---- adapter/inbound/manager.go | 7 +++---- adapter/lifecycle.go | 11 +++++++++-- adapter/outbound/manager.go | 11 +++++------ adapter/service/manager.go | 7 +++---- box.go | 6 +++--- 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/adapter/endpoint/manager.go b/adapter/endpoint/manager.go index 8b7c287f..7d7aa4bc 100644 --- a/adapter/endpoint/manager.go +++ b/adapter/endpoint/manager.go @@ -12,7 +12,6 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" ) var _ adapter.EndpointManager = (*Manager)(nil) @@ -55,7 +54,7 @@ func (m *Manager) Start(stage adapter.StartStage) error { if err != nil { return E.Cause(err, stage, " ", name) } - m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, stage, " ", name) } return nil } @@ -80,7 +79,7 @@ func (m *Manager) Close() error { return E.Cause(err, "close ", name) }) monitor.Finish() - m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, "close ", name) } return nil } @@ -137,7 +136,7 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log. if err != nil { return E.Cause(err, stage, " ", name) } - m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, stage, " ", name) } } if existsEndpoint, loaded := m.endpointByTag[tag]; loaded { diff --git a/adapter/inbound/manager.go b/adapter/inbound/manager.go index 438c20f4..2d1b0d2e 100644 --- a/adapter/inbound/manager.go +++ b/adapter/inbound/manager.go @@ -12,7 +12,6 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" ) var _ adapter.InboundManager = (*Manager)(nil) @@ -54,7 +53,7 @@ func (m *Manager) Start(stage adapter.StartStage) error { if err != nil { return E.Cause(err, stage, " ", name) } - m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, stage, " ", name) } return nil } @@ -79,7 +78,7 @@ func (m *Manager) Close() error { return E.Cause(err, "close ", name) }) monitor.Finish() - m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, "close ", name) } return nil } @@ -139,7 +138,7 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log. if err != nil { return E.Cause(err, stage, " ", name) } - m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, stage, " ", name) } } if existsInbound, loaded := m.inboundByTag[tag]; loaded { diff --git a/adapter/lifecycle.go b/adapter/lifecycle.go index b969c98a..ebc0a303 100644 --- a/adapter/lifecycle.go +++ b/adapter/lifecycle.go @@ -83,7 +83,7 @@ func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) er if err != nil { return err } - logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + LogElapsed(logger, startTime, stage, " ", name) } return nil } @@ -96,7 +96,14 @@ func StartNamed(logger log.ContextLogger, stage StartStage, services []Lifecycle if err != nil { return E.Cause(err, stage.String(), " ", service.Name()) } - logger.Trace(stage, " ", service.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + LogElapsed(logger, startTime, stage, " ", service.Name()) } return nil } + +func LogElapsed(logger log.ContextLogger, startTime time.Time, description ...any) { + duration := time.Since(startTime) + if duration > time.Second { + logger.Trace(append(description, " completed (", F.Seconds(duration.Seconds()), "s)")...) + } +} diff --git a/adapter/outbound/manager.go b/adapter/outbound/manager.go index 5c1b5d99..287f1a91 100644 --- a/adapter/outbound/manager.go +++ b/adapter/outbound/manager.go @@ -14,7 +14,6 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" ) @@ -90,7 +89,7 @@ func (m *Manager) Start(stage adapter.StartStage) error { if err != nil { return E.Cause(err, stage, " ", name) } - m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, stage, " ", name) } } return nil @@ -125,7 +124,7 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error { if err != nil { return E.Cause(err, "start ", name) } - m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, "start ", name) } else if starter, isStarter := outboundToStart.(interface { Start() error }); isStarter { @@ -137,7 +136,7 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error { if err != nil { return E.Cause(err, "start ", name) } - m.logger.Trace("start ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, "start ", name) } } if len(started) == len(outbounds) { @@ -192,7 +191,7 @@ func (m *Manager) Close() error { return E.Cause(err, "close ", name) }) monitor.Finish() - m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, "close ", name) } } return nil @@ -281,7 +280,7 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log. if err != nil { return E.Cause(err, stage, " ", name) } - m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, stage, " ", name) } } m.access.Lock() diff --git a/adapter/service/manager.go b/adapter/service/manager.go index f17aa07e..8ae961de 100644 --- a/adapter/service/manager.go +++ b/adapter/service/manager.go @@ -12,7 +12,6 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" ) var _ adapter.ServiceManager = (*Manager)(nil) @@ -52,7 +51,7 @@ func (m *Manager) Start(stage adapter.StartStage) error { if err != nil { return E.Cause(err, stage, " ", name) } - m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, stage, " ", name) } return nil } @@ -77,7 +76,7 @@ func (m *Manager) Close() error { return E.Cause(err, "close ", name) }) monitor.Finish() - m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, "close ", name) } return nil } @@ -134,7 +133,7 @@ func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag stri if err != nil { return E.Cause(err, stage, " ", name) } - m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(m.logger, startTime, stage, " ", name) } } if existsService, loaded := m.serviceByTag[tag]; loaded { diff --git a/box.go b/box.go index a72884e0..549f2fe0 100644 --- a/box.go +++ b/box.go @@ -520,7 +520,7 @@ func (s *Box) Close() error { err = E.Append(err, closeItem.service.Close(), func(err error) error { return E.Cause(err, "close ", closeItem.name) }) - s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(s.logger, startTime, "close ", closeItem.name) } for _, lifecycleService := range s.internalService { s.logger.Trace("close ", lifecycleService.Name()) @@ -528,14 +528,14 @@ func (s *Box) Close() error { err = E.Append(err, lifecycleService.Close(), func(err error) error { return E.Cause(err, "close ", lifecycleService.Name()) }) - s.logger.Trace("close ", lifecycleService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(s.logger, startTime, "close ", lifecycleService.Name()) } s.logger.Trace("close logger") startTime := time.Now() err = E.Append(err, s.logFactory.Close(), func(err error) error { return E.Cause(err, "close logger") }) - s.logger.Trace("close logger completed (", F.Seconds(time.Since(startTime).Seconds()), "s)") + adapter.LogElapsed(s.logger, startTime, "close logger") return err } From b3523abad54b25347cc6bfa1db5db5eb37b38c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 20 Apr 2026 07:42:16 +0800 Subject: [PATCH 90/99] tun: Fix multi include/exclude interfaces --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 099f3359..38f33d5a 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.8 + github.com/sagernet/sing-tun v0.8.9 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 @@ -135,7 +135,7 @@ require ( github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260413092954-cd09eb3e271b // indirect github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260413092954-cd09eb3e271b // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect - github.com/sagernet/nftables v0.3.0-beta.4 // indirect + github.com/sagernet/nftables v0.3.0-mod.2 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect diff --git a/go.sum b/go.sum index e83f7537..b03c857a 100644 --- a/go.sum +++ b/go.sum @@ -232,8 +232,8 @@ github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/ github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1/go.mod h1:NJKBtm9nVEK3iyOYWsUlrDQuoGh4zJ4KOPhSYVidvQ4= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= -github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= +github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje+vW5Q0OQ= +github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= github.com/sagernet/sing v0.8.8 h1:1dRlGJ3wm4d2nwjKI1R/dr/7GKDKgUvXyD4OAWlQyt8= @@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.8 h1:cnEjNKUPiE3F20l4umFBaWxEjL4insMiNFxwaXqooEc= -github.com/sagernet/sing-tun v0.8.8/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs= +github.com/sagernet/sing-tun v0.8.9 h1:ixFKKUGdVcJl4wb0xbL36hobiw9l6DIH497EQf5ILpM= +github.com/sagernet/sing-tun v0.8.9/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= From c3de6a25fbe1fca4d0313109e91b07af28937b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 20 Apr 2026 07:43:55 +0800 Subject: [PATCH 91/99] documentation: Remove warp ads --- README.md | 8 -------- docs/sponsors.md | 6 ------ 2 files changed, 14 deletions(-) diff --git a/README.md b/README.md index 90be2a83..7b1c833a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,3 @@ -> Sponsored by [Warp](https://go.warp.dev/sing-box), built for coding with multiple AI agents - - -Warp sponsorship - - ---- - # sing-box The universal proxy platform. diff --git a/docs/sponsors.md b/docs/sponsors.md index 33898928..4e4dd07c 100644 --- a/docs/sponsors.md +++ b/docs/sponsors.md @@ -11,12 +11,6 @@ the project maintainer via [GitHub Sponsors](https://github.com/sponsors/nekohas ![](https://nekohasekai.github.io/sponsor-images/sponsors.svg) -## Commercial Sponsors - -> [Warp](https://go.warp.dev/sing-box), Built for coding with multiple AI agents. - -[![](https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png)](https://go.warp.dev/sing-box) - ## Special Sponsors > Viral Tech, Inc. From d942ecc90474c600c30bd00ecce822ac91b7643d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 20 Apr 2026 07:42:45 +0800 Subject: [PATCH 92/99] Bump version 1.13.9 --- clients/android | 2 +- clients/apple | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/clients/android b/clients/android index ab099186..67a19777 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit ab09918615e3fbfb6f2f17a06f1b49448dcccbb1 +Subproject commit 67a19777ce5787843f40fc8bc8b83fe6a1274fa0 diff --git a/clients/apple b/clients/apple index ad7434d6..43a2bf46 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit ad7434d6769382a9b9e6919fb925840f5cabe43e +Subproject commit 43a2bf467fb31e303b7dc988d87ad3ea77608405 diff --git a/docs/changelog.md b/docs/changelog.md index 8f826099..096b7228 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.13.9 + +* Fixes and improvements + #### 1.13.8 * Update naiveproxy to v147.0.7727.49-1 From 71f6a2ab4ebc2cbf3664e35e8be1e2a07e984abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 21 Apr 2026 15:23:05 +0800 Subject: [PATCH 93/99] Fix process search skipped for TUN --- common/dialer/default_parallel_interface.go | 6 ++++++ experimental/libbox/service.go | 5 +---- route/rule/rule_network_interface_address.go | 4 ++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/common/dialer/default_parallel_interface.go b/common/dialer/default_parallel_interface.go index ca374b2e..eafab75a 100644 --- a/common/dialer/default_parallel_interface.go +++ b/common/dialer/default_parallel_interface.go @@ -184,6 +184,12 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) { interfaces := networkManager.NetworkInterfaces() + myInterface := networkManager.InterfaceMonitor().MyInterface() + if myInterface != "" { + interfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { + return it.Name != myInterface + }) + } switch strategy { case C.NetworkStrategyDefault: if len(interfaceType) == 0 { diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 0a841a1b..7d0b3004 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -103,14 +103,11 @@ func (w *platformInterfaceWrapper) NetworkInterfaces() ([]adapter.NetworkInterfa } var interfaces []adapter.NetworkInterface for _, netInterface := range iteratorToArray[*NetworkInterface](interfaceIterator) { - if netInterface.Name == w.myTunName { - continue - } w.defaultInterfaceAccess.Lock() // (GOOS=windows) SA4006: this value of `isDefault` is never used // Why not used? //nolint:staticcheck - isDefault := w.defaultInterface != nil && int(netInterface.Index) == w.defaultInterface.Index + isDefault := netInterface.Name != w.myTunName && w.defaultInterface != nil && int(netInterface.Index) == w.defaultInterface.Index w.defaultInterfaceAccess.Unlock() interfaces = append(interfaces, adapter.NetworkInterface{ Interface: control.Interface{ diff --git a/route/rule/rule_network_interface_address.go b/route/rule/rule_network_interface_address.go index c699c593..135a703e 100644 --- a/route/rule/rule_network_interface_address.go +++ b/route/rule/rule_network_interface_address.go @@ -40,9 +40,13 @@ func NewNetworkInterfaceAddressItem(networkManager adapter.NetworkManager, inter func (r *NetworkInterfaceAddressItem) Match(metadata *adapter.InboundContext) bool { interfaces := r.networkManager.NetworkInterfaces() + myInterface := r.networkManager.InterfaceMonitor().MyInterface() match: for ifType, addresses := range r.interfaceAddresses { for _, networkInterface := range interfaces { + if networkInterface.Name == myInterface { + continue + } if networkInterface.Type != ifType { continue } From 83d3a6d4e1ee88dc6731410ce4cc18add9f71642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 21 Apr 2026 17:15:16 +0800 Subject: [PATCH 94/99] Hide lifecycle logs for fast operations --- adapter/endpoint/manager.go | 16 ++++++---------- adapter/inbound/manager.go | 16 ++++++---------- adapter/lifecycle.go | 25 +++++++++++++++---------- adapter/outbound/manager.go | 26 ++++++++++---------------- adapter/service/manager.go | 16 ++++++---------- box.go | 15 ++++++--------- 6 files changed, 49 insertions(+), 65 deletions(-) diff --git a/adapter/endpoint/manager.go b/adapter/endpoint/manager.go index 7d7aa4bc..535c31b4 100644 --- a/adapter/endpoint/manager.go +++ b/adapter/endpoint/manager.go @@ -4,7 +4,6 @@ import ( "context" "os" "sync" - "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/taskmonitor" @@ -48,13 +47,12 @@ func (m *Manager) Start(stage adapter.StartStage) error { } for _, endpoint := range m.endpoints { name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" - m.logger.Trace(stage, " ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, stage, " ", name) err := adapter.LegacyStart(endpoint, stage) + done() if err != nil { return E.Cause(err, stage, " ", name) } - adapter.LogElapsed(m.logger, startTime, stage, " ", name) } return nil } @@ -72,14 +70,13 @@ func (m *Manager) Close() error { var err error for _, endpoint := range endpoints { name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" - m.logger.Trace("close ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, "close ", name) monitor.Start("close ", name) err = E.Append(err, endpoint.Close(), func(err error) error { return E.Cause(err, "close ", name) }) monitor.Finish() - adapter.LogElapsed(m.logger, startTime, "close ", name) + done() } return nil } @@ -130,13 +127,12 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log. if m.started { name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]" for _, stage := range adapter.ListStartStages { - m.logger.Trace(stage, " ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, stage, " ", name) err = adapter.LegacyStart(endpoint, stage) + done() if err != nil { return E.Cause(err, stage, " ", name) } - adapter.LogElapsed(m.logger, startTime, stage, " ", name) } } if existsEndpoint, loaded := m.endpointByTag[tag]; loaded { diff --git a/adapter/inbound/manager.go b/adapter/inbound/manager.go index 2d1b0d2e..d6567cde 100644 --- a/adapter/inbound/manager.go +++ b/adapter/inbound/manager.go @@ -4,7 +4,6 @@ import ( "context" "os" "sync" - "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/taskmonitor" @@ -47,13 +46,12 @@ func (m *Manager) Start(stage adapter.StartStage) error { m.access.Unlock() for _, inbound := range inbounds { name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" - m.logger.Trace(stage, " ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, stage, " ", name) err := adapter.LegacyStart(inbound, stage) + done() if err != nil { return E.Cause(err, stage, " ", name) } - adapter.LogElapsed(m.logger, startTime, stage, " ", name) } return nil } @@ -71,14 +69,13 @@ func (m *Manager) Close() error { var err error for _, inbound := range inbounds { name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" - m.logger.Trace("close ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, "close ", name) monitor.Start("close ", name) err = E.Append(err, inbound.Close(), func(err error) error { return E.Cause(err, "close ", name) }) monitor.Finish() - adapter.LogElapsed(m.logger, startTime, "close ", name) + done() } return nil } @@ -132,13 +129,12 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log. if m.started { name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]" for _, stage := range adapter.ListStartStages { - m.logger.Trace(stage, " ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, stage, " ", name) err = adapter.LegacyStart(inbound, stage) + done() if err != nil { return E.Cause(err, stage, " ", name) } - adapter.LogElapsed(m.logger, startTime, stage, " ", name) } } if existsInbound, loaded := m.inboundByTag[tag]; loaded { diff --git a/adapter/lifecycle.go b/adapter/lifecycle.go index ebc0a303..2a6a8ed7 100644 --- a/adapter/lifecycle.go +++ b/adapter/lifecycle.go @@ -77,33 +77,38 @@ func getServiceName(service any) string { func Start(logger log.ContextLogger, stage StartStage, services ...Lifecycle) error { for _, service := range services { name := getServiceName(service) - logger.Trace(stage, " ", name) - startTime := time.Now() + done := LogElapsed(logger, stage, " ", name) err := service.Start(stage) + done() if err != nil { return err } - LogElapsed(logger, startTime, stage, " ", name) } return nil } func StartNamed(logger log.ContextLogger, stage StartStage, services []LifecycleService) error { for _, service := range services { - logger.Trace(stage, " ", service.Name()) - startTime := time.Now() + done := LogElapsed(logger, stage, " ", service.Name()) err := service.Start(stage) + done() if err != nil { return E.Cause(err, stage.String(), " ", service.Name()) } - LogElapsed(logger, startTime, stage, " ", service.Name()) } return nil } -func LogElapsed(logger log.ContextLogger, startTime time.Time, description ...any) { - duration := time.Since(startTime) - if duration > time.Second { - logger.Trace(append(description, " completed (", F.Seconds(duration.Seconds()), "s)")...) +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)") } } diff --git a/adapter/outbound/manager.go b/adapter/outbound/manager.go index 287f1a91..1bbad69e 100644 --- a/adapter/outbound/manager.go +++ b/adapter/outbound/manager.go @@ -6,7 +6,6 @@ import ( "os" "strings" "sync" - "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/taskmonitor" @@ -83,13 +82,12 @@ func (m *Manager) Start(stage adapter.StartStage) error { m.access.Unlock() for _, outbound := range outbounds { name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" - m.logger.Trace(stage, " ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, stage, " ", name) err := adapter.LegacyStart(outbound, stage) + done() if err != nil { return E.Cause(err, stage, " ", name) } - adapter.LogElapsed(m.logger, startTime, stage, " ", name) } } return nil @@ -116,27 +114,25 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error { canContinue = true name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]" if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter { - m.logger.Trace("start ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, "start ", name) monitor.Start("start ", name) err := starter.Start(adapter.StartStateStart) monitor.Finish() + done() if err != nil { return E.Cause(err, "start ", name) } - adapter.LogElapsed(m.logger, startTime, "start ", name) } else if starter, isStarter := outboundToStart.(interface { Start() error }); isStarter { - m.logger.Trace("start ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, "start ", name) monitor.Start("start ", name) err := starter.Start() monitor.Finish() + done() if err != nil { return E.Cause(err, "start ", name) } - adapter.LogElapsed(m.logger, startTime, "start ", name) } } if len(started) == len(outbounds) { @@ -184,14 +180,13 @@ func (m *Manager) Close() error { for _, outbound := range outbounds { if closer, isCloser := outbound.(io.Closer); isCloser { name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" - m.logger.Trace("close ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, "close ", name) monitor.Start("close ", name) err = E.Append(err, closer.Close(), func(err error) error { return E.Cause(err, "close ", name) }) monitor.Finish() - adapter.LogElapsed(m.logger, startTime, "close ", name) + done() } } return nil @@ -274,13 +269,12 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log. if m.started { name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]" for _, stage := range adapter.ListStartStages { - m.logger.Trace(stage, " ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, stage, " ", name) err = adapter.LegacyStart(outbound, stage) + done() if err != nil { return E.Cause(err, stage, " ", name) } - adapter.LogElapsed(m.logger, startTime, stage, " ", name) } } m.access.Lock() diff --git a/adapter/service/manager.go b/adapter/service/manager.go index 8ae961de..1a83c503 100644 --- a/adapter/service/manager.go +++ b/adapter/service/manager.go @@ -4,7 +4,6 @@ import ( "context" "os" "sync" - "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/taskmonitor" @@ -45,13 +44,12 @@ func (m *Manager) Start(stage adapter.StartStage) error { m.access.Unlock() for _, service := range services { name := "service/" + service.Type() + "[" + service.Tag() + "]" - m.logger.Trace(stage, " ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, stage, " ", name) err := adapter.LegacyStart(service, stage) + done() if err != nil { return E.Cause(err, stage, " ", name) } - adapter.LogElapsed(m.logger, startTime, stage, " ", name) } return nil } @@ -69,14 +67,13 @@ func (m *Manager) Close() error { var err error for _, service := range services { name := "service/" + service.Type() + "[" + service.Tag() + "]" - m.logger.Trace("close ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, "close ", name) monitor.Start("close ", name) err = E.Append(err, service.Close(), func(err error) error { return E.Cause(err, "close ", name) }) monitor.Finish() - adapter.LogElapsed(m.logger, startTime, "close ", name) + done() } return nil } @@ -127,13 +124,12 @@ func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag stri if m.started { name := "service/" + service.Type() + "[" + service.Tag() + "]" for _, stage := range adapter.ListStartStages { - m.logger.Trace(stage, " ", name) - startTime := time.Now() + done := adapter.LogElapsed(m.logger, stage, " ", name) err = adapter.LegacyStart(service, stage) + done() if err != nil { return E.Cause(err, stage, " ", name) } - adapter.LogElapsed(m.logger, startTime, stage, " ", name) } } if existsService, loaded := m.serviceByTag[tag]; loaded { diff --git a/box.go b/box.go index 549f2fe0..c88a9bc9 100644 --- a/box.go +++ b/box.go @@ -515,27 +515,24 @@ func (s *Box) Close() error { {"dns-transport", s.dnsTransport}, {"network", s.network}, } { - s.logger.Trace("close ", closeItem.name) - startTime := time.Now() + done := adapter.LogElapsed(s.logger, "close ", closeItem.name) err = E.Append(err, closeItem.service.Close(), func(err error) error { return E.Cause(err, "close ", closeItem.name) }) - adapter.LogElapsed(s.logger, startTime, "close ", closeItem.name) + done() } for _, lifecycleService := range s.internalService { - s.logger.Trace("close ", lifecycleService.Name()) - startTime := time.Now() + done := adapter.LogElapsed(s.logger, "close ", lifecycleService.Name()) err = E.Append(err, lifecycleService.Close(), func(err error) error { return E.Cause(err, "close ", lifecycleService.Name()) }) - adapter.LogElapsed(s.logger, startTime, "close ", lifecycleService.Name()) + done() } - s.logger.Trace("close logger") - startTime := time.Now() + done := adapter.LogElapsed(s.logger, "close logger") err = E.Append(err, s.logFactory.Close(), func(err error) error { return E.Cause(err, "close logger") }) - adapter.LogElapsed(s.logger, startTime, "close logger") + done() return err } From 8947cb243e453a60beb298676887427b59a20ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 21 Apr 2026 18:54:45 +0800 Subject: [PATCH 95/99] sing: Fix UoT write race --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 38f33d5a..2b7f9435 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.8 + github.com/sagernet/sing v0.8.9 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.1 github.com/sagernet/sing-shadowsocks v0.2.8 diff --git a/go.sum b/go.sum index b03c857a..ccb4c909 100644 --- a/go.sum +++ b/go.sum @@ -236,8 +236,8 @@ github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.8 h1:1dRlGJ3wm4d2nwjKI1R/dr/7GKDKgUvXyD4OAWlQyt8= -github.com/sagernet/sing v0.8.8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.9 h1:iX8FyMrWNl/divVgTe7cLT9n36v6bfzfnCYlcM1cLaU= +github.com/sagernet/sing v0.8.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw= From a3fc14f35f610de6d741912e80ccff925a5ff4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 21 Apr 2026 17:31:41 +0800 Subject: [PATCH 96/99] Bump version --- clients/android | 2 +- clients/apple | 2 +- docs/changelog.md | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/clients/android b/clients/android index 67a19777..a3f4ca31 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit 67a19777ce5787843f40fc8bc8b83fe6a1274fa0 +Subproject commit a3f4ca31d122756f36e8f41e5d3d8d676ba3e4dd diff --git a/clients/apple b/clients/apple index 43a2bf46..376f927c 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit 43a2bf467fb31e303b7dc988d87ad3ea77608405 +Subproject commit 376f927ccd943e369c3af0b3e845418137251524 diff --git a/docs/changelog.md b/docs/changelog.md index 096b7228..c6b0b585 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ icon: material/alert-decagram --- +#### 1.13.10 + +* Fix process searcher failure introduced in 1.13.9 + #### 1.13.9 * Fixes and improvements From 3312b8da50fb2fc073845950eb640577838a077e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 23 Apr 2026 01:33:38 +0800 Subject: [PATCH 97/99] Clean up DNS transports --- adapter/dns.go | 2 + dns/transport/base.go | 145 -------- dns/transport/conn_pool.go | 547 ++++++++++++++++++++++++++++ dns/transport/connector.go | 321 ---------------- dns/transport/connector_test.go | 407 --------------------- dns/transport/quic/quic.go | 100 +++-- dns/transport/tls.go | 90 ++--- dns/transport/udp.go | 80 ++-- protocol/tailscale/dns_transport.go | 75 +++- 9 files changed, 736 insertions(+), 1031 deletions(-) delete mode 100644 dns/transport/base.go create mode 100644 dns/transport/conn_pool.go delete mode 100644 dns/transport/connector.go delete mode 100644 dns/transport/connector_test.go diff --git a/adapter/dns.go b/adapter/dns.go index 8f065e2e..23fbc9de 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -68,6 +68,8 @@ type DNSTransport interface { Type() string Tag() 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() Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error) } diff --git a/dns/transport/base.go b/dns/transport/base.go deleted file mode 100644 index 06e41fd0..00000000 --- a/dns/transport/base.go +++ /dev/null @@ -1,145 +0,0 @@ -package transport - -import ( - "context" - "os" - "sync" - - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/dns" - E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/logger" -) - -type TransportState int - -const ( - StateNew TransportState = iota - StateStarted - StateClosing - StateClosed -) - -var ( - ErrTransportClosed = os.ErrClosed - ErrConnectionReset = E.New("connection reset") -) - -type BaseTransport struct { - dns.TransportAdapter - Logger logger.ContextLogger - - mutex sync.Mutex - state TransportState - inFlight int32 - queriesComplete chan struct{} - closeCtx context.Context - closeCancel context.CancelFunc -} - -func NewBaseTransport(adapter dns.TransportAdapter, logger logger.ContextLogger) *BaseTransport { - ctx, cancel := context.WithCancel(context.Background()) - return &BaseTransport{ - TransportAdapter: adapter, - Logger: logger, - state: StateNew, - closeCtx: ctx, - closeCancel: cancel, - } -} - -func (t *BaseTransport) State() TransportState { - t.mutex.Lock() - defer t.mutex.Unlock() - return t.state -} - -func (t *BaseTransport) SetStarted() error { - t.mutex.Lock() - defer t.mutex.Unlock() - switch t.state { - case StateNew: - t.state = StateStarted - return nil - case StateStarted: - return nil - default: - return ErrTransportClosed - } -} - -func (t *BaseTransport) BeginQuery() bool { - t.mutex.Lock() - defer t.mutex.Unlock() - if t.state != StateStarted { - return false - } - t.inFlight++ - return true -} - -func (t *BaseTransport) EndQuery() { - t.mutex.Lock() - if t.inFlight > 0 { - t.inFlight-- - } - if t.inFlight == 0 && t.queriesComplete != nil { - close(t.queriesComplete) - t.queriesComplete = nil - } - t.mutex.Unlock() -} - -func (t *BaseTransport) CloseContext() context.Context { - return t.closeCtx -} - -func (t *BaseTransport) Shutdown(ctx context.Context) error { - t.mutex.Lock() - - if t.state >= StateClosing { - t.mutex.Unlock() - return nil - } - - if t.state == StateNew { - t.state = StateClosed - t.mutex.Unlock() - t.closeCancel() - return nil - } - - t.state = StateClosing - - if t.inFlight == 0 { - t.state = StateClosed - t.mutex.Unlock() - t.closeCancel() - return nil - } - - t.queriesComplete = make(chan struct{}) - queriesComplete := t.queriesComplete - t.mutex.Unlock() - - t.closeCancel() - - select { - case <-queriesComplete: - t.mutex.Lock() - t.state = StateClosed - t.mutex.Unlock() - return nil - case <-ctx.Done(): - t.mutex.Lock() - t.state = StateClosed - t.mutex.Unlock() - return ctx.Err() - } -} - -func (t *BaseTransport) Close() error { - ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout) - defer cancel() - return t.Shutdown(ctx) -} diff --git a/dns/transport/conn_pool.go b/dns/transport/conn_pool.go new file mode 100644 index 00000000..6161e9bd --- /dev/null +++ b/dns/transport/conn_pool.go @@ -0,0 +1,547 @@ +package transport + +import ( + "context" + "net" + "sync" + "time" + + "github.com/sagernet/sing/common/x/list" +) + +type ConnPoolMode int + +const ( + ConnPoolSingle ConnPoolMode = iota + ConnPoolOrdered +) + +type ConnPoolOptions[T comparable] struct { + Mode ConnPoolMode + IsAlive func(T) bool + Close func(T, error) +} + +type ConnPool[T comparable] struct { + options ConnPoolOptions[T] + + access sync.Mutex + closed bool + state *connPoolState[T] +} + +type connPoolState[T comparable] struct { + ctx context.Context + cancel context.CancelCauseFunc + + all map[T]struct{} + + idle list.List[T] + idleElements map[T]*list.Element[T] + + shared T + hasShared bool + sharedClaimed bool + sharedCtx context.Context + sharedCancel context.CancelCauseFunc + + connecting *connPoolConnect[T] +} + +type connPoolConnect[T comparable] struct { + done chan struct{} + err error +} + +type connPoolDialContext struct { + context.Context + parent context.Context +} + +func (c connPoolDialContext) Deadline() (time.Time, bool) { + return c.parent.Deadline() +} + +func (c connPoolDialContext) Value(key any) any { + return c.parent.Value(key) +} + +func NewConnPool[T comparable](options ConnPoolOptions[T]) *ConnPool[T] { + return &ConnPool[T]{ + options: options, + state: newConnPoolState[T](options.Mode), + } +} + +func newConnPoolState[T comparable](mode ConnPoolMode) *connPoolState[T] { + ctx, cancel := context.WithCancelCause(context.Background()) + state := &connPoolState[T]{ + ctx: ctx, + cancel: cancel, + all: make(map[T]struct{}), + } + if mode == ConnPoolOrdered { + state.idleElements = make(map[T]*list.Element[T]) + } + return state +} + +func (p *ConnPool[T]) Acquire(ctx context.Context, dial func(context.Context) (T, error)) (T, bool, error) { + switch p.options.Mode { + case ConnPoolSingle: + conn, _, created, err := p.acquireShared(ctx, dial) + return conn, created, err + case ConnPoolOrdered: + return p.acquireOrdered(ctx, dial) + default: + var zero T + return zero, false, net.ErrClosed + } +} + +func (p *ConnPool[T]) AcquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) { + if p.options.Mode != ConnPoolSingle { + var zero T + return zero, nil, false, net.ErrClosed + } + return p.acquireShared(ctx, dial) +} + +func (p *ConnPool[T]) Release(conn T, reuse bool) { + var ( + closeConn bool + closeErr error + ) + + p.access.Lock() + if p.closed || p.state == nil { + closeConn = true + closeErr = net.ErrClosed + p.access.Unlock() + if closeConn { + p.options.Close(conn, closeErr) + } + return + } + + currentState := p.state + _, tracked := currentState.all[conn] + if !tracked { + closeConn = true + closeErr = p.closeCause(currentState) + p.access.Unlock() + if closeConn { + p.options.Close(conn, closeErr) + } + return + } + + if !reuse || !p.options.IsAlive(conn) { + delete(currentState.all, conn) + switch p.options.Mode { + case ConnPoolSingle: + if currentState.hasShared && currentState.shared == conn { + var zero T + currentState.shared = zero + currentState.hasShared = false + currentState.sharedClaimed = false + currentState.sharedCtx = nil + if currentState.sharedCancel != nil { + currentState.sharedCancel(net.ErrClosed) + currentState.sharedCancel = nil + } + } + case ConnPoolOrdered: + if element, loaded := currentState.idleElements[conn]; loaded { + currentState.idle.Remove(element) + delete(currentState.idleElements, conn) + } + } + closeConn = true + closeErr = net.ErrClosed + p.access.Unlock() + if closeConn { + p.options.Close(conn, closeErr) + } + return + } + + if p.options.Mode == ConnPoolOrdered { + if _, loaded := currentState.idleElements[conn]; !loaded { + currentState.idleElements[conn] = currentState.idle.PushBack(conn) + } + } + p.access.Unlock() +} + +func (p *ConnPool[T]) Invalidate(conn T, cause error) { + p.access.Lock() + if p.closed || p.state == nil { + p.access.Unlock() + p.options.Close(conn, cause) + return + } + + currentState := p.state + _, tracked := currentState.all[conn] + if !tracked { + p.access.Unlock() + return + } + + delete(currentState.all, conn) + switch p.options.Mode { + case ConnPoolSingle: + if currentState.hasShared && currentState.shared == conn { + var zero T + currentState.shared = zero + currentState.hasShared = false + currentState.sharedClaimed = false + currentState.sharedCtx = nil + if currentState.sharedCancel != nil { + currentState.sharedCancel(cause) + currentState.sharedCancel = nil + } + } + case ConnPoolOrdered: + if element, loaded := currentState.idleElements[conn]; loaded { + currentState.idle.Remove(element) + delete(currentState.idleElements, conn) + } + } + p.access.Unlock() + + p.options.Close(conn, cause) +} + +func (p *ConnPool[T]) Reset() { + p.access.Lock() + if p.closed { + p.access.Unlock() + return + } + + oldState := p.state + p.state = newConnPoolState[T](p.options.Mode) + p.access.Unlock() + + p.closeState(oldState, net.ErrClosed) +} + +func (p *ConnPool[T]) Close() error { + p.access.Lock() + if p.closed { + p.access.Unlock() + return nil + } + + p.closed = true + oldState := p.state + p.state = nil + p.access.Unlock() + + p.closeState(oldState, net.ErrClosed) + return nil +} + +func (p *ConnPool[T]) acquireOrdered(ctx context.Context, dial func(context.Context) (T, error)) (T, bool, error) { + var zero T + for { + var ( + staleConn T + hasStale bool + ) + + p.access.Lock() + if p.closed { + p.access.Unlock() + return zero, false, net.ErrClosed + } + + currentState := p.state + if element := currentState.idle.Front(); element != nil { + conn := currentState.idle.Remove(element) + delete(currentState.idleElements, conn) + if p.options.IsAlive(conn) { + p.access.Unlock() + return conn, false, nil + } + delete(currentState.all, conn) + staleConn = conn + hasStale = true + } + p.access.Unlock() + + if hasStale { + p.options.Close(staleConn, net.ErrClosed) + continue + } + + conn, err := p.dial(ctx, currentState, dial) + if err != nil { + return zero, false, err + } + + p.access.Lock() + if p.closed { + p.access.Unlock() + p.options.Close(conn, net.ErrClosed) + return zero, false, net.ErrClosed + } + if p.state != currentState { + cause := p.closeCause(currentState) + p.access.Unlock() + p.options.Close(conn, cause) + return zero, false, cause + } + currentState.all[conn] = struct{}{} + p.access.Unlock() + return conn, true, nil + } +} + +func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Context) (T, error)) (T, context.Context, bool, error) { + var zero T + for { + var ( + staleConn T + hasStale bool + state *connPoolConnect[T] + current *connPoolState[T] + startDial bool + ) + + p.access.Lock() + if p.closed { + p.access.Unlock() + return zero, nil, false, net.ErrClosed + } + + current = p.state + if current.hasShared { + conn := current.shared + if p.options.IsAlive(conn) { + created := !current.sharedClaimed + current.sharedClaimed = true + connCtx := current.sharedCtx + p.access.Unlock() + return conn, connCtx, created, nil + } + delete(current.all, conn) + var zeroConn T + current.shared = zeroConn + current.hasShared = false + current.sharedClaimed = false + current.sharedCtx = nil + if current.sharedCancel != nil { + current.sharedCancel(net.ErrClosed) + current.sharedCancel = nil + } + staleConn = conn + hasStale = true + p.access.Unlock() + p.options.Close(staleConn, net.ErrClosed) + continue + } + + if current.connecting == nil { + current.connecting = &connPoolConnect[T]{ + done: make(chan struct{}), + } + startDial = true + } + state = current.connecting + p.access.Unlock() + + if hasStale { + continue + } + if startDial { + go p.connectSingle(current, state, ctx, dial) + } + + select { + case <-state.done: + conn, connCtx, created, retry, err := p.collectShared(current, state, startDial) + if retry { + continue + } + return conn, connCtx, created, err + case <-ctx.Done(): + return zero, nil, false, ctx.Err() + case <-current.ctx.Done(): + p.access.Lock() + closed := p.closed + p.access.Unlock() + if closed { + return zero, nil, false, net.ErrClosed + } + } + } +} + +func (p *ConnPool[T]) connectSingle(current *connPoolState[T], state *connPoolConnect[T], ctx context.Context, dial func(context.Context) (T, error)) { + conn, err := p.dial(ctx, current, dial) + if err != nil { + p.access.Lock() + if current.connecting == state { + current.connecting = nil + } + state.err = err + p.access.Unlock() + close(state.done) + return + } + + var closeErr error + + p.access.Lock() + if current.connecting == state { + current.connecting = nil + } + if p.closed { + closeErr = net.ErrClosed + state.err = closeErr + } else if p.state != current { + closeErr = p.closeCause(current) + state.err = closeErr + } else { + sharedCtx, sharedCancel := context.WithCancelCause(current.ctx) + current.shared = conn + current.hasShared = true + current.sharedClaimed = false + current.sharedCtx = sharedCtx + current.sharedCancel = sharedCancel + current.all[conn] = struct{}{} + } + p.access.Unlock() + + if closeErr != nil { + p.options.Close(conn, closeErr) + } + close(state.done) +} + +func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolConnect[T], startDial bool) (T, context.Context, bool, bool, error) { + var zero T + + p.access.Lock() + if state.err != nil { + err := state.err + p.access.Unlock() + if startDial { + return zero, nil, false, false, err + } + return zero, nil, false, true, nil + } + if p.closed { + p.access.Unlock() + return zero, nil, false, false, net.ErrClosed + } + if p.state != current { + cause := p.closeCause(current) + p.access.Unlock() + return zero, nil, false, false, cause + } + if !current.hasShared { + p.access.Unlock() + return zero, nil, false, true, nil + } + + conn := current.shared + if !p.options.IsAlive(conn) { + delete(current.all, conn) + var zeroConn T + current.shared = zeroConn + current.hasShared = false + current.sharedClaimed = false + current.sharedCtx = nil + if current.sharedCancel != nil { + current.sharedCancel(net.ErrClosed) + current.sharedCancel = nil + } + p.access.Unlock() + p.options.Close(conn, net.ErrClosed) + return zero, nil, false, true, nil + } + + created := !current.sharedClaimed + current.sharedClaimed = true + connCtx := current.sharedCtx + p.access.Unlock() + return conn, connCtx, created, false, nil +} + +func (p *ConnPool[T]) dial(ctx context.Context, current *connPoolState[T], dial func(context.Context) (T, error)) (T, error) { + var zero T + + if err := ctx.Err(); err != nil { + return zero, err + } + if cause := context.Cause(current.ctx); cause != nil { + return zero, cause + } + + dialCtx, cancel := context.WithCancelCause(current.ctx) + var ( + stateAccess sync.Mutex + dialComplete bool + ) + stopCancel := context.AfterFunc(ctx, func() { + stateAccess.Lock() + if !dialComplete { + cancel(context.Cause(ctx)) + } + stateAccess.Unlock() + }) + + select { + case <-ctx.Done(): + stateAccess.Lock() + dialComplete = true + stateAccess.Unlock() + stopCancel() + cancel(context.Cause(ctx)) + return zero, ctx.Err() + default: + } + + conn, err := dial(connPoolDialContext{ + Context: dialCtx, + parent: ctx, + }) + stateAccess.Lock() + dialComplete = true + stateAccess.Unlock() + stopCancel() + if err != nil { + if cause := context.Cause(dialCtx); cause != nil { + return zero, cause + } + return zero, err + } + if cause := context.Cause(dialCtx); cause != nil { + p.options.Close(conn, cause) + return zero, cause + } + return conn, nil +} + +func (p *ConnPool[T]) closeState(state *connPoolState[T], cause error) { + if state == nil { + return + } + + state.cancel(cause) + if state.sharedCancel != nil { + state.sharedCancel(cause) + } + for conn := range state.all { + p.options.Close(conn, cause) + } +} + +func (p *ConnPool[T]) closeCause(state *connPoolState[T]) error { + _ = state + return net.ErrClosed +} diff --git a/dns/transport/connector.go b/dns/transport/connector.go deleted file mode 100644 index 3a87456d..00000000 --- a/dns/transport/connector.go +++ /dev/null @@ -1,321 +0,0 @@ -package transport - -import ( - "context" - "net" - "sync" - "time" - - E "github.com/sagernet/sing/common/exceptions" -) - -type ConnectorCallbacks[T any] struct { - IsClosed func(connection T) bool - Close func(connection T) - Reset func(connection T) -} - -type Connector[T any] struct { - dial func(ctx context.Context) (T, error) - callbacks ConnectorCallbacks[T] - - access sync.Mutex - connection T - hasConnection bool - connectionCancel context.CancelFunc - connecting chan struct{} - - closeCtx context.Context - closed bool -} - -func NewConnector[T any](closeCtx context.Context, dial func(context.Context) (T, error), callbacks ConnectorCallbacks[T]) *Connector[T] { - return &Connector[T]{ - dial: dial, - callbacks: callbacks, - closeCtx: closeCtx, - } -} - -func NewSingleflightConnector(closeCtx context.Context, dial func(context.Context) (*Connection, error)) *Connector[*Connection] { - return NewConnector(closeCtx, dial, ConnectorCallbacks[*Connection]{ - IsClosed: func(connection *Connection) bool { - return connection.IsClosed() - }, - Close: func(connection *Connection) { - connection.CloseWithError(ErrTransportClosed) - }, - Reset: func(connection *Connection) { - connection.CloseWithError(ErrConnectionReset) - }, - }) -} - -type contextKeyConnecting struct{} - -var errRecursiveConnectorDial = E.New("recursive connector dial") - -type connectorDialResult[T any] struct { - connection T - cancel context.CancelFunc - err error -} - -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 - } - - connecting := make(chan struct{}) - c.connecting = connecting - dialContext := context.WithValue(ctx, contextKeyConnecting{}, c) - dialResult := make(chan connectorDialResult[T], 1) - c.access.Unlock() - - go func() { - connection, cancel, err := c.dialWithCancellation(dialContext) - dialResult <- connectorDialResult[T]{ - connection: connection, - cancel: cancel, - err: err, - } - }() - - select { - case result := <-dialResult: - return c.completeDial(ctx, connecting, result) - case <-ctx.Done(): - go func() { - result := <-dialResult - _, _ = c.completeDial(ctx, connecting, result) - }() - return zero, ctx.Err() - case <-c.closeCtx.Done(): - go func() { - result := <-dialResult - _, _ = c.completeDial(ctx, connecting, result) - }() - return zero, ErrTransportClosed - } - } -} - -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]) completeDial(ctx context.Context, connecting chan struct{}, result connectorDialResult[T]) (T, error) { - var zero T - - c.access.Lock() - defer c.access.Unlock() - defer func() { - if c.connecting == connecting { - c.connecting = nil - } - close(connecting) - }() - - if result.err != nil { - return zero, result.err - } - if c.closed || c.closeCtx.Err() != nil { - result.cancel() - c.callbacks.Close(result.connection) - return zero, ErrTransportClosed - } - if err := ctx.Err(); err != nil { - result.cancel() - c.callbacks.Close(result.connection) - return zero, err - } - - c.connection = result.connection - c.hasConnection = true - c.connectionCancel = result.cancel - return c.connection, nil -} - -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 -} diff --git a/dns/transport/connector_test.go b/dns/transport/connector_test.go deleted file mode 100644 index 309b28c8..00000000 --- a/dns/transport/connector_test.go +++ /dev/null @@ -1,407 +0,0 @@ -package transport - -import ( - "context" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -type testConnectorConnection struct{} - -func TestConnectorRecursiveGetFailsFast(t *testing.T) { - t.Parallel() - - var ( - dialCount atomic.Int32 - closeCount atomic.Int32 - connector *Connector[*testConnectorConnection] - ) - - dial := func(ctx context.Context) (*testConnectorConnection, error) { - dialCount.Add(1) - _, err := connector.Get(ctx) - if err != nil { - return nil, err - } - return &testConnectorConnection{}, nil - } - - connector = NewConnector(context.Background(), dial, ConnectorCallbacks[*testConnectorConnection]{ - IsClosed: func(connection *testConnectorConnection) bool { - return false - }, - Close: func(connection *testConnectorConnection) { - closeCount.Add(1) - }, - Reset: func(connection *testConnectorConnection) { - closeCount.Add(1) - }, - }) - - _, err := connector.Get(context.Background()) - require.ErrorIs(t, err, errRecursiveConnectorDial) - require.EqualValues(t, 1, dialCount.Load()) - require.EqualValues(t, 0, closeCount.Load()) -} - -func TestConnectorRecursiveGetAcrossConnectorsAllowed(t *testing.T) { - t.Parallel() - - var ( - outerDialCount atomic.Int32 - innerDialCount atomic.Int32 - outerConnector *Connector[*testConnectorConnection] - innerConnector *Connector[*testConnectorConnection] - ) - - innerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { - innerDialCount.Add(1) - return &testConnectorConnection{}, nil - }, ConnectorCallbacks[*testConnectorConnection]{ - IsClosed: func(connection *testConnectorConnection) bool { - return false - }, - Close: func(connection *testConnectorConnection) {}, - Reset: func(connection *testConnectorConnection) {}, - }) - - outerConnector = NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { - outerDialCount.Add(1) - _, err := innerConnector.Get(ctx) - if err != nil { - return nil, err - } - return &testConnectorConnection{}, nil - }, ConnectorCallbacks[*testConnectorConnection]{ - IsClosed: func(connection *testConnectorConnection) bool { - return false - }, - Close: func(connection *testConnectorConnection) {}, - Reset: func(connection *testConnectorConnection) {}, - }) - - _, err := outerConnector.Get(context.Background()) - require.NoError(t, err) - require.EqualValues(t, 1, outerDialCount.Load()) - require.EqualValues(t, 1, innerDialCount.Load()) -} - -func TestConnectorDialContextPreservesValueAndDeadline(t *testing.T) { - t.Parallel() - - type contextKey struct{} - - var ( - dialValue any - dialDeadline time.Time - dialHasDeadline bool - ) - - connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { - dialValue = ctx.Value(contextKey{}) - dialDeadline, dialHasDeadline = ctx.Deadline() - return &testConnectorConnection{}, nil - }, ConnectorCallbacks[*testConnectorConnection]{ - IsClosed: func(connection *testConnectorConnection) bool { - return false - }, - Close: func(connection *testConnectorConnection) {}, - Reset: func(connection *testConnectorConnection) {}, - }) - - deadline := time.Now().Add(time.Minute) - requestContext, cancel := context.WithDeadline(context.WithValue(context.Background(), contextKey{}, "test-value"), deadline) - defer cancel() - - _, err := connector.Get(requestContext) - require.NoError(t, err) - require.Equal(t, "test-value", dialValue) - require.True(t, dialHasDeadline) - require.WithinDuration(t, deadline, dialDeadline, time.Second) -} - -func TestConnectorDialSkipsCanceledRequest(t *testing.T) { - t.Parallel() - - var dialCount atomic.Int32 - connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { - dialCount.Add(1) - return &testConnectorConnection{}, nil - }, ConnectorCallbacks[*testConnectorConnection]{ - IsClosed: func(connection *testConnectorConnection) bool { - return false - }, - Close: func(connection *testConnectorConnection) {}, - Reset: func(connection *testConnectorConnection) {}, - }) - - requestContext, cancel := context.WithCancel(context.Background()) - cancel() - - _, err := connector.Get(requestContext) - require.ErrorIs(t, err, context.Canceled) - require.EqualValues(t, 0, dialCount.Load()) -} - -func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) { - t.Parallel() - - var ( - dialCount atomic.Int32 - closeCount atomic.Int32 - ) - dialStarted := make(chan struct{}, 1) - releaseDial := make(chan struct{}) - - connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { - dialCount.Add(1) - select { - case dialStarted <- struct{}{}: - default: - } - <-releaseDial - return &testConnectorConnection{}, nil - }, ConnectorCallbacks[*testConnectorConnection]{ - IsClosed: func(connection *testConnectorConnection) bool { - return false - }, - Close: func(connection *testConnectorConnection) { - closeCount.Add(1) - }, - Reset: func(connection *testConnectorConnection) {}, - }) - - requestContext, cancel := context.WithCancel(context.Background()) - result := make(chan error, 1) - go func() { - _, err := connector.Get(requestContext) - result <- err - }() - - <-dialStarted - cancel() - close(releaseDial) - - err := <-result - require.ErrorIs(t, err, context.Canceled) - require.EqualValues(t, 1, dialCount.Load()) - require.Eventually(t, func() bool { - return closeCount.Load() == 1 - }, time.Second, 10*time.Millisecond) - - _, err = connector.Get(context.Background()) - require.NoError(t, err) - require.EqualValues(t, 2, dialCount.Load()) -} - -func TestConnectorCanceledRequestReturnsBeforeIgnoredDialCompletes(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() - - select { - case err := <-result: - require.ErrorIs(t, err, context.Canceled) - case <-time.After(time.Second): - t.Fatal("Get did not return after request cancel") - } - - require.EqualValues(t, 1, dialCount.Load()) - require.EqualValues(t, 0, closeCount.Load()) - - close(releaseDial) - - require.Eventually(t, func() bool { - return closeCount.Load() == 1 - }, time.Second, 10*time.Millisecond) - - _, err := connector.Get(context.Background()) - require.NoError(t, err) - require.EqualValues(t, 2, dialCount.Load()) -} - -func TestConnectorWaiterDoesNotStartNewDialBeforeCanceledDialCompletes(t *testing.T) { - t.Parallel() - - var ( - dialCount atomic.Int32 - closeCount atomic.Int32 - ) - firstDialStarted := make(chan struct{}, 1) - secondDialStarted := make(chan struct{}, 1) - releaseFirstDial := make(chan struct{}) - - connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) { - attempt := dialCount.Add(1) - switch attempt { - case 1: - select { - case firstDialStarted <- struct{}{}: - default: - } - <-releaseFirstDial - case 2: - select { - case secondDialStarted <- struct{}{}: - default: - } - } - 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()) - firstResult := make(chan error, 1) - go func() { - _, err := connector.Get(requestContext) - firstResult <- err - }() - - <-firstDialStarted - cancel() - - secondResult := make(chan error, 1) - go func() { - _, err := connector.Get(context.Background()) - secondResult <- err - }() - - select { - case <-secondDialStarted: - t.Fatal("second dial started before first dial completed") - case <-time.After(100 * time.Millisecond): - } - - select { - case err := <-firstResult: - require.ErrorIs(t, err, context.Canceled) - case <-time.After(time.Second): - t.Fatal("first Get did not return after request cancel") - } - - close(releaseFirstDial) - - require.Eventually(t, func() bool { - return closeCount.Load() == 1 - }, time.Second, 10*time.Millisecond) - - select { - case <-secondDialStarted: - case <-time.After(time.Second): - t.Fatal("second dial did not start after first dial completed") - } - - err := <-secondResult - 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") - } -} diff --git a/dns/transport/quic/quic.go b/dns/transport/quic/quic.go index 26461006..3a7b6163 100644 --- a/dns/transport/quic/quic.go +++ b/dns/transport/quic/quic.go @@ -31,14 +31,13 @@ func RegisterTransport(registry *dns.TransportRegistry) { } type Transport struct { - *transport.BaseTransport + dns.TransportAdapter - ctx context.Context dialer N.Dialer serverAddr M.Socksaddr tlsConfig tls.Config - connector *transport.Connector[*quic.Conn] + connection *transport.ConnPool[*quic.Conn] } func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) { @@ -63,93 +62,76 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options return nil, E.New("invalid server address: ", serverAddr) } - t := &Transport{ - BaseTransport: transport.NewBaseTransport( - dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions), - logger, - ), - ctx: ctx, - dialer: transportDialer, - serverAddr: serverAddr, - tlsConfig: tlsConfig, - } - - t.connector = transport.NewConnector(t.CloseContext(), t.dial, transport.ConnectorCallbacks[*quic.Conn]{ - IsClosed: func(connection *quic.Conn) bool { - return common.Done(connection.Context()) - }, - 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 + return &Transport{ + TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions), + dialer: transportDialer, + serverAddr: serverAddr, + tlsConfig: tlsConfig, + connection: transport.NewConnPool(transport.ConnPoolOptions[*quic.Conn]{ + Mode: transport.ConnPoolSingle, + IsAlive: func(conn *quic.Conn) bool { + return conn != nil && !common.Done(conn.Context()) + }, + Close: func(conn *quic.Conn, _ error) { + conn.CloseWithError(0, "") + }, + }), + }, nil } func (t *Transport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } - err := t.SetStarted() - if err != nil { - return err - } return dialer.InitializeDetour(t.dialer) } func (t *Transport) Close() error { - return E.Errors(t.BaseTransport.Close(), t.connector.Close()) + return t.connection.Close() } func (t *Transport) Reset() { - t.connector.Reset() + t.connection.Reset() } func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { - if !t.BeginQuery() { - return nil, transport.ErrTransportClosed - } - defer t.EndQuery() - var ( conn *quic.Conn err error response *mDNS.Msg ) for i := 0; i < 2; i++ { - conn, err = t.connector.Get(ctx) + conn, _, err = t.connection.Acquire(ctx, func(ctx context.Context) (*quic.Conn, error) { + rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr) + if err != nil { + return nil, E.Cause(err, "dial UDP connection") + } + earlyConnection, err := sQUIC.DialEarly( + ctx, + bufio.NewUnbindPacketConn(rawConn), + t.serverAddr.UDPAddr(), + t.tlsConfig, + nil, + ) + if err != nil { + rawConn.Close() + return nil, E.Cause(err, "establish QUIC connection") + } + return earlyConnection, nil + }) if err != nil { return nil, err } response, err = t.exchange(ctx, message, conn) if err == nil { + t.connection.Release(conn, true) return response, nil } else if !isQUICRetryError(err) { + t.connection.Release(conn, true) return nil, err } else { - t.connector.Reset() + t.connection.Release(conn, true) + t.Reset() continue } } diff --git a/dns/transport/tls.go b/dns/transport/tls.go index 4d463296..43978b6f 100644 --- a/dns/transport/tls.go +++ b/dns/transport/tls.go @@ -2,7 +2,6 @@ package transport import ( "context" - "sync" "time" "github.com/sagernet/sing-box/adapter" @@ -17,7 +16,6 @@ import ( "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" mDNS "github.com/miekg/dns" ) @@ -29,13 +27,13 @@ func RegisterTLS(registry *dns.TransportRegistry) { } type TLSTransport struct { - *BaseTransport + dns.TransportAdapter + logger logger.ContextLogger dialer tls.Dialer serverAddr M.Socksaddr tlsConfig tls.Config - access sync.Mutex - connections list.List[*tlsDNSConn] + connections *ConnPool[*tlsDNSConn] } type tlsDNSConn struct { @@ -66,10 +64,20 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport { return &TLSTransport{ - BaseTransport: NewBaseTransport(adapter, logger), - dialer: tls.NewDialer(dialer, tlsConfig), - serverAddr: serverAddr, - tlsConfig: tlsConfig, + TransportAdapter: adapter, + logger: logger, + dialer: tls.NewDialer(dialer, tlsConfig), + serverAddr: serverAddr, + tlsConfig: tlsConfig, + connections: NewConnPool(ConnPoolOptions[*tlsDNSConn]{ + Mode: ConnPoolOrdered, + IsAlive: func(conn *tlsDNSConn) bool { + return conn != nil + }, + Close: func(conn *tlsDNSConn, _ error) { + conn.Close() + }, + }), } } @@ -77,53 +85,43 @@ func (t *TLSTransport) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } - err := t.SetStarted() - if err != nil { - return err - } return dialer.InitializeDetour(t.dialer) } func (t *TLSTransport) Close() error { - 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() + return t.connections.Close() } func (t *TLSTransport) Reset() { - t.access.Lock() - defer t.access.Unlock() - for connection := t.connections.Front(); connection != nil; connection = connection.Next() { - connection.Value.Close() - } - t.connections.Init() + t.connections.Reset() } func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { - if !t.BeginQuery() { - return nil, ErrTransportClosed - } - defer t.EndQuery() - - t.access.Lock() - conn := t.connections.PopFront() - t.access.Unlock() - if conn != nil { + var lastErr error + for attempt := 0; attempt < 2; attempt++ { + conn, created, err := t.connections.Acquire(ctx, func(ctx context.Context) (*tlsDNSConn, error) { + tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr) + if err != nil { + return nil, E.Cause(err, "dial TLS connection") + } + return &tlsDNSConn{Conn: tlsConn}, nil + }) + if err != nil { + return nil, err + } response, err := t.exchange(ctx, message, conn) if err == nil { + t.connections.Release(conn, true) return response, nil } - t.Logger.DebugContext(ctx, "discarded pooled connection: ", err) + lastErr = err + t.logger.DebugContext(ctx, "discarded pooled connection: ", err) + t.connections.Release(conn, false) + if created { + return nil, err + } } - tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr) - if err != nil { - return nil, E.Cause(err, "dial TLS connection") - } - return t.exchange(ctx, message, &tlsDNSConn{Conn: tlsConn}) + return nil, lastErr } func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) { @@ -133,22 +131,12 @@ func (t *TLSTransport) exchange(ctx context.Context, message *mDNS.Msg, conn *tl conn.queryId++ err := WriteMessage(conn, conn.queryId, message) if err != nil { - conn.Close() return nil, E.Cause(err, "write request") } response, err := ReadMessage(conn) if err != nil { - conn.Close() 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{}) - t.connections.PushBack(conn) - t.access.Unlock() return response, nil } diff --git a/dns/transport/udp.go b/dns/transport/udp.go index a7272545..c9f520e3 100644 --- a/dns/transport/udp.go +++ b/dns/transport/udp.go @@ -2,6 +2,7 @@ package transport import ( "context" + "net" "sync" "sync/atomic" @@ -27,13 +28,14 @@ func RegisterUDP(registry *dns.TransportRegistry) { } type UDPTransport struct { - *BaseTransport + dns.TransportAdapter + logger logger.ContextLogger dialer N.Dialer serverAddr M.Socksaddr udpSize atomic.Int32 - connector *Connector[*Connection] + connection *ConnPool[net.Conn] callbackAccess sync.RWMutex queryId uint16 @@ -63,43 +65,38 @@ func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options o func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialerInstance N.Dialer, serverAddr M.Socksaddr) *UDPTransport { t := &UDPTransport{ - BaseTransport: NewBaseTransport(adapter, logger), - dialer: dialerInstance, - serverAddr: serverAddr, - callbacks: make(map[uint16]*udpCallback), + TransportAdapter: adapter, + logger: logger, + dialer: dialerInstance, + serverAddr: serverAddr, + callbacks: make(map[uint16]*udpCallback), + connection: NewConnPool(ConnPoolOptions[net.Conn]{ + Mode: ConnPoolSingle, + IsAlive: func(conn net.Conn) bool { + return conn != nil + }, + Close: func(conn net.Conn, cause error) { + conn.Close() + }, + }), } t.udpSize.Store(2048) - t.connector = NewSingleflightConnector(t.CloseContext(), t.dial) 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 { if stage != adapter.StartStateStart { return nil } - err := t.SetStarted() - if err != nil { - return err - } return dialer.InitializeDetour(t.dialer) } func (t *UDPTransport) Close() error { - return E.Errors(t.BaseTransport.Close(), t.connector.Close()) + return t.connection.Close() } func (t *UDPTransport) Reset() { - t.connector.Reset() + t.connection.Reset() } func (t *UDPTransport) nextAvailableQueryId() (uint16, error) { @@ -116,17 +113,12 @@ func (t *UDPTransport) nextAvailableQueryId() (uint16, error) { } func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { - if !t.BeginQuery() { - return nil, ErrTransportClosed - } - defer t.EndQuery() - response, err := t.exchange(ctx, message) if err != nil { return nil, err } 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 response, nil @@ -158,16 +150,25 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M break } if t.udpSize.CompareAndSwap(current, udpSize) { - t.connector.Reset() + t.Reset() break } } } - conn, err := t.connector.Get(ctx) + conn, connCtx, created, err := t.connection.AcquireShared(ctx, func(ctx context.Context) (net.Conn, error) { + rawConn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr) + if err != nil { + return nil, E.Cause(err, "dial UDP connection") + } + return rawConn, nil + }) if err != nil { return nil, err } + if created { + go t.recvLoop(conn) + } callback := &udpCallback{ done: make(chan struct{}), @@ -177,6 +178,7 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M queryId, err := t.nextAvailableQueryId() if err != nil { t.callbackAccess.Unlock() + t.connection.Release(conn, true) return nil, err } t.callbacks[queryId] = callback @@ -203,30 +205,30 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M _, err = conn.Write(rawMessage) if err != nil { - conn.CloseWithError(err) + t.connection.Invalidate(conn, err) return nil, E.Cause(err, "write request") } select { case <-callback.done: + t.connection.Release(conn, true) callback.response.Id = originalId return callback.response, nil - case <-conn.Done(): - return nil, conn.CloseError() - case <-t.CloseContext().Done(): - return nil, ErrTransportClosed + case <-connCtx.Done(): + return nil, context.Cause(connCtx) case <-ctx.Done(): + t.connection.Release(conn, true) return nil, ctx.Err() } } -func (t *UDPTransport) recvLoop(conn *Connection) { +func (t *UDPTransport) recvLoop(conn net.Conn) { for { buffer := buf.NewSize(int(t.udpSize.Load())) _, err := buffer.ReadOnceFrom(conn) if err != nil { buffer.Release() - conn.CloseWithError(err) + t.connection.Invalidate(conn, err) return } @@ -234,7 +236,7 @@ func (t *UDPTransport) recvLoop(conn *Connection) { err = message.Unpack(buffer.Bytes()) buffer.Release() if err != nil { - t.Logger.Debug("discarded malformed UDP response: ", err) + t.logger.Debug("discarded malformed UDP response: ", err) continue } diff --git a/protocol/tailscale/dns_transport.go b/protocol/tailscale/dns_transport.go index 3a92a66b..4195235c 100644 --- a/protocol/tailscale/dns_transport.go +++ b/protocol/tailscale/dns_transport.go @@ -49,6 +49,7 @@ type DNSTransport struct { dnsRouter adapter.DNSRouter endpointManager adapter.EndpointManager endpoint *Endpoint + access sync.RWMutex routePrefixes []netip.Prefix routes map[string][]adapter.DNSTransport hosts map[string][]netip.Addr @@ -91,6 +92,12 @@ func (t *DNSTransport) Start(stage adapter.StartStage) error { } func (t *DNSTransport) Reset() { + t.access.RLock() + transports := t.collectResolversLocked() + t.access.RUnlock() + for _, transport := range transports { + transport.Reset() + } } func (t *DNSTransport) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *nDNS.Config) { @@ -101,7 +108,7 @@ func (t *DNSTransport) onReconfig(cfg *wgcfg.Config, routerCfg *router.Config, d } func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *nDNS.Config) error { - t.routePrefixes = buildRoutePrefixes(routeConfig) + routePrefixes := buildRoutePrefixes(routeConfig) directDialerOnce := sync.OnceValue(func() N.Dialer { directDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{})) return &DNSDialer{transport: t, fallbackDialer: directDialer} @@ -130,9 +137,19 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n } defaultResolvers = append(defaultResolvers, myResolver) } + + t.access.Lock() + oldResolvers := t.collectResolversLocked() + t.routePrefixes = routePrefixes t.routes = routes t.hosts = hosts t.defaultResolvers = defaultResolvers + t.access.Unlock() + + for _, transport := range oldResolvers { + transport.Close() + } + if len(defaultResolvers) > 0 { t.logger.Info("updated ", len(routes), " routes, ", len(hosts), " hosts, default resolvers: ", strings.Join(common.Map(dnsConfig.DefaultResolvers, func(it *dnstype.Resolver) string { return it.Addr }), " ")) @@ -207,7 +224,22 @@ func buildRoutePrefixes(routeConfig *router.Config) []netip.Prefix { } func (t *DNSTransport) Close() error { - return nil + t.access.Lock() + transports := t.collectResolversLocked() + t.routePrefixes = nil + t.routes = nil + t.hosts = nil + t.defaultResolvers = nil + t.access.Unlock() + + var err error + for _, transport := range transports { + name := "resolver/" + transport.Type() + "[" + transport.Tag() + "]" + err = E.Append(err, transport.Close(), func(err error) error { + return E.Cause(err, "close ", name) + }) + } + return err } func (t *DNSTransport) Raw() bool { @@ -219,7 +251,15 @@ func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M return nil, os.ErrInvalid } question := message.Question[0] - addresses, hostsLoaded := t.hosts[question.Name] + + t.access.RLock() + hosts := t.hosts + routes := t.routes + defaultResolvers := t.defaultResolvers + acceptDefaultResolvers := t.acceptDefaultResolvers + t.access.RUnlock() + + addresses, hostsLoaded := hosts[question.Name] if hostsLoaded { switch question.Qtype { case mDNS.TypeA: @@ -238,7 +278,7 @@ func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M } } } - for domainSuffix, transports := range t.routes { + for domainSuffix, transports := range routes { if strings.HasSuffix(question.Name, domainSuffix) { if len(transports) == 0 { return &mDNS.Msg{ @@ -262,10 +302,10 @@ func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M return nil, lastErr } } - if t.acceptDefaultResolvers { - if len(t.defaultResolvers) > 0 { + if acceptDefaultResolvers { + if len(defaultResolvers) > 0 { var lastErr error - for _, resolver := range t.defaultResolvers { + for _, resolver := range defaultResolvers { response, err := resolver.Exchange(ctx, message) if err != nil { lastErr = err @@ -281,6 +321,15 @@ func (t *DNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M return nil, dns.RcodeNameError } +func (t *DNSTransport) collectResolversLocked() []adapter.DNSTransport { + var transports []adapter.DNSTransport + for _, resolvers := range t.routes { + transports = append(transports, resolvers...) + } + transports = append(transports, t.defaultResolvers...) + return transports +} + type DNSDialer struct { transport *DNSTransport fallbackDialer N.Dialer @@ -290,7 +339,8 @@ func (d *DNSDialer) DialContext(ctx context.Context, network string, destination if destination.IsDomain() { panic("invalid request here") } - for _, prefix := range d.transport.routePrefixes { + routePrefixes := d.transport.routePrefixesSnapshot() + for _, prefix := range routePrefixes { if prefix.Contains(destination.Addr) { return d.transport.endpoint.DialContext(ctx, network, destination) } @@ -302,10 +352,17 @@ func (d *DNSDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) ( if destination.IsDomain() { panic("invalid request here") } - for _, prefix := range d.transport.routePrefixes { + routePrefixes := d.transport.routePrefixesSnapshot() + for _, prefix := range routePrefixes { if prefix.Contains(destination.Addr) { return d.transport.endpoint.ListenPacket(ctx, destination) } } return d.fallbackDialer.ListenPacket(ctx, destination) } + +func (t *DNSTransport) routePrefixesSnapshot() []netip.Prefix { + t.access.RLock() + defer t.access.RUnlock() + return append([]netip.Prefix(nil), t.routePrefixes...) +} From f102ef1d948221eae802ec3627bc34c7384f2002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 23 Apr 2026 05:52:14 +0800 Subject: [PATCH 98/99] Fix process search skipped for Android again --- adapter/platform.go | 4 ++++ experimental/libbox/config.go | 5 +++++ experimental/libbox/service.go | 17 +++++++++++++++++ route/process_cache.go | 13 ++++++++----- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/adapter/platform.go b/adapter/platform.go index fa4cbc2e..df1f4471 100644 --- a/adapter/platform.go +++ b/adapter/platform.go @@ -1,6 +1,8 @@ package adapter import ( + "net/netip" + "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/logger" @@ -36,6 +38,8 @@ type PlatformInterface interface { UsePlatformNotification() bool SendNotification(notification *Notification) error + + MyInterfaceAddress() []netip.Addr } type FindConnectionOwnerRequest struct { diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index 122425d2..b1676ab6 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -3,6 +3,7 @@ package libbox import ( "bytes" "context" + "net/netip" "os" box "github.com/sagernet/sing-box" @@ -144,6 +145,10 @@ func (s *platformInterfaceStub) SendNotification(notification *adapter.Notificat return nil } +func (s *platformInterfaceStub) MyInterfaceAddress() []netip.Addr { + return nil +} + func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool { return false } diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 7d0b3004..37fd56c9 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -29,6 +29,7 @@ type platformInterfaceWrapper struct { useProcFS bool networkManager adapter.NetworkManager myTunName string + myTunAddress []netip.Addr defaultInterfaceAccess sync.Mutex defaultInterface *control.Interface isExpensive bool @@ -78,9 +79,25 @@ func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformO } options.FileDescriptor = dupFd w.myTunName = options.Name + w.myTunAddress = myTunAddress(options) return tun.New(*options) } +func myTunAddress(options *tun.Options) []netip.Addr { + addresses := make([]netip.Addr, 0, len(options.Inet4Address)+len(options.Inet6Address)) + for _, prefix := range options.Inet4Address { + addresses = append(addresses, prefix.Addr()) + } + for _, prefix := range options.Inet6Address { + addresses = append(addresses, prefix.Addr()) + } + return addresses +} + +func (w *platformInterfaceWrapper) MyInterfaceAddress() []netip.Addr { + return w.myTunAddress +} + func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool { return true } diff --git a/route/process_cache.go b/route/process_cache.go index 01b477c4..44ee3fcf 100644 --- a/route/process_cache.go +++ b/route/process_cache.go @@ -74,16 +74,19 @@ func (r *Router) searchProcessInfo(ctx context.Context, metadata *adapter.Inboun } func (r *Router) isLocalSource(source netip.Addr) bool { - if !source.IsValid() { - return false - } - source = source.Unmap() if source.IsLoopback() { return true } + if r.platformInterface != nil { + for _, addr := range r.platformInterface.MyInterfaceAddress() { + if addr == source { + return true + } + } + } for _, netInterface := range r.network.InterfaceFinder().Interfaces() { for _, prefix := range netInterface.Addresses { - if prefix.Addr().Unmap() == source { + if prefix.Addr() == source { return true } } From 553cfa1f9f99f4da6118f93d507294e580db00d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 23 Apr 2026 07:22:32 +0800 Subject: [PATCH 99/99] Bump version --- clients/android | 2 +- clients/apple | 2 +- docs/changelog.md | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/clients/android b/clients/android index a3f4ca31..3b3883ef 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit a3f4ca31d122756f36e8f41e5d3d8d676ba3e4dd +Subproject commit 3b3883ef2c7fa5cb4d7bc7fe9846a7661f636e98 diff --git a/clients/apple b/clients/apple index 376f927c..e5d6ab4c 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit 376f927ccd943e369c3af0b3e845418137251524 +Subproject commit e5d6ab4c77a2fab310633e440a84f24e6ec82b1b diff --git a/docs/changelog.md b/docs/changelog.md index c6b0b585..f38e84de 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,11 @@ icon: material/alert-decagram --- +#### 1.13.11 + +* Fix process searcher failure introduced in 1.13.9 +* Fixes and improvements + #### 1.13.10 * Fix process searcher failure introduced in 1.13.9