Compare commits
19 Commits
oldstable
...
v1.13.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3c108b3a6 | ||
|
|
315e0f720a | ||
|
|
3e39ac8558 | ||
|
|
9f4aa88ca8 | ||
|
|
0ed86db4ee | ||
|
|
3de913c420 | ||
|
|
d35e71bee6 | ||
|
|
c79fc86c36 | ||
|
|
e398e01269 | ||
|
|
a6a34c1968 | ||
|
|
25b9ae17c4 | ||
|
|
354cfe9899 | ||
|
|
ca6575b582 | ||
|
|
897c18b3e0 | ||
|
|
67d1ab760f | ||
|
|
1d91ec5c50 | ||
|
|
884144c522 | ||
|
|
2413c7caef | ||
|
|
d53947be2a |
23
.fpm_pacman
23
.fpm_pacman
@@ -1,23 +0,0 @@
|
|||||||
-s dir
|
|
||||||
--name sing-box
|
|
||||||
--category net
|
|
||||||
--license GPL-3.0-or-later
|
|
||||||
--description "The universal proxy platform."
|
|
||||||
--url "https://sing-box.sagernet.org/"
|
|
||||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
|
||||||
--config-files etc/sing-box/config.json
|
|
||||||
--after-install release/config/sing-box.postinst
|
|
||||||
|
|
||||||
release/config/config.json=/etc/sing-box/config.json
|
|
||||||
|
|
||||||
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
|
|
||||||
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
|
|
||||||
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
|
|
||||||
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
|
|
||||||
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
|
||||||
|
|
||||||
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
|
||||||
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
|
||||||
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
|
||||||
|
|
||||||
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
|
||||||
33
.github/detect_track.sh
vendored
33
.github/detect_track.sh
vendored
@@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
branches=$(git branch -r --contains HEAD)
|
|
||||||
if echo "$branches" | grep -q 'origin/stable'; then
|
|
||||||
track=stable
|
|
||||||
elif echo "$branches" | grep -q 'origin/testing'; then
|
|
||||||
track=testing
|
|
||||||
elif echo "$branches" | grep -q 'origin/oldstable'; then
|
|
||||||
track=oldstable
|
|
||||||
else
|
|
||||||
echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$track" == "stable" ]]; then
|
|
||||||
tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
|
|
||||||
if [[ -n "$tag" && "$tag" == *"-"* ]]; then
|
|
||||||
track=beta
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$track" in
|
|
||||||
stable) name=sing-box; docker_tag=latest ;;
|
|
||||||
beta) name=sing-box-beta; docker_tag=latest-beta ;;
|
|
||||||
testing) name=sing-box-testing; docker_tag=latest-testing ;;
|
|
||||||
oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2
|
|
||||||
echo "TRACK=${track}" >> "$GITHUB_ENV"
|
|
||||||
echo "NAME=${name}" >> "$GITHUB_ENV"
|
|
||||||
echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV"
|
|
||||||
46
.github/setup_go_for_windows7.sh
vendored
46
.github/setup_go_for_windows7.sh
vendored
@@ -1,46 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
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
|
|
||||||
# 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:
|
|
||||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
|
||||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
|
||||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
|
||||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
|
||||||
# fixes:
|
|
||||||
# bed309eff415bcb3c77dd4bc3277b682b89a388d: "Fix os.RemoveAll not working on Windows7"
|
|
||||||
# 34b899c2fb39b092db4fa67c4417e41dc046be4b: "Revert \"os: remove 5ms sleep on Windows in (*Process).Wait\""
|
|
||||||
|
|
||||||
for patch_commit in "${PATCH_COMMITS[@]}"; do
|
|
||||||
curl "${CURL_ARGS[@]}" "https://github.com/MetaCubeX/go/commit/${patch_commit}.diff" | patch --verbose -p 1
|
|
||||||
done
|
|
||||||
25
.github/setup_legacy_go.sh
vendored
Executable file
25
.github/setup_legacy_go.sh
vendored
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
VERSION="1.23.12"
|
||||||
|
|
||||||
|
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_legacy
|
||||||
|
cd go_legacy
|
||||||
|
|
||||||
|
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||||
|
# this patch file only works on golang1.23.x
|
||||||
|
# that means after golang1.24 release it must be changed
|
||||||
|
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
|
||||||
|
# revert:
|
||||||
|
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
|
||||||
|
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
|
||||||
76
.github/workflows/build.yml
vendored
76
.github/workflows/build.yml
vendored
@@ -25,7 +25,8 @@ on:
|
|||||||
- publish-android
|
- publish-android
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- oldstable
|
- main-next
|
||||||
|
- dev-next
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
||||||
@@ -45,7 +46,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.0
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -87,9 +88,9 @@ jobs:
|
|||||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
||||||
|
|
||||||
- { os: windows, arch: amd64 }
|
- { os: windows, arch: amd64 }
|
||||||
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
|
- { os: windows, arch: amd64, legacy_go123: true, legacy_name: "windows-7" }
|
||||||
- { os: windows, arch: "386" }
|
- { os: windows, arch: "386" }
|
||||||
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
|
- { os: windows, arch: "386", legacy_go123: true, legacy_name: "windows-7" }
|
||||||
- { os: windows, arch: arm64 }
|
- { os: windows, arch: arm64 }
|
||||||
|
|
||||||
- { os: darwin, arch: amd64 }
|
- { os: darwin, arch: amd64 }
|
||||||
@@ -106,32 +107,32 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
|
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.0
|
||||||
- name: Setup Go 1.24
|
- name: Setup Go 1.24
|
||||||
if: matrix.legacy_go124
|
if: matrix.legacy_go124
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.24.10
|
go-version: ~1.24.6
|
||||||
- name: Cache Go for Windows 7
|
- name: Cache Go 1.23
|
||||||
if: matrix.legacy_win7
|
if: matrix.legacy_go123
|
||||||
id: cache-go-for-windows7
|
id: cache-legacy-go
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/go_win7
|
~/go/go_legacy
|
||||||
key: go_win7_1255
|
key: go_legacy_12312
|
||||||
- name: Setup Go for Windows 7
|
- name: Setup Go 1.23
|
||||||
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
if: matrix.legacy_go123 && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
||||||
run: |-
|
run: |-
|
||||||
.github/setup_go_for_windows7.sh
|
.github/setup_legacy_go.sh
|
||||||
- name: Setup Go for Windows 7
|
- name: Setup Go 1.23
|
||||||
if: matrix.legacy_win7
|
if: matrix.legacy_go123
|
||||||
run: |-
|
run: |-
|
||||||
echo "PATH=$HOME/go/go_win7/bin:$PATH" >> $GITHUB_ENV
|
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
|
||||||
echo "GOROOT=$HOME/go/go_win7" >> $GITHUB_ENV
|
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
if: matrix.os == 'android'
|
if: matrix.os == 'android'
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -242,7 +243,7 @@ jobs:
|
|||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libarchive-tools
|
sudo apt-get install -y libarchive-tools
|
||||||
cp .fpm_pacman .fpm
|
cp .fpm_systemd .fpm
|
||||||
fpm -t pacman \
|
fpm -t pacman \
|
||||||
-v "$PKG_VERSION" \
|
-v "$PKG_VERSION" \
|
||||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
||||||
@@ -286,7 +287,7 @@ jobs:
|
|||||||
path: "dist"
|
path: "dist"
|
||||||
build_android:
|
build_android:
|
||||||
name: Build Android
|
name: Build Android
|
||||||
if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android') && github.ref != 'refs/heads/oldstable'
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
@@ -299,7 +300,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.0
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -322,12 +323,12 @@ jobs:
|
|||||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
|
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: github.ref == 'refs/heads/testing'
|
if: github.ref == 'refs/heads/dev-next'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -366,7 +367,7 @@ jobs:
|
|||||||
path: 'dist'
|
path: 'dist'
|
||||||
publish_android:
|
publish_android:
|
||||||
name: Publish Android
|
name: Publish Android
|
||||||
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' && github.ref != 'refs/heads/oldstable'
|
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
@@ -379,7 +380,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.0
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -402,12 +403,12 @@ jobs:
|
|||||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
|
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: github.ref == 'refs/heads/testing'
|
if: github.ref == 'refs/heads/dev-next'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -431,8 +432,7 @@ jobs:
|
|||||||
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
|
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
|
||||||
build_apple:
|
build_apple:
|
||||||
name: Build Apple clients
|
name: Build Apple clients
|
||||||
runs-on: macos-26
|
runs-on: macos-15
|
||||||
if: false
|
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
strategy:
|
strategy:
|
||||||
@@ -478,7 +478,15 @@ jobs:
|
|||||||
if: matrix.if
|
if: matrix.if
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.0
|
||||||
|
- name: Setup Xcode stable
|
||||||
|
if: matrix.if && github.ref == 'refs/heads/main-next'
|
||||||
|
run: |-
|
||||||
|
sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||||
|
- name: Setup Xcode beta
|
||||||
|
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
||||||
|
run: |-
|
||||||
|
sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
@@ -486,12 +494,12 @@ jobs:
|
|||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: matrix.if && github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
|
if: matrix.if && github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: matrix.if && github.ref == 'refs/heads/testing'
|
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -577,7 +585,7 @@ jobs:
|
|||||||
-authenticationKeyID $ASC_KEY_ID \
|
-authenticationKeyID $ASC_KEY_ID \
|
||||||
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||||
- name: Publish to TestFlight
|
- name: Publish to TestFlight
|
||||||
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/testing'
|
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/dev-next'
|
||||||
run: |-
|
run: |-
|
||||||
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
|
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
|
||||||
- name: Build image
|
- name: Build image
|
||||||
|
|||||||
18
.github/workflows/docker.yml
vendored
18
.github/workflows/docker.yml
vendored
@@ -99,13 +99,13 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "ref=$ref"
|
echo "ref=$ref"
|
||||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||||
- name: Checkout
|
if [[ $ref == *"-"* ]]; then
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
latest=latest-beta
|
||||||
with:
|
else
|
||||||
ref: ${{ steps.ref.outputs.ref }}
|
latest=latest
|
||||||
fetch-depth: 0
|
fi
|
||||||
- name: Detect track
|
echo "latest=$latest"
|
||||||
run: bash .github/detect_track.sh
|
echo "latest=$latest" >> $GITHUB_OUTPUT
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
@@ -124,10 +124,10 @@ jobs:
|
|||||||
working-directory: /tmp/digests
|
working-directory: /tmp/digests
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create \
|
docker buildx imagetools create \
|
||||||
-t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \
|
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
|
||||||
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
|
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
|
||||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||||
- name: Inspect image
|
- name: Inspect image
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}
|
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
|
||||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
||||||
|
|||||||
16
.github/workflows/lint.yml
vendored
16
.github/workflows/lint.yml
vendored
@@ -3,20 +3,18 @@ name: Lint
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- oldstable
|
- stable-next
|
||||||
- stable
|
- main-next
|
||||||
- testing
|
- dev-next
|
||||||
- unstable
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- '.github/**'
|
- '.github/**'
|
||||||
- '!.github/workflows/lint.yml'
|
- '!.github/workflows/lint.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- oldstable
|
- stable-next
|
||||||
- stable
|
- main-next
|
||||||
- testing
|
- dev-next
|
||||||
- unstable
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -30,7 +28,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.24.10
|
go-version: ~1.24.6
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v8
|
uses: golangci/golangci-lint-action@v8
|
||||||
with:
|
with:
|
||||||
|
|||||||
19
.github/workflows/linux.yml
vendored
19
.github/workflows/linux.yml
vendored
@@ -7,6 +7,11 @@ on:
|
|||||||
description: "Version name"
|
description: "Version name"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
forceBeta:
|
||||||
|
description: "Force beta"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
@@ -25,7 +30,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.0
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -66,7 +71,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.25.8
|
go-version: ^1.25.0
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
if: matrix.os == 'android'
|
if: matrix.os == 'android'
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -98,8 +103,14 @@ jobs:
|
|||||||
- name: Set mtime
|
- name: Set mtime
|
||||||
run: |-
|
run: |-
|
||||||
TZ=UTC touch -t '197001010000' dist/sing-box
|
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||||
- name: Detect track
|
- name: Set name
|
||||||
run: bash .github/detect_track.sh
|
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
|
||||||
|
run: |-
|
||||||
|
echo "NAME=sing-box" >> "$GITHUB_ENV"
|
||||||
|
- name: Set beta name
|
||||||
|
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta
|
||||||
|
run: |-
|
||||||
|
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||||
- name: Set version
|
- name: Set version
|
||||||
run: |-
|
run: |-
|
||||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -15,6 +15,4 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/config.d/
|
/config.d/
|
||||||
/venv/
|
/venv/
|
||||||
CLAUDE.md
|
|
||||||
AGENTS.md
|
|
||||||
/.claude/
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ RUN set -ex \
|
|||||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
&& apk upgrade \
|
||||||
|
&& apk add bash tzdata ca-certificates nftables \
|
||||||
|
&& rm -rf /var/cache/apk/*
|
||||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
||||||
ENTRYPOINT ["sing-box"]
|
ENTRYPOINT ["sing-box"]
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -6,7 +6,7 @@ GOHOSTOS = $(shell go env GOHOSTOS)
|
|||||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
|
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
|
||||||
|
|
||||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
|
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid= -checklinkname=0"
|
||||||
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
||||||
MAIN = ./cmd/sing-box
|
MAIN = ./cmd/sing-box
|
||||||
PREFIX ?= $(shell go env GOPATH)
|
PREFIX ?= $(shell go env GOPATH)
|
||||||
@@ -17,10 +17,6 @@ build:
|
|||||||
export GOTOOLCHAIN=local && \
|
export GOTOOLCHAIN=local && \
|
||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
race:
|
|
||||||
export GOTOOLCHAIN=local && \
|
|
||||||
go build -race $(MAIN_PARAMS) $(MAIN)
|
|
||||||
|
|
||||||
ci_build:
|
ci_build:
|
||||||
export GOTOOLCHAIN=local && \
|
export GOTOOLCHAIN=local && \
|
||||||
go build $(PARAMS) $(MAIN) && \
|
go build $(PARAMS) $(MAIN) && \
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
> Sponsored by [Warp](https://go.warp.dev/sing-box), built for coding with multiple AI agents
|
|
||||||
|
|
||||||
<a href="https://go.warp.dev/sing-box">
|
|
||||||
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png">
|
|
||||||
</a>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# sing-box
|
# sing-box
|
||||||
|
|
||||||
The universal proxy platform.
|
The universal proxy platform.
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ type DNSClient interface {
|
|||||||
Start()
|
Start()
|
||||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||||
|
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
|
||||||
|
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
|
||||||
ClearCache()
|
ClearCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ type InboundContext struct {
|
|||||||
Domain string
|
Domain string
|
||||||
Client string
|
Client string
|
||||||
SniffContext any
|
SniffContext any
|
||||||
SnifferNames []string
|
|
||||||
SniffError error
|
SniffError error
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
@@ -18,6 +19,11 @@ type Outbound interface {
|
|||||||
N.Dialer
|
N.Dialer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OutboundWithPreferredRoutes interface {
|
||||||
|
PreferredDomain(domain string) bool
|
||||||
|
PreferredAddress(address netip.Addr) bool
|
||||||
|
}
|
||||||
|
|
||||||
type OutboundRegistry interface {
|
type OutboundRegistry interface {
|
||||||
option.OutboundOptionsRegistry
|
option.OutboundOptionsRegistry
|
||||||
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func NewUpstreamContextHandlerEx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
_, myMetadata := ExtendContext(ctx)
|
myMetadata := ContextFrom(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
_, myMetadata := ExtendContext(ctx)
|
myMetadata := ContextFrom(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ type routeContextHandlerWrapperEx struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
_, metadata := ExtendContext(ctx)
|
metadata := ContextFrom(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
metadata.Source = source
|
metadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
_, metadata := ExtendContext(ctx)
|
metadata := ContextFrom(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
metadata.Source = source
|
metadata.Source = source
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
|
|||||||
// Deprecated: removed
|
// Deprecated: removed
|
||||||
func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
func UpstreamMetadata(metadata InboundContext) M.Metadata {
|
||||||
return M.Metadata{
|
return M.Metadata{
|
||||||
Source: metadata.Source.Unwrap(),
|
Source: metadata.Source,
|
||||||
Destination: metadata.Destination.Unwrap(),
|
Destination: metadata.Destination,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
box.go
7
box.go
@@ -323,13 +323,14 @@ func New(options Options) (*Box, error) {
|
|||||||
option.DirectOutboundOptions{},
|
option.DirectOutboundOptions{},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
dnsTransportManager.Initialize(common.Must1(
|
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||||
local.NewTransport(
|
return local.NewTransport(
|
||||||
ctx,
|
ctx,
|
||||||
logFactory.NewLogger("dns/local"),
|
logFactory.NewLogger("dns/local"),
|
||||||
"local",
|
"local",
|
||||||
option.LocalDNSServerOptions{},
|
option.LocalDNSServerOptions{},
|
||||||
)))
|
)
|
||||||
|
})
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
err = platformInterface.Initialize(networkManager)
|
err = platformInterface.Initialize(networkManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Submodule clients/android updated: 134402995e...24b2613007
Submodule clients/apple updated: 97402ba8b6...c5734677bd
@@ -134,7 +134,6 @@ func publishTestflight(ctx context.Context) error {
|
|||||||
asc.PlatformTVOS,
|
asc.PlatformTVOS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
waitingForProcess := false
|
|
||||||
for _, platform := range platforms {
|
for _, platform := range platforms {
|
||||||
log.Info(string(platform), " list builds")
|
log.Info(string(platform), " list builds")
|
||||||
for {
|
for {
|
||||||
@@ -146,13 +145,12 @@ func publishTestflight(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
build := builds.Data[0]
|
build := builds.Data[0]
|
||||||
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
|
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
|
||||||
log.Info(string(platform), " ", tag, " waiting for process")
|
log.Info(string(platform), " ", tag, " waiting for process")
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if *build.Attributes.ProcessingState != "VALID" {
|
if *build.Attributes.ProcessingState != "VALID" {
|
||||||
waitingForProcess = true
|
|
||||||
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(15 * time.Second)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ var (
|
|||||||
sharedFlags []string
|
sharedFlags []string
|
||||||
debugFlags []string
|
debugFlags []string
|
||||||
sharedTags []string
|
sharedTags []string
|
||||||
darwinTags []string
|
macOSTags []string
|
||||||
memcTags []string
|
memcTags []string
|
||||||
notMemcTags []string
|
notMemcTags []string
|
||||||
debugTags []string
|
debugTags []string
|
||||||
@@ -63,7 +63,7 @@ func init() {
|
|||||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||||
|
|
||||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
|
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
|
||||||
darwinTags = append(darwinTags, "with_dhcp")
|
macOSTags = append(macOSTags, "with_dhcp")
|
||||||
memcTags = append(memcTags, "with_tailscale")
|
memcTags = append(memcTags, "with_tailscale")
|
||||||
notMemcTags = append(notMemcTags, "with_low_memory")
|
notMemcTags = append(notMemcTags, "with_low_memory")
|
||||||
debugTags = append(debugTags, "debug")
|
debugTags = append(debugTags, "debug")
|
||||||
@@ -160,7 +160,9 @@ func buildApple() {
|
|||||||
"-tags-not-macos=with_low_memory",
|
"-tags-not-macos=with_low_memory",
|
||||||
}
|
}
|
||||||
if !withTailscale {
|
if !withTailscale {
|
||||||
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
|
args = append(args, "-tags-macos="+strings.Join(append(macOSTags, memcTags...), ","))
|
||||||
|
} else {
|
||||||
|
args = append(args, "-tags-macos="+strings.Join(macOSTags, ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
@@ -169,7 +171,7 @@ func buildApple() {
|
|||||||
args = append(args, debugFlags...)
|
args = append(args, debugFlags...)
|
||||||
}
|
}
|
||||||
|
|
||||||
tags := append(sharedTags, darwinTags...)
|
tags := sharedTags
|
||||||
if withTailscale {
|
if withTailscale {
|
||||||
tags = append(tags, memcTags...)
|
tags = append(tags, memcTags...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/srs"
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-box/route/rule"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -69,7 +71,7 @@ func compileRuleSet(sourcePath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version)
|
err = srs.Write(outputFile, plainRuleSet.Options, downgradeRuleSetVersion(plainRuleSet.Version, plainRuleSet.Options))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
outputFile.Close()
|
outputFile.Close()
|
||||||
os.Remove(outputPath)
|
os.Remove(outputPath)
|
||||||
@@ -78,3 +80,18 @@ func compileRuleSet(sourcePath string) error {
|
|||||||
outputFile.Close()
|
outputFile.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
|
||||||
|
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||||
|
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
||||||
|
len(rule.DefaultInterfaceAddress) > 0
|
||||||
|
}) {
|
||||||
|
version = C.RuleSetVersion3
|
||||||
|
}
|
||||||
|
if version == C.RuleSetVersion3 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||||
|
return len(rule.NetworkType) > 0 || rule.NetworkIsExpensive || rule.NetworkIsConstrained
|
||||||
|
}) {
|
||||||
|
version = C.RuleSetVersion2
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sagernet/fswatch"
|
"github.com/sagernet/fswatch"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -22,7 +21,6 @@ import (
|
|||||||
var _ adapter.CertificateStore = (*Store)(nil)
|
var _ adapter.CertificateStore = (*Store)(nil)
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
access sync.RWMutex
|
|
||||||
systemPool *x509.CertPool
|
systemPool *x509.CertPool
|
||||||
currentPool *x509.CertPool
|
currentPool *x509.CertPool
|
||||||
certificate string
|
certificate string
|
||||||
@@ -117,14 +115,10 @@ func (s *Store) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Pool() *x509.CertPool {
|
func (s *Store) Pool() *x509.CertPool {
|
||||||
s.access.RLock()
|
|
||||||
defer s.access.RUnlock()
|
|
||||||
return s.currentPool
|
return s.currentPool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) update() error {
|
func (s *Store) update() error {
|
||||||
s.access.Lock()
|
|
||||||
defer s.access.Unlock()
|
|
||||||
var currentPool *x509.CertPool
|
var currentPool *x509.CertPool
|
||||||
if s.systemPool == nil {
|
if s.systemPool == nil {
|
||||||
currentPool = x509.NewCertPool()
|
currentPool = x509.NewCertPool()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/atomic"
|
||||||
"github.com/sagernet/sing/common/control"
|
"github.com/sagernet/sing/common/control"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
@@ -42,7 +43,7 @@ type DefaultDialer struct {
|
|||||||
networkType []C.InterfaceType
|
networkType []C.InterfaceType
|
||||||
fallbackNetworkType []C.InterfaceType
|
fallbackNetworkType []C.InterfaceType
|
||||||
networkFallbackDelay time.Duration
|
networkFallbackDelay time.Duration
|
||||||
networkLastFallback common.TypedValue[time.Time]
|
networkLastFallback atomic.TypedValue[time.Time]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
||||||
@@ -88,40 +89,42 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
|
|
||||||
if networkManager != nil {
|
if networkManager != nil {
|
||||||
defaultOptions := networkManager.DefaultOptions()
|
defaultOptions := networkManager.DefaultOptions()
|
||||||
if defaultOptions.BindInterface != "" && !disableDefaultBind {
|
if !disableDefaultBind {
|
||||||
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
if defaultOptions.BindInterface != "" {
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
|
||||||
} else if networkManager.AutoDetectInterface() && !disableDefaultBind {
|
|
||||||
if platformInterface != nil {
|
|
||||||
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
|
|
||||||
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
|
|
||||||
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
|
|
||||||
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
|
|
||||||
networkStrategy = defaultOptions.NetworkStrategy
|
|
||||||
networkType = defaultOptions.NetworkType
|
|
||||||
fallbackNetworkType = defaultOptions.FallbackNetworkType
|
|
||||||
}
|
|
||||||
networkFallbackDelay = time.Duration(options.FallbackDelay)
|
|
||||||
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
|
|
||||||
networkFallbackDelay = defaultOptions.FallbackDelay
|
|
||||||
}
|
|
||||||
if networkStrategy == nil {
|
|
||||||
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
|
|
||||||
defaultNetworkStrategy = true
|
|
||||||
}
|
|
||||||
bindFunc := networkManager.ProtectFunc()
|
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
|
||||||
} else {
|
|
||||||
bindFunc := networkManager.AutoDetectInterfaceFunc()
|
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
|
} else if networkManager.AutoDetectInterface() {
|
||||||
|
if platformInterface != nil {
|
||||||
|
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
|
||||||
|
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
|
||||||
|
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
|
||||||
|
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
|
||||||
|
networkStrategy = defaultOptions.NetworkStrategy
|
||||||
|
networkType = defaultOptions.NetworkType
|
||||||
|
fallbackNetworkType = defaultOptions.FallbackNetworkType
|
||||||
|
}
|
||||||
|
networkFallbackDelay = time.Duration(options.FallbackDelay)
|
||||||
|
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
|
||||||
|
networkFallbackDelay = defaultOptions.FallbackDelay
|
||||||
|
}
|
||||||
|
if networkStrategy == nil {
|
||||||
|
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
|
||||||
|
defaultNetworkStrategy = true
|
||||||
|
}
|
||||||
|
bindFunc := networkManager.ProtectFunc()
|
||||||
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
|
} else {
|
||||||
|
bindFunc := networkManager.AutoDetectInterfaceFunc()
|
||||||
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
|
||||||
|
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
||||||
|
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
|
|
||||||
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
|
||||||
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if networkManager != nil {
|
if networkManager != nil {
|
||||||
@@ -142,7 +145,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
dialer.Timeout = C.TCPConnectTimeout
|
dialer.Timeout = C.TCPConnectTimeout
|
||||||
}
|
}
|
||||||
// TODO: Add an option to customize the keep alive period
|
// TODO: Add an option to customize the keep alive period
|
||||||
setKeepAliveConfig(&dialer, C.TCPKeepAliveInitial, C.TCPKeepAliveInterval)
|
dialer.KeepAlive = C.TCPKeepAliveInitial
|
||||||
|
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval))
|
||||||
var udpFragment bool
|
var udpFragment bool
|
||||||
if options.UDPFragment != nil {
|
if options.UDPFragment != nil {
|
||||||
udpFragment = *options.UDPFragment
|
udpFragment = *options.UDPFragment
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
//go:build go1.23
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setKeepAliveConfig(dialer *net.Dialer, idle time.Duration, interval time.Duration) {
|
|
||||||
dialer.KeepAliveConfig = net.KeepAliveConfig{
|
|
||||||
Enable: true,
|
|
||||||
Idle: idle,
|
|
||||||
Interval: interval,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
//go:build !go1.23
|
|
||||||
|
|
||||||
package dialer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/control"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setKeepAliveConfig(dialer *net.Dialer, idle time.Duration, interval time.Duration) {
|
|
||||||
dialer.KeepAlive = idle
|
|
||||||
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(idle, interval))
|
|
||||||
}
|
|
||||||
@@ -145,7 +145,3 @@ type ParallelNetworkDialer interface {
|
|||||||
DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||||
ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
|
ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PacketDialerWithDestination interface {
|
|
||||||
ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/atomic"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return common.DefaultValue[T](), E.Cause(err, "get current netns")
|
return common.DefaultValue[T](), E.Cause(err, "get current netns")
|
||||||
}
|
}
|
||||||
defer currentNs.Close()
|
|
||||||
defer netns.Set(currentNs)
|
defer netns.Set(currentNs)
|
||||||
var targetNs netns.NsHandle
|
var targetNs netns.NsHandle
|
||||||
if strings.HasPrefix(nameOrPath, "/") {
|
if strings.HasPrefix(nameOrPath, "/") {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package listener
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
|||||||
if l.tproxy {
|
if l.tproxy {
|
||||||
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
|
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
|
||||||
return control.Raw(conn, func(fd uintptr) error {
|
return control.Raw(conn, func(fd uintptr) error {
|
||||||
return redir.TProxy(fd, !strings.HasSuffix(network, "4"), false)
|
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -42,7 +41,7 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
|||||||
if l.tproxy {
|
if l.tproxy {
|
||||||
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
|
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
|
||||||
return control.Raw(conn, func(fd uintptr) error {
|
return control.Raw(conn, func(fd uintptr) error {
|
||||||
return redir.TProxy(fd, !strings.HasSuffix(network, "4"), true)
|
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,6 +303,8 @@ find:
|
|||||||
metadata.Protocol = C.ProtocolQUIC
|
metadata.Protocol = C.ProtocolQUIC
|
||||||
fingerprint, err := ja3.Compute(buffer.Bytes())
|
fingerprint, err := ja3.Compute(buffer.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
metadata.Protocol = C.ProtocolQUIC
|
||||||
|
metadata.Client = C.ClientChromium
|
||||||
metadata.SniffContext = fragments
|
metadata.SniffContext = fragments
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return E.Cause1(ErrNeedMoreData, err)
|
||||||
}
|
}
|
||||||
@@ -332,7 +334,7 @@ find:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
|
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
|
||||||
if isQUICGo(fingerprint) {
|
if maybeUQUIC(fingerprint) {
|
||||||
metadata.Client = C.ClientQUICGo
|
metadata.Client = C.ClientQUICGo
|
||||||
} else {
|
} else {
|
||||||
metadata.Client = C.ClientChromium
|
metadata.Client = C.ClientChromium
|
||||||
|
|||||||
@@ -1,29 +1,21 @@
|
|||||||
package sniff
|
package sniff
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/ja3"
|
"github.com/sagernet/sing-box/common/ja3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
|
||||||
// X25519Kyber768Draft00 - post-quantum curve used by Go crypto/tls
|
// The cronet without this behavior does not have version 115
|
||||||
x25519Kyber768Draft00 uint16 = 0x11EC // 4588
|
var uQUICChrome115 = &ja3.ClientHello{
|
||||||
// renegotiation_info extension used by Go crypto/tls
|
Version: tls.VersionTLS12,
|
||||||
extensionRenegotiationInfo uint16 = 0xFF01 // 65281
|
CipherSuites: []uint16{4865, 4866, 4867},
|
||||||
)
|
Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513},
|
||||||
|
EllipticCurves: []uint16{29, 23, 24},
|
||||||
// isQUICGo detects native quic-go by checking for Go crypto/tls specific features.
|
SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},
|
||||||
// Note: uQUIC with Chromium mimicry cannot be reliably distinguished from real Chromium
|
}
|
||||||
// since it uses the same TLS fingerprint, so it will be identified as Chromium.
|
|
||||||
func isQUICGo(fingerprint *ja3.ClientHello) bool {
|
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
|
||||||
for _, curve := range fingerprint.EllipticCurves {
|
return !uQUICChrome115.Equals(fingerprint, true)
|
||||||
if curve == x25519Kyber768Draft00 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, ext := range fingerprint.Extensions {
|
|
||||||
if ext == extensionRenegotiationInfo {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
package sniff_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/quic-go"
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/sniff"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSniffQUICQuicGoFingerprint(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
const testSNI = "test.example.com"
|
|
||||||
|
|
||||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer udpConn.Close()
|
|
||||||
|
|
||||||
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
|
||||||
packetsChan := make(chan [][]byte, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
var packets [][]byte
|
|
||||||
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
buf := make([]byte, 2048)
|
|
||||||
n, _, err := udpConn.ReadFromUDP(buf)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
packets = append(packets, buf[:n])
|
|
||||||
}
|
|
||||||
packetsChan <- packets
|
|
||||||
}()
|
|
||||||
|
|
||||||
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer clientConn.Close()
|
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
ServerName: testSNI,
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
NextProtos: []string{"h3"},
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
|
||||||
|
|
||||||
select {
|
|
||||||
case packets := <-packetsChan:
|
|
||||||
t.Logf("Captured %d packets", len(packets))
|
|
||||||
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
for i, pkt := range packets {
|
|
||||||
err := sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
|
||||||
t.Logf("Packet %d: err=%v, domain=%s, client=%s", i, err, metadata.Domain, metadata.Client)
|
|
||||||
if metadata.Domain != "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("\n=== quic-go TLS Fingerprint Analysis ===")
|
|
||||||
t.Logf("Domain: %s", metadata.Domain)
|
|
||||||
t.Logf("Client: %s", metadata.Client)
|
|
||||||
t.Logf("Protocol: %s", metadata.Protocol)
|
|
||||||
|
|
||||||
// The client should be identified as quic-go, not chromium
|
|
||||||
// Current issue: it's being identified as chromium
|
|
||||||
if metadata.Client == "chromium" {
|
|
||||||
t.Log("WARNING: quic-go is being misidentified as chromium!")
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Fatal("Timeout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSniffQUICInitialFromQuicGo(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const testSNI = "test.example.com"
|
|
||||||
|
|
||||||
// Create UDP listener to capture ALL initial packets
|
|
||||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer udpConn.Close()
|
|
||||||
|
|
||||||
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
|
||||||
|
|
||||||
// Channel to receive captured packets
|
|
||||||
packetsChan := make(chan [][]byte, 1)
|
|
||||||
|
|
||||||
// Start goroutine to capture packets
|
|
||||||
go func() {
|
|
||||||
var packets [][]byte
|
|
||||||
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
||||||
for i := 0; i < 5; i++ { // Capture up to 5 packets
|
|
||||||
buf := make([]byte, 2048)
|
|
||||||
n, _, err := udpConn.ReadFromUDP(buf)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
packets = append(packets, buf[:n])
|
|
||||||
}
|
|
||||||
packetsChan <- packets
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Create QUIC client connection (will fail but we capture the initial packet)
|
|
||||||
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer clientConn.Close()
|
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
ServerName: testSNI,
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
NextProtos: []string{"h3"},
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// This will fail (no server) but sends initial packet
|
|
||||||
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
|
||||||
|
|
||||||
// Wait for captured packets
|
|
||||||
select {
|
|
||||||
case packets := <-packetsChan:
|
|
||||||
t.Logf("Captured %d QUIC packets", len(packets))
|
|
||||||
|
|
||||||
for i, packet := range packets {
|
|
||||||
t.Logf("Packet %d: length=%d, first 30 bytes: %x", i, len(packet), packet[:min(30, len(packet))])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test sniffer with first packet
|
|
||||||
if len(packets) > 0 {
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err := sniff.QUICClientHello(context.Background(), &metadata, packets[0])
|
|
||||||
|
|
||||||
t.Logf("First packet sniff error: %v", err)
|
|
||||||
t.Logf("Protocol: %s", metadata.Protocol)
|
|
||||||
t.Logf("Domain: %s", metadata.Domain)
|
|
||||||
t.Logf("Client: %s", metadata.Client)
|
|
||||||
|
|
||||||
// If first packet needs more data, try with subsequent packets
|
|
||||||
// IMPORTANT: reuse metadata to accumulate CRYPTO fragments via SniffContext
|
|
||||||
if errors.Is(err, sniff.ErrNeedMoreData) && len(packets) > 1 {
|
|
||||||
t.Log("First packet needs more data, trying subsequent packets with shared context...")
|
|
||||||
for i := 1; i < len(packets); i++ {
|
|
||||||
// Reuse same metadata to accumulate fragments
|
|
||||||
err = sniff.QUICClientHello(context.Background(), &metadata, packets[i])
|
|
||||||
t.Logf("Packet %d sniff result: err=%v, domain=%s, sniffCtx=%v", i, err, metadata.Domain, metadata.SniffContext != nil)
|
|
||||||
if metadata.Domain != "" || (err != nil && !errors.Is(err, sniff.ErrNeedMoreData)) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print hex dump for debugging
|
|
||||||
t.Logf("First packet hex:\n%s", hex.Dump(packets[0][:min(256, len(packets[0]))]))
|
|
||||||
|
|
||||||
// Log final results
|
|
||||||
t.Logf("Final: Protocol=%s, Domain=%s, Client=%s", metadata.Protocol, metadata.Domain, metadata.Client)
|
|
||||||
|
|
||||||
// Verify SNI extraction
|
|
||||||
if metadata.Domain == "" {
|
|
||||||
t.Errorf("Failed to extract SNI, expected: %s", testSNI)
|
|
||||||
} else {
|
|
||||||
require.Equal(t, testSNI, metadata.Domain, "SNI should match")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check client identification - quic-go should be identified as quic-go, not chromium
|
|
||||||
t.Logf("Client identified as: %s (expected: quic-go)", metadata.Client)
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Fatal("Timeout waiting for QUIC packets")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@ func TestSniffQUICChromeNew(t *testing.T) {
|
|||||||
var metadata adapter.InboundContext
|
var metadata adapter.InboundContext
|
||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
require.Empty(t, metadata.Client)
|
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||||
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -39,7 +39,7 @@ func TestSniffQUICChromium(t *testing.T) {
|
|||||||
var metadata adapter.InboundContext
|
var metadata adapter.InboundContext
|
||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
require.Empty(t, metadata.Client)
|
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||||
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -56,7 +56,7 @@ func TestSniffUQUICChrome115(t *testing.T) {
|
|||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
require.Equal(t, metadata.Client, C.ClientQUICGo)
|
||||||
require.Equal(t, metadata.Domain, "www.google.com")
|
require.Equal(t, metadata.Domain, "www.google.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/domain"
|
"github.com/sagernet/sing/common/domain"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
|
"github.com/sagernet/sing/common/json/badoption"
|
||||||
"github.com/sagernet/sing/common/varbin"
|
"github.com/sagernet/sing/common/varbin"
|
||||||
|
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
@@ -41,6 +43,8 @@ const (
|
|||||||
ruleItemNetworkType
|
ruleItemNetworkType
|
||||||
ruleItemNetworkIsExpensive
|
ruleItemNetworkIsExpensive
|
||||||
ruleItemNetworkIsConstrained
|
ruleItemNetworkIsConstrained
|
||||||
|
ruleItemNetworkInterfaceAddress
|
||||||
|
ruleItemDefaultInterfaceAddress
|
||||||
ruleItemFinal uint8 = 0xFF
|
ruleItemFinal uint8 = 0xFF
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -230,6 +234,51 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
|
|||||||
rule.NetworkIsExpensive = true
|
rule.NetworkIsExpensive = true
|
||||||
case ruleItemNetworkIsConstrained:
|
case ruleItemNetworkIsConstrained:
|
||||||
rule.NetworkIsConstrained = true
|
rule.NetworkIsConstrained = true
|
||||||
|
case ruleItemNetworkInterfaceAddress:
|
||||||
|
rule.NetworkInterfaceAddress = new(badjson.TypedMap[option.InterfaceType, badoption.Listable[*badoption.Prefixable]])
|
||||||
|
var size uint64
|
||||||
|
size, err = binary.ReadUvarint(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := uint64(0); i < size; i++ {
|
||||||
|
var key uint8
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var value []*badoption.Prefixable
|
||||||
|
var prefixCount uint64
|
||||||
|
prefixCount, err = binary.ReadUvarint(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for j := uint64(0); j < prefixCount; j++ {
|
||||||
|
var prefix netip.Prefix
|
||||||
|
prefix, err = readPrefix(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value = append(value, common.Ptr(badoption.Prefixable(prefix)))
|
||||||
|
}
|
||||||
|
rule.NetworkInterfaceAddress.Put(option.InterfaceType(key), value)
|
||||||
|
}
|
||||||
|
case ruleItemDefaultInterfaceAddress:
|
||||||
|
var value []*badoption.Prefixable
|
||||||
|
var prefixCount uint64
|
||||||
|
prefixCount, err = binary.ReadUvarint(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for j := uint64(0); j < prefixCount; j++ {
|
||||||
|
var prefix netip.Prefix
|
||||||
|
prefix, err = readPrefix(reader)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value = append(value, common.Ptr(badoption.Prefixable(prefix)))
|
||||||
|
}
|
||||||
|
rule.DefaultInterfaceAddress = value
|
||||||
case ruleItemFinal:
|
case ruleItemFinal:
|
||||||
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
|
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
|
||||||
return
|
return
|
||||||
@@ -346,7 +395,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
|||||||
}
|
}
|
||||||
if len(rule.NetworkType) > 0 {
|
if len(rule.NetworkType) > 0 {
|
||||||
if generateVersion < C.RuleSetVersion3 {
|
if generateVersion < C.RuleSetVersion3 {
|
||||||
return E.New("network_type rule item is only supported in version 3 or later")
|
return E.New("`network_type` rule item is only supported in version 3 or later")
|
||||||
}
|
}
|
||||||
err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType)
|
err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -354,17 +403,71 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if rule.NetworkIsExpensive {
|
if rule.NetworkIsExpensive {
|
||||||
|
if generateVersion < C.RuleSetVersion3 {
|
||||||
|
return E.New("`network_is_expensive` rule item is only supported in version 3 or later")
|
||||||
|
}
|
||||||
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive)
|
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if rule.NetworkIsConstrained {
|
if rule.NetworkIsConstrained {
|
||||||
|
if generateVersion < C.RuleSetVersion3 {
|
||||||
|
return E.New("`network_is_constrained` rule item is only supported in version 3 or later")
|
||||||
|
}
|
||||||
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained)
|
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 {
|
||||||
|
if generateVersion < C.RuleSetVersion4 {
|
||||||
|
return E.New("`network_interface_address` rule item is only supported in version 4 or later")
|
||||||
|
}
|
||||||
|
err = writer.WriteByte(ruleItemNetworkInterfaceAddress)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = varbin.WriteUvarint(writer, uint64(rule.NetworkInterfaceAddress.Size()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, entry := range rule.NetworkInterfaceAddress.Entries() {
|
||||||
|
err = binary.Write(writer, binary.BigEndian, uint8(entry.Key.Build()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = varbin.WriteUvarint(writer, uint64(len(entry.Value)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, rawPrefix := range entry.Value {
|
||||||
|
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(rule.DefaultInterfaceAddress) > 0 {
|
||||||
|
if generateVersion < C.RuleSetVersion4 {
|
||||||
|
return E.New("`default_interface_address` rule item is only supported in version 4 or later")
|
||||||
|
}
|
||||||
|
err = writer.WriteByte(ruleItemDefaultInterfaceAddress)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = varbin.WriteUvarint(writer, uint64(len(rule.DefaultInterfaceAddress)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, rawPrefix := range rule.DefaultInterfaceAddress {
|
||||||
|
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(rule.WIFISSID) > 0 {
|
if len(rule.WIFISSID) > 0 {
|
||||||
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
|
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
33
common/srs/ip_cidr.go
Normal file
33
common/srs/ip_cidr.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package srs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
"github.com/sagernet/sing/common/varbin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
|
||||||
|
addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian)
|
||||||
|
if err != nil {
|
||||||
|
return netip.Prefix{}, err
|
||||||
|
}
|
||||||
|
prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian)
|
||||||
|
if err != nil {
|
||||||
|
return netip.Prefix{}, err
|
||||||
|
}
|
||||||
|
return netip.PrefixFrom(M.AddrFromIP(addrSlice), int(prefixBits)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePrefix(writer varbin.Writer, prefix netip.Prefix) error {
|
||||||
|
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,10 +2,11 @@ package tls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/badtls"
|
"github.com/sagernet/sing-box/common/badtls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
@@ -14,7 +15,7 @@ import (
|
|||||||
aTLS "github.com/sagernet/sing/common/tls"
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDialerFromOptions(ctx context.Context, router adapter.Router, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
func NewDialerFromOptions(ctx context.Context, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return dialer, nil
|
return dialer, nil
|
||||||
}
|
}
|
||||||
@@ -79,20 +80,29 @@ func (d *defaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
|
func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
|
||||||
return d.dialContext(ctx, destination)
|
return d.dialContext(ctx, destination, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
|
func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr, echRetry bool) (Conn, error) {
|
||||||
conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination)
|
conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tlsConn, err := aTLS.ClientHandshake(ctx, conn, d.config)
|
tlsConn, err := ClientHandshake(ctx, conn, d.config)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
conn.Close()
|
return tlsConn, nil
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return tlsConn, nil
|
conn.Close()
|
||||||
|
if echRetry {
|
||||||
|
var echErr *tls.ECHRejectionError
|
||||||
|
if errors.As(err, &echErr) && len(echErr.RetryConfigList) > 0 {
|
||||||
|
if echConfig, isECH := d.config.(ECHCapableConfig); isECH {
|
||||||
|
echConfig.SetECHConfigList(echErr.RetryConfigList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d.dialContext(ctx, destination, false)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *defaultDialer) Upstream() any {
|
func (d *defaultDialer) Upstream() any {
|
||||||
|
|||||||
@@ -69,7 +69,11 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
|
|||||||
} else {
|
} else {
|
||||||
return E.New("missing ECH keys")
|
return E.New("missing ECH keys")
|
||||||
}
|
}
|
||||||
echKeys, err := parseECHKeys(echKey)
|
block, rest := pem.Decode(echKey)
|
||||||
|
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
||||||
|
return E.New("invalid ECH keys pem")
|
||||||
|
}
|
||||||
|
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "parse ECH keys")
|
return E.Cause(err, "parse ECH keys")
|
||||||
}
|
}
|
||||||
@@ -81,29 +85,21 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
|
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
||||||
echKeys, err := parseECHKeys(echKey)
|
echKey, err := os.ReadFile(echKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return E.Cause(err, "reload ECH keys from ", echKeyPath)
|
||||||
}
|
}
|
||||||
c.access.Lock()
|
|
||||||
config := c.config.Clone()
|
|
||||||
config.EncryptedClientHelloKeys = echKeys
|
|
||||||
c.config = config
|
|
||||||
c.access.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) {
|
|
||||||
block, _ := pem.Decode(echKey)
|
block, _ := pem.Decode(echKey)
|
||||||
if block == nil || block.Type != "ECH KEYS" {
|
if block == nil || block.Type != "ECH KEYS" {
|
||||||
return nil, E.New("invalid ECH keys pem")
|
return E.New("invalid ECH keys pem")
|
||||||
}
|
}
|
||||||
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "parse ECH keys")
|
return E.Cause(err, "parse ECH keys")
|
||||||
}
|
}
|
||||||
return echKeys, nil
|
tlsConfig.EncryptedClientHelloKeys = echKeys
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ECHClientConfig struct {
|
type ECHClientConfig struct {
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
|
|||||||
return E.New("ECH requires go1.24, please recompile your binary.")
|
return E.New("ECH requires go1.24, please recompile your binary.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
|
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
||||||
panic("unreachable")
|
return E.New("ECH requires go1.24, please recompile your binary.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/fswatch"
|
"github.com/sagernet/fswatch"
|
||||||
@@ -21,7 +20,6 @@ import (
|
|||||||
var errInsecureUnused = E.New("tls: insecure unused")
|
var errInsecureUnused = E.New("tls: insecure unused")
|
||||||
|
|
||||||
type STDServerConfig struct {
|
type STDServerConfig struct {
|
||||||
access sync.RWMutex
|
|
||||||
config *tls.Config
|
config *tls.Config
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
acmeService adapter.SimpleLifecycle
|
acmeService adapter.SimpleLifecycle
|
||||||
@@ -34,22 +32,14 @@ type STDServerConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) ServerName() string {
|
func (c *STDServerConfig) ServerName() string {
|
||||||
c.access.RLock()
|
|
||||||
defer c.access.RUnlock()
|
|
||||||
return c.config.ServerName
|
return c.config.ServerName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) SetServerName(serverName string) {
|
func (c *STDServerConfig) SetServerName(serverName string) {
|
||||||
c.access.Lock()
|
c.config.ServerName = serverName
|
||||||
defer c.access.Unlock()
|
|
||||||
config := c.config.Clone()
|
|
||||||
config.ServerName = serverName
|
|
||||||
c.config = config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) NextProtos() []string {
|
func (c *STDServerConfig) NextProtos() []string {
|
||||||
c.access.RLock()
|
|
||||||
defer c.access.RUnlock()
|
|
||||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
||||||
return c.config.NextProtos[1:]
|
return c.config.NextProtos[1:]
|
||||||
} else {
|
} else {
|
||||||
@@ -58,15 +48,11 @@ func (c *STDServerConfig) NextProtos() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||||
c.access.Lock()
|
|
||||||
defer c.access.Unlock()
|
|
||||||
config := c.config.Clone()
|
|
||||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
||||||
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
c.config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
||||||
} else {
|
} else {
|
||||||
config.NextProtos = nextProto
|
c.config.NextProtos = nextProto
|
||||||
}
|
}
|
||||||
c.config = config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) Config() (*STDConfig, error) {
|
func (c *STDServerConfig) Config() (*STDConfig, error) {
|
||||||
@@ -91,6 +77,9 @@ func (c *STDServerConfig) Start() error {
|
|||||||
if c.acmeService != nil {
|
if c.acmeService != nil {
|
||||||
return c.acmeService.Start()
|
return c.acmeService.Start()
|
||||||
} else {
|
} else {
|
||||||
|
if c.certificatePath == "" && c.keyPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
err := c.startWatcher()
|
err := c.startWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Warn("create fsnotify watcher: ", err)
|
c.logger.Warn("create fsnotify watcher: ", err)
|
||||||
@@ -110,9 +99,6 @@ func (c *STDServerConfig) startWatcher() error {
|
|||||||
if c.echKeyPath != "" {
|
if c.echKeyPath != "" {
|
||||||
watchPath = append(watchPath, c.echKeyPath)
|
watchPath = append(watchPath, c.echKeyPath)
|
||||||
}
|
}
|
||||||
if len(watchPath) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||||
Path: watchPath,
|
Path: watchPath,
|
||||||
Callback: func(path string) {
|
Callback: func(path string) {
|
||||||
@@ -152,18 +138,10 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "reload key pair")
|
return E.Cause(err, "reload key pair")
|
||||||
}
|
}
|
||||||
c.access.Lock()
|
c.config.Certificates = []tls.Certificate{keyPair}
|
||||||
config := c.config.Clone()
|
|
||||||
config.Certificates = []tls.Certificate{keyPair}
|
|
||||||
c.config = config
|
|
||||||
c.access.Unlock()
|
|
||||||
c.logger.Info("reloaded TLS certificate")
|
c.logger.Info("reloaded TLS certificate")
|
||||||
} else if path == c.echKeyPath {
|
} else if path == c.echKeyPath {
|
||||||
echKey, err := os.ReadFile(c.echKeyPath)
|
err := reloadECHKeys(c.echKeyPath, c.config)
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "reload ECH keys from ", c.echKeyPath)
|
|
||||||
}
|
|
||||||
err = c.setECHServerConfig(echKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -284,7 +262,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
serverConfig := &STDServerConfig{
|
return &STDServerConfig{
|
||||||
config: tlsConfig,
|
config: tlsConfig,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
acmeService: acmeService,
|
acmeService: acmeService,
|
||||||
@@ -293,11 +271,5 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
|||||||
certificatePath: options.CertificatePath,
|
certificatePath: options.CertificatePath,
|
||||||
keyPath: options.KeyPath,
|
keyPath: options.KeyPath,
|
||||||
echKeyPath: echKeyPath,
|
echKeyPath: echKeyPath,
|
||||||
}
|
}, nil
|
||||||
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
|
||||||
serverConfig.access.Lock()
|
|
||||||
defer serverConfig.access.Unlock()
|
|
||||||
return serverConfig.config, nil
|
|
||||||
}
|
|
||||||
return serverConfig, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,15 +47,15 @@ func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory
|
|||||||
func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
delete(s.delayHistory, tag)
|
delete(s.delayHistory, tag)
|
||||||
s.notifyUpdated()
|
|
||||||
s.access.Unlock()
|
s.access.Unlock()
|
||||||
|
s.notifyUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
|
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
s.delayHistory[tag] = history
|
s.delayHistory[tag] = history
|
||||||
s.notifyUpdated()
|
|
||||||
s.access.Unlock()
|
s.access.Unlock()
|
||||||
|
s.notifyUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HistoryStorage) notifyUpdated() {
|
func (s *HistoryStorage) notifyUpdated() {
|
||||||
@@ -69,8 +69,6 @@ func (s *HistoryStorage) notifyUpdated() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *HistoryStorage) Close() error {
|
func (s *HistoryStorage) Close() error {
|
||||||
s.access.Lock()
|
|
||||||
defer s.access.Unlock()
|
|
||||||
s.updateHook = nil
|
s.updateHook = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ const (
|
|||||||
RuleSetVersion1 = 1 + iota
|
RuleSetVersion1 = 1 + iota
|
||||||
RuleSetVersion2
|
RuleSetVersion2
|
||||||
RuleSetVersion3
|
RuleSetVersion3
|
||||||
RuleSetVersionCurrent = RuleSetVersion3
|
RuleSetVersion4
|
||||||
|
RuleSetVersionCurrent = RuleSetVersion4
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
230
dns/client.go
230
dns/client.go
@@ -2,14 +2,12 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/compatible"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -19,7 +17,7 @@ import (
|
|||||||
"github.com/sagernet/sing/contrab/freelru"
|
"github.com/sagernet/sing/contrab/freelru"
|
||||||
"github.com/sagernet/sing/contrab/maphash"
|
"github.com/sagernet/sing/contrab/maphash"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
dns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -32,18 +30,16 @@ var (
|
|||||||
var _ adapter.DNSClient = (*Client)(nil)
|
var _ adapter.DNSClient = (*Client)(nil)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
disableCache bool
|
disableCache bool
|
||||||
disableExpire bool
|
disableExpire bool
|
||||||
independentCache bool
|
independentCache bool
|
||||||
clientSubnet netip.Prefix
|
clientSubnet netip.Prefix
|
||||||
rdrc adapter.RDRCStore
|
rdrc adapter.RDRCStore
|
||||||
initRDRCFunc func() adapter.RDRCStore
|
initRDRCFunc func() adapter.RDRCStore
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
cache freelru.Cache[dns.Question, *dns.Msg]
|
cache freelru.Cache[dns.Question, *dns.Msg]
|
||||||
cacheLock compatible.Map[dns.Question, chan struct{}]
|
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
|
||||||
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
|
|
||||||
transportCacheLock compatible.Map[dns.Question, chan struct{}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientOptions struct {
|
type ClientOptions struct {
|
||||||
@@ -95,34 +91,22 @@ func (c *Client) Start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
|
||||||
for _, record := range response.Ns {
|
|
||||||
if soa, isSOA := record.(*dns.SOA); isSOA {
|
|
||||||
soaTTL := soa.Header().Ttl
|
|
||||||
soaMinimum := soa.Minttl
|
|
||||||
if soaTTL < soaMinimum {
|
|
||||||
return soaTTL, true
|
|
||||||
}
|
|
||||||
return soaMinimum, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
|
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
|
||||||
if len(message.Question) == 0 {
|
if len(message.Question) == 0 {
|
||||||
if c.logger != nil {
|
if c.logger != nil {
|
||||||
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
||||||
}
|
}
|
||||||
return FixedResponseStatus(message, dns.RcodeFormatError), nil
|
responseMessage := dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: message.Id,
|
||||||
|
Response: true,
|
||||||
|
Rcode: dns.RcodeFormatError,
|
||||||
|
},
|
||||||
|
Question: message.Question,
|
||||||
|
}
|
||||||
|
return &responseMessage, nil
|
||||||
}
|
}
|
||||||
question := message.Question[0]
|
question := message.Question[0]
|
||||||
if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only {
|
|
||||||
if c.logger != nil {
|
|
||||||
c.logger.DebugContext(ctx, "strategy rejected")
|
|
||||||
}
|
|
||||||
return FixedResponseStatus(message, dns.RcodeSuccess), nil
|
|
||||||
}
|
|
||||||
clientSubnet := options.ClientSubnet
|
clientSubnet := options.ClientSubnet
|
||||||
if !clientSubnet.IsValid() {
|
if !clientSubnet.IsValid() {
|
||||||
clientSubnet = c.clientSubnet
|
clientSubnet = c.clientSubnet
|
||||||
@@ -130,46 +114,12 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
if clientSubnet.IsValid() {
|
if clientSubnet.IsValid() {
|
||||||
message = SetClientSubnet(message, clientSubnet)
|
message = SetClientSubnet(message, clientSubnet)
|
||||||
}
|
}
|
||||||
|
|
||||||
isSimpleRequest := len(message.Question) == 1 &&
|
isSimpleRequest := len(message.Question) == 1 &&
|
||||||
len(message.Ns) == 0 &&
|
len(message.Ns) == 0 &&
|
||||||
(len(message.Extra) == 0 || len(message.Extra) == 1 &&
|
len(message.Extra) == 0 &&
|
||||||
message.Extra[0].Header().Rrtype == dns.TypeOPT &&
|
|
||||||
message.Extra[0].Header().Class > 0 &&
|
|
||||||
message.Extra[0].Header().Ttl == 0 &&
|
|
||||||
len(message.Extra[0].(*dns.OPT).Option) == 0) &&
|
|
||||||
!options.ClientSubnet.IsValid()
|
!options.ClientSubnet.IsValid()
|
||||||
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
||||||
if !disableCache {
|
if !disableCache {
|
||||||
if c.cache != nil {
|
|
||||||
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
|
|
||||||
if loaded {
|
|
||||||
select {
|
|
||||||
case <-cond:
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
defer func() {
|
|
||||||
c.cacheLock.Delete(question)
|
|
||||||
close(cond)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
} else if c.transportCache != nil {
|
|
||||||
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
|
|
||||||
if loaded {
|
|
||||||
select {
|
|
||||||
case <-cond:
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
defer func() {
|
|
||||||
c.transportCacheLock.Delete(question)
|
|
||||||
close(cond)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response, ttl := c.loadResponse(question, transport)
|
response, ttl := c.loadResponse(question, transport)
|
||||||
if response != nil {
|
if response != nil {
|
||||||
logCachedResponse(c.logger, ctx, response, ttl)
|
logCachedResponse(c.logger, ctx, response, ttl)
|
||||||
@@ -177,14 +127,27 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only {
|
||||||
|
responseMessage := dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: message.Id,
|
||||||
|
Response: true,
|
||||||
|
Rcode: dns.RcodeSuccess,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{question},
|
||||||
|
}
|
||||||
|
if c.logger != nil {
|
||||||
|
c.logger.DebugContext(ctx, "strategy rejected")
|
||||||
|
}
|
||||||
|
return &responseMessage, nil
|
||||||
|
}
|
||||||
messageId := message.Id
|
messageId := message.Id
|
||||||
contextTransport, clientSubnetLoaded := transportTagFromContext(ctx)
|
contextTransport, clientSubnetLoaded := transportTagFromContext(ctx)
|
||||||
if clientSubnetLoaded && transport.Tag() == contextTransport {
|
if clientSubnetLoaded && transport.Tag() == contextTransport {
|
||||||
return nil, E.New("DNS query loopback in transport[", contextTransport, "]")
|
return nil, E.New("DNS query loopback in transport[", contextTransport, "]")
|
||||||
}
|
}
|
||||||
ctx = contextWithTransportTag(ctx, transport.Tag())
|
ctx = contextWithTransportTag(ctx, transport.Tag())
|
||||||
if !disableCache && responseChecker != nil && c.rdrc != nil {
|
if responseChecker != nil && c.rdrc != nil {
|
||||||
rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype)
|
rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype)
|
||||||
if rejected {
|
if rejected {
|
||||||
return nil, ErrResponseRejectedCached
|
return nil, ErrResponseRejectedCached
|
||||||
@@ -194,12 +157,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
response, err := transport.Exchange(ctx, message)
|
response, err := transport.Exchange(ctx, message)
|
||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var rcodeError RcodeError
|
return nil, err
|
||||||
if errors.As(err, &rcodeError) {
|
|
||||||
response = FixedResponseStatus(message, int(rcodeError))
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
|
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
|
||||||
validResponse := response
|
validResponse := response
|
||||||
@@ -236,19 +194,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
response.Answer = append(response.Answer, validResponse.Answer...)
|
response.Answer = append(response.Answer, validResponse.Answer...)
|
||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
|
|
||||||
if responseChecker != nil {
|
if responseChecker != nil {
|
||||||
var rejected bool
|
var rejected bool
|
||||||
// TODO: add accept_any rule and support to check response instead of addresses
|
if !(response.Rcode == dns.RcodeSuccess || response.Rcode == dns.RcodeNameError) {
|
||||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
|
||||||
rejected = true
|
rejected = true
|
||||||
} else if len(response.Answer) == 0 {
|
|
||||||
rejected = !responseChecker(nil)
|
|
||||||
} else {
|
} else {
|
||||||
rejected = !responseChecker(MessageToAddresses(response))
|
rejected = !responseChecker(MessageToAddresses(response))
|
||||||
}
|
}
|
||||||
if rejected {
|
if rejected {
|
||||||
if !disableCache && c.rdrc != nil {
|
if c.rdrc != nil {
|
||||||
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
|
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
|
||||||
}
|
}
|
||||||
logRejectedResponse(c.logger, ctx, response)
|
logRejectedResponse(c.logger, ctx, response)
|
||||||
@@ -275,17 +229,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var timeToLive uint32
|
var timeToLive uint32
|
||||||
if len(response.Answer) == 0 {
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
|
for _, record := range recordList {
|
||||||
timeToLive = soaTTL
|
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||||
}
|
timeToLive = record.Header().Ttl
|
||||||
}
|
|
||||||
if timeToLive == 0 {
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
|
||||||
timeToLive = record.Header().Ttl
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,7 +259,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
logExchangedResponse(c.logger, ctx, response, timeToLive)
|
logExchangedResponse(c.logger, ctx, response, timeToLive)
|
||||||
return response, nil
|
return response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||||
@@ -358,11 +305,70 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
|||||||
func (c *Client) ClearCache() {
|
func (c *Client) ClearCache() {
|
||||||
if c.cache != nil {
|
if c.cache != nil {
|
||||||
c.cache.Purge()
|
c.cache.Purge()
|
||||||
} else if c.transportCache != nil {
|
}
|
||||||
|
if c.transportCache != nil {
|
||||||
c.transportCache.Purge()
|
c.transportCache.Purge()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool) {
|
||||||
|
if c.disableCache || c.independentCache {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if dns.IsFqdn(domain) {
|
||||||
|
domain = domain[:len(domain)-1]
|
||||||
|
}
|
||||||
|
dnsName := dns.Fqdn(domain)
|
||||||
|
if strategy == C.DomainStrategyIPv4Only {
|
||||||
|
response, err := c.questionCache(dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}, nil)
|
||||||
|
if err != ErrNotCached {
|
||||||
|
return response, true
|
||||||
|
}
|
||||||
|
} else if strategy == C.DomainStrategyIPv6Only {
|
||||||
|
response, err := c.questionCache(dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeAAAA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}, nil)
|
||||||
|
if err != ErrNotCached {
|
||||||
|
return response, true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response4, _ := c.questionCache(dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}, nil)
|
||||||
|
response6, _ := c.questionCache(dns.Question{
|
||||||
|
Name: dnsName,
|
||||||
|
Qtype: dns.TypeAAAA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}, nil)
|
||||||
|
if len(response4) > 0 || len(response6) > 0 {
|
||||||
|
return sortAddresses(response4, response6, strategy), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool) {
|
||||||
|
if c.disableCache || c.independentCache || len(message.Question) != 1 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
question := message.Question[0]
|
||||||
|
response, ttl := c.loadResponse(question, nil)
|
||||||
|
if response == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
logCachedResponse(c.logger, ctx, response, ttl)
|
||||||
|
response.Id = message.Id
|
||||||
|
return response, true
|
||||||
|
}
|
||||||
|
|
||||||
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
|
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
|
||||||
if strategy == C.DomainStrategyPreferIPv6 {
|
if strategy == C.DomainStrategyPreferIPv6 {
|
||||||
return append(response6, response4...)
|
return append(response6, response4...)
|
||||||
@@ -384,15 +390,15 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
|
|||||||
transportTag: transport.Tag(),
|
transportTag: transport.Tag(),
|
||||||
}, message)
|
}, message)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !c.independentCache {
|
||||||
|
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
|
||||||
} else {
|
} else {
|
||||||
if !c.independentCache {
|
c.transportCache.AddWithLifetime(transportCacheKey{
|
||||||
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
|
Question: question,
|
||||||
} else {
|
transportTag: transport.Tag(),
|
||||||
c.transportCache.AddWithLifetime(transportCacheKey{
|
}, message, time.Second*time.Duration(timeToLive))
|
||||||
Question: question,
|
|
||||||
transportTag: transport.Tag(),
|
|
||||||
}, message, time.Second*time.Duration(timeToLive))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,9 +517,6 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MessageToAddresses(response *dns.Msg) []netip.Addr {
|
func MessageToAddresses(response *dns.Msg) []netip.Addr {
|
||||||
if response == nil || response.Rcode != dns.RcodeSuccess {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||||
for _, rawAnswer := range response.Answer {
|
for _, rawAnswer := range response.Answer {
|
||||||
switch answer := rawAnswer.(type) {
|
switch answer := rawAnswer.(type) {
|
||||||
@@ -558,12 +561,9 @@ func transportTagFromContext(ctx context.Context) (string, bool) {
|
|||||||
func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg {
|
func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg {
|
||||||
return &dns.Msg{
|
return &dns.Msg{
|
||||||
MsgHdr: dns.MsgHdr{
|
MsgHdr: dns.MsgHdr{
|
||||||
Id: message.Id,
|
Id: message.Id,
|
||||||
Response: true,
|
Rcode: rcode,
|
||||||
Authoritative: true,
|
Response: true,
|
||||||
RecursionDesired: true,
|
|
||||||
RecursionAvailable: true,
|
|
||||||
Rcode: rcode,
|
|
||||||
},
|
},
|
||||||
Question: message.Question,
|
Question: message.Question,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
|
|||||||
}
|
}
|
||||||
responseLen := response.Len()
|
responseLen := response.Len()
|
||||||
if responseLen > maxLen {
|
if responseLen > maxLen {
|
||||||
response = response.Copy()
|
copyResponse := *response
|
||||||
|
response = ©Response
|
||||||
response.Truncate(maxLen)
|
response.Truncate(maxLen)
|
||||||
}
|
}
|
||||||
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RcodeSuccess RcodeError = mDNS.RcodeSuccess
|
|
||||||
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
||||||
RcodeNameError RcodeError = mDNS.RcodeNameError
|
RcodeNameError RcodeError = mDNS.RcodeNameError
|
||||||
RcodeRefused RcodeError = mDNS.RcodeRefused
|
RcodeRefused RcodeError = mDNS.RcodeRefused
|
||||||
|
|||||||
191
dns/router.go
191
dns/router.go
@@ -214,88 +214,96 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
|||||||
}
|
}
|
||||||
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
||||||
var (
|
var (
|
||||||
response *mDNS.Msg
|
|
||||||
transport adapter.DNSTransport
|
transport adapter.DNSTransport
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
var metadata *adapter.InboundContext
|
response, cached := r.client.ExchangeCache(ctx, message)
|
||||||
ctx, metadata = adapter.ExtendContext(ctx)
|
if !cached {
|
||||||
metadata.Destination = M.Socksaddr{}
|
var metadata *adapter.InboundContext
|
||||||
metadata.QueryType = message.Question[0].Qtype
|
ctx, metadata = adapter.ExtendContext(ctx)
|
||||||
switch metadata.QueryType {
|
metadata.Destination = M.Socksaddr{}
|
||||||
case mDNS.TypeA:
|
metadata.QueryType = message.Question[0].Qtype
|
||||||
metadata.IPVersion = 4
|
switch metadata.QueryType {
|
||||||
case mDNS.TypeAAAA:
|
case mDNS.TypeA:
|
||||||
metadata.IPVersion = 6
|
metadata.IPVersion = 4
|
||||||
}
|
case mDNS.TypeAAAA:
|
||||||
metadata.Domain = FqdnToDomain(message.Question[0].Name)
|
metadata.IPVersion = 6
|
||||||
if options.Transport != nil {
|
}
|
||||||
transport = options.Transport
|
metadata.Domain = FqdnToDomain(message.Question[0].Name)
|
||||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
if options.Transport != nil {
|
||||||
|
transport = options.Transport
|
||||||
|
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||||
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
|
options.Strategy = legacyTransport.LegacyStrategy()
|
||||||
|
}
|
||||||
|
if !options.ClientSubnet.IsValid() {
|
||||||
|
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||||
|
}
|
||||||
|
}
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
options.Strategy = legacyTransport.LegacyStrategy()
|
options.Strategy = r.defaultDomainStrategy
|
||||||
}
|
}
|
||||||
if !options.ClientSubnet.IsValid() {
|
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
||||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
} else {
|
||||||
}
|
var (
|
||||||
}
|
rule adapter.DNSRule
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
ruleIndex int
|
||||||
options.Strategy = r.defaultDomainStrategy
|
)
|
||||||
}
|
ruleIndex = -1
|
||||||
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
for {
|
||||||
} else {
|
dnsCtx := adapter.OverrideContext(ctx)
|
||||||
var (
|
dnsOptions := options
|
||||||
rule adapter.DNSRule
|
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
||||||
ruleIndex int
|
if rule != nil {
|
||||||
)
|
switch action := rule.Action().(type) {
|
||||||
ruleIndex = -1
|
case *R.RuleActionReject:
|
||||||
for {
|
switch action.Method {
|
||||||
dnsCtx := adapter.OverrideContext(ctx)
|
case C.RuleActionRejectMethodDefault:
|
||||||
dnsOptions := options
|
return &mDNS.Msg{
|
||||||
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
MsgHdr: mDNS.MsgHdr{
|
||||||
if rule != nil {
|
Id: message.Id,
|
||||||
switch action := rule.Action().(type) {
|
Rcode: mDNS.RcodeRefused,
|
||||||
case *R.RuleActionReject:
|
Response: true,
|
||||||
switch action.Method {
|
},
|
||||||
case C.RuleActionRejectMethodDefault:
|
Question: []mDNS.Question{message.Question[0]},
|
||||||
return &mDNS.Msg{
|
}, nil
|
||||||
MsgHdr: mDNS.MsgHdr{
|
case C.RuleActionRejectMethodDrop:
|
||||||
Id: message.Id,
|
return nil, tun.ErrDrop
|
||||||
Rcode: mDNS.RcodeRefused,
|
}
|
||||||
Response: true,
|
case *R.RuleActionPredefined:
|
||||||
},
|
return action.Response(message), nil
|
||||||
Question: []mDNS.Question{message.Question[0]},
|
|
||||||
}, nil
|
|
||||||
case C.RuleActionRejectMethodDrop:
|
|
||||||
return nil, tun.ErrDrop
|
|
||||||
}
|
}
|
||||||
case *R.RuleActionPredefined:
|
|
||||||
return action.Response(message), nil
|
|
||||||
}
|
}
|
||||||
}
|
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||||
responseCheck := addressLimitResponseCheck(rule, metadata)
|
if rule != nil && rule.WithAddressLimit() {
|
||||||
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
responseCheck = func(responseAddrs []netip.Addr) bool {
|
||||||
dnsOptions.Strategy = r.defaultDomainStrategy
|
metadata.DestinationAddresses = responseAddrs
|
||||||
}
|
return rule.MatchAddressLimit(metadata)
|
||||||
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
|
}
|
||||||
var rejected bool
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, ErrResponseRejectedCached) {
|
|
||||||
rejected = true
|
|
||||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
|
|
||||||
} else if errors.Is(err, ErrResponseRejected) {
|
|
||||||
rejected = true
|
|
||||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
|
||||||
} else if len(message.Question) > 0 {
|
|
||||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
|
||||||
} else {
|
|
||||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
|
|
||||||
}
|
}
|
||||||
|
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||||
|
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||||
|
}
|
||||||
|
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
|
||||||
|
var rejected bool
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrResponseRejectedCached) {
|
||||||
|
rejected = true
|
||||||
|
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
|
||||||
|
} else if errors.Is(err, ErrResponseRejected) {
|
||||||
|
rejected = true
|
||||||
|
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
||||||
|
} else if len(message.Question) > 0 {
|
||||||
|
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
||||||
|
} else {
|
||||||
|
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if responseCheck != nil && rejected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if responseCheck != nil && rejected {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -319,6 +327,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
|||||||
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||||
var (
|
var (
|
||||||
responseAddrs []netip.Addr
|
responseAddrs []netip.Addr
|
||||||
|
cached bool
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
printResult := func() {
|
printResult := func() {
|
||||||
@@ -338,6 +347,13 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
|||||||
err = E.Cause(err, "lookup ", domain)
|
err = E.Cause(err, "lookup ", domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
|
||||||
|
if cached {
|
||||||
|
if len(responseAddrs) == 0 {
|
||||||
|
return nil, E.New("lookup ", domain, ": empty result (cached)")
|
||||||
|
}
|
||||||
|
return responseAddrs, nil
|
||||||
|
}
|
||||||
r.logger.DebugContext(ctx, "lookup domain ", domain)
|
r.logger.DebugContext(ctx, "lookup domain ", domain)
|
||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
metadata.Destination = M.Socksaddr{}
|
metadata.Destination = M.Socksaddr{}
|
||||||
@@ -370,13 +386,16 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
|||||||
if rule != nil {
|
if rule != nil {
|
||||||
switch action := rule.Action().(type) {
|
switch action := rule.Action().(type) {
|
||||||
case *R.RuleActionReject:
|
case *R.RuleActionReject:
|
||||||
return nil, &R.RejectedError{Cause: action.Error(ctx)}
|
switch action.Method {
|
||||||
|
case C.RuleActionRejectMethodDefault:
|
||||||
|
return nil, nil
|
||||||
|
case C.RuleActionRejectMethodDrop:
|
||||||
|
return nil, tun.ErrDrop
|
||||||
|
}
|
||||||
case *R.RuleActionPredefined:
|
case *R.RuleActionPredefined:
|
||||||
responseAddrs = nil
|
|
||||||
if action.Rcode != mDNS.RcodeSuccess {
|
if action.Rcode != mDNS.RcodeSuccess {
|
||||||
err = RcodeError(action.Rcode)
|
err = RcodeError(action.Rcode)
|
||||||
} else {
|
} else {
|
||||||
err = nil
|
|
||||||
for _, answer := range action.Answer {
|
for _, answer := range action.Answer {
|
||||||
switch record := answer.(type) {
|
switch record := answer.(type) {
|
||||||
case *mDNS.A:
|
case *mDNS.A:
|
||||||
@@ -389,7 +408,13 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
|||||||
goto response
|
goto response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
responseCheck := addressLimitResponseCheck(rule, metadata)
|
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||||
|
if rule != nil && rule.WithAddressLimit() {
|
||||||
|
responseCheck = func(responseAddrs []netip.Addr) bool {
|
||||||
|
metadata.DestinationAddresses = responseAddrs
|
||||||
|
return rule.MatchAddressLimit(metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||||
dnsOptions.Strategy = r.defaultDomainStrategy
|
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||||
}
|
}
|
||||||
@@ -417,18 +442,6 @@ func isAddressQuery(message *mDNS.Msg) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(responseAddrs []netip.Addr) bool {
|
|
||||||
if rule == nil || !rule.WithAddressLimit() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
responseMetadata := *metadata
|
|
||||||
return func(responseAddrs []netip.Addr) bool {
|
|
||||||
checkMetadata := responseMetadata
|
|
||||||
checkMetadata.DestinationAddresses = responseAddrs
|
|
||||||
return rule.MatchAddressLimit(&checkMetadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) ClearCache() {
|
func (r *Router) ClearCache() {
|
||||||
r.client.ClearCache()
|
r.client.ClearCache()
|
||||||
if r.platformInterface != nil {
|
if r.platformInterface != nil {
|
||||||
|
|||||||
@@ -2,13 +2,10 @@ package dhcp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -198,17 +195,7 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *control.Interface)
|
|||||||
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
||||||
listenAddr = "255.255.255.255:68"
|
listenAddr = "255.255.255.255:68"
|
||||||
}
|
}
|
||||||
var (
|
packetConn, err := listener.ListenPacket(t.ctx, "udp4", listenAddr)
|
||||||
packetConn net.PacketConn
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
packetConn, err = listener.ListenPacket(t.ctx, "udp4", listenAddr)
|
|
||||||
if err == nil || !errors.Is(err, syscall.EADDRINUSE) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -243,12 +230,8 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
|
|||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
buffer.Reset()
|
|
||||||
_, _, err := buffer.ReadPacketFrom(packetConn)
|
_, _, err := buffer.ReadPacketFrom(packetConn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, io.ErrShortBuffer) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ package dhcp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"time"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/dns"
|
"github.com/sagernet/sing-box/dns"
|
||||||
"github.com/sagernet/sing-box/dns/transport"
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
@@ -17,6 +16,11 @@ import (
|
|||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// net.maxDNSPacketSize
|
||||||
|
maxDNSPacketSize = 1232
|
||||||
|
)
|
||||||
|
|
||||||
func (t *Transport) exchangeSingleRequest(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
func (t *Transport) exchangeSingleRequest(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for _, fqdn := range t.nameList(domain) {
|
for _, fqdn := range t.nameList(domain) {
|
||||||
@@ -44,7 +48,7 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr,
|
|||||||
if response.Rcode != mDNS.RcodeSuccess {
|
if response.Rcode != mDNS.RcodeSuccess {
|
||||||
err = dns.RcodeError(response.Rcode)
|
err = dns.RcodeError(response.Rcode)
|
||||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||||
err = dns.RcodeSuccess
|
err = E.New(fqdn, ": empty result")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
@@ -84,7 +88,7 @@ func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn
|
|||||||
server := servers[j]
|
server := servers[j]
|
||||||
question := message.Question[0]
|
question := message.Question[0]
|
||||||
question.Name = fqdn
|
question.Name = fqdn
|
||||||
response, err := t.exchangeOne(ctx, server, question)
|
response, err := t.exchangeOne(ctx, server, question, C.DNSTimeout, false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lastErr = err
|
lastErr = err
|
||||||
continue
|
continue
|
||||||
@@ -95,77 +99,62 @@ func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn
|
|||||||
return nil, E.Cause(lastErr, fqdn)
|
return nil, E.Cause(lastErr, fqdn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question) (*mDNS.Msg, error) {
|
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) {
|
||||||
if server.Port == 0 {
|
if server.Port == 0 {
|
||||||
server.Port = 53
|
server.Port = 53
|
||||||
}
|
}
|
||||||
|
var networks []string
|
||||||
|
if useTCP {
|
||||||
|
networks = []string{N.NetworkTCP}
|
||||||
|
} else {
|
||||||
|
networks = []string{N.NetworkUDP, N.NetworkTCP}
|
||||||
|
}
|
||||||
request := &mDNS.Msg{
|
request := &mDNS.Msg{
|
||||||
MsgHdr: mDNS.MsgHdr{
|
MsgHdr: mDNS.MsgHdr{
|
||||||
Id: uint16(rand.Uint32()),
|
Id: uint16(rand.Uint32()),
|
||||||
RecursionDesired: true,
|
RecursionDesired: true,
|
||||||
AuthenticatedData: true,
|
AuthenticatedData: ad,
|
||||||
},
|
},
|
||||||
Question: []mDNS.Question{question},
|
Question: []mDNS.Question{question},
|
||||||
Compress: true,
|
Compress: true,
|
||||||
}
|
}
|
||||||
request.SetEdns0(buf.UDPBufferSize, false)
|
request.SetEdns0(maxDNSPacketSize, false)
|
||||||
return t.exchangeUDP(ctx, server, request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
|
||||||
conn.SetDeadline(deadline)
|
|
||||||
}
|
|
||||||
buffer := buf.Get(buf.UDPBufferSize)
|
buffer := buf.Get(buf.UDPBufferSize)
|
||||||
defer buf.Put(buffer)
|
defer buf.Put(buffer)
|
||||||
rawMessage, err := request.PackBuffer(buffer)
|
for _, network := range networks {
|
||||||
if err != nil {
|
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
|
||||||
return nil, E.Cause(err, "pack request")
|
defer cancel()
|
||||||
}
|
conn, err := t.dialer.DialContext(ctx, network, server)
|
||||||
_, err = conn.Write(rawMessage)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
if errors.Is(err, syscall.EMSGSIZE) {
|
|
||||||
return t.exchangeTCP(ctx, server, request)
|
|
||||||
}
|
}
|
||||||
return nil, E.Cause(err, "write request")
|
defer conn.Close()
|
||||||
}
|
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||||
n, err := conn.Read(buffer)
|
conn.SetDeadline(deadline)
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, syscall.EMSGSIZE) {
|
|
||||||
return t.exchangeTCP(ctx, server, request)
|
|
||||||
}
|
}
|
||||||
return nil, E.Cause(err, "read response")
|
rawMessage, err := request.PackBuffer(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "pack request")
|
||||||
|
}
|
||||||
|
_, err = conn.Write(rawMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "write request")
|
||||||
|
}
|
||||||
|
n, err := conn.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read response")
|
||||||
|
}
|
||||||
|
var response mDNS.Msg
|
||||||
|
err = response.Unpack(buffer[:n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "unpack response")
|
||||||
|
}
|
||||||
|
if response.Truncated && network == N.NetworkUDP {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
}
|
}
|
||||||
var response mDNS.Msg
|
panic("unexpected")
|
||||||
err = response.Unpack(buffer[:n])
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "unpack response")
|
|
||||||
}
|
|
||||||
if response.Truncated {
|
|
||||||
return t.exchangeTCP(ctx, server, request)
|
|
||||||
}
|
|
||||||
return &response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
|
||||||
conn.SetDeadline(deadline)
|
|
||||||
}
|
|
||||||
err = transport.WriteMessage(conn, 0, request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return transport.ReadMessage(conn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) nameList(name string) []string {
|
func (t *Transport) nameList(name string) []string {
|
||||||
|
|||||||
@@ -17,43 +17,18 @@ type Store struct {
|
|||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
inet4Range netip.Prefix
|
inet4Range netip.Prefix
|
||||||
inet6Range netip.Prefix
|
inet6Range netip.Prefix
|
||||||
inet4Last netip.Addr
|
|
||||||
inet6Last netip.Addr
|
|
||||||
storage adapter.FakeIPStorage
|
storage adapter.FakeIPStorage
|
||||||
inet4Current netip.Addr
|
inet4Current netip.Addr
|
||||||
inet6Current netip.Addr
|
inet6Current netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store {
|
func NewStore(ctx context.Context, logger logger.Logger, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store {
|
||||||
store := &Store{
|
return &Store{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
inet4Range: inet4Range,
|
inet4Range: inet4Range,
|
||||||
inet6Range: inet6Range,
|
inet6Range: inet6Range,
|
||||||
}
|
}
|
||||||
if inet4Range.IsValid() {
|
|
||||||
store.inet4Last = broadcastAddress(inet4Range)
|
|
||||||
}
|
|
||||||
if inet6Range.IsValid() {
|
|
||||||
store.inet6Last = broadcastAddress(inet6Range)
|
|
||||||
}
|
|
||||||
return store
|
|
||||||
}
|
|
||||||
|
|
||||||
func broadcastAddress(prefix netip.Prefix) netip.Addr {
|
|
||||||
addr := prefix.Addr()
|
|
||||||
raw := addr.As16()
|
|
||||||
bits := prefix.Bits()
|
|
||||||
if addr.Is4() {
|
|
||||||
bits += 96
|
|
||||||
}
|
|
||||||
for i := bits; i < 128; i++ {
|
|
||||||
raw[i/8] |= 1 << (7 - i%8)
|
|
||||||
}
|
|
||||||
if addr.Is4() {
|
|
||||||
return netip.AddrFrom4([4]byte(raw[12:]))
|
|
||||||
}
|
|
||||||
return netip.AddrFrom16(raw)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) Start() error {
|
func (s *Store) Start() error {
|
||||||
@@ -71,10 +46,10 @@ func (s *Store) Start() error {
|
|||||||
s.inet6Current = metadata.Inet6Current
|
s.inet6Current = metadata.Inet6Current
|
||||||
} else {
|
} else {
|
||||||
if s.inet4Range.IsValid() {
|
if s.inet4Range.IsValid() {
|
||||||
s.inet4Current = s.inet4Range.Addr().Next()
|
s.inet4Current = s.inet4Range.Addr().Next().Next()
|
||||||
}
|
}
|
||||||
if s.inet6Range.IsValid() {
|
if s.inet6Range.IsValid() {
|
||||||
s.inet6Current = s.inet6Range.Addr().Next()
|
s.inet6Current = s.inet6Range.Addr().Next().Next()
|
||||||
}
|
}
|
||||||
_ = storage.FakeIPReset()
|
_ = storage.FakeIPReset()
|
||||||
}
|
}
|
||||||
@@ -108,7 +83,7 @@ func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
|
|||||||
return netip.Addr{}, E.New("missing IPv4 fakeip address range")
|
return netip.Addr{}, E.New("missing IPv4 fakeip address range")
|
||||||
}
|
}
|
||||||
nextAddress := s.inet4Current.Next()
|
nextAddress := s.inet4Current.Next()
|
||||||
if nextAddress == s.inet4Last || !s.inet4Range.Contains(nextAddress) {
|
if !s.inet4Range.Contains(nextAddress) {
|
||||||
nextAddress = s.inet4Range.Addr().Next().Next()
|
nextAddress = s.inet4Range.Addr().Next().Next()
|
||||||
}
|
}
|
||||||
s.inet4Current = nextAddress
|
s.inet4Current = nextAddress
|
||||||
@@ -118,7 +93,7 @@ func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
|
|||||||
return netip.Addr{}, E.New("missing IPv6 fakeip address range")
|
return netip.Addr{}, E.New("missing IPv6 fakeip address range")
|
||||||
}
|
}
|
||||||
nextAddress := s.inet6Current.Next()
|
nextAddress := s.inet6Current.Next()
|
||||||
if nextAddress == s.inet6Last || !s.inet6Range.Contains(nextAddress) {
|
if !s.inet6Range.Contains(nextAddress) {
|
||||||
nextAddress = s.inet6Range.Addr().Next().Next()
|
nextAddress = s.inet6Range.Addr().Next().Next()
|
||||||
}
|
}
|
||||||
s.inet6Current = nextAddress
|
s.inet6Current = nextAddress
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -25,6 +26,7 @@ import (
|
|||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
@@ -46,7 +48,7 @@ type HTTPSTransport struct {
|
|||||||
destination *url.URL
|
destination *url.URL
|
||||||
headers http.Header
|
headers http.Header
|
||||||
transportAccess sync.Mutex
|
transportAccess sync.Mutex
|
||||||
transport *HTTPSTransportWrapper
|
transport *http.Transport
|
||||||
transportResetAt time.Time
|
transportResetAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +63,11 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(tlsConfig.NextProtos()) == 0 {
|
if common.Error(tlsConfig.Config()) == nil && !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
|
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), http2.NextProtoTLS))
|
||||||
|
}
|
||||||
|
if !common.Contains(tlsConfig.NextProtos(), "http/1.1") {
|
||||||
|
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), "http/1.1"))
|
||||||
}
|
}
|
||||||
headers := options.Headers.Build()
|
headers := options.Headers.Build()
|
||||||
host := headers.Get("Host")
|
host := headers.Get("Host")
|
||||||
@@ -120,13 +125,37 @@ func NewHTTPSRaw(
|
|||||||
serverAddr M.Socksaddr,
|
serverAddr M.Socksaddr,
|
||||||
tlsConfig tls.Config,
|
tlsConfig tls.Config,
|
||||||
) *HTTPSTransport {
|
) *HTTPSTransport {
|
||||||
|
var transport *http.Transport
|
||||||
|
if tlsConfig != nil {
|
||||||
|
transport = &http.Transport{
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
tcpConn, hErr := dialer.DialContext(ctx, network, serverAddr)
|
||||||
|
if hErr != nil {
|
||||||
|
return nil, hErr
|
||||||
|
}
|
||||||
|
tlsConn, hErr := aTLS.ClientHandshake(ctx, tcpConn, tlsConfig)
|
||||||
|
if hErr != nil {
|
||||||
|
tcpConn.Close()
|
||||||
|
return nil, hErr
|
||||||
|
}
|
||||||
|
return tlsConn, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
transport = &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, network, serverAddr)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
return &HTTPSTransport{
|
return &HTTPSTransport{
|
||||||
TransportAdapter: adapter,
|
TransportAdapter: adapter,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
destination: destination,
|
destination: destination,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
|
transport: transport,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +178,7 @@ func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
|||||||
startAt := time.Now()
|
startAt := time.Now()
|
||||||
response, err := t.exchange(ctx, message)
|
response, err := t.exchange(ctx, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
t.transportAccess.Lock()
|
t.transportAccess.Lock()
|
||||||
defer t.transportAccess.Unlock()
|
defer t.transportAccess.Unlock()
|
||||||
if t.transportResetAt.After(startAt) {
|
if t.transportResetAt.After(startAt) {
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errFallback = E.New("fallback to HTTP/1.1")
|
|
||||||
|
|
||||||
type HTTPSTransportWrapper struct {
|
|
||||||
http2Transport *http2.Transport
|
|
||||||
httpTransport *http.Transport
|
|
||||||
fallback *atomic.Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper {
|
|
||||||
var fallback atomic.Bool
|
|
||||||
return &HTTPSTransportWrapper{
|
|
||||||
http2Transport: &http2.Transport{
|
|
||||||
DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) {
|
|
||||||
tlsConn, err := dialer.DialTLSContext(ctx, serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
state := tlsConn.ConnectionState()
|
|
||||||
if state.NegotiatedProtocol == http2.NextProtoTLS {
|
|
||||||
return tlsConn, nil
|
|
||||||
}
|
|
||||||
tlsConn.Close()
|
|
||||||
fallback.Store(true)
|
|
||||||
return nil, errFallback
|
|
||||||
},
|
|
||||||
},
|
|
||||||
httpTransport: &http.Transport{
|
|
||||||
DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
||||||
return dialer.DialTLSContext(ctx, serverAddr)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fallback: &fallback,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
if h.fallback.Load() {
|
|
||||||
return h.httpTransport.RoundTrip(request)
|
|
||||||
} else {
|
|
||||||
response, err := h.http2Transport.RoundTrip(request)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, errFallback) {
|
|
||||||
return h.httpTransport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
|
|
||||||
h.http2Transport.CloseIdleConnections()
|
|
||||||
h.httpTransport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
|
|
||||||
return &HTTPSTransportWrapper{
|
|
||||||
httpTransport: h.httpTransport,
|
|
||||||
http2Transport: &http2.Transport{
|
|
||||||
DialTLSContext: h.http2Transport.DialTLSContext,
|
|
||||||
},
|
|
||||||
fallback: h.fallback,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,37 @@
|
|||||||
|
//go:build !darwin
|
||||||
|
|
||||||
package local
|
package local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"math/rand"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/dns"
|
"github.com/sagernet/sing-box/dns"
|
||||||
"github.com/sagernet/sing-box/dns/transport"
|
|
||||||
"github.com/sagernet/sing-box/dns/transport/hosts"
|
"github.com/sagernet/sing-box/dns/transport/hosts"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
"github.com/sagernet/sing/common/logger"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||||
|
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport)
|
||||||
|
}
|
||||||
|
|
||||||
var _ adapter.DNSTransport = (*Transport)(nil)
|
var _ adapter.DNSTransport = (*Transport)(nil)
|
||||||
|
|
||||||
type Transport struct {
|
type Transport struct {
|
||||||
dns.TransportAdapter
|
dns.TransportAdapter
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
hosts *hosts.File
|
logger logger.ContextLogger
|
||||||
dialer N.Dialer
|
hosts *hosts.File
|
||||||
|
dialer N.Dialer
|
||||||
|
preferGo bool
|
||||||
|
resolved ResolvedResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
|
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
@@ -39,195 +42,52 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
|||||||
return &Transport{
|
return &Transport{
|
||||||
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
|
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
logger: logger,
|
||||||
hosts: hosts.NewFile(hosts.DefaultPath),
|
hosts: hosts.NewFile(hosts.DefaultPath),
|
||||||
dialer: transportDialer,
|
dialer: transportDialer,
|
||||||
|
preferGo: options.PreferGo,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||||
|
switch stage {
|
||||||
|
case adapter.StartStateInitialize:
|
||||||
|
if !t.preferGo {
|
||||||
|
resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger)
|
||||||
|
if err == nil {
|
||||||
|
err = resolvedResolver.Start()
|
||||||
|
if err == nil {
|
||||||
|
t.resolved = resolvedResolver
|
||||||
|
} else {
|
||||||
|
t.logger.Warn(E.Cause(err, "initialize resolved resolver"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Close() error {
|
func (t *Transport) Close() error {
|
||||||
|
if t.resolved != nil {
|
||||||
|
return t.resolved.Close()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
if t.resolved != nil {
|
||||||
|
resolverObject := t.resolved.Object()
|
||||||
|
if resolverObject != nil {
|
||||||
|
return t.resolved.Exchange(resolverObject, ctx, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
question := message.Question[0]
|
question := message.Question[0]
|
||||||
|
domain := dns.FqdnToDomain(question.Name)
|
||||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||||
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
|
addresses := t.hosts.Lookup(domain)
|
||||||
if len(addresses) > 0 {
|
if len(addresses) > 0 {
|
||||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
systemConfig := getSystemDNSConfig(t.ctx)
|
return t.exchange(ctx, message, domain)
|
||||||
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
|
|
||||||
return t.exchangeSingleRequest(ctx, systemConfig, message, question.Name)
|
|
||||||
} else {
|
|
||||||
return t.exchangeParallel(ctx, systemConfig, message, question.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
|
||||||
var lastErr error
|
|
||||||
for _, fqdn := range systemConfig.nameList(domain) {
|
|
||||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
return nil, lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
|
||||||
returned := make(chan struct{})
|
|
||||||
defer close(returned)
|
|
||||||
type queryResult struct {
|
|
||||||
response *mDNS.Msg
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
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 = dns.RcodeSuccess
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case results <- queryResult{response, err}:
|
|
||||||
case <-returned:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
queryCtx, queryCancel := context.WithCancel(ctx)
|
|
||||||
defer queryCancel()
|
|
||||||
var nameCount int
|
|
||||||
for _, fqdn := range systemConfig.nameList(domain) {
|
|
||||||
nameCount++
|
|
||||||
go startRacer(queryCtx, fqdn)
|
|
||||||
}
|
|
||||||
var errors []error
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
case result := <-results:
|
|
||||||
if result.err == nil {
|
|
||||||
return result.response, nil
|
|
||||||
}
|
|
||||||
errors = append(errors, result.err)
|
|
||||||
if len(errors) == nameCount {
|
|
||||||
return nil, E.Errors(errors...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
serverOffset := config.serverOffset()
|
|
||||||
sLen := uint32(len(config.servers))
|
|
||||||
var lastErr error
|
|
||||||
for i := 0; i < config.attempts; i++ {
|
|
||||||
for j := uint32(0); j < sLen; j++ {
|
|
||||||
server := config.servers[(serverOffset+j)%sLen]
|
|
||||||
question := message.Question[0]
|
|
||||||
question.Name = fqdn
|
|
||||||
response, err := t.exchangeOne(ctx, M.ParseSocksaddr(server), question, config.timeout, config.useTCP, config.trustAD)
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, E.Cause(lastErr, fqdn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) {
|
|
||||||
if server.Port == 0 {
|
|
||||||
server.Port = 53
|
|
||||||
}
|
|
||||||
request := &mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
Id: uint16(rand.Uint32()),
|
|
||||||
RecursionDesired: true,
|
|
||||||
AuthenticatedData: ad,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{question},
|
|
||||||
Compress: true,
|
|
||||||
}
|
|
||||||
request.SetEdns0(buf.UDPBufferSize, false)
|
|
||||||
if !useTCP {
|
|
||||||
return t.exchangeUDP(ctx, server, request, timeout)
|
|
||||||
} else {
|
|
||||||
return t.exchangeTCP(ctx, server, request, timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
|
|
||||||
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
|
||||||
newDeadline := time.Now().Add(timeout)
|
|
||||||
if deadline.After(newDeadline) {
|
|
||||||
deadline = newDeadline
|
|
||||||
}
|
|
||||||
conn.SetDeadline(deadline)
|
|
||||||
}
|
|
||||||
buffer := buf.Get(buf.UDPBufferSize)
|
|
||||||
defer buf.Put(buffer)
|
|
||||||
rawMessage, err := request.PackBuffer(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "pack request")
|
|
||||||
}
|
|
||||||
_, err = conn.Write(rawMessage)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, syscall.EMSGSIZE) {
|
|
||||||
return t.exchangeTCP(ctx, server, request, timeout)
|
|
||||||
}
|
|
||||||
return nil, E.Cause(err, "write request")
|
|
||||||
}
|
|
||||||
n, err := conn.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, syscall.EMSGSIZE) {
|
|
||||||
return t.exchangeTCP(ctx, server, request, timeout)
|
|
||||||
}
|
|
||||||
return nil, E.Cause(err, "read response")
|
|
||||||
}
|
|
||||||
var response mDNS.Msg
|
|
||||||
err = response.Unpack(buffer[:n])
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "unpack response")
|
|
||||||
}
|
|
||||||
if response.Truncated {
|
|
||||||
return t.exchangeTCP(ctx, server, request, timeout)
|
|
||||||
}
|
|
||||||
return &response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
|
|
||||||
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
|
||||||
newDeadline := time.Now().Add(timeout)
|
|
||||||
if deadline.After(newDeadline) {
|
|
||||||
deadline = newDeadline
|
|
||||||
}
|
|
||||||
conn.SetDeadline(deadline)
|
|
||||||
}
|
|
||||||
err = transport.WriteMessage(conn, 0, request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return transport.ReadMessage(conn)
|
|
||||||
}
|
}
|
||||||
|
|||||||
142
dns/transport/local/local_darwin.go
Normal file
142
dns/transport/local/local_darwin.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport/hosts"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||||
|
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.DNSTransport = (*Transport)(nil)
|
||||||
|
|
||||||
|
type Transport struct {
|
||||||
|
dns.TransportAdapter
|
||||||
|
ctx context.Context
|
||||||
|
logger logger.ContextLogger
|
||||||
|
hosts *hosts.File
|
||||||
|
dialer N.Dialer
|
||||||
|
preferGo bool
|
||||||
|
fallback bool
|
||||||
|
dhcpTransport dhcpTransport
|
||||||
|
resolver net.Resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
type dhcpTransport interface {
|
||||||
|
adapter.DNSTransport
|
||||||
|
Fetch() ([]M.Socksaddr, error)
|
||||||
|
Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
|
transportDialer, err := dns.NewLocalDialer(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
transportAdapter := dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options)
|
||||||
|
return &Transport{
|
||||||
|
TransportAdapter: transportAdapter,
|
||||||
|
ctx: ctx,
|
||||||
|
logger: logger,
|
||||||
|
hosts: hosts.NewFile(hosts.DefaultPath),
|
||||||
|
dialer: transportDialer,
|
||||||
|
preferGo: options.PreferGo,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStateStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
inboundManager := service.FromContext[adapter.InboundManager](t.ctx)
|
||||||
|
for _, inbound := range inboundManager.Inbounds() {
|
||||||
|
if inbound.Type() == C.TypeTun {
|
||||||
|
t.fallback = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !C.IsIos {
|
||||||
|
if t.fallback {
|
||||||
|
t.dhcpTransport = newDHCPTransport(t.TransportAdapter, log.ContextWithOverrideLevel(t.ctx, log.LevelDebug), t.dialer, t.logger)
|
||||||
|
if t.dhcpTransport != nil {
|
||||||
|
err := t.dhcpTransport.Start(stage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Close() error {
|
||||||
|
return common.Close(
|
||||||
|
t.dhcpTransport,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
question := message.Question[0]
|
||||||
|
domain := dns.FqdnToDomain(question.Name)
|
||||||
|
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||||
|
addresses := t.hosts.Lookup(domain)
|
||||||
|
if len(addresses) > 0 {
|
||||||
|
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !t.fallback {
|
||||||
|
return t.exchange(ctx, message, domain)
|
||||||
|
}
|
||||||
|
if !C.IsIos {
|
||||||
|
if t.dhcpTransport != nil {
|
||||||
|
dhcpTransports, _ := t.dhcpTransport.Fetch()
|
||||||
|
if len(dhcpTransports) > 0 {
|
||||||
|
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.preferGo {
|
||||||
|
// Assuming the user knows what they are doing, we still execute the query which will fail.
|
||||||
|
return t.exchange(ctx, message, domain)
|
||||||
|
}
|
||||||
|
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||||
|
var network string
|
||||||
|
if question.Qtype == mDNS.TypeA {
|
||||||
|
network = "ip4"
|
||||||
|
} else {
|
||||||
|
network = "ip6"
|
||||||
|
}
|
||||||
|
addresses, err := t.resolver.LookupNetIP(ctx, network, domain)
|
||||||
|
if err != nil {
|
||||||
|
var dnsError *net.DNSError
|
||||||
|
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||||
|
return nil, dns.RcodeRefused
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||||
|
}
|
||||||
|
if C.IsIos {
|
||||||
|
return nil, E.New("only A and AAAA queries are supported on iOS and tvOS when using NetworkExtension.")
|
||||||
|
} else {
|
||||||
|
return nil, E.New("only A and AAAA queries are supported on macOS when using NetworkExtension and DHCP unavailable.")
|
||||||
|
}
|
||||||
|
}
|
||||||
16
dns/transport/local/local_darwin_dhcp.go
Normal file
16
dns/transport/local/local_darwin_dhcp.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build darwin && with_dhcp
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport/dhcp"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport {
|
||||||
|
return dhcp.NewRawTransport(transportAdapter, ctx, dialer, logger)
|
||||||
|
}
|
||||||
15
dns/transport/local/local_darwin_nodhcp.go
Normal file
15
dns/transport/local/local_darwin_nodhcp.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build darwin && !with_dhcp
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newDHCPTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) dhcpTransport {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
|
||||||
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewFallbackTransport)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FallbackTransport struct {
|
|
||||||
adapter.DNSTransport
|
|
||||||
ctx context.Context
|
|
||||||
fallback bool
|
|
||||||
resolver net.Resolver
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFallbackTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
|
|
||||||
transport, err := NewTransport(ctx, logger, tag, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &FallbackTransport{
|
|
||||||
DNSTransport: transport,
|
|
||||||
ctx: ctx,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FallbackTransport) Start(stage adapter.StartStage) error {
|
|
||||||
if stage != adapter.StartStateStart {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
platformInterface := service.FromContext[platform.Interface](f.ctx)
|
|
||||||
if platformInterface == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
inboundManager := service.FromContext[adapter.InboundManager](f.ctx)
|
|
||||||
for _, inbound := range inboundManager.Inbounds() {
|
|
||||||
if inbound.Type() == C.TypeTun {
|
|
||||||
// platform tun hijacks DNS, so we can only use cgo resolver here
|
|
||||||
f.fallback = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FallbackTransport) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
if !f.fallback {
|
|
||||||
return f.DNSTransport.Exchange(ctx, message)
|
|
||||||
}
|
|
||||||
question := message.Question[0]
|
|
||||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
|
||||||
var network string
|
|
||||||
if question.Qtype == mDNS.TypeA {
|
|
||||||
network = "ip4"
|
|
||||||
} else {
|
|
||||||
network = "ip6"
|
|
||||||
}
|
|
||||||
addresses, err := f.resolver.LookupNetIP(ctx, network, question.Name)
|
|
||||||
if err != nil {
|
|
||||||
var dnsError *net.DNSError
|
|
||||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
|
||||||
return nil, dns.RcodeRefused
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
|
||||||
} else if question.Qtype == mDNS.TypeNS {
|
|
||||||
records, err := f.resolver.LookupNS(ctx, question.Name)
|
|
||||||
if err != nil {
|
|
||||||
var dnsError *net.DNSError
|
|
||||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
|
||||||
return nil, dns.RcodeRefused
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
response := &mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Rcode: mDNS.RcodeSuccess,
|
|
||||||
Response: true,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{question},
|
|
||||||
}
|
|
||||||
for _, record := range records {
|
|
||||||
response.Answer = append(response.Answer, &mDNS.NS{
|
|
||||||
Hdr: mDNS.RR_Header{
|
|
||||||
Name: question.Name,
|
|
||||||
Rrtype: mDNS.TypeNS,
|
|
||||||
Class: mDNS.ClassINET,
|
|
||||||
Ttl: C.DefaultDNSTTL,
|
|
||||||
},
|
|
||||||
Ns: record.Host,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
} else if question.Qtype == mDNS.TypeCNAME {
|
|
||||||
cname, err := f.resolver.LookupCNAME(ctx, question.Name)
|
|
||||||
if err != nil {
|
|
||||||
var dnsError *net.DNSError
|
|
||||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
|
||||||
return nil, dns.RcodeRefused
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Rcode: mDNS.RcodeSuccess,
|
|
||||||
Response: true,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{question},
|
|
||||||
Answer: []mDNS.RR{
|
|
||||||
&mDNS.CNAME{
|
|
||||||
Hdr: mDNS.RR_Header{
|
|
||||||
Name: question.Name,
|
|
||||||
Rrtype: mDNS.TypeCNAME,
|
|
||||||
Class: mDNS.ClassINET,
|
|
||||||
Ttl: C.DefaultDNSTTL,
|
|
||||||
},
|
|
||||||
Target: cname,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
} else if question.Qtype == mDNS.TypeTXT {
|
|
||||||
records, err := f.resolver.LookupTXT(ctx, question.Name)
|
|
||||||
if err != nil {
|
|
||||||
var dnsError *net.DNSError
|
|
||||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
|
||||||
return nil, dns.RcodeRefused
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Rcode: mDNS.RcodeSuccess,
|
|
||||||
Response: true,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{question},
|
|
||||||
Answer: []mDNS.RR{
|
|
||||||
&mDNS.TXT{
|
|
||||||
Hdr: mDNS.RR_Header{
|
|
||||||
Name: question.Name,
|
|
||||||
Rrtype: mDNS.TypeCNAME,
|
|
||||||
Class: mDNS.ClassINET,
|
|
||||||
Ttl: C.DefaultDNSTTL,
|
|
||||||
},
|
|
||||||
Txt: records,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
} else if question.Qtype == mDNS.TypeMX {
|
|
||||||
records, err := f.resolver.LookupMX(ctx, question.Name)
|
|
||||||
if err != nil {
|
|
||||||
var dnsError *net.DNSError
|
|
||||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
|
||||||
return nil, dns.RcodeRefused
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
response := &mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Rcode: mDNS.RcodeSuccess,
|
|
||||||
Response: true,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{question},
|
|
||||||
}
|
|
||||||
for _, record := range records {
|
|
||||||
response.Answer = append(response.Answer, &mDNS.MX{
|
|
||||||
Hdr: mDNS.RR_Header{
|
|
||||||
Name: question.Name,
|
|
||||||
Rrtype: mDNS.TypeA,
|
|
||||||
Class: mDNS.ClassINET,
|
|
||||||
Ttl: C.DefaultDNSTTL,
|
|
||||||
},
|
|
||||||
Preference: record.Pref,
|
|
||||||
Mx: record.Host,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
} else {
|
|
||||||
return nil, E.New("only A, AAAA, NS, CNAME, TXT, MX queries are supported on current platform when using TUN, please switch to a fixed DNS server.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
dns/transport/local/local_resolved.go
Normal file
14
dns/transport/local/local_resolved.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResolvedResolver interface {
|
||||||
|
Start() error
|
||||||
|
Close() error
|
||||||
|
Object() any
|
||||||
|
Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
|
||||||
|
}
|
||||||
150
dns/transport/local/local_resolved_linux.go
Normal file
150
dns/transport/local/local_resolved_linux.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/service/resolved"
|
||||||
|
"github.com/sagernet/sing-tun"
|
||||||
|
"github.com/sagernet/sing/common/atomic"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBusResolvedResolver struct {
|
||||||
|
logger logger.ContextLogger
|
||||||
|
interfaceMonitor tun.DefaultInterfaceMonitor
|
||||||
|
systemBus *dbus.Conn
|
||||||
|
resoledObject atomic.TypedValue[dbus.BusObject]
|
||||||
|
closeOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
|
||||||
|
interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor()
|
||||||
|
if interfaceMonitor == nil {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
systemBus, err := dbus.SystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &DBusResolvedResolver{
|
||||||
|
logger: logger,
|
||||||
|
interfaceMonitor: interfaceMonitor,
|
||||||
|
systemBus: systemBus,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *DBusResolvedResolver) Start() error {
|
||||||
|
t.updateStatus()
|
||||||
|
err := t.systemBus.BusObject().AddMatchSignal(
|
||||||
|
"org.freedesktop.DBus",
|
||||||
|
"NameOwnerChanged",
|
||||||
|
dbus.WithMatchSender("org.freedesktop.DBus"),
|
||||||
|
dbus.WithMatchArg(0, "org.freedesktop.resolve1.Manager"),
|
||||||
|
).Err
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "configure resolved restart listener")
|
||||||
|
}
|
||||||
|
go t.loopUpdateStatus()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *DBusResolvedResolver) Close() error {
|
||||||
|
t.closeOnce.Do(func() {
|
||||||
|
if t.systemBus != nil {
|
||||||
|
_ = t.systemBus.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *DBusResolvedResolver) Object() any {
|
||||||
|
return t.resoledObject.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *DBusResolvedResolver) Exchange(object any, ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
defaultInterface := t.interfaceMonitor.DefaultInterface()
|
||||||
|
if defaultInterface == nil {
|
||||||
|
return nil, E.New("missing default interface")
|
||||||
|
}
|
||||||
|
question := message.Question[0]
|
||||||
|
call := object.(*dbus.Object).CallWithContext(
|
||||||
|
ctx,
|
||||||
|
"org.freedesktop.resolve1.Manager.ResolveRecord",
|
||||||
|
0,
|
||||||
|
int32(defaultInterface.Index),
|
||||||
|
question.Name,
|
||||||
|
question.Qclass,
|
||||||
|
question.Qtype,
|
||||||
|
uint64(0),
|
||||||
|
)
|
||||||
|
if call.Err != nil {
|
||||||
|
return nil, E.Cause(call.Err, " resolve record via resolved")
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
records []resolved.ResourceRecord
|
||||||
|
outflags uint64
|
||||||
|
)
|
||||||
|
err := call.Store(&records, &outflags)
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) == "" {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
restarted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if restarted {
|
||||||
|
t.updateStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *DBusResolvedResolver) updateStatus() {
|
||||||
|
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
|
||||||
|
err := dbusObject.Call("org.freedesktop.DBus.Peer.Ping", 0).Err
|
||||||
|
if err != nil {
|
||||||
|
if t.resoledObject.Swap(nil) != nil {
|
||||||
|
t.logger.Debug("systemd-resolved service is gone")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.resoledObject.Store(dbusObject)
|
||||||
|
t.logger.Debug("using systemd-resolved service as resolver")
|
||||||
|
}
|
||||||
14
dns/transport/local/local_resolved_stub.go
Normal file
14
dns/transport/local/local_resolved_stub.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
161
dns/transport/local/local_shared.go
Normal file
161
dns/transport/local/local_shared.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||||
|
systemConfig := getSystemDNSConfig(t.ctx)
|
||||||
|
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
|
||||||
|
return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
|
||||||
|
} else {
|
||||||
|
return t.exchangeParallel(ctx, systemConfig, message, domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||||
|
var lastErr error
|
||||||
|
for _, fqdn := range systemConfig.nameList(domain) {
|
||||||
|
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
return nil, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||||
|
returned := make(chan struct{})
|
||||||
|
defer close(returned)
|
||||||
|
type queryResult struct {
|
||||||
|
response *mDNS.Msg
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
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:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queryCtx, queryCancel := context.WithCancel(ctx)
|
||||||
|
defer queryCancel()
|
||||||
|
var nameCount int
|
||||||
|
for _, fqdn := range systemConfig.nameList(domain) {
|
||||||
|
nameCount++
|
||||||
|
go startRacer(queryCtx, fqdn)
|
||||||
|
}
|
||||||
|
var errors []error
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case result := <-results:
|
||||||
|
if result.err == nil {
|
||||||
|
return result.response, nil
|
||||||
|
}
|
||||||
|
errors = append(errors, result.err)
|
||||||
|
if len(errors) == nameCount {
|
||||||
|
return nil, E.Errors(errors...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
serverOffset := config.serverOffset()
|
||||||
|
sLen := uint32(len(config.servers))
|
||||||
|
var lastErr error
|
||||||
|
for i := 0; i < config.attempts; i++ {
|
||||||
|
for j := uint32(0); j < sLen; j++ {
|
||||||
|
server := config.servers[(serverOffset+j)%sLen]
|
||||||
|
question := message.Question[0]
|
||||||
|
question.Name = fqdn
|
||||||
|
response, err := t.exchangeOne(ctx, M.ParseSocksaddr(server), question, config.timeout, config.useTCP, config.trustAD)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, E.Cause(lastErr, fqdn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) {
|
||||||
|
if server.Port == 0 {
|
||||||
|
server.Port = 53
|
||||||
|
}
|
||||||
|
var networks []string
|
||||||
|
if useTCP {
|
||||||
|
networks = []string{N.NetworkTCP}
|
||||||
|
} else {
|
||||||
|
networks = []string{N.NetworkUDP, N.NetworkTCP}
|
||||||
|
}
|
||||||
|
request := &mDNS.Msg{
|
||||||
|
MsgHdr: mDNS.MsgHdr{
|
||||||
|
Id: uint16(rand.Uint32()),
|
||||||
|
RecursionDesired: true,
|
||||||
|
AuthenticatedData: ad,
|
||||||
|
},
|
||||||
|
Question: []mDNS.Question{question},
|
||||||
|
Compress: true,
|
||||||
|
}
|
||||||
|
request.SetEdns0(maxDNSPacketSize, false)
|
||||||
|
buffer := buf.Get(buf.UDPBufferSize)
|
||||||
|
defer buf.Put(buffer)
|
||||||
|
for _, network := range networks {
|
||||||
|
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
|
||||||
|
defer cancel()
|
||||||
|
conn, err := t.dialer.DialContext(ctx, network, server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||||
|
conn.SetDeadline(deadline)
|
||||||
|
}
|
||||||
|
rawMessage, err := request.PackBuffer(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "pack request")
|
||||||
|
}
|
||||||
|
_, err = conn.Write(rawMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "write request")
|
||||||
|
}
|
||||||
|
n, err := conn.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read response")
|
||||||
|
}
|
||||||
|
var response mDNS.Msg
|
||||||
|
err = response.Unpack(buffer[:n])
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "unpack response")
|
||||||
|
}
|
||||||
|
if response.Truncated && network == N.NetworkUDP {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
panic("unexpected")
|
||||||
|
}
|
||||||
@@ -10,6 +10,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// net.maxDNSPacketSize
|
||||||
|
maxDNSPacketSize = 1232
|
||||||
|
)
|
||||||
|
|
||||||
type resolverConfig struct {
|
type resolverConfig struct {
|
||||||
initOnce sync.Once
|
initOnce sync.Once
|
||||||
ch chan struct{}
|
ch chan struct{}
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
//go:build darwin && cgo
|
|
||||||
|
|
||||||
package local
|
|
||||||
|
|
||||||
/*
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <resolv.h>
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dnsReadConfig(_ context.Context, _ string) *dnsConfig {
|
|
||||||
var state C.struct___res_state
|
|
||||||
if C.res_ninit(&state) != 0 {
|
|
||||||
return &dnsConfig{
|
|
||||||
servers: defaultNS,
|
|
||||||
search: dnsDefaultSearch(),
|
|
||||||
ndots: 1,
|
|
||||||
timeout: 5 * time.Second,
|
|
||||||
attempts: 2,
|
|
||||||
err: E.New("libresolv initialization failed"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conf := &dnsConfig{
|
|
||||||
ndots: 1,
|
|
||||||
timeout: 5 * time.Second,
|
|
||||||
attempts: int(state.retry),
|
|
||||||
}
|
|
||||||
for i := 0; i < int(state.nscount); i++ {
|
|
||||||
ns := state.nsaddr_list[i]
|
|
||||||
addr := C.inet_ntoa(ns.sin_addr)
|
|
||||||
if addr == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
conf.servers = append(conf.servers, C.GoString(addr))
|
|
||||||
}
|
|
||||||
for i := 0; ; i++ {
|
|
||||||
search := state.dnsrch[i]
|
|
||||||
if search == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
conf.search = append(conf.search, dns.Fqdn(C.GoString(search)))
|
|
||||||
}
|
|
||||||
return conf
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !windows && !(darwin && cgo)
|
//go:build !windows
|
||||||
|
|
||||||
package local
|
package local
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@@ -64,9 +63,6 @@ func dnsReadConfig(ctx context.Context, _ string) *dnsConfig {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dnsServerAddr = netip.AddrFrom16(sockaddr.Addr)
|
dnsServerAddr = netip.AddrFrom16(sockaddr.Addr)
|
||||||
if sockaddr.ZoneId != 0 {
|
|
||||||
dnsServerAddr = dnsServerAddr.WithZone(strconv.FormatInt(int64(sockaddr.ZoneId), 10))
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
// Unexpected type.
|
// Unexpected type.
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func RegisterTLS(registry *dns.TransportRegistry) {
|
|||||||
type TLSTransport struct {
|
type TLSTransport struct {
|
||||||
dns.TransportAdapter
|
dns.TransportAdapter
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
dialer N.Dialer
|
dialer tls.Dialer
|
||||||
serverAddr M.Socksaddr
|
serverAddr M.Socksaddr
|
||||||
tlsConfig tls.Config
|
tlsConfig tls.Config
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
@@ -67,7 +67,7 @@ func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer
|
|||||||
return &TLSTransport{
|
return &TLSTransport{
|
||||||
TransportAdapter: adapter,
|
TransportAdapter: adapter,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
dialer: dialer,
|
dialer: tls.NewDialer(dialer, tlsConfig),
|
||||||
serverAddr: serverAddr,
|
serverAddr: serverAddr,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
@@ -100,15 +100,10 @@ func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tcpConn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
|
tlsConn, err := t.dialer.DialTLSContext(ctx, t.serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tlsConn, err := tls.ClientHandshake(ctx, tcpConn, t.tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
tcpConn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return t.exchange(message, &tlsDNSConn{Conn: tlsConn})
|
return t.exchange(message, &tlsDNSConn{Conn: tlsConn})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type TransportManager struct {
|
|||||||
transportByTag map[string]adapter.DNSTransport
|
transportByTag map[string]adapter.DNSTransport
|
||||||
dependByTag map[string][]string
|
dependByTag map[string][]string
|
||||||
defaultTransport adapter.DNSTransport
|
defaultTransport adapter.DNSTransport
|
||||||
defaultTransportFallback adapter.DNSTransport
|
defaultTransportFallback func() (adapter.DNSTransport, error)
|
||||||
fakeIPTransport adapter.FakeIPTransport
|
fakeIPTransport adapter.FakeIPTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ func NewTransportManager(logger logger.ContextLogger, registry adapter.DNSTransp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *TransportManager) Initialize(defaultTransportFallback adapter.DNSTransport) {
|
func (m *TransportManager) Initialize(defaultTransportFallback func() (adapter.DNSTransport, error)) {
|
||||||
m.defaultTransportFallback = defaultTransportFallback
|
m.defaultTransportFallback = defaultTransportFallback
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,14 +56,27 @@ func (m *TransportManager) Start(stage adapter.StartStage) error {
|
|||||||
}
|
}
|
||||||
m.started = true
|
m.started = true
|
||||||
m.stage = stage
|
m.stage = stage
|
||||||
transports := m.transports
|
|
||||||
m.access.Unlock()
|
|
||||||
if stage == adapter.StartStateStart {
|
if stage == adapter.StartStateStart {
|
||||||
if m.defaultTag != "" && m.defaultTransport == nil {
|
if m.defaultTag != "" && m.defaultTransport == nil {
|
||||||
|
m.access.Unlock()
|
||||||
return E.New("default DNS server not found: ", m.defaultTag)
|
return E.New("default DNS server not found: ", m.defaultTag)
|
||||||
}
|
}
|
||||||
return m.startTransports(m.transports)
|
if m.defaultTransport == nil {
|
||||||
|
defaultTransport, err := m.defaultTransportFallback()
|
||||||
|
if err != nil {
|
||||||
|
m.access.Unlock()
|
||||||
|
return E.Cause(err, "default DNS server fallback")
|
||||||
|
}
|
||||||
|
m.transports = append(m.transports, defaultTransport)
|
||||||
|
m.transportByTag[defaultTransport.Tag()] = defaultTransport
|
||||||
|
m.defaultTransport = defaultTransport
|
||||||
|
}
|
||||||
|
transports := m.transports
|
||||||
|
m.access.Unlock()
|
||||||
|
return m.startTransports(transports)
|
||||||
} else {
|
} else {
|
||||||
|
transports := m.transports
|
||||||
|
m.access.Unlock()
|
||||||
for _, outbound := range transports {
|
for _, outbound := range transports {
|
||||||
err := adapter.LegacyStart(outbound, stage)
|
err := adapter.LegacyStart(outbound, stage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -172,11 +185,7 @@ func (m *TransportManager) Transport(tag string) (adapter.DNSTransport, bool) {
|
|||||||
func (m *TransportManager) Default() adapter.DNSTransport {
|
func (m *TransportManager) Default() adapter.DNSTransport {
|
||||||
m.access.RLock()
|
m.access.RLock()
|
||||||
defer m.access.RUnlock()
|
defer m.access.RUnlock()
|
||||||
if m.defaultTransport != nil {
|
return m.defaultTransport
|
||||||
return m.defaultTransport
|
|
||||||
} else {
|
|
||||||
return m.defaultTransportFallback
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *TransportManager) FakeIP() adapter.FakeIPTransport {
|
func (m *TransportManager) FakeIP() adapter.FakeIPTransport {
|
||||||
|
|||||||
@@ -2,113 +2,7 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
#### 1.12.25
|
#### 1.13.0-alpha.5
|
||||||
|
|
||||||
* Backport fixes
|
|
||||||
|
|
||||||
#### 1.12.25
|
|
||||||
|
|
||||||
* Backport fixes
|
|
||||||
|
|
||||||
#### 1.12.23
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.22
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.21
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.20
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.19
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.18
|
|
||||||
|
|
||||||
* Add fallback routing rule for `auto_redirect` **1**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
|
|
||||||
ensuring traffic is routed to the sing-box table when no route is found in system tables.
|
|
||||||
|
|
||||||
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
|
|
||||||
|
|
||||||
#### 1.12.17
|
|
||||||
|
|
||||||
* Update uTLS to v1.8.2 **1**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
This update fixes missing padding extension for Chrome 120+ fingerprints.
|
|
||||||
|
|
||||||
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
|
|
||||||
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
|
|
||||||
use NaiveProxy instead for TLS fingerprint resistance.
|
|
||||||
|
|
||||||
#### 1.12.16
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.15
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.14
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.13
|
|
||||||
|
|
||||||
* Fix naive inbound
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
__Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:
|
|
||||||
because system extensions require signatures to function, we have had to temporarily halt its release.__
|
|
||||||
|
|
||||||
__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then,
|
|
||||||
only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__
|
|
||||||
|
|
||||||
#### 1.12.12
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.11
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.10
|
|
||||||
|
|
||||||
* Update uTLS to v1.8.1 **1**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected,
|
|
||||||
see https://github.com/refraction-networking/utls/pull/375.
|
|
||||||
|
|
||||||
#### 1.12.9
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.8
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.5
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
#### 1.12.4
|
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -116,15 +10,55 @@ see https://github.com/refraction-networking/utls/pull/375.
|
|||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.0-alpha.5
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.12.2
|
#### 1.12.2
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.13.0-alpha.3
|
||||||
|
|
||||||
|
* Improve `local` DNS server **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
On Apple platforms, Windows, and Linux (when using systemd-resolved),
|
||||||
|
`local` DNS server now works with Tun inbound which overrides system DNS servers.
|
||||||
|
|
||||||
|
See [Local DNS Server](/configuration/dns/server/local/).
|
||||||
|
|
||||||
|
#### 1.13.0-alpha.2
|
||||||
|
|
||||||
|
* Add `preferred_by` rule item **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
The new `preferred_by` routing rule item allows you to
|
||||||
|
match preferred domains and addresses for specific outbounds.
|
||||||
|
|
||||||
|
See [Route Rule](/configuration/route/rule/#preferred_by).
|
||||||
|
|
||||||
|
#### 1.13.0-alpha.1
|
||||||
|
|
||||||
|
* Add interface address rule items **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
New interface address rules allow you to dynamically adjust rules based on your network environment.
|
||||||
|
|
||||||
|
See [Route Rule](/configuration/route/rule/), [DNS Route Rule](/configuration/dns/rule/)
|
||||||
|
and [Headless Rule](/configuration/rule-set/headless-rule/).
|
||||||
|
|
||||||
#### 1.12.1
|
#### 1.12.1
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.12.0
|
### 1.12.0
|
||||||
|
|
||||||
* Refactor DNS servers **1**
|
* Refactor DNS servers **1**
|
||||||
* Add domain resolver options**2**
|
* Add domain resolver options**2**
|
||||||
@@ -190,8 +124,7 @@ See [Tailscale](/configuration/endpoint/tailscale/).
|
|||||||
|
|
||||||
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
|
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
|
||||||
|
|
||||||
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches
|
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
|
||||||
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
|
|
||||||
|
|
||||||
**7**:
|
**7**:
|
||||||
|
|
||||||
@@ -253,8 +186,7 @@ See [Tun](/configuration/inbound/tun/#loopback_address).
|
|||||||
|
|
||||||
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
|
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
|
||||||
|
|
||||||
The following data was tested
|
The following data was tested using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro.
|
||||||
using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro.
|
|
||||||
|
|
||||||
| Version | Stack | MTU | Upload | Download |
|
| Version | Stack | MTU | Upload | Download |
|
||||||
|-------------|--------|-------|--------|----------|
|
|-------------|--------|-------|--------|----------|
|
||||||
@@ -273,11 +205,11 @@ using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/interna
|
|||||||
|
|
||||||
**18**:
|
**18**:
|
||||||
|
|
||||||
We continue to experience issues updating our sing-box apps on the App Store and Play Store.
|
We continue to experience issues updating our sing-box apps on the App Store and Play Store.
|
||||||
Until we rewrite and resubmit the apps, they are considered irrecoverable.
|
Until we rewrite and resubmit the apps, they are considered irrecoverable.
|
||||||
Therefore, after this release, we will not be repeating this notice unless there is new information.
|
Therefore, after this release, we will not be repeating this notice unless there is new information.
|
||||||
|
|
||||||
### 1.11.15
|
#### 1.11.15
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -293,7 +225,7 @@ violated the rules (TestFlight users are not affected)._
|
|||||||
|
|
||||||
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
|
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
|
||||||
|
|
||||||
### 1.11.14
|
#### 1.11.14
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -343,7 +275,7 @@ You can now choose what the DERP home page shows, just like with derper's `-home
|
|||||||
|
|
||||||
See [DERP](/configuration/service/derp/#home).
|
See [DERP](/configuration/service/derp/#home).
|
||||||
|
|
||||||
### 1.11.13
|
#### 1.11.13
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -381,7 +313,7 @@ SSM API service is a RESTful API server for managing Shadowsocks servers.
|
|||||||
|
|
||||||
See [SSM API Service](/configuration/service/ssm-api/).
|
See [SSM API Service](/configuration/service/ssm-api/).
|
||||||
|
|
||||||
### 1.11.11
|
#### 1.11.11
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -413,7 +345,7 @@ You can now set `bind_interface`, `routing_mark` and `reuse_addr` in Listen Fiel
|
|||||||
|
|
||||||
See [Listen Fields](/configuration/shared/listen/).
|
See [Listen Fields](/configuration/shared/listen/).
|
||||||
|
|
||||||
### 1.11.10
|
#### 1.11.10
|
||||||
|
|
||||||
* Undeprecate the `block` outbound **1**
|
* Undeprecate the `block` outbound **1**
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
@@ -431,7 +363,7 @@ violated the rules (TestFlight users are not affected)._
|
|||||||
* Update quic-go to v0.51.0
|
* Update quic-go to v0.51.0
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
### 1.11.9
|
#### 1.11.9
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -442,7 +374,7 @@ violated the rules (TestFlight users are not affected)._
|
|||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
### 1.11.8
|
#### 1.11.8
|
||||||
|
|
||||||
* Improve `auto_redirect` **1**
|
* Improve `auto_redirect` **1**
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
@@ -459,7 +391,7 @@ violated the rules (TestFlight users are not affected)._
|
|||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
### 1.11.7
|
#### 1.11.7
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -475,7 +407,7 @@ violated the rules (TestFlight users are not affected)._
|
|||||||
Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks,
|
Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks,
|
||||||
see [Tun](/configuration/inbound/tun/#auto_redirect).
|
see [Tun](/configuration/inbound/tun/#auto_redirect).
|
||||||
|
|
||||||
### 1.11.6
|
#### 1.11.6
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -516,7 +448,7 @@ See [Protocol Sniff](/configuration/route/sniff/).
|
|||||||
|
|
||||||
See [Dial Fields](/configuration/shared/dial/#domain_resolver).
|
See [Dial Fields](/configuration/shared/dial/#domain_resolver).
|
||||||
|
|
||||||
### 1.11.5
|
#### 1.11.5
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -532,7 +464,7 @@ violated the rules (TestFlight users are not affected)._
|
|||||||
|
|
||||||
See [DNS Rule Action](/configuration/dns/rule_action/#predefined).
|
See [DNS Rule Action](/configuration/dns/rule_action/#predefined).
|
||||||
|
|
||||||
### 1.11.4
|
#### 1.11.4
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -555,8 +487,7 @@ See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/conf
|
|||||||
|
|
||||||
**2**:
|
**2**:
|
||||||
|
|
||||||
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions,
|
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions, see [Route Action](/configuration/route/rule_action).
|
||||||
see [Route Action](/configuration/route/rule_action).
|
|
||||||
|
|
||||||
**3**:
|
**3**:
|
||||||
|
|
||||||
@@ -587,10 +518,9 @@ See [Tailscale](/configuration/endpoint/tailscale/).
|
|||||||
|
|
||||||
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
|
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
|
||||||
|
|
||||||
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches
|
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
|
||||||
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
|
|
||||||
|
|
||||||
### 1.11.3
|
#### 1.11.3
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -601,7 +531,7 @@ process._
|
|||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
### 1.11.1
|
#### 1.11.1
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -780,7 +710,7 @@ See [Hysteria2](/configuration/outbound/hysteria2/).
|
|||||||
|
|
||||||
When `up_mbps` and `down_mbps` are set, `ignore_client_bandwidth` instead denies clients from using BBR CC.
|
When `up_mbps` and `down_mbps` are set, `ignore_client_bandwidth` instead denies clients from using BBR CC.
|
||||||
|
|
||||||
### 1.10.7
|
#### 1.10.7
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -875,7 +805,7 @@ and the old outbound will be removed in sing-box 1.13.0.
|
|||||||
See [Endpoint](/configuration/endpoint/), [WireGuard Endpoint](/configuration/endpoint/wireguard/)
|
See [Endpoint](/configuration/endpoint/), [WireGuard Endpoint](/configuration/endpoint/wireguard/)
|
||||||
and [Migrate WireGuard outbound fields to route options](/migration/#migrate-wireguard-outbound-to-endpoint).
|
and [Migrate WireGuard outbound fields to route options](/migration/#migrate-wireguard-outbound-to-endpoint).
|
||||||
|
|
||||||
### 1.10.2
|
#### 1.10.2
|
||||||
|
|
||||||
* Add deprecated warnings
|
* Add deprecated warnings
|
||||||
* Fix proxying websocket connections in HTTP/mixed inbounds
|
* Fix proxying websocket connections in HTTP/mixed inbounds
|
||||||
@@ -1012,7 +942,7 @@ See [Rule Action](/configuration/route/rule_action/).
|
|||||||
* Update quic-go to v0.48.0
|
* Update quic-go to v0.48.0
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
### 1.10.1
|
#### 1.10.1
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
|
|||||||
|
|
||||||
!!! failure ""
|
!!! failure ""
|
||||||
|
|
||||||
Due to non-technical reasons, we are temporarily unable to update the sing-box app on the App Store and release the standalone version of the macOS client (TestFlight users are not affected)
|
We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected).
|
||||||
|
|
||||||
## :material-graph: Requirements
|
## :material-graph: Requirements
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
|
|||||||
|
|
||||||
## :material-download: Download
|
## :material-download: Download
|
||||||
|
|
||||||
* ~~[App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)~~
|
* [App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)
|
||||||
* TestFlight (Beta)
|
* TestFlight (Beta)
|
||||||
|
|
||||||
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
|
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
|
||||||
@@ -26,15 +26,15 @@ TestFlight quota is only available to [sponsors](https://github.com/sponsors/nek
|
|||||||
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
|
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
|
||||||
or sending us your Apple ID [via email](mailto:contact@sagernet.org).
|
or sending us your Apple ID [via email](mailto:contact@sagernet.org).
|
||||||
|
|
||||||
## ~~:material-file-download: Download (macOS standalone version)~~
|
## :material-file-download: Download (macOS standalone version)
|
||||||
|
|
||||||
* ~~[Homebrew Cask](https://formulae.brew.sh/cask/sfm)~~
|
* [Homebrew Cask](https://formulae.brew.sh/cask/sfm)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# brew install sfm
|
brew install sfm
|
||||||
```
|
```
|
||||||
|
|
||||||
* ~~[GitHub Releases](https://github.com/SagerNet/sing-box/releases)~~
|
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases)
|
||||||
|
|
||||||
## :material-source-repository: Source code
|
## :material-source-repository: Source code
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
|
:material-plus: [interface_address](#interface_address)
|
||||||
|
:material-plus: [network_interface_address](#network_interface_address)
|
||||||
|
:material-plus: [default_interface_address](#default_interface_address)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.0"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-plus: [ip_accept_any](#ip_accept_any)
|
:material-plus: [ip_accept_any](#ip_accept_any)
|
||||||
@@ -130,6 +136,19 @@ icon: material/alert-decagram
|
|||||||
],
|
],
|
||||||
"network_is_expensive": false,
|
"network_is_expensive": false,
|
||||||
"network_is_constrained": false,
|
"network_is_constrained": false,
|
||||||
|
"interface_address": {
|
||||||
|
"en0": [
|
||||||
|
"2000::/3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"network_interface_address": {
|
||||||
|
"wifi": [
|
||||||
|
"2000::/3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default_interface_address": [
|
||||||
|
"2000::/3"
|
||||||
|
],
|
||||||
"wifi_ssid": [
|
"wifi_ssid": [
|
||||||
"My WIFI"
|
"My WIFI"
|
||||||
],
|
],
|
||||||
@@ -359,6 +378,36 @@ such as Cellular or a Personal Hotspot (on Apple platforms).
|
|||||||
|
|
||||||
Match if network is in Low Data Mode.
|
Match if network is in Low Data Mode.
|
||||||
|
|
||||||
|
#### interface_address
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux, Windows, and macOS.
|
||||||
|
|
||||||
|
Match interface address.
|
||||||
|
|
||||||
|
#### network_interface_address
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported in graphical clients on Android and Apple platforms.
|
||||||
|
|
||||||
|
Matches network interface (same values as `network_type`) address.
|
||||||
|
|
||||||
|
#### default_interface_address
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux, Windows, and macOS.
|
||||||
|
|
||||||
|
Match default interface address.
|
||||||
|
|
||||||
#### wifi_ssid
|
#### wifi_ssid
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [interface_address](#interface_address)
|
||||||
|
:material-plus: [network_interface_address](#network_interface_address)
|
||||||
|
:material-plus: [default_interface_address](#default_interface_address)
|
||||||
|
|
||||||
!!! quote "sing-box 1.12.0 中的更改"
|
!!! quote "sing-box 1.12.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [ip_accept_any](#ip_accept_any)
|
:material-plus: [ip_accept_any](#ip_accept_any)
|
||||||
@@ -130,6 +136,19 @@ icon: material/alert-decagram
|
|||||||
],
|
],
|
||||||
"network_is_expensive": false,
|
"network_is_expensive": false,
|
||||||
"network_is_constrained": false,
|
"network_is_constrained": false,
|
||||||
|
"interface_address": {
|
||||||
|
"en0": [
|
||||||
|
"2000::/3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"network_interface_address": {
|
||||||
|
"wifi": [
|
||||||
|
"2000::/3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default_interface_address": [
|
||||||
|
"2000::/3"
|
||||||
|
],
|
||||||
"wifi_ssid": [
|
"wifi_ssid": [
|
||||||
"My WIFI"
|
"My WIFI"
|
||||||
],
|
],
|
||||||
@@ -358,6 +377,36 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
匹配如果网络在低数据模式下。
|
匹配如果网络在低数据模式下。
|
||||||
|
|
||||||
|
#### interface_address
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux、Windows 和 macOS.
|
||||||
|
|
||||||
|
匹配接口地址。
|
||||||
|
|
||||||
|
#### network_interface_address
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅在 Android 与 Apple 平台图形客户端中支持。
|
||||||
|
|
||||||
|
匹配网络接口(可用值同 `network_type`)地址。
|
||||||
|
|
||||||
|
#### default_interface_address
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux、Windows 和 macOS.
|
||||||
|
|
||||||
|
匹配默认接口地址。
|
||||||
|
|
||||||
#### wifi_ssid
|
#### wifi_ssid
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
|
:material-plus: [prefer_go](#prefer_go)
|
||||||
|
|
||||||
!!! question "Since sing-box 1.12.0"
|
!!! question "Since sing-box 1.12.0"
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
@@ -15,6 +19,7 @@ icon: material/new-box
|
|||||||
{
|
{
|
||||||
"type": "local",
|
"type": "local",
|
||||||
"tag": "",
|
"tag": "",
|
||||||
|
"prefer_go": false
|
||||||
|
|
||||||
// Dial Fields
|
// Dial Fields
|
||||||
}
|
}
|
||||||
@@ -24,10 +29,33 @@ icon: material/new-box
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! info "Difference from legacy local server"
|
!!! info "Difference from legacy local server"
|
||||||
|
|
||||||
* The old legacy local server only handles IP requests; the new one handles all types of requests and supports concurrent for IP requests.
|
* The old legacy local server only handles IP requests; the new one handles all types of requests and supports concurrent for IP requests.
|
||||||
* The old local server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
* The old local server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### prefer_go
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
When enabled, `local` DNS server will resolve DNS by dialing itself whenever possible.
|
||||||
|
|
||||||
|
Specifically, it disables following behaviors which was added as features in sing-box 1.13.0:
|
||||||
|
|
||||||
|
1. On Apple platforms: Attempt to resolve A/AAAA requests using `getaddrinfo` in NetworkExtension.
|
||||||
|
2. On Linux: Resolve through `systemd-resolvd`'s DBus interface when available.
|
||||||
|
|
||||||
|
As a sole exception, it cannot disable the following behavior:
|
||||||
|
|
||||||
|
1. In the Android graphical client,
|
||||||
|
`local` will always resolve DNS through the platform interface,
|
||||||
|
as there is no other way to obtain upstream DNS servers;
|
||||||
|
On devices running Android versions lower than 10, this interface can only resolve A/AAAA requests.
|
||||||
|
|
||||||
|
2. On macOS, `local` will try DHCP first in Network Extension, since DHCP respects DIal Fields,
|
||||||
|
it will not be disabled by `prefer_go`.
|
||||||
|
|
||||||
### Dial Fields
|
### Dial Fields
|
||||||
|
|
||||||
See [Dial Fields](/configuration/shared/dial/) for details.
|
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
---
|
|
||||||
icon: material/new-box
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.11.0"
|
!!! question "Since sing-box 1.11.0"
|
||||||
|
|
||||||
# Endpoint
|
# Endpoint
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
---
|
|
||||||
icon: material/new-box
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.11.0 起"
|
!!! question "自 sing-box 1.11.0 起"
|
||||||
|
|
||||||
# 端点
|
# 端点
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
---
|
|
||||||
icon: material/new-box
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.11.0"
|
!!! question "Since sing-box 1.11.0"
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
---
|
|
||||||
icon: material/new-box
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.11.0 起"
|
!!! question "自 sing-box 1.11.0 起"
|
||||||
|
|
||||||
### 结构
|
### 结构
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
"method": "2022-blake3-aes-128-gcm",
|
"method": "2022-blake3-aes-128-gcm",
|
||||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||||
"managed": false,
|
|
||||||
"multiplex": {}
|
"multiplex": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -87,10 +86,6 @@ Both if empty.
|
|||||||
| 2022 methods | `sing-box generate rand --base64 <Key Length>` |
|
| 2022 methods | `sing-box generate rand --base64 <Key Length>` |
|
||||||
| other methods | any string |
|
| other methods | any string |
|
||||||
|
|
||||||
#### managed
|
|
||||||
|
|
||||||
Defaults to `false`. Enable this when the inbound is managed by the [SSM API](/configuration/service/ssm-api) for dynamic user.
|
|
||||||
|
|
||||||
#### multiplex
|
#### multiplex
|
||||||
|
|
||||||
See [Multiplex](/configuration/shared/multiplex#inbound) for details.
|
See [Multiplex](/configuration/shared/multiplex#inbound) for details.
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
"method": "2022-blake3-aes-128-gcm",
|
"method": "2022-blake3-aes-128-gcm",
|
||||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||||
"managed": false,
|
|
||||||
"multiplex": {}
|
"multiplex": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -87,10 +86,6 @@ See [Listen Fields](/configuration/shared/listen/) for details.
|
|||||||
| 2022 methods | `sing-box generate rand --base64 <密钥长度>` |
|
| 2022 methods | `sing-box generate rand --base64 <密钥长度>` |
|
||||||
| other methods | 任意字符串 |
|
| other methods | 任意字符串 |
|
||||||
|
|
||||||
#### managed
|
|
||||||
|
|
||||||
默认为 `false`。当该入站需要由 [SSM API](/zh/configuration/service/ssm-api) 管理用户时必须启用此字段。
|
|
||||||
|
|
||||||
#### multiplex
|
#### multiplex
|
||||||
|
|
||||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.18"
|
|
||||||
|
|
||||||
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.0"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-plus: [loopback_address](#loopback_address)
|
:material-plus: [loopback_address](#loopback_address)
|
||||||
@@ -67,7 +63,6 @@ icon: material/new-box
|
|||||||
"auto_redirect": true,
|
"auto_redirect": true,
|
||||||
"auto_redirect_input_mark": "0x2023",
|
"auto_redirect_input_mark": "0x2023",
|
||||||
"auto_redirect_output_mark": "0x2024",
|
"auto_redirect_output_mark": "0x2024",
|
||||||
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
|
||||||
"loopback_address": [
|
"loopback_address": [
|
||||||
"10.7.0.1"
|
"10.7.0.1"
|
||||||
],
|
],
|
||||||
@@ -283,17 +278,6 @@ Connection output mark used by `auto_redirect`.
|
|||||||
|
|
||||||
`0x2024` is used by default.
|
`0x2024` is used by default.
|
||||||
|
|
||||||
#### auto_redirect_iproute2_fallback_rule_index
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.12.18"
|
|
||||||
|
|
||||||
Linux iproute2 fallback rule index generated by `auto_redirect`.
|
|
||||||
|
|
||||||
This rule is checked after system default rules (32766: main, 32767: default),
|
|
||||||
routing traffic to the sing-box table only when no route is found in system tables.
|
|
||||||
|
|
||||||
`32768` is used by default.
|
|
||||||
|
|
||||||
#### loopback_address
|
#### loopback_address
|
||||||
|
|
||||||
!!! question "Since sing-box 1.12.0"
|
!!! question "Since sing-box 1.12.0"
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! quote "sing-box 1.12.18 中的更改"
|
|
||||||
|
|
||||||
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
|
||||||
|
|
||||||
!!! quote "sing-box 1.12.0 中的更改"
|
!!! quote "sing-box 1.12.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [loopback_address](#loopback_address)
|
:material-plus: [loopback_address](#loopback_address)
|
||||||
@@ -67,7 +63,6 @@ icon: material/new-box
|
|||||||
"auto_redirect": true,
|
"auto_redirect": true,
|
||||||
"auto_redirect_input_mark": "0x2023",
|
"auto_redirect_input_mark": "0x2023",
|
||||||
"auto_redirect_output_mark": "0x2024",
|
"auto_redirect_output_mark": "0x2024",
|
||||||
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
|
||||||
"loopback_address": [
|
"loopback_address": [
|
||||||
"10.7.0.1"
|
"10.7.0.1"
|
||||||
],
|
],
|
||||||
@@ -282,17 +277,6 @@ tun 接口的 IPv6 前缀。
|
|||||||
|
|
||||||
默认使用 `0x2024`。
|
默认使用 `0x2024`。
|
||||||
|
|
||||||
#### auto_redirect_iproute2_fallback_rule_index
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.12.18 起"
|
|
||||||
|
|
||||||
`auto_redirect` 生成的 iproute2 回退规则索引。
|
|
||||||
|
|
||||||
此规则在系统默认规则(32766: main,32767: default)之后检查,
|
|
||||||
仅当系统路由表中未找到路由时才将流量路由到 sing-box 路由表。
|
|
||||||
|
|
||||||
默认使用 `32768`。
|
|
||||||
|
|
||||||
#### loopback_address
|
#### loopback_address
|
||||||
|
|
||||||
!!! question "自 sing-box 1.12.0 起"
|
!!! question "自 sing-box 1.12.0 起"
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
---
|
|
||||||
icon: material/new-box
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.11.0"
|
!!! quote "Changes in sing-box 1.11.0"
|
||||||
|
|
||||||
:material-plus: [server_ports](#server_ports)
|
:material-plus: [server_ports](#server_ports)
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
---
|
|
||||||
icon: material/new-box
|
|
||||||
---
|
|
||||||
|
|
||||||
!!! quote "sing-box 1.11.0 中的更改"
|
!!! quote "sing-box 1.11.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [server_ports](#server_ports)
|
:material-plus: [server_ports](#server_ports)
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
|
:material-plus: [interface_address](#interface_address)
|
||||||
|
:material-plus: [network_interface_address](#network_interface_address)
|
||||||
|
:material-plus: [default_interface_address](#default_interface_address)
|
||||||
|
:material-plus: [preferred_by](#preferred_by)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.11.0"
|
!!! quote "Changes in sing-box 1.11.0"
|
||||||
|
|
||||||
:material-plus: [action](#action)
|
:material-plus: [action](#action)
|
||||||
@@ -128,12 +135,29 @@ icon: material/new-box
|
|||||||
],
|
],
|
||||||
"network_is_expensive": false,
|
"network_is_expensive": false,
|
||||||
"network_is_constrained": false,
|
"network_is_constrained": false,
|
||||||
|
"interface_address": {
|
||||||
|
"en0": [
|
||||||
|
"2000::/3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"network_interface_address": {
|
||||||
|
"wifi": [
|
||||||
|
"2000::/3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default_interface_address": [
|
||||||
|
"2000::/3"
|
||||||
|
],
|
||||||
"wifi_ssid": [
|
"wifi_ssid": [
|
||||||
"My WIFI"
|
"My WIFI"
|
||||||
],
|
],
|
||||||
"wifi_bssid": [
|
"wifi_bssid": [
|
||||||
"00:00:00:00:00:00"
|
"00:00:00:00:00:00"
|
||||||
],
|
],
|
||||||
|
"preferred_by": [
|
||||||
|
"tailscale",
|
||||||
|
"wireguard"
|
||||||
|
],
|
||||||
"rule_set": [
|
"rule_set": [
|
||||||
"geoip-cn",
|
"geoip-cn",
|
||||||
"geosite-cn"
|
"geosite-cn"
|
||||||
@@ -363,6 +387,36 @@ such as Cellular or a Personal Hotspot (on Apple platforms).
|
|||||||
|
|
||||||
Match if network is in Low Data Mode.
|
Match if network is in Low Data Mode.
|
||||||
|
|
||||||
|
#### interface_address
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux, Windows, and macOS.
|
||||||
|
|
||||||
|
Match interface address.
|
||||||
|
|
||||||
|
#### network_interface_address
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported in graphical clients on Android and Apple platforms.
|
||||||
|
|
||||||
|
Matches network interface (same values as `network_type`) address.
|
||||||
|
|
||||||
|
#### default_interface_address
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux, Windows, and macOS.
|
||||||
|
|
||||||
|
Match default interface address.
|
||||||
|
|
||||||
#### wifi_ssid
|
#### wifi_ssid
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
@@ -379,6 +433,17 @@ Match WiFi SSID.
|
|||||||
|
|
||||||
Match WiFi BSSID.
|
Match WiFi BSSID.
|
||||||
|
|
||||||
|
#### preferred_by
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
Match specified outbounds' preferred routes.
|
||||||
|
|
||||||
|
| Type | Match |
|
||||||
|
|-------------|-----------------------------------------------|
|
||||||
|
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
|
||||||
|
| `wireguard` | Match peers's allowed IPs |
|
||||||
|
|
||||||
#### rule_set
|
#### rule_set
|
||||||
|
|
||||||
!!! question "Since sing-box 1.8.0"
|
!!! question "Since sing-box 1.8.0"
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [interface_address](#interface_address)
|
||||||
|
:material-plus: [network_interface_address](#network_interface_address)
|
||||||
|
:material-plus: [default_interface_address](#default_interface_address)
|
||||||
|
:material-plus: [preferred_by](#preferred_by)
|
||||||
|
|
||||||
!!! quote "sing-box 1.11.0 中的更改"
|
!!! quote "sing-box 1.11.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [action](#action)
|
:material-plus: [action](#action)
|
||||||
@@ -125,12 +132,29 @@ icon: material/new-box
|
|||||||
],
|
],
|
||||||
"network_is_expensive": false,
|
"network_is_expensive": false,
|
||||||
"network_is_constrained": false,
|
"network_is_constrained": false,
|
||||||
|
"interface_address": {
|
||||||
|
"en0": [
|
||||||
|
"2000::/3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"network_interface_address": {
|
||||||
|
"wifi": [
|
||||||
|
"2000::/3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default_interface_address": [
|
||||||
|
"2000::/3"
|
||||||
|
],
|
||||||
"wifi_ssid": [
|
"wifi_ssid": [
|
||||||
"My WIFI"
|
"My WIFI"
|
||||||
],
|
],
|
||||||
"wifi_bssid": [
|
"wifi_bssid": [
|
||||||
"00:00:00:00:00:00"
|
"00:00:00:00:00:00"
|
||||||
],
|
],
|
||||||
|
"preferred_by": [
|
||||||
|
"tailscale",
|
||||||
|
"wireguard"
|
||||||
|
],
|
||||||
"rule_set": [
|
"rule_set": [
|
||||||
"geoip-cn",
|
"geoip-cn",
|
||||||
"geosite-cn"
|
"geosite-cn"
|
||||||
@@ -337,7 +361,7 @@ icon: material/new-box
|
|||||||
|
|
||||||
匹配网络类型。
|
匹配网络类型。
|
||||||
|
|
||||||
Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
可用值: `wifi`, `cellular`, `ethernet` and `other`.
|
||||||
|
|
||||||
#### network_is_expensive
|
#### network_is_expensive
|
||||||
|
|
||||||
@@ -360,6 +384,36 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
匹配如果网络在低数据模式下。
|
匹配如果网络在低数据模式下。
|
||||||
|
|
||||||
|
#### interface_address
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux、Windows 和 macOS.
|
||||||
|
|
||||||
|
匹配接口地址。
|
||||||
|
|
||||||
|
#### network_interface_address
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅在 Android 与 Apple 平台图形客户端中支持。
|
||||||
|
|
||||||
|
匹配网络接口(可用值同 `network_type`)地址。
|
||||||
|
|
||||||
|
#### default_interface_address
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux、Windows 和 macOS.
|
||||||
|
|
||||||
|
匹配默认接口地址。
|
||||||
|
|
||||||
#### wifi_ssid
|
#### wifi_ssid
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
@@ -376,6 +430,17 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
匹配 WiFi BSSID。
|
匹配 WiFi BSSID。
|
||||||
|
|
||||||
|
#### preferred_by
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
匹配制定出站的首选路由。
|
||||||
|
|
||||||
|
| 类型 | 匹配 |
|
||||||
|
|-------------|--------------------------------|
|
||||||
|
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
|
||||||
|
| `wireguard` | 匹配对端的 allowed IPs |
|
||||||
|
|
||||||
#### rule_set
|
#### rule_set
|
||||||
|
|
||||||
!!! question "自 sing-box 1.8.0 起"
|
!!! question "自 sing-box 1.8.0 起"
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
|
:material-plus: [network_interface_address](#network_interface_address)
|
||||||
|
:material-plus: [default_interface_address](#default_interface_address)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.11.0"
|
!!! quote "Changes in sing-box 1.11.0"
|
||||||
|
|
||||||
:material-plus: [network_type](#network_type)
|
:material-plus: [network_type](#network_type)
|
||||||
@@ -78,6 +83,14 @@ icon: material/new-box
|
|||||||
],
|
],
|
||||||
"network_is_expensive": false,
|
"network_is_expensive": false,
|
||||||
"network_is_constrained": false,
|
"network_is_constrained": false,
|
||||||
|
"network_interface_address": {
|
||||||
|
"wifi": [
|
||||||
|
"2000::/3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default_interface_address": [
|
||||||
|
"2000::/3"
|
||||||
|
],
|
||||||
"wifi_ssid": [
|
"wifi_ssid": [
|
||||||
"My WIFI"
|
"My WIFI"
|
||||||
],
|
],
|
||||||
@@ -225,6 +238,26 @@ such as Cellular or a Personal Hotspot (on Apple platforms).
|
|||||||
|
|
||||||
Match if network is in Low Data Mode.
|
Match if network is in Low Data Mode.
|
||||||
|
|
||||||
|
#### network_interface_address
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported in graphical clients on Android and Apple platforms.
|
||||||
|
|
||||||
|
Matches network interface (same values as `network_type`) address.
|
||||||
|
|
||||||
|
#### default_interface_address
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux, Windows, and macOS.
|
||||||
|
|
||||||
|
Match default interface address.
|
||||||
|
|
||||||
#### wifi_ssid
|
#### wifi_ssid
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [network_interface_address](#network_interface_address)
|
||||||
|
:material-plus: [default_interface_address](#default_interface_address)
|
||||||
|
|
||||||
!!! quote "sing-box 1.11.0 中的更改"
|
!!! quote "sing-box 1.11.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [network_type](#network_type)
|
:material-plus: [network_type](#network_type)
|
||||||
@@ -78,6 +83,14 @@ icon: material/new-box
|
|||||||
],
|
],
|
||||||
"network_is_expensive": false,
|
"network_is_expensive": false,
|
||||||
"network_is_constrained": false,
|
"network_is_constrained": false,
|
||||||
|
"network_interface_address": {
|
||||||
|
"wifi": [
|
||||||
|
"2000::/3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default_interface_address": [
|
||||||
|
"2000::/3"
|
||||||
|
],
|
||||||
"wifi_ssid": [
|
"wifi_ssid": [
|
||||||
"My WIFI"
|
"My WIFI"
|
||||||
],
|
],
|
||||||
@@ -221,6 +234,26 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
匹配如果网络在低数据模式下。
|
匹配如果网络在低数据模式下。
|
||||||
|
|
||||||
|
#### network_interface_address
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅在 Android 与 Apple 平台图形客户端中支持。
|
||||||
|
|
||||||
|
匹配网络接口(可用值同 `network_type`)地址。
|
||||||
|
|
||||||
|
#### default_interface_address
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux、Windows 和 macOS.
|
||||||
|
|
||||||
|
匹配默认接口地址。
|
||||||
|
|
||||||
#### wifi_ssid
|
#### wifi_ssid
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
|
:material-plus: version `4`
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.11.0"
|
!!! quote "Changes in sing-box 1.11.0"
|
||||||
|
|
||||||
:material-plus: version `3`
|
:material-plus: version `3`
|
||||||
@@ -36,6 +40,7 @@ Version of rule-set.
|
|||||||
* 1: sing-box 1.8.0: Initial rule-set version.
|
* 1: sing-box 1.8.0: Initial rule-set version.
|
||||||
* 2: sing-box 1.10.0: Optimized memory usages of `domain_suffix` rules in binary rule-sets.
|
* 2: sing-box 1.10.0: Optimized memory usages of `domain_suffix` rules in binary rule-sets.
|
||||||
* 3: sing-box 1.11.0: Added `network_type`, `network_is_expensive` and `network_is_constrainted` rule items.
|
* 3: sing-box 1.11.0: Added `network_type`, `network_is_expensive` and `network_is_constrainted` rule items.
|
||||||
|
* 4: sing-box 1.13.0: Added `network_interface_address` and `default_interface_address` rule items.
|
||||||
|
|
||||||
#### rules
|
#### rules
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: version `4`
|
||||||
|
|
||||||
!!! quote "sing-box 1.11.0 中的更改"
|
!!! quote "sing-box 1.11.0 中的更改"
|
||||||
|
|
||||||
:material-plus: version `3`
|
:material-plus: version `3`
|
||||||
@@ -36,6 +40,7 @@ icon: material/new-box
|
|||||||
* 1: sing-box 1.8.0: 初始规则集版本。
|
* 1: sing-box 1.8.0: 初始规则集版本。
|
||||||
* 2: sing-box 1.10.0: 优化了二进制规则集中 `domain_suffix` 规则的内存使用。
|
* 2: sing-box 1.10.0: 优化了二进制规则集中 `domain_suffix` 规则的内存使用。
|
||||||
* 3: sing-box 1.11.0: 添加了 `network_type`、 `network_is_expensive` 和 `network_is_constrainted` 规则项。
|
* 3: sing-box 1.11.0: 添加了 `network_type`、 `network_is_expensive` 和 `network_is_constrainted` 规则项。
|
||||||
|
* 4: sing-box 1.13.0: 添加了 `network_interface_address` 和 `default_interface_address` 规则项。
|
||||||
|
|
||||||
#### rules
|
#### rules
|
||||||
|
|
||||||
|
|||||||
@@ -230,18 +230,9 @@ The path to the server private key, in PEM format.
|
|||||||
|
|
||||||
==Client only==
|
==Client only==
|
||||||
|
|
||||||
!!! failure "Not Recommended"
|
!!! failure ""
|
||||||
|
|
||||||
uTLS has had repeated fingerprinting vulnerabilities discovered by researchers.
|
There is no evidence that GFW detects and blocks servers based on TLS client fingerprinting, and using an imperfect emulation that has not been security reviewed could pose security risks.
|
||||||
|
|
||||||
uTLS is a Go library that attempts to imitate browser TLS fingerprints by copying
|
|
||||||
ClientHello structure. However, browsers use completely different TLS stacks
|
|
||||||
(Chrome uses BoringSSL, Firefox uses NSS) with distinct implementation behaviors
|
|
||||||
that cannot be replicated by simply copying the handshake format, making detection possible.
|
|
||||||
Additionally, the library lacks active maintenance and has poor code quality,
|
|
||||||
making it unsuitable for censorship circumvention.
|
|
||||||
|
|
||||||
For TLS fingerprint resistance, use [NaiveProxy](/configuration/inbound/naive/) instead.
|
|
||||||
|
|
||||||
uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance.
|
uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance.
|
||||||
|
|
||||||
|
|||||||
@@ -220,16 +220,9 @@ TLS 版本值:
|
|||||||
|
|
||||||
==仅客户端==
|
==仅客户端==
|
||||||
|
|
||||||
!!! failure "不推荐"
|
!!! failure ""
|
||||||
|
|
||||||
uTLS 已被研究人员多次发现其指纹可被识别的漏洞。
|
没有证据表明 GFW 根据 TLS 客户端指纹检测并阻止服务器,并且,使用一个未经安全审查的不完美模拟可能带来安全隐患。
|
||||||
|
|
||||||
uTLS 是一个试图通过复制 ClientHello 结构来模仿浏览器 TLS 指纹的 Go 库。
|
|
||||||
然而,浏览器使用完全不同的 TLS 实现(Chrome 使用 BoringSSL,Firefox 使用 NSS),
|
|
||||||
其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。
|
|
||||||
此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。
|
|
||||||
|
|
||||||
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/configuration/inbound/naive/)。
|
|
||||||
|
|
||||||
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。
|
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ icon: material/horse
|
|||||||
|
|
||||||
# Trojan
|
# Trojan
|
||||||
|
|
||||||
Trojan is the most commonly used TLS proxy made in China. It can be used in various combinations.
|
Torjan is the most commonly used TLS proxy made in China. It can be used in various combinations,
|
||||||
|
but only the combination of uTLS and multiplexing is recommended.
|
||||||
|
|
||||||
| Protocol and implementation combination | Specification | Resists passive detection | Resists active probes |
|
| Protocol and implementation combination | Specification | Resists passive detection | Resists active probes |
|
||||||
|-----------------------------------------|----------------------------------------------------------------------|---------------------------|-----------------------|
|
|-----------------------------------------|----------------------------------------------------------------------|---------------------------|-----------------------|
|
||||||
@@ -139,7 +140,11 @@ Trojan is the most commonly used TLS proxy made in China. It can be used in vari
|
|||||||
"password": "password",
|
"password": "password",
|
||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.org"
|
"server_name": "example.org",
|
||||||
|
"utls": {
|
||||||
|
"enabled": true,
|
||||||
|
"fingerprint": "firefox"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"multiplex": {
|
"multiplex": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
@@ -166,7 +171,11 @@ Trojan is the most commonly used TLS proxy made in China. It can be used in vari
|
|||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.org",
|
"server_name": "example.org",
|
||||||
"certificate_path": "/path/to/certificate.pem"
|
"certificate_path": "/path/to/certificate.pem",
|
||||||
|
"utls": {
|
||||||
|
"enabled": true,
|
||||||
|
"fingerprint": "firefox"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"multiplex": {
|
"multiplex": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
@@ -189,7 +198,11 @@ Trojan is the most commonly used TLS proxy made in China. It can be used in vari
|
|||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.org",
|
"server_name": "example.org",
|
||||||
"insecure": true
|
"insecure": true,
|
||||||
|
"utls": {
|
||||||
|
"enabled": true,
|
||||||
|
"fingerprint": "firefox"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"multiplex": {
|
"multiplex": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
|||||||
@@ -11,22 +11,16 @@ the project maintainer via [GitHub Sponsors](https://github.com/sponsors/nekohas
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Commercial Sponsors
|
### Special Sponsors
|
||||||
|
|
||||||
> [Warp](https://go.warp.dev/sing-box), Built for coding with multiple AI agents.
|
**Viral Tech, Inc.**
|
||||||
|
|
||||||
[](https://go.warp.dev/sing-box)
|
|
||||||
|
|
||||||
## Special Sponsors
|
|
||||||
|
|
||||||
> Viral Tech, Inc.
|
|
||||||
|
|
||||||
Helping us re-list sing-box apps on the Apple Store.
|
Helping us re-list sing-box apps on the Apple Store.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> [JetBrains](https://www.jetbrains.com)
|
[](https://www.jetbrains.com)
|
||||||
|
|
||||||
Free license for the amazing IDEs.
|
Free license for the amazing IDEs.
|
||||||
|
|
||||||
[](https://www.jetbrains.com)
|
---
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package clashapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
@@ -28,7 +27,7 @@ func (s *Server) setupMetaAPI(r chi.Router) {
|
|||||||
})
|
})
|
||||||
r.Mount("/", middleware.Profiler())
|
r.Mount("/", middleware.Profiler())
|
||||||
}
|
}
|
||||||
r.Get("/memory", memory(s.ctx, s.trafficManager))
|
r.Get("/memory", memory(s.trafficManager))
|
||||||
r.Mount("/group", groupRouter(s))
|
r.Mount("/group", groupRouter(s))
|
||||||
r.Mount("/upgrade", upgradeRouter(s))
|
r.Mount("/upgrade", upgradeRouter(s))
|
||||||
}
|
}
|
||||||
@@ -38,7 +37,7 @@ type Memory struct {
|
|||||||
OSLimit uint64 `json:"oslimit"` // maybe we need it in the future
|
OSLimit uint64 `json:"oslimit"` // maybe we need it in the future
|
||||||
}
|
}
|
||||||
|
|
||||||
func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var conn net.Conn
|
var conn net.Conn
|
||||||
if r.Header.Get("Upgrade") == "websocket" {
|
if r.Header.Get("Upgrade") == "websocket" {
|
||||||
@@ -47,7 +46,6 @@ func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w h
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
@@ -60,12 +58,7 @@ func memory(ctx context.Context, trafficManager *trafficontrol.Manager) func(w h
|
|||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
var err error
|
var err error
|
||||||
first := true
|
first := true
|
||||||
for {
|
for range tick.C {
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-tick.C:
|
|
||||||
}
|
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
|
|
||||||
inuse := trafficManager.Snapshot().Memory
|
inuse := trafficManager.Snapshot().Memory
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
func cacheRouter(ctx context.Context) http.Handler {
|
func cacheRouter(ctx context.Context) http.Handler {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Post("/fakeip/flush", flushFakeip(ctx))
|
r.Post("/fakeip/flush", flushFakeip(ctx))
|
||||||
|
r.Post("/dns/flush", flushDNS(ctx))
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,3 +32,13 @@ func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Reques
|
|||||||
render.NoContent(w, r)
|
render.NoContent(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func flushDNS(ctx context.Context) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
|
||||||
|
if dnsRouter != nil {
|
||||||
|
dnsRouter.ClearCache()
|
||||||
|
}
|
||||||
|
render.NoContent(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package clashapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -18,15 +17,15 @@ import (
|
|||||||
"github.com/gofrs/uuid/v5"
|
"github.com/gofrs/uuid/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func connectionRouter(ctx context.Context, router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
|
func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Get("/", getConnections(ctx, trafficManager))
|
r.Get("/", getConnections(trafficManager))
|
||||||
r.Delete("/", closeAllConnections(router, trafficManager))
|
r.Delete("/", closeAllConnections(router, trafficManager))
|
||||||
r.Delete("/{id}", closeConnection(trafficManager))
|
r.Delete("/{id}", closeConnection(trafficManager))
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Header.Get("Upgrade") != "websocket" {
|
if r.Header.Get("Upgrade") != "websocket" {
|
||||||
snapshot := trafficManager.Snapshot()
|
snapshot := trafficManager.Snapshot()
|
||||||
@@ -38,7 +37,6 @@ func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
intervalStr := r.URL.Query().Get("interval")
|
intervalStr := r.URL.Query().Get("interval")
|
||||||
interval := 1000
|
interval := 1000
|
||||||
@@ -69,12 +67,7 @@ func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager)
|
|||||||
|
|
||||||
tick := time.NewTicker(time.Millisecond * time.Duration(interval))
|
tick := time.NewTicker(time.Millisecond * time.Duration(interval))
|
||||||
defer tick.Stop()
|
defer tick.Stop()
|
||||||
for {
|
for range tick.C {
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-tick.C:
|
|
||||||
}
|
|
||||||
if err = sendSnapshot(); err != nil {
|
if err = sendSnapshot(); err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,13 +114,13 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
|||||||
chiRouter.Group(func(r chi.Router) {
|
chiRouter.Group(func(r chi.Router) {
|
||||||
r.Use(authentication(options.Secret))
|
r.Use(authentication(options.Secret))
|
||||||
r.Get("/", hello(options.ExternalUI != ""))
|
r.Get("/", hello(options.ExternalUI != ""))
|
||||||
r.Get("/logs", getLogs(s.ctx, logFactory))
|
r.Get("/logs", getLogs(logFactory))
|
||||||
r.Get("/traffic", traffic(s.ctx, trafficManager))
|
r.Get("/traffic", traffic(trafficManager))
|
||||||
r.Get("/version", version)
|
r.Get("/version", version)
|
||||||
r.Mount("/configs", configRouter(s, logFactory))
|
r.Mount("/configs", configRouter(s, logFactory))
|
||||||
r.Mount("/proxies", proxyRouter(s, s.router))
|
r.Mount("/proxies", proxyRouter(s, s.router))
|
||||||
r.Mount("/rules", ruleRouter(s.router))
|
r.Mount("/rules", ruleRouter(s.router))
|
||||||
r.Mount("/connections", connectionRouter(s.ctx, s.router, trafficManager))
|
r.Mount("/connections", connectionRouter(s.router, trafficManager))
|
||||||
r.Mount("/providers/proxies", proxyProviderRouter())
|
r.Mount("/providers/proxies", proxyProviderRouter())
|
||||||
r.Mount("/providers/rules", ruleProviderRouter())
|
r.Mount("/providers/rules", ruleProviderRouter())
|
||||||
r.Mount("/script", scriptRouter())
|
r.Mount("/script", scriptRouter())
|
||||||
@@ -305,7 +305,7 @@ type Traffic struct {
|
|||||||
Down int64 `json:"down"`
|
Down int64 `json:"down"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func traffic(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
func traffic(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var conn net.Conn
|
var conn net.Conn
|
||||||
if r.Header.Get("Upgrade") == "websocket" {
|
if r.Header.Get("Upgrade") == "websocket" {
|
||||||
@@ -326,12 +326,7 @@ func traffic(ctx context.Context, trafficManager *trafficontrol.Manager) func(w
|
|||||||
defer tick.Stop()
|
defer tick.Stop()
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
uploadTotal, downloadTotal := trafficManager.Total()
|
uploadTotal, downloadTotal := trafficManager.Total()
|
||||||
for {
|
for range tick.C {
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-tick.C:
|
|
||||||
}
|
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
uploadTotalNew, downloadTotalNew := trafficManager.Total()
|
uploadTotalNew, downloadTotalNew := trafficManager.Total()
|
||||||
err := json.NewEncoder(buf).Encode(Traffic{
|
err := json.NewEncoder(buf).Encode(Traffic{
|
||||||
@@ -362,7 +357,7 @@ type Log struct {
|
|||||||
Payload string `json:"payload"`
|
Payload string `json:"payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLogs(ctx context.Context, logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) {
|
func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
levelText := r.URL.Query().Get("level")
|
levelText := r.URL.Query().Get("level")
|
||||||
if levelText == "" {
|
if levelText == "" {
|
||||||
@@ -401,8 +396,6 @@ func getLogs(ctx context.Context, logFactory log.ObservableFactory) func(w http.
|
|||||||
var logEntry log.Entry
|
var logEntry log.Entry
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-done:
|
case <-done:
|
||||||
return
|
return
|
||||||
case logEntry = <-subscription:
|
case logEntry = <-subscription:
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package trafficontrol
|
|||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/compatible"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/experimental/clashapi/compatible"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/atomic"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user