Compare commits

..

8 Commits

Author SHA1 Message Date
世界
a4c600d94a documentation: Bump version 2025-08-15 15:06:47 +08:00
xchacha20-poly1305
900a2c1ef1 Fix rule set version 2025-08-15 15:06:47 +08:00
世界
4b3861232b documentation: Add preferred_by route rule item 2025-08-15 15:06:47 +08:00
世界
31a0bc94ae Add preferred_by route rule item 2025-08-15 15:06:47 +08:00
世界
8c1d1e9b0e documentation: Add interface address rule items 2025-08-15 15:05:53 +08:00
世界
735b52fee5 Add interface address rule items 2025-08-15 15:05:52 +08:00
neletor
9fc9dccf46 Add support for ech retry configs 2025-08-15 15:05:52 +08:00
Zephyruso
c64f7fd8a1 Add /dns/flush-clash meta api 2025-08-15 15:05:52 +08:00
153 changed files with 2857 additions and 3252 deletions

View File

@@ -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

View File

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

View File

@@ -1,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
View 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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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:

View File

@@ -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
View File

@@ -15,6 +15,4 @@
.DS_Store .DS_Store
/config.d/ /config.d/
/venv/ /venv/
CLAUDE.md
AGENTS.md
/.claude/

View File

@@ -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"]

View File

@@ -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) && \

View File

@@ -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.

View File

@@ -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()
} }

View File

@@ -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

View File

@@ -20,7 +20,6 @@ type NetworkManager interface {
DefaultOptions() NetworkOptions DefaultOptions() NetworkOptions
RegisterAutoRedirectOutputMark(mark uint32) error RegisterAutoRedirectOutputMark(mark uint32) error
AutoRedirectOutputMark() uint32 AutoRedirectOutputMark() uint32
AutoRedirectOutputMarkFunc() control.Func
NetworkMonitor() tun.NetworkUpdateMonitor NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager PackageManager() tun.PackageManager

View File

@@ -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)

View File

@@ -30,7 +30,7 @@ type Manager struct {
outboundByTag map[string]adapter.Outbound outboundByTag map[string]adapter.Outbound
dependByTag map[string][]string dependByTag map[string][]string
defaultOutbound adapter.Outbound defaultOutbound adapter.Outbound
defaultOutboundFallback func() (adapter.Outbound, error) defaultOutboundFallback adapter.Outbound
} }
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager { func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
@@ -44,7 +44,7 @@ func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry,
} }
} }
func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, error)) { func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) {
m.defaultOutboundFallback = defaultOutboundFallback m.defaultOutboundFallback = defaultOutboundFallback
} }
@@ -55,31 +55,18 @@ func (m *Manager) Start(stage adapter.StartStage) error {
} }
m.started = true m.started = true
m.stage = stage m.stage = stage
outbounds := m.outbounds
m.access.Unlock()
if stage == adapter.StartStateStart { if stage == adapter.StartStateStart {
if m.defaultTag != "" && m.defaultOutbound == nil { if m.defaultTag != "" && m.defaultOutbound == nil {
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag) defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
if !loaded { if !loaded {
m.access.Unlock()
return E.New("default outbound not found: ", m.defaultTag) return E.New("default outbound not found: ", m.defaultTag)
} }
m.defaultOutbound = defaultEndpoint m.defaultOutbound = defaultEndpoint
} }
if m.defaultOutbound == nil {
directOutbound, err := m.defaultOutboundFallback()
if err != nil {
m.access.Unlock()
return E.Cause(err, "create direct outbound for fallback")
}
m.outbounds = append(m.outbounds, directOutbound)
m.outboundByTag[directOutbound.Tag()] = directOutbound
m.defaultOutbound = directOutbound
}
outbounds := m.outbounds
m.access.Unlock()
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...)) return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
} else { } else {
outbounds := m.outbounds
m.access.Unlock()
for _, outbound := range outbounds { for _, outbound := range outbounds {
err := adapter.LegacyStart(outbound, stage) err := adapter.LegacyStart(outbound, stage)
if err != nil { if err != nil {
@@ -200,7 +187,11 @@ func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
func (m *Manager) Default() adapter.Outbound { func (m *Manager) Default() adapter.Outbound {
m.access.RLock() m.access.RLock()
defer m.access.RUnlock() defer m.access.RUnlock()
return m.defaultOutbound if m.defaultOutbound != nil {
return m.defaultOutbound
} else {
return m.defaultOutboundFallback
}
} }
func (m *Manager) Remove(tag string) error { func (m *Manager) Remove(tag string) error {

View File

@@ -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
} }

View File

@@ -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,
} }
} }

8
box.go
View File

@@ -314,15 +314,15 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize service[", i, "]") return nil, E.Cause(err, "initialize service[", i, "]")
} }
} }
outboundManager.Initialize(func() (adapter.Outbound, error) { outboundManager.Initialize(common.Must1(
return direct.NewOutbound( direct.NewOutbound(
ctx, ctx,
router, router,
logFactory.NewLogger("outbound/direct"), logFactory.NewLogger("outbound/direct"),
"direct", "direct",
option.DirectOutboundOptions{}, option.DirectOutboundOptions{},
) ),
}) ))
dnsTransportManager.Initialize(common.Must1( dnsTransportManager.Initialize(common.Must1(
local.NewTransport( local.NewTransport(
ctx, ctx,

View File

@@ -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

View File

@@ -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
}

View File

@@ -128,10 +128,6 @@ func (c *ReadWaitConn) Upstream() any {
return c.Conn return c.Conn
} }
func (c *ReadWaitConn) ReaderReplaceable() bool {
return true
}
var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
func init() { func init() {

View File

@@ -6,26 +6,22 @@ import (
"net" "net"
_ "unsafe" _ "unsafe"
"github.com/sagernet/sing/common"
"github.com/metacubex/utls" "github.com/metacubex/utls"
) )
func init() { func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) { tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
switch tlsConn := conn.(type) { tlsConn, loaded := common.Cast[*tls.UConn](conn)
case *tls.UConn: if !loaded {
return true, func() error { return
return utlsReadRecord(tlsConn.Conn)
}, func() error {
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
}
case *tls.Conn:
return true, func() error {
return utlsReadRecord(tlsConn)
}, func() error {
return utlsHandlePostHandshakeMessage(tlsConn)
}
} }
return return true, func() error {
return utlsReadRecord(tlsConn.Conn)
}, func() error {
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
}
}) })
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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()

View File

@@ -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,35 +89,37 @@ 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 { if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
@@ -124,11 +127,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true)) listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
} }
} }
if networkManager != nil {
markFunc := networkManager.AutoRedirectOutputMarkFunc()
dialer.Control = control.Append(dialer.Control, markFunc)
listener.Control = control.Append(listener.Control, markFunc)
}
if options.ReuseAddr { if options.ReuseAddr {
listener.Control = control.Append(listener.Control, control.ReuseAddr()) listener.Control = control.Append(listener.Control, control.ReuseAddr())
} }
@@ -142,7 +140,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

View File

@@ -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,
}
}

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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"

View File

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

View File

@@ -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)
}) })
}) })
} }

View File

@@ -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)
}) })
}) })
} }

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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")
}
}

View File

@@ -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")
} }

View File

@@ -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, 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, 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,67 @@ 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
}
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
View 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
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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.")
} }

View File

@@ -307,11 +307,3 @@ func (c *realityClientConnWrapper) Upstream() any {
func (c *realityClientConnWrapper) CloseWrite() error { func (c *realityClientConnWrapper) CloseWrite() error {
return c.Close() return c.Close()
} }
func (c *realityClientConnWrapper) ReaderReplaceable() bool {
return true
}
func (c *realityClientConnWrapper) WriterReplaceable() bool {
return true
}

View File

@@ -206,11 +206,3 @@ func (c *realityConnWrapper) Upstream() any {
func (c *realityConnWrapper) CloseWrite() error { func (c *realityConnWrapper) CloseWrite() error {
return c.Close() return c.Close()
} }
func (c *realityConnWrapper) ReaderReplaceable() bool {
return true
}
func (c *realityConnWrapper) WriterReplaceable() bool {
return true
}

View File

@@ -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
} }

View File

@@ -106,14 +106,6 @@ func (c *utlsConnWrapper) Upstream() any {
return c.UConn return c.UConn
} }
func (c *utlsConnWrapper) ReaderReplaceable() bool {
return true
}
func (c *utlsConnWrapper) WriterReplaceable() bool {
return true
}
type utlsALPNWrapper struct { type utlsALPNWrapper struct {
utlsConnWrapper utlsConnWrapper
nextProtocols []string nextProtocols []string

View File

@@ -109,9 +109,6 @@ func (c *Conn) Write(b []byte) (n int, err error) {
if err != nil { if err != nil {
return return
} }
if i != len(splitIndexes) {
time.Sleep(c.fallbackDelay)
}
} }
} }
} }

View File

@@ -9,10 +9,6 @@ import (
) )
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error { func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
_, err := conn.Write(payload)
if err != nil {
return err
}
time.Sleep(fallbackDelay) time.Sleep(fallbackDelay)
return nil return nil
} }

View File

@@ -16,9 +16,6 @@ func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fal
err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload) err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload)
if err != nil { if err != nil {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) { if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
if _, err := conn.Write(payload); err != nil {
return err
}
time.Sleep(fallbackDelay) time.Sleep(fallbackDelay)
return nil return nil
} }

View File

@@ -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
} }

View File

@@ -22,7 +22,8 @@ const (
RuleSetVersion1 = 1 + iota RuleSetVersion1 = 1 + iota
RuleSetVersion2 RuleSetVersion2
RuleSetVersion3 RuleSetVersion3
RuleSetVersionCurrent = RuleSetVersion3 RuleSetVersion4
RuleSetVersionCurrent = RuleSetVersion4
) )
const ( const (

View File

@@ -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,
} }

View File

@@ -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 = &copyResponse
response.Truncate(maxLen) response.Truncate(maxLen)
} }
buffer := buf.NewSize(headroom*2 + 1 + responseLen) buffer := buf.NewSize(headroom*2 + 1 + responseLen)

View File

@@ -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

View File

@@ -214,88 +214,101 @@ 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 responseCheck!= nil && errors.Is(err, RcodeError(mDNS.RcodeNameError)) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
*/
} else if len(message.Question) > 0 {
rejected = true
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 +332,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 +352,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 +391,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 +413,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 +447,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 {

View File

@@ -2,18 +2,17 @@ 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"
"github.com/sagernet/sing-box/common/dialer"
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/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
@@ -30,7 +29,6 @@ import (
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
"golang.org/x/exp/slices"
) )
func RegisterTransport(registry *dns.TransportRegistry) { func RegisterTransport(registry *dns.TransportRegistry) {
@@ -47,12 +45,9 @@ type Transport struct {
networkManager adapter.NetworkManager networkManager adapter.NetworkManager
interfaceName string interfaceName string
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
transportLock sync.RWMutex transports []adapter.DNSTransport
updateAccess sync.Mutex
updatedAt time.Time updatedAt time.Time
servers []M.Socksaddr
search []string
ndots int
attempts int
} }
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) { func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) {
@@ -67,40 +62,27 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
logger: logger, logger: logger,
networkManager: service.FromContext[adapter.NetworkManager](ctx), networkManager: service.FromContext[adapter.NetworkManager](ctx),
interfaceName: options.Interface, interfaceName: options.Interface,
ndots: 1,
attempts: 2,
}, nil }, nil
} }
func NewRawTransport(transportAdapter dns.TransportAdapter, ctx context.Context, dialer N.Dialer, logger log.ContextLogger) *Transport {
return &Transport{
TransportAdapter: transportAdapter,
ctx: ctx,
dialer: dialer,
logger: logger,
networkManager: service.FromContext[adapter.NetworkManager](ctx),
ndots: 1,
attempts: 2,
}
}
func (t *Transport) Start(stage adapter.StartStage) error { func (t *Transport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.fetchServers()
if err != nil {
return err
}
if t.interfaceName == "" { if t.interfaceName == "" {
t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated) t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated)
} }
go func() {
_, err := t.Fetch()
if err != nil {
t.logger.Error(E.Cause(err, "fetch DNS servers"))
}
}()
return nil return nil
} }
func (t *Transport) Close() error { func (t *Transport) Close() error {
for _, transport := range t.transports {
transport.Close()
}
if t.interfaceCallback != nil { if t.interfaceCallback != nil {
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback) t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
} }
@@ -108,44 +90,23 @@ func (t *Transport) Close() error {
} }
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) {
servers, err := t.Fetch() err := t.fetchServers()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(servers) == 0 {
if len(t.transports) == 0 {
return nil, E.New("dhcp: empty DNS servers from response") return nil, E.New("dhcp: empty DNS servers from response")
} }
return t.Exchange0(ctx, message, servers)
}
func (t *Transport) Exchange0(ctx context.Context, message *mDNS.Msg, servers []M.Socksaddr) (*mDNS.Msg, error) { var response *mDNS.Msg
question := message.Question[0] for _, transport := range t.transports {
domain := dns.FqdnToDomain(question.Name) response, err = transport.Exchange(ctx, message)
if len(servers) == 1 || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) { if err == nil {
return t.exchangeSingleRequest(ctx, servers, message, domain) return response, nil
} else { }
return t.exchangeParallel(ctx, servers, message, domain)
} }
} return nil, err
func (t *Transport) Fetch() ([]M.Socksaddr, error) {
t.transportLock.RLock()
updatedAt := t.updatedAt
servers := t.servers
t.transportLock.RUnlock()
if time.Since(updatedAt) < C.DHCPTTL {
return servers, nil
}
t.transportLock.Lock()
defer t.transportLock.Unlock()
if time.Since(t.updatedAt) < C.DHCPTTL {
return t.servers, nil
}
err := t.updateServers()
if err != nil {
return nil, err
}
return t.servers, nil
} }
func (t *Transport) fetchInterface() (*control.Interface, error) { func (t *Transport) fetchInterface() (*control.Interface, error) {
@@ -163,6 +124,18 @@ func (t *Transport) fetchInterface() (*control.Interface, error) {
} }
} }
func (t *Transport) fetchServers() error {
if time.Since(t.updatedAt) < C.DHCPTTL {
return nil
}
t.updateAccess.Lock()
defer t.updateAccess.Unlock()
if time.Since(t.updatedAt) < C.DHCPTTL {
return nil
}
return t.updateServers()
}
func (t *Transport) updateServers() error { func (t *Transport) updateServers() error {
iface, err := t.fetchInterface() iface, err := t.fetchInterface()
if err != nil { if err != nil {
@@ -175,7 +148,7 @@ func (t *Transport) updateServers() error {
cancel() cancel()
if err != nil { if err != nil {
return err return err
} else if len(t.servers) == 0 { } else if len(t.transports) == 0 {
return E.New("dhcp: empty DNS servers response") return E.New("dhcp: empty DNS servers response")
} else { } else {
t.updatedAt = time.Now() t.updatedAt = time.Now()
@@ -198,27 +171,13 @@ 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
} }
defer packetConn.Close() defer packetConn.Close()
discovery, err := dhcpv4.NewDiscovery(iface.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions( discovery, err := dhcpv4.NewDiscovery(iface.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer))
dhcpv4.OptionDomainName,
dhcpv4.OptionDomainNameServer,
dhcpv4.OptionDNSDomainSearchList,
))
if err != nil { if err != nil {
return err return err
} }
@@ -243,12 +202,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
} }
@@ -268,23 +223,31 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
continue continue
} }
return t.recreateServers(iface, dhcpPacket) dns := dhcpPacket.DNS()
if len(dns) == 0 {
return nil
}
return t.recreateServers(iface, common.Map(dns, func(it net.IP) M.Socksaddr {
return M.SocksaddrFrom(M.AddrFromIP(it), 53)
}))
} }
} }
func (t *Transport) recreateServers(iface *control.Interface, dhcpPacket *dhcpv4.DHCPv4) error { func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []M.Socksaddr) error {
searchList := dhcpPacket.DomainSearch() if len(serverAddrs) > 0 {
if searchList != nil && len(searchList.Labels) > 0 { t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "]")
t.search = searchList.Labels
} else if dhcpPacket.DomainName() != "" {
t.search = []string{dhcpPacket.DomainName()}
} }
serverAddrs := common.Map(dhcpPacket.DNS(), func(it net.IP) M.Socksaddr { serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{
return M.SocksaddrFrom(M.AddrFromIP(it), 53) BindInterface: iface.Name,
}) UDPFragmentDefault: true,
if len(serverAddrs) > 0 && !slices.Equal(t.servers, serverAddrs) { }))
t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, M.Socksaddr.String), ","), "], search: [", strings.Join(t.search, ","), "]") var transports []adapter.DNSTransport
for _, serverAddr := range serverAddrs {
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, serverAddr))
} }
t.servers = serverAddrs for _, transport := range t.transports {
transport.Close()
}
t.transports = transports
return nil return nil
} }

View File

@@ -1,213 +0,0 @@
package dhcp
import (
"context"
"errors"
"math/rand"
"strings"
"syscall"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
mDNS "github.com/miekg/dns"
)
func (t *Transport) exchangeSingleRequest(ctx context.Context, servers []M.Socksaddr, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
var lastErr error
for _, fqdn := range t.nameList(domain) {
response, err := t.tryOneName(ctx, servers, fqdn, message)
if err != nil {
lastErr = err
continue
}
return response, nil
}
return nil, lastErr
}
func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr, 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, servers, fqdn, message)
if err == nil {
if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 {
err = dns.RcodeSuccess
}
}
select {
case results <- queryResult{response, err}:
case <-returned:
}
}
queryCtx, queryCancel := context.WithCancel(ctx)
defer queryCancel()
var nameCount int
for _, fqdn := range t.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, servers []M.Socksaddr, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) {
sLen := len(servers)
var lastErr error
for i := 0; i < t.attempts; i++ {
for j := 0; j < sLen; j++ {
server := servers[j]
question := message.Question[0]
question.Name = fqdn
response, err := t.exchangeOne(ctx, server, question)
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) (*mDNS.Msg, error) {
if server.Port == 0 {
server.Port = 53
}
request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: uint16(rand.Uint32()),
RecursionDesired: true,
AuthenticatedData: true,
},
Question: []mDNS.Question{question},
Compress: true,
}
request.SetEdns0(buf.UDPBufferSize, 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)
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)
}
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)
}
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)
}
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 {
l := len(name)
rooted := l > 0 && name[l-1] == '.'
if l > 254 || l == 254 && !rooted {
return nil
}
if rooted {
if avoidDNS(name) {
return nil
}
return []string{name}
}
hasNdots := strings.Count(name, ".") >= t.ndots
name += "."
// l++
names := make([]string, 0, 1+len(t.search))
if hasNdots && !avoidDNS(name) {
names = append(names, name)
}
for _, suffix := range t.search {
fqdn := name + suffix
if !avoidDNS(fqdn) && len(fqdn) <= 254 {
names = append(names, fqdn)
}
}
if !hasNdots && !avoidDNS(name) {
names = append(names, name)
}
return names
}
func avoidDNS(name string) bool {
if name == "" {
return true
}
if name[len(name)-1] == '.' {
name = name[:len(name)-1]
}
return strings.HasSuffix(name, ".onion")
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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,
}
}

View File

@@ -2,15 +2,12 @@ package local
import ( import (
"context" "context"
"errors"
"math/rand" "math/rand"
"syscall"
"time" "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"
@@ -54,17 +51,18 @@ func (t *Transport) Close() error {
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) {
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) systemConfig := getSystemDNSConfig(t.ctx)
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) { if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
return t.exchangeSingleRequest(ctx, systemConfig, message, question.Name) return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
} else { } else {
return t.exchangeParallel(ctx, systemConfig, message, question.Name) return t.exchangeParallel(ctx, systemConfig, message, domain)
} }
} }
@@ -95,7 +93,7 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
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 {
@@ -151,6 +149,12 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
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()),
@@ -160,74 +164,41 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
Question: []mDNS.Question{question}, Question: []mDNS.Question{question},
Compress: true, Compress: true,
} }
request.SetEdns0(buf.UDPBufferSize, false) request.SetEdns0(maxDNSPacketSize, 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) 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, timeout)
} }
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, timeout)
} }
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, 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)
} }

View File

@@ -67,6 +67,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
return f.DNSTransport.Exchange(ctx, message) return f.DNSTransport.Exchange(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 {
var network string var network string
if question.Qtype == mDNS.TypeA { if question.Qtype == mDNS.TypeA {
@@ -74,7 +75,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
} else { } else {
network = "ip6" network = "ip6"
} }
addresses, err := f.resolver.LookupNetIP(ctx, network, question.Name) addresses, err := f.resolver.LookupNetIP(ctx, network, domain)
if err != nil { if err != nil {
var dnsError *net.DNSError var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound { if errors.As(err, &dnsError) && dnsError.IsNotFound {
@@ -84,7 +85,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
} }
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
} else if question.Qtype == mDNS.TypeNS { } else if question.Qtype == mDNS.TypeNS {
records, err := f.resolver.LookupNS(ctx, question.Name) records, err := f.resolver.LookupNS(ctx, domain)
if err != nil { if err != nil {
var dnsError *net.DNSError var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound { if errors.As(err, &dnsError) && dnsError.IsNotFound {
@@ -113,7 +114,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
} }
return response, nil return response, nil
} else if question.Qtype == mDNS.TypeCNAME { } else if question.Qtype == mDNS.TypeCNAME {
cname, err := f.resolver.LookupCNAME(ctx, question.Name) cname, err := f.resolver.LookupCNAME(ctx, domain)
if err != nil { if err != nil {
var dnsError *net.DNSError var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound { if errors.As(err, &dnsError) && dnsError.IsNotFound {
@@ -141,7 +142,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
}, },
}, nil }, nil
} else if question.Qtype == mDNS.TypeTXT { } else if question.Qtype == mDNS.TypeTXT {
records, err := f.resolver.LookupTXT(ctx, question.Name) records, err := f.resolver.LookupTXT(ctx, domain)
if err != nil { if err != nil {
var dnsError *net.DNSError var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound { if errors.As(err, &dnsError) && dnsError.IsNotFound {
@@ -169,7 +170,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
}, },
}, nil }, nil
} else if question.Qtype == mDNS.TypeMX { } else if question.Qtype == mDNS.TypeMX {
records, err := f.resolver.LookupMX(ctx, question.Name) records, err := f.resolver.LookupMX(ctx, domain)
if err != nil { if err != nil {
var dnsError *net.DNSError var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound { if errors.As(err, &dnsError) && dnsError.IsNotFound {

View File

@@ -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{}

View File

@@ -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

View File

@@ -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})
} }

View File

@@ -2,129 +2,35 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.12.25 #### 1.13.0-alpha.2
* Backport fixes * Add `preferred_by` rule item **1**
#### 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 * Fixes and improvements
**1**: **1**:
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default), The new `preferred_by` routing rule item allows you to
ensuring traffic is routed to the sing-box table when no route is found in system tables. match preferred domains and addresses for specific outbounds.
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768). See [Route Rule](/configuration/route/rule/#preferred_by).
#### 1.12.17 #### 1.13.0-alpha.1
* Update uTLS to v1.8.2 **1** * Add interface address rule items **1**
* Fixes and improvements * Fixes and improvements
**1**: **1**:
This update fixes missing padding extension for Chrome 120+ fingerprints. New interface address rules allow you to dynamically adjust rules based on your network environment.
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities. See [Route Rule](/configuration/route/rule/), [DNS Route Rule](/configuration/dns/rule/)
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations; and [Headless Rule](/configuration/rule-set/headless-rule/).
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
#### 1.12.3
* Fixes and improvements
#### 1.12.2
* Fixes and improvements
#### 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 +96,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 +158,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 +177,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 +197,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 +247,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 +285,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 +317,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 +335,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 +346,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 +363,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 +379,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 +420,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 +436,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 +459,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 +490,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 +503,7 @@ process._
* Fixes and improvements * Fixes and improvements
### 1.11.1 #### 1.11.1
* Fixes and improvements * Fixes and improvements
@@ -780,7 +682,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 +777,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 +914,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

View File

@@ -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

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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.

View File

@@ -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)。

View File

@@ -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"

View File

@@ -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: main32767: default之后检查
仅当系统路由表中未找到路由时才将流量路由到 sing-box 路由表。
默认使用 `32768`
#### loopback_address #### loopback_address
!!! question "自 sing-box 1.12.0 起" !!! question "自 sing-box 1.12.0 起"

View File

@@ -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"

View File

@@ -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 起"

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -220,16 +220,9 @@ TLS 版本值:
==仅客户端== ==仅客户端==
!!! failure "不推荐" !!! failure ""
uTLS 已被研究人员多次发现其指纹可被识别的漏洞 没有证据表明 GFW 根据 TLS 客户端指纹检测并阻止服务器,并且,使用一个未经安全审查的不完美模拟可能带来安全隐患
uTLS 是一个试图通过复制 ClientHello 结构来模仿浏览器 TLS 指纹的 Go 库。
然而,浏览器使用完全不同的 TLS 实现Chrome 使用 BoringSSLFirefox 使用 NSS
其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。
此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/configuration/inbound/naive/)。
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。 uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。

View File

@@ -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

View File

@@ -11,22 +11,16 @@ the project maintainer via [GitHub Sponsors](https://github.com/sponsors/nekohas
![](https://nekohasekai.github.io/sponsor-images/sponsors.svg) ![](https://nekohasekai.github.io/sponsor-images/sponsors.svg)
## Commercial Sponsors ### Special Sponsors
> [Warp](https://go.warp.dev/sing-box), Built for coding with multiple AI agents. **Viral Tech, Inc.**
[![](https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png)](https://go.warp.dev/sing-box)
## Special Sponsors
> Viral Tech, Inc.
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) [![JetBrains logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://www.jetbrains.com)
Free license for the amazing IDEs. Free license for the amazing IDEs.
[![JetBrains logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://www.jetbrains.com) ---

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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
} }

View File

@@ -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:

View File

@@ -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"

View File

@@ -2,11 +2,11 @@ package trafficontrol
import ( import (
"net" "net"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"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"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"

View File

@@ -12,7 +12,9 @@ type iOSPauseFields struct {
func (s *BoxService) Pause() { func (s *BoxService) Pause() {
s.pauseManager.DevicePause() s.pauseManager.DevicePause()
if C.IsIos { if !C.IsIos {
s.instance.Router().ResetNetwork()
} else {
if s.endPauseTimer == nil { if s.endPauseTimer == nil {
s.endPauseTimer = time.AfterFunc(time.Minute, s.pauseManager.DeviceWake) s.endPauseTimer = time.AfterFunc(time.Minute, s.pauseManager.DeviceWake)
} else { } else {
@@ -24,6 +26,7 @@ func (s *BoxService) Pause() {
func (s *BoxService) Wake() { func (s *BoxService) Wake() {
if !C.IsIos { if !C.IsIos {
s.pauseManager.DeviceWake() s.pauseManager.DeviceWake()
s.instance.Router().ResetNetwork()
} }
} }

View File

@@ -7,11 +7,11 @@ import (
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"

22
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/sagernet/sing-box
go 1.23.1 go 1.23.1
require ( require (
github.com/anytls/sing-anytls v0.0.11 github.com/anytls/sing-anytls v0.0.8
github.com/caddyserver/certmagic v0.23.0 github.com/caddyserver/certmagic v0.23.0
github.com/coder/websocket v1.8.13 github.com/coder/websocket v1.8.13
github.com/cretz/bine v0.2.0 github.com/cretz/bine v0.2.0
@@ -15,8 +15,8 @@ require (
github.com/libdns/alidns v1.0.5-libdns.v1.beta1 github.com/libdns/alidns v1.0.5-libdns.v1.beta1
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422
github.com/metacubex/utls v1.8.4 github.com/metacubex/utls v1.8.0
github.com/mholt/acmez/v3 v3.1.2 github.com/mholt/acmez/v3 v3.1.2
github.com/miekg/dns v1.1.67 github.com/miekg/dns v1.1.67
github.com/oschwald/maxminddb-golang v1.13.1 github.com/oschwald/maxminddb-golang v1.13.1
@@ -26,17 +26,17 @@ require (
github.com/sagernet/fswatch v0.1.1 github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.8 github.com/sagernet/gomobile v0.1.8
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 github.com/sagernet/quic-go v0.52.0-beta.1
github.com/sagernet/sing v0.7.18 github.com/sagernet/sing v0.7.6-0.20250815070458-d33ece7a184f
github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-mux v0.3.3
github.com/sagernet/sing-quic v0.5.3 github.com/sagernet/sing-quic v0.5.0
github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.7.13 github.com/sagernet/sing-tun v0.7.0-beta.1
github.com/sagernet/sing-vmess v0.2.7 github.com/sagernet/sing-vmess v0.2.6
github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/smux v1.5.34-mod.2
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2 github.com/sagernet/tailscale v1.80.3-mod.5
github.com/sagernet/wireguard-go v0.0.1-beta.7 github.com/sagernet/wireguard-go v0.0.1-beta.7
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1

54
go.sum
View File

@@ -8,8 +8,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc= github.com/anytls/sing-anytls v0.0.8 h1:1u/fnH1HoeeMV5mX7/eUOjLBvPdkd1UJRmXiRi6Vymc=
github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= github.com/anytls/sing-anytls v0.0.8/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU= github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
@@ -28,6 +28,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
@@ -121,10 +122,10 @@ github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg= github.com/metacubex/utls v1.8.0 h1:mSYi6FMnmc5riARl5UZDmWVy710z+P5b7xuGW0lV9ac=
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= github.com/metacubex/utls v1.8.0/go.mod h1:FdjYzVfCtgtna19hX0ER1Xsa5uJInwdQ4IcaaI98lEQ=
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0= github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
@@ -163,28 +164,29 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12pfEGScygnOxjTL56w= github.com/sagernet/quic-go v0.52.0-beta.1 h1:hWkojLg64zjV+MJOvJU/kOeWndm3tiEfBLx5foisszs=
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4= github.com/sagernet/quic-go v0.52.0-beta.1/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E= github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.7.6-0.20250815070458-d33ece7a184f h1:HIBo8l+tsS3wLwuI1E56uRTQw46QytXSUpZTP3vwG/U=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing v0.7.6-0.20250815070458-d33ece7a184f/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=
github.com/sagernet/sing-quic v0.5.3 h1:K937DKJN98xqyztijRkLJqbBfyV4rEZcYxFyP3EBikU= github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.5.3/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI= github.com/sagernet/sing-quic v0.5.0 h1:jNLIyVk24lFPvu8A4x+ZNEnZdI+Tg1rp7eCJ6v0Csak=
github.com/sagernet/sing-quic v0.5.0/go.mod h1:SAv/qdeDN+75msGG5U5ZIwG+3Ua50jVIKNrRSY8pkx0=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.7.13 h1:GuUBCymnGV/2jyxPae3eM9HlX1d7FaNxM97EqktDfy0= github.com/sagernet/sing-tun v0.7.0-beta.1 h1:mBIFXYAnGO5ey/HcCYanqnBx61E7yF8zTFGRZonGYmY=
github.com/sagernet/sing-tun v0.7.13/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM= github.com/sagernet/sing-tun v0.7.0-beta.1/go.mod h1:AHJuRrLbNRJuivuFZ2VhXwDj4ViYp14szG5EkkKAqRQ=
github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk= github.com/sagernet/sing-vmess v0.2.6 h1:1c4dGzeGy0kpBXXrT1sgiMZtHhdJylIT8eWrGhJYZec=
github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs= github.com/sagernet/sing-vmess v0.2.6/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8= github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2 h1:MO7s4ni2bSfAOhcan2rdQSWCztkMXmqyg6jYPZp8bEE= github.com/sagernet/tailscale v1.80.3-mod.5 h1:7V7z+p2C//TGtff20pPnDCt3qP6uFyY62peJoKF9z/A=
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI= github.com/sagernet/tailscale v1.80.3-mod.5/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI= github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI=
github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo= github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
@@ -194,7 +196,14 @@ github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wx
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
@@ -275,6 +284,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

View File

@@ -5,7 +5,6 @@ import (
"context" "context"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
) )
@@ -61,40 +60,37 @@ func checkOptions(options *Options) error {
func checkInbounds(inbounds []Inbound) error { func checkInbounds(inbounds []Inbound) error {
seen := make(map[string]bool) seen := make(map[string]bool)
for i, inbound := range inbounds { for _, inbound := range inbounds {
tag := inbound.Tag if inbound.Tag == "" {
if tag == "" { continue
tag = F.ToString(i)
} }
if seen[tag] { if seen[inbound.Tag] {
return E.New("duplicate inbound tag: ", tag) return E.New("duplicate inbound tag: ", inbound.Tag)
} }
seen[tag] = true seen[inbound.Tag] = true
} }
return nil return nil
} }
func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error { func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error {
seen := make(map[string]bool) seen := make(map[string]bool)
for i, outbound := range outbounds { for _, outbound := range outbounds {
tag := outbound.Tag if outbound.Tag == "" {
if tag == "" { continue
tag = F.ToString(i)
} }
if seen[tag] { if seen[outbound.Tag] {
return E.New("duplicate outbound/endpoint tag: ", tag) return E.New("duplicate outbound/endpoint tag: ", outbound.Tag)
} }
seen[tag] = true seen[outbound.Tag] = true
} }
for i, endpoint := range endpoints { for _, endpoint := range endpoints {
tag := endpoint.Tag if endpoint.Tag == "" {
if tag == "" { continue
tag = F.ToString(i)
} }
if seen[tag] { if seen[endpoint.Tag] {
return E.New("duplicate outbound/endpoint tag: ", tag) return E.New("duplicate outbound/endpoint tag: ", endpoint.Tag)
} }
seen[tag] = true seen[endpoint.Tag] = true
} }
return nil return nil
} }

View File

@@ -67,42 +67,46 @@ func (r Rule) IsValid() bool {
} }
type RawDefaultRule struct { type RawDefaultRule struct {
Inbound badoption.Listable[string] `json:"inbound,omitempty"` Inbound badoption.Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"` IPVersion int `json:"ip_version,omitempty"`
Network badoption.Listable[string] `json:"network,omitempty"` Network badoption.Listable[string] `json:"network,omitempty"`
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
Protocol badoption.Listable[string] `json:"protocol,omitempty"` Protocol badoption.Listable[string] `json:"protocol,omitempty"`
Client badoption.Listable[string] `json:"client,omitempty"` Client badoption.Listable[string] `json:"client,omitempty"`
Domain badoption.Listable[string] `json:"domain,omitempty"` Domain badoption.Listable[string] `json:"domain,omitempty"`
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
Geosite badoption.Listable[string] `json:"geosite,omitempty"` Geosite badoption.Listable[string] `json:"geosite,omitempty"`
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
GeoIP badoption.Listable[string] `json:"geoip,omitempty"` GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"` IPIsPrivate bool `json:"ip_is_private,omitempty"`
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
Port badoption.Listable[uint16] `json:"port,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"`
PortRange badoption.Listable[string] `json:"port_range,omitempty"` PortRange badoption.Listable[string] `json:"port_range,omitempty"`
ProcessName badoption.Listable[string] `json:"process_name,omitempty"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
PackageName badoption.Listable[string] `json:"package_name,omitempty"` PackageName badoption.Listable[string] `json:"package_name,omitempty"`
User badoption.Listable[string] `json:"user,omitempty"` User badoption.Listable[string] `json:"user,omitempty"`
UserID badoption.Listable[int32] `json:"user_id,omitempty"` UserID badoption.Listable[int32] `json:"user_id,omitempty"`
ClashMode string `json:"clash_mode,omitempty"` ClashMode string `json:"clash_mode,omitempty"`
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` InterfaceAddress *badjson.TypedMap[string, badoption.Listable[badoption.Prefixable]] `json:"interface_address,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[badoption.Prefixable]] `json:"network_interface_address,omitempty"`
Invert bool `json:"invert,omitempty"` DefaultInterfaceAddress badoption.Listable[badoption.Prefixable] `json:"default_interface_address,omitempty"`
PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
Invert bool `json:"invert,omitempty"`
// Deprecated: renamed to rule_set_ip_cidr_match_source // Deprecated: renamed to rule_set_ip_cidr_match_source
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`

View File

@@ -68,45 +68,48 @@ func (r DNSRule) IsValid() bool {
} }
type RawDefaultDNSRule struct { type RawDefaultDNSRule struct {
Inbound badoption.Listable[string] `json:"inbound,omitempty"` Inbound badoption.Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"` IPVersion int `json:"ip_version,omitempty"`
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
Network badoption.Listable[string] `json:"network,omitempty"` Network badoption.Listable[string] `json:"network,omitempty"`
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
Protocol badoption.Listable[string] `json:"protocol,omitempty"` Protocol badoption.Listable[string] `json:"protocol,omitempty"`
Domain badoption.Listable[string] `json:"domain,omitempty"` Domain badoption.Listable[string] `json:"domain,omitempty"`
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
Geosite badoption.Listable[string] `json:"geosite,omitempty"` Geosite badoption.Listable[string] `json:"geosite,omitempty"`
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
GeoIP badoption.Listable[string] `json:"geoip,omitempty"` GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"` IPIsPrivate bool `json:"ip_is_private,omitempty"`
IPAcceptAny bool `json:"ip_accept_any,omitempty"` IPAcceptAny bool `json:"ip_accept_any,omitempty"`
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
Port badoption.Listable[uint16] `json:"port,omitempty"` Port badoption.Listable[uint16] `json:"port,omitempty"`
PortRange badoption.Listable[string] `json:"port_range,omitempty"` PortRange badoption.Listable[string] `json:"port_range,omitempty"`
ProcessName badoption.Listable[string] `json:"process_name,omitempty"` ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
PackageName badoption.Listable[string] `json:"package_name,omitempty"` PackageName badoption.Listable[string] `json:"package_name,omitempty"`
User badoption.Listable[string] `json:"user,omitempty"` User badoption.Listable[string] `json:"user,omitempty"`
UserID badoption.Listable[int32] `json:"user_id,omitempty"` UserID badoption.Listable[int32] `json:"user_id,omitempty"`
Outbound badoption.Listable[string] `json:"outbound,omitempty"` Outbound badoption.Listable[string] `json:"outbound,omitempty"`
ClashMode string `json:"clash_mode,omitempty"` ClashMode string `json:"clash_mode,omitempty"`
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` InterfaceAddress *badjson.TypedMap[string, badoption.Listable[badoption.Prefixable]] `json:"interface_address,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[badoption.Prefixable]] `json:"network_interface_address,omitempty"`
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` DefaultInterfaceAddress badoption.Listable[badoption.Prefixable] `json:"default_interface_address,omitempty"`
Invert bool `json:"invert,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
Invert bool `json:"invert,omitempty"`
// Deprecated: renamed to rule_set_ip_cidr_match_source // Deprecated: renamed to rule_set_ip_cidr_match_source
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`

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