Compare commits

..

58 Commits

Author SHA1 Message Date
世界
5cca8893c9 Add Surge MITM and scripts 2025-03-25 10:25:37 +08:00
世界
58fdae33bb documentation: Bump version 2025-03-24 20:40:33 +08:00
世界
29ad1e3888 Fail when default DNS server not found 2025-03-24 20:40:33 +08:00
世界
799a005ead Update gVisor to 20250319.0 2025-03-24 20:40:32 +08:00
世界
0d5737dded release: Do not build tailscale on iOS and tvOS 2025-03-24 20:40:32 +08:00
世界
6b6f02ea0f Explicitly reject detour to empty direct outbounds 2025-03-24 20:40:32 +08:00
世界
6c0785eb50 Ignore UDP offload error 2025-03-24 20:40:31 +08:00
世界
74f9bec2ba Add netns support 2025-03-24 20:40:31 +08:00
世界
405560622a Add wildcard name support for predefined records 2025-03-24 20:40:31 +08:00
世界
b5740dae0e Remove map usage in options 2025-03-24 20:40:31 +08:00
世界
4c8b868553 Fix unhandled DNS loop 2025-03-24 20:40:30 +08:00
世界
967940cfcc Add wildcard-sni support for shadow-tls inbound 2025-03-24 20:40:30 +08:00
世界
236e0765a5 Fix Tailscale DNS 2025-03-24 20:40:30 +08:00
k9982874
fcc5ec8a5d Add ntp protocol sniffing 2025-03-24 20:40:30 +08:00
世界
3012c47695 option: Fix marshal legacy DNS options 2025-03-24 20:40:29 +08:00
世界
2bc58c5f11 Make domain_resolver optional when only one DNS server is configured 2025-03-24 20:40:29 +08:00
世界
d5e174c2a8 Fix DNS lookup context pollution 2025-03-24 20:40:29 +08:00
世界
5f5d5f8b3d Fix http3 DNS server connecting to wrong address 2025-03-24 20:40:29 +08:00
Restia-Ashbell
87f122b2ab documentation: Fix typo 2025-03-24 20:40:28 +08:00
anytls
c7cc28d61f Update sing-anytls
Co-authored-by: anytls <anytls>
2025-03-24 20:40:28 +08:00
k9982874
ba8b85f0d7 Fix hosts DNS server 2025-03-24 20:40:28 +08:00
世界
3419df90e2 Fix UDP DNS server crash 2025-03-24 20:40:28 +08:00
世界
3348c65c9b documentation: Fix missing ip_accept_any DNS rule option 2025-03-24 20:40:27 +08:00
世界
c9d6848eae Fix anytls dialer usage 2025-03-24 20:40:27 +08:00
世界
9615f48327 Move predefined DNS server to rule action 2025-03-24 20:40:26 +08:00
世界
1823bbde91 Fix domain resolver on direct outbound 2025-03-24 20:40:26 +08:00
Zephyruso
3834c1f5cd Fix missing AnyTLS display name 2025-03-24 20:40:26 +08:00
anytls
fc3424af15 Update sing-anytls
Co-authored-by: anytls <anytls>
2025-03-24 20:40:26 +08:00
Estel
f031d759ae documentation: Fix typo
Signed-off-by: Estel <callmebedrockdigger@gmail.com>
2025-03-24 20:40:25 +08:00
TargetLocked
e1fc66f072 Fix parsing legacy DNS options 2025-03-24 20:40:25 +08:00
世界
00a0911252 Fix DNS fallback 2025-03-24 20:40:25 +08:00
世界
e16c021308 documentation: Fix missing hosts DNS server 2025-03-24 20:40:25 +08:00
anytls
9b5d9588ab Add MinIdleSession option to AnyTLS outbound
Co-authored-by: anytls <anytls>
2025-03-24 20:40:24 +08:00
ReleTor
7a1fb6b825 documentation: Minor fixes 2025-03-24 20:40:24 +08:00
libtry486
fa64180e49 documentation: Fix typo
fix typo

Signed-off-by: libtry486 <89328481+libtry486@users.noreply.github.com>
2025-03-24 20:40:23 +08:00
Alireza Ahmadi
9319e58a61 Fix Outbound deadlock 2025-03-24 20:40:23 +08:00
世界
f2f50e7f79 documentation: Fix AnyTLS doc 2025-03-24 20:40:23 +08:00
anytls
3ea305b882 Add AnyTLS protocol 2025-03-24 20:40:23 +08:00
世界
1c4bcefa8f Migrate to stdlib ECH support 2025-03-24 20:40:22 +08:00
世界
e5d12c9b72 Add fallback local DNS server for iOS 2025-03-24 20:40:22 +08:00
世界
6a75487b34 Get darwin local DNS server from libresolv 2025-03-24 20:40:22 +08:00
世界
fa4ed37d23 Improve resolve action 2025-03-24 20:40:21 +08:00
世界
219cc6a944 Fix toolchain version 2025-03-24 20:40:21 +08:00
世界
0ff9e3833e Add back port hopping to hysteria 1 2025-03-24 20:40:20 +08:00
世界
a71b583a1a Update dependencies 2025-03-24 20:40:20 +08:00
xchacha20-poly1305
1462208d68 Remove single quotes of raw Moziila certs 2025-03-24 20:40:20 +08:00
世界
16babfc8ac Add Tailscale endpoint 2025-03-24 20:40:19 +08:00
世界
a35c5d0fae Build legacy binaries with latest Go 2025-03-24 20:40:19 +08:00
世界
259dae4399 documentation: Remove outdated icons 2025-03-24 20:40:19 +08:00
世界
96dabc048f documentation: Certificate store 2025-03-24 20:40:18 +08:00
世界
833dbe7cb8 documentation: TLS fragment 2025-03-24 20:40:18 +08:00
世界
b810f34c30 documentation: Outbound domain resolver 2025-03-24 20:40:18 +08:00
世界
e0a2e9412b documentation: Refactor DNS 2025-03-24 20:40:17 +08:00
世界
f43974dd8c Add certificate store 2025-03-24 20:40:17 +08:00
世界
6bfad0200e Add TLS fragment support 2025-03-24 20:40:17 +08:00
世界
045fb72420 refactor: Outbound domain resolver 2025-03-24 20:40:17 +08:00
世界
ab4ea6ed68 refactor: DNS 2025-03-24 20:40:17 +08:00
世界
cf72196a1a Bump version 2025-03-24 20:38:42 +08:00
193 changed files with 8374 additions and 2051 deletions

View File

@@ -1,13 +1,11 @@
-s dir -s dir
--name sing-box --name sing-box
--category net --category net
--license GPL-3.0-or-later --license GPLv3-or-later
--description "The universal proxy platform." --description "The universal proxy platform."
--url "https://sing-box.sagernet.org/" --url "https://sing-box.sagernet.org/"
--maintainer "nekohasekai <contact-git@sekai.icu>" --maintainer "nekohasekai <contact-git@sekai.icu>"
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues" --deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes
--config-files /etc/sing-box/config.json
release/config/config.json=/etc/sing-box/config.json release/config/config.json=/etc/sing-box/config.json

View File

@@ -1,30 +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>"
--no-deb-generate-changes
--config-files /etc/config/sing-box
--config-files /etc/sing-box/config.json
--depends ca-bundle
--depends kmod-inet-diag
--depends kmod-tun
--depends firewall4
--before-remove release/config/openwrt.prerm
release/config/config.json=/etc/sing-box/config.json
release/config/openwrt.conf=/etc/config/sing-box
release/config/openwrt.init=/etc/init.d/sing-box
release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box
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

28
.github/deb2ipk.sh vendored
View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683
set -e -o pipefail
PROJECT=$(dirname "$0")/../..
TMP_PATH=`mktemp -d`
cp $2 $TMP_PATH
pushd $TMP_PATH
DEB_NAME=`ls *.deb`
ar x $DEB_NAME
mkdir control
pushd control
tar xf ../control.tar.gz
rm md5sums
sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i
cat control
tar czf ../control.tar.gz ./*
popd
DEB_NAME=${DEB_NAME%.deb}
tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary
popd
cp $TMP_PATH/$DEB_NAME.ipk $3
rm -r $TMP_PATH

View File

@@ -46,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.24.2 go-version: ^1.24
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -55,7 +55,7 @@ jobs:
- name: Calculate version - name: Calculate version
if: github.event_name != 'workflow_dispatch' if: github.event_name != 'workflow_dispatch'
run: |- run: |-
go run -v ./cmd/internal/read_tag --ci --nightly go run -v ./cmd/internal/read_tag --nightly
- name: Set outputs - name: Set outputs
id: outputs id: outputs
run: |- run: |-
@@ -68,38 +68,31 @@ jobs:
- calculate_version - calculate_version
strategy: strategy:
matrix: matrix:
os: [ linux, windows, darwin, android ]
arch: [ "386", amd64, arm64 ]
legacy_go: [ false ]
include: include:
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" } - { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" } - { os: linux, arch: "386", debian: i386, rpm: i386 }
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" } - { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" } - { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" } - { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" } - { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl, openwrt: "arm_cortex-a5_vfpv4 arm_cortex-a7_neon-vfpv4 arm_cortex-a7_vfpv4 arm_cortex-a8_vfpv3 arm_cortex-a9_neon arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4" } - { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
- { os: linux, arch: s390x, debian: s390x, rpm: s390x } - { os: linux, arch: s390x, debian: s390x, rpm: s390x }
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le } - { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" } - { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" } - { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
- { os: windows, arch: amd64 }
- { os: windows, arch: amd64, legacy_go: true }
- { os: windows, arch: "386" }
- { os: windows, arch: "386", legacy_go: true } - { os: windows, arch: "386", legacy_go: true }
- { os: windows, arch: arm64 } - { os: windows, arch: amd64, legacy_go: true }
- { os: darwin, arch: amd64 }
- { os: darwin, arch: arm64 }
- { os: android, arch: "386", ndk: "i686-linux-android21" }
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" } - { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" } - { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" } exclude:
- { os: android, arch: "386", ndk: "i686-linux-android21" } - { os: darwin, arch: "386" }
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
@@ -109,7 +102,7 @@ jobs:
if: ${{ ! matrix.legacy_go }} if: ${{ ! matrix.legacy_go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.2 go-version: ^1.24
- name: Cache Legacy Go - name: Cache Legacy Go
if: matrix.require_legacy_go if: matrix.require_legacy_go
id: cache-legacy-go id: cache-legacy-go
@@ -140,7 +133,10 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale' TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api'
if [ ! '${{ matrix.legacy_go }}' = 'true' ]; then
TAGS="${TAGS},with_ech"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build - name: Build
if: matrix.os != 'android' if: matrix.os != 'android'
@@ -154,10 +150,7 @@ jobs:
CGO_ENABLED: "0" CGO_ENABLED: "0"
GOOS: ${{ matrix.os }} GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }} GOARCH: ${{ matrix.arch }}
GO386: ${{ matrix.go386 }}
GOARM: ${{ matrix.goarm }} GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
GOMIPS64: ${{ matrix.gomips }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Android - name: Build Android
if: matrix.os == 'android' if: matrix.os == 'android'
@@ -177,31 +170,21 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set name - name: Set name
run: |- run: |-
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}" ARM_VERSION=$([ -n '${{ matrix.goarm}}' ] && echo 'v${{ matrix.goarm}}' || true)
if [[ -n "${{ matrix.goarm }}" ]]; then LEGACY=$([ '${{ matrix.legacy_go }}' = 'true' ] && echo "-legacy" || true)
DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}" DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}${ARM_VERSION}${LEGACY}"
elif [[ -n "${{ matrix.go386 }}" && "${{ matrix.go386 }}" != 'sse2' ]]; then PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}"
DIR_NAME="${DIR_NAME}-${{ matrix.go386 }}"
elif [[ -n "${{ matrix.gomips }}" && "${{ matrix.gomips }}" != 'hardfloat' ]]; then
DIR_NAME="${DIR_NAME}-${{ matrix.gomips }}"
elif [[ "${{ matrix.legacy_go }}" == 'true' ]]; then
DIR_NAME="${DIR_NAME}-legacy"
fi
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}" echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}" echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB - name: Package DEB
if: matrix.debian != '' if: matrix.debian != ''
run: | run: |
set -xeuo pipefail set -xeuo pipefail
sudo gem install fpm sudo gem install fpm
sudo apt-get update
sudo apt-get install -y debsigs sudo apt-get install -y debsigs
cp .fpm_systemd .fpm
fpm -t deb \ fpm -t deb \
-v "$PKG_VERSION" \ -v "${{ needs.calculate_version.outputs.version }}" \
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \ -p "dist/${PKG_NAME}.deb" \
--architecture ${{ matrix.debian }} \ --architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box dist/sing-box=/usr/bin/sing-box
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff' curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
@@ -216,10 +199,9 @@ jobs:
run: |- run: |-
set -xeuo pipefail set -xeuo pipefail
sudo gem install fpm sudo gem install fpm
cp .fpm_systemd .fpm
fpm -t rpm \ fpm -t rpm \
-v "$PKG_VERSION" \ -v "${{ needs.calculate_version.outputs.version }}" \
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.rpm }}.rpm" \ -p "dist/${PKG_NAME}.rpm" \
--architecture ${{ matrix.rpm }} \ --architecture ${{ matrix.rpm }} \
dist/sing-box=/usr/bin/sing-box dist/sing-box=/usr/bin/sing-box
cat > $HOME/.rpmmacros <<EOF cat > $HOME/.rpmmacros <<EOF
@@ -235,37 +217,20 @@ jobs:
run: |- run: |-
set -xeuo pipefail set -xeuo pipefail
sudo gem install fpm sudo gem install fpm
sudo apt-get update
sudo apt-get install -y libarchive-tools sudo apt-get install -y libarchive-tools
cp .fpm_systemd .fpm
fpm -t pacman \ fpm -t pacman \
-v "$PKG_VERSION" \ -v "${{ needs.calculate_version.outputs.version }}" \
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \ -p "dist/${PKG_NAME}.pkg.tar.zst" \
--architecture ${{ matrix.pacman }} \ --architecture ${{ matrix.pacman }} \
dist/sing-box=/usr/bin/sing-box dist/sing-box=/usr/bin/sing-box
- name: Package OpenWrt
if: matrix.openwrt != ''
run: |-
set -xeuo pipefail
sudo gem install fpm
cp .fpm_openwrt .fpm
fpm -t deb \
-v "$PKG_VERSION" \
-p "dist/openwrt.deb" \
--architecture all \
dist/sing-box=/usr/bin/sing-box
for architecture in ${{ matrix.openwrt }}; do
.github/deb2ipk.sh "$architecture" "dist/openwrt.deb" "dist/sing-box_${{ needs.calculate_version.outputs.version }}_openwrt_${architecture}.ipk"
done
rm "dist/openwrt.deb"
- name: Archive - name: Archive
run: | run: |
set -xeuo pipefail set -xeuo pipefail
cd dist cd dist
mkdir -p "${DIR_NAME}" mkdir -p "${DIR_NAME}"
cp ../LICENSE "${DIR_NAME}" cp ../LICENSE "${DIR_NAME}"
if [ '${{ matrix.os }}' = 'windows' ]; then if [ '${{ matrix.os }}' = 'windoes' ]; then
cp sing-box "${DIR_NAME}/sing-box.exe" cp sing-box.exe "${DIR_NAME}"
zip -r "${DIR_NAME}.zip" "${DIR_NAME}" zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
else else
cp sing-box "${DIR_NAME}" cp sing-box "${DIR_NAME}"
@@ -277,7 +242,7 @@ jobs:
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_go && '-legacy' || '' }} name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
path: "dist" path: "dist"
build_android: build_android:
name: Build Android name: Build Android
@@ -294,7 +259,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.2 go-version: ^1.24
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -351,9 +316,9 @@ jobs:
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
- name: Prepare upload - name: Prepare upload
run: |- run: |-
mkdir -p dist mkdir -p dist/release
cp clients/android/app/build/outputs/apk/play/release/*.apk dist cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@@ -374,7 +339,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.2 go-version: ^1.24
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -472,7 +437,7 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.2 go-version: ^1.24
- name: Setup Xcode stable - name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next' if: matrix.if && github.ref == 'refs/heads/main-next'
run: |- run: |-
@@ -549,13 +514,10 @@ jobs:
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version) MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV" echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
- name: Update version
if: matrix.if && matrix.name != 'iOS'
run: |-
go run -v ./cmd/internal/update_apple_version --ci
- name: Build - name: Build
if: matrix.if if: matrix.if
run: |- run: |-
go run -v ./cmd/internal/update_apple_version --ci
cd clients/apple cd clients/apple
xcodebuild archive \ xcodebuild archive \
-scheme "${{ matrix.scheme }}" \ -scheme "${{ matrix.scheme }}" \
@@ -604,9 +566,9 @@ jobs:
zip -r SFM.dSYMs.zip dSYMs zip -r SFM.dSYMs.zip dSYMs
popd popd
mkdir -p dist mkdir -p dist/release
cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg" cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg"
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip" cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip"
- name: Upload image - name: Upload image
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch' if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View File

@@ -28,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.2 go-version: ^1.24
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v6
with: with:

View File

@@ -25,7 +25,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.2 go-version: ^1.24
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -34,7 +34,7 @@ jobs:
- name: Calculate version - name: Calculate version
if: github.event_name != 'workflow_dispatch' if: github.event_name != 'workflow_dispatch'
run: |- run: |-
go run -v ./cmd/internal/read_tag --ci --nightly go run -v ./cmd/internal/read_tag --nightly
- name: Set outputs - name: Set outputs
id: outputs id: outputs
run: |- run: |-
@@ -66,7 +66,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24.2 go-version: ^1.24
- 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
@@ -80,7 +80,10 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api' TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api'
if [ ! '${{ matrix.legacy_go }}' = 'true' ]; then
TAGS="${TAGS},with_ech"
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build - name: Build
run: | run: |
@@ -106,24 +109,17 @@ jobs:
if: contains(needs.calculate_version.outputs.version, '-') if: contains(needs.calculate_version.outputs.version, '-')
run: |- run: |-
echo "NAME=sing-box-beta" >> "$GITHUB_ENV" echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
- name: Set version
run: |-
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB - name: Package DEB
if: matrix.debian != '' if: matrix.debian != ''
run: | run: |
set -xeuo pipefail set -xeuo pipefail
sudo gem install fpm sudo gem install fpm
sudo apt-get install -y debsigs sudo apt-get install -y debsigs
cp .fpm_systemd .fpm
fpm -t deb \ fpm -t deb \
--name "${NAME}" \ -v "${{ needs.calculate_version.outputs.version }}" \
-v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \ -p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
--architecture ${{ matrix.debian }} \ --architecture ${{ matrix.debian }} \
dist/sing-box=/usr/bin/sing-box dist/sing-box=/usr/bin/${NAME}
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff' curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff' sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
rm -rf $HOME/.gnupg rm -rf $HOME/.gnupg
@@ -136,13 +132,11 @@ jobs:
run: |- run: |-
set -xeuo pipefail set -xeuo pipefail
sudo gem install fpm sudo gem install fpm
cp .fpm_systemd .fpm
fpm -t rpm \ fpm -t rpm \
--name "${NAME}" \ -v "${{ needs.calculate_version.outputs.version }}" \
-v "$PKG_VERSION" \
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \ -p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
--architecture ${{ matrix.rpm }} \ --architecture ${{ matrix.rpm }} \
dist/sing-box=/usr/bin/sing-box dist/sing-box=/usr/bin/${NAME}
cat > $HOME/.rpmmacros <<EOF cat > $HOME/.rpmmacros <<EOF
%_gpg_name ${{ secrets.GPG_KEY_ID }} %_gpg_name ${{ secrets.GPG_KEY_ID }}
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }} %_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
@@ -181,4 +175,6 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Publish packages - name: Publish packages
run: |- run: |-
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/ wget -O fury-cli.deb https://github.com/gemfury/cli/releases/download/v0.23.0/fury-cli_0.23.0_linux_amd64.deb
sudo dpkg -i fury-cli.deb
fury migrate dist --as=sagernet --api-token ${{ secrets.FURY_TOKEN }}

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
/.idea/ /.idea/
/vendor/ /vendor/
/*.json /*.json
/*.js
/*.srs /*.srs
/*.db /*.db
/site/ /site/

View File

@@ -28,8 +28,10 @@ run:
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_script
issues: issues:
exclude-dirs: exclude-dirs:

View File

@@ -15,6 +15,7 @@ builds:
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
@@ -49,7 +50,7 @@ nfpms:
contents: contents:
- src: release/config/config.json - src: release/config/config.json
dst: /etc/sing-box/config.json dst: /etc/sing-box/config.json
type: "config|noreplace" type: config
- src: release/config/sing-box.service - src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service

View File

@@ -17,9 +17,11 @@ builds:
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
- with_script
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
- GOTOOLCHAIN=local - GOTOOLCHAIN=local
@@ -46,9 +48,11 @@ builds:
- with_dhcp - with_dhcp
- with_wireguard - with_wireguard
- with_utls - with_utls
- with_reality_server
- with_acme - with_acme
- with_clash_api - with_clash_api
- with_tailscale - with_tailscale
- with_script
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
- GOROOT={{ .Env.GOPATH }}/go_legacy - GOROOT={{ .Env.GOPATH }}/go_legacy
@@ -130,7 +134,7 @@ nfpms:
contents: contents:
- src: release/config/config.json - src: release/config/config.json
dst: /etc/sing-box/config.json dst: /etc/sing-box/config.json
type: "config|noreplace" type: config
- src: release/config/sing-box.service - src: release/config/sing-box.service
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service

View File

@@ -13,7 +13,7 @@ RUN set -ex \
&& export COMMIT=$(git rev-parse --short HEAD) \ && export COMMIT=$(git rev-parse --short HEAD) \
&& export VERSION=$(go run ./cmd/internal/read_tag) \ && export VERSION=$(go run ./cmd/internal/read_tag) \
&& go build -v -trimpath -tags \ && go build -v -trimpath -tags \
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale" \ "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api" \
-o /go/bin/sing-box \ -o /go/bin/sing-box \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \ -ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
./cmd/sing-box ./cmd/sing-box

View File

@@ -1,14 +1,14 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_clash_api,with_quic,with_utls,with_tailscale TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls,with_tailscale,with_script
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls,with_reality_server
GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH) GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag) VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid=" PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)" MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
MAIN = ./cmd/sing-box MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH) PREFIX ?= $(shell go env GOPATH)
@@ -24,7 +24,7 @@ ci_build:
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
generate_completions: generate_completions:
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN) go run -v --tags $(TAGS),generate,generate_completions $(MAIN)
install: install:
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN) go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
@@ -226,8 +226,8 @@ lib:
go run ./cmd/internal/build_libbox -target ios go run ./cmd/internal/build_libbox -target ios
lib_install: lib_install:
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.6 go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.5
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.6 go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.5
docs: docs:
venv/bin/mkdocs serve venv/bin/mkdocs serve

View File

@@ -10,6 +10,9 @@ import (
type CertificateStore interface { type CertificateStore interface {
LifecycleService LifecycleService
Pool() *x509.CertPool Pool() *x509.CertPool
TLSDecryptionEnabled() bool
TLSDecryptionCertificate() *x509.Certificate
TLSDecryptionPrivateKey() any
} }
func RootPoolFromContext(ctx context.Context) *x509.CertPool { func RootPoolFromContext(ctx context.Context) *x509.CertPool {

View File

@@ -52,6 +52,10 @@ type CacheFile interface {
StoreGroupExpand(group string, expand bool) error StoreGroupExpand(group string, expand bool) error
LoadRuleSet(tag string) *SavedBinary LoadRuleSet(tag string) *SavedBinary
SaveRuleSet(tag string, set *SavedBinary) error SaveRuleSet(tag string, set *SavedBinary) error
LoadScript(tag string) *SavedBinary
SaveScript(tag string, script *SavedBinary) error
SurgePersistentStoreRead(key string) string
SurgePersistentStoreWrite(key string, value string) error
} }
type SavedBinary struct { type SavedBinary struct {

View File

@@ -2,6 +2,8 @@ package adapter
import ( import (
"context" "context"
"crypto/tls"
"net/http"
"net/netip" "net/netip"
"time" "time"
@@ -58,6 +60,8 @@ type InboundContext struct {
Client string Client string
SniffContext any SniffContext any
PacketSniffError error PacketSniffError error
HTTPRequest *http.Request
ClientHello *tls.ClientHelloInfo
// cache // cache
@@ -74,6 +78,7 @@ type InboundContext struct {
UDPTimeout time.Duration UDPTimeout time.Duration
TLSFragment bool TLSFragment bool
TLSFragmentFallbackDelay time.Duration TLSFragmentFallbackDelay time.Duration
MITM *option.MITMRouteOptions
NetworkStrategy *C.NetworkStrategy NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType NetworkType []C.InterfaceType

View File

@@ -1,6 +1,8 @@
package adapter package adapter
import E "github.com/sagernet/sing/common/exceptions" import (
E "github.com/sagernet/sing/common/exceptions"
)
type StartStage uint8 type StartStage uint8
@@ -45,6 +47,9 @@ type LifecycleService interface {
func Start(stage StartStage, services ...Lifecycle) error { func Start(stage StartStage, services ...Lifecycle) error {
for _, service := range services { for _, service := range services {
if service == nil {
continue
}
err := service.Start(stage) err := service.Start(stage)
if err != nil { if err != nil {
return err return err

13
adapter/mitm.go Normal file
View File

@@ -0,0 +1,13 @@
package adapter
import (
"context"
"net"
N "github.com/sagernet/sing/common/network"
)
type MITMEngine interface {
Lifecycle
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
}

View File

@@ -24,7 +24,7 @@ type Router interface {
RuleSet(tag string) (RuleSet, bool) RuleSet(tag string) (RuleSet, bool)
NeedWIFIState() bool NeedWIFIState() bool
Rules() []Rule Rules() []Rule
AppendTracker(tracker ConnectionTracker) SetTracker(tracker ConnectionTracker)
ResetNetwork() ResetNetwork()
} }

54
adapter/script.go Normal file
View File

@@ -0,0 +1,54 @@
package adapter
import (
"context"
"net/http"
"sync"
"time"
)
type ScriptManager interface {
Lifecycle
Scripts() []Script
Script(name string) (Script, bool)
SurgeCache() *SurgeInMemoryCache
}
type SurgeInMemoryCache struct {
sync.RWMutex
Data map[string]string
}
type Script interface {
Type() string
Tag() string
StartContext(ctx context.Context, startContext *HTTPStartContext) error
PostStart() error
Close() error
}
type SurgeScript interface {
Script
ExecuteGeneric(ctx context.Context, scriptType string, timeout time.Duration, arguments []string) error
ExecuteHTTPRequest(ctx context.Context, timeout time.Duration, request *http.Request, body []byte, binaryBody bool, arguments []string) (*HTTPRequestScriptResult, error)
ExecuteHTTPResponse(ctx context.Context, timeout time.Duration, request *http.Request, response *http.Response, body []byte, binaryBody bool, arguments []string) (*HTTPResponseScriptResult, error)
}
type HTTPRequestScriptResult struct {
URL string
Headers http.Header
Body []byte
Response *HTTPRequestScriptResponse
}
type HTTPRequestScriptResponse struct {
Status int
Headers http.Header
Body []byte
}
type HTTPResponseScriptResult struct {
Status int
Headers http.Header
Body []byte
}

55
box.go
View File

@@ -23,9 +23,11 @@ import (
"github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/mitm"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/direct" "github.com/sagernet/sing-box/protocol/direct"
"github.com/sagernet/sing-box/route" "github.com/sagernet/sing-box/route"
"github.com/sagernet/sing-box/script"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
@@ -48,6 +50,8 @@ type Box struct {
dnsRouter *dns.Router dnsRouter *dns.Router
connection *route.ConnectionManager connection *route.ConnectionManager
router *route.Router router *route.Router
script *script.Manager
mitm adapter.MITMEngine //*mitm.Engine
services []adapter.LifecycleService services []adapter.LifecycleService
done chan struct{} done chan struct{}
} }
@@ -143,18 +147,12 @@ func New(options Options) (*Box, error) {
} }
var services []adapter.LifecycleService var services []adapter.LifecycleService
certificateOptions := common.PtrValueOrDefault(options.Certificate) certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), common.PtrValueOrDefault(options.Certificate))
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem || if err != nil {
len(certificateOptions.Certificate) > 0 || return nil, err
len(certificateOptions.CertificatePath) > 0 ||
len(certificateOptions.CertificateDirectoryPath) > 0 {
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
if err != nil {
return nil, err
}
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
services = append(services, certificateStore)
} }
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
services = append(services, certificateStore)
routeOptions := common.PtrValueOrDefault(options.Route) routeOptions := common.PtrValueOrDefault(options.Route)
dnsOptions := common.PtrValueOrDefault(options.DNS) dnsOptions := common.PtrValueOrDefault(options.DNS)
@@ -173,7 +171,7 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize network manager") return nil, E.Cause(err, "initialize network manager")
} }
service.MustRegister[adapter.NetworkManager](ctx, networkManager) service.MustRegister[adapter.NetworkManager](ctx, networkManager)
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection")) connectionManager := route.NewConnectionManager(ctx, logFactory.NewLogger("connection"))
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager) service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions) router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
service.MustRegister[adapter.Router](ctx, router) service.MustRegister[adapter.Router](ctx, router)
@@ -181,8 +179,8 @@ func New(options Options) (*Box, error) {
if err != nil { if err != nil {
return nil, E.Cause(err, "initialize router") return nil, E.Cause(err, "initialize router")
} }
ntpOptions := common.PtrValueOrDefault(options.NTP)
var timeService *tls.TimeServiceWrapper var timeService *tls.TimeServiceWrapper
ntpOptions := common.PtrValueOrDefault(options.NTP)
if ntpOptions.Enabled { if ntpOptions.Enabled {
timeService = new(tls.TimeServiceWrapper) timeService = new(tls.TimeServiceWrapper)
service.MustRegister[ntp.TimeService](ctx, timeService) service.MustRegister[ntp.TimeService](ctx, timeService)
@@ -296,6 +294,11 @@ func New(options Options) (*Box, error) {
"local", "local",
option.LocalDNSServerOptions{}, option.LocalDNSServerOptions{},
))) )))
scriptManager, err := script.NewManager(ctx, logFactory, options.Scripts)
if err != nil {
return nil, E.Cause(err, "initialize script manager")
}
service.MustRegister[adapter.ScriptManager](ctx, scriptManager)
if platformInterface != nil { if platformInterface != nil {
err = platformInterface.Initialize(networkManager) err = platformInterface.Initialize(networkManager)
if err != nil { if err != nil {
@@ -314,7 +317,7 @@ func New(options Options) (*Box, error) {
if err != nil { if err != nil {
return nil, E.Cause(err, "create clash-server") return nil, E.Cause(err, "create clash-server")
} }
router.AppendTracker(clashServer) router.SetTracker(clashServer)
service.MustRegister[adapter.ClashServer](ctx, clashServer) service.MustRegister[adapter.ClashServer](ctx, clashServer)
services = append(services, clashServer) services = append(services, clashServer)
} }
@@ -324,7 +327,7 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "create v2ray-server") return nil, E.Cause(err, "create v2ray-server")
} }
if v2rayServer.StatsService() != nil { if v2rayServer.StatsService() != nil {
router.AppendTracker(v2rayServer.StatsService()) router.SetTracker(v2rayServer.StatsService())
services = append(services, v2rayServer) services = append(services, v2rayServer)
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer) service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
} }
@@ -345,6 +348,16 @@ func New(options Options) (*Box, error) {
timeService.TimeService = ntpService timeService.TimeService = ntpService
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service")) services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
} }
mitmOptions := common.PtrValueOrDefault(options.MITM)
var mitmEngine adapter.MITMEngine
if mitmOptions.Enabled {
engine, err := mitm.NewEngine(ctx, logFactory.NewLogger("mitm"), mitmOptions)
if err != nil {
return nil, E.Cause(err, "create MITM engine")
}
service.MustRegister[adapter.MITMEngine](ctx, engine)
mitmEngine = engine
}
return &Box{ return &Box{
network: networkManager, network: networkManager,
endpoint: endpointManager, endpoint: endpointManager,
@@ -354,6 +367,8 @@ func New(options Options) (*Box, error) {
dnsRouter: dnsRouter, dnsRouter: dnsRouter,
connection: connectionManager, connection: connectionManager,
router: router, router: router,
script: scriptManager,
mitm: mitmEngine,
createdAt: createdAt, createdAt: createdAt,
logFactory: logFactory, logFactory: logFactory,
logger: logFactory.Logger(), logger: logFactory.Logger(),
@@ -412,11 +427,11 @@ func (s *Box) preStart() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint) err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router) err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router, s.script, s.mitm)
if err != nil { if err != nil {
return err return err
} }
@@ -440,7 +455,7 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint) err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.inbound, s.endpoint)
if err != nil { if err != nil {
return err return err
} }
@@ -448,7 +463,7 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint) err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
if err != nil { if err != nil {
return err return err
} }
@@ -467,7 +482,7 @@ func (s *Box) Close() error {
close(s.done) close(s.done)
} }
err := common.Close( err := common.Close(
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network, s.inbound, s.outbound, s.endpoint, s.mitm, s.script, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
) )
for _, lifecycleService := range s.services { for _, lifecycleService := range s.services {
err = E.Append(err, lifecycleService.Close(), func(err error) error { err = E.Append(err, lifecycleService.Close(), func(err error) error {

View File

@@ -59,7 +59,7 @@ func init() {
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=") sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag) debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api") sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_script")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack") iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
memcTags = append(memcTags, "with_tailscale") memcTags = append(memcTags, "with_tailscale")
debugTags = append(debugTags, "debug") debugTags = append(debugTags, "debug")

View File

@@ -5,49 +5,40 @@ import (
"os" "os"
"github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/common/badversion"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
) )
var ( var nightly bool
flagRunInCI bool
flagRunNightly bool
)
func init() { func init() {
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI") flag.BoolVar(&nightly, "nightly", false, "Print nightly tag")
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
} }
func main() { func main() {
flag.Parse() flag.Parse()
var ( if nightly {
versionStr string version, err := build_shared.ReadTagVersionRev()
err error
)
if flagRunNightly {
var version badversion.Version
version, err = build_shared.ReadTagVersion()
if err == nil {
versionStr = version.String()
}
} else {
versionStr, err = build_shared.ReadTag()
}
if flagRunInCI {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
var versionStr string
if version.PreReleaseIdentifier != "" {
versionStr = version.VersionString() + "-nightly"
} else {
version.Patch++
versionStr = version.VersionString() + "-nightly"
}
err = setGitHubEnv("version", versionStr) err = setGitHubEnv("version", versionStr)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
tag, err := build_shared.ReadTag()
if err != nil { if err != nil {
log.Error(err) log.Error(err)
os.Stdout.WriteString("unknown\n") os.Stdout.WriteString("unknown\n")
} else { } else {
os.Stdout.WriteString(versionStr + "\n") os.Stdout.WriteString(tag + "\n")
} }
} }
} }

View File

@@ -0,0 +1,121 @@
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"strings"
"time"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json"
"github.com/spf13/cobra"
"software.sslmate.com/src/go-pkcs12"
)
var (
flagGenerateCAName string
flagGenerateCAPKCS12Password string
flagGenerateOutput string
)
var commandGenerateCAKeyPair = &cobra.Command{
Use: "ca-keypair",
Short: "Generate CA key pair",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
err := generateCAKeyPair()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAName, "name", "n", "", "Set custom CA name")
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAPKCS12Password, "p12-password", "p", "", "Set custom PKCS12 password")
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateOutput, "output", "o", ".", "Set output directory")
commandGenerate.AddCommand(commandGenerateCAKeyPair)
}
func generateCAKeyPair() error {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return err
}
spkiASN1, err := x509.MarshalPKIXPublicKey(privateKey.Public())
var spki struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}
_, err = asn1.Unmarshal(spkiASN1, &spki)
if err != nil {
return err
}
skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
var caName string
if flagGenerateCAName != "" {
caName = flagGenerateCAName
} else {
caName = "sing-box Generated CA " + strings.ToUpper(hex.EncodeToString(skid[:4]))
}
caTpl := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{caName},
CommonName: caName,
},
SubjectKeyId: skid[:],
NotAfter: time.Now().AddDate(10, 0, 0),
NotBefore: time.Now(),
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLenZero: true,
}
publicDer, err := x509.CreateCertificate(rand.Reader, caTpl, caTpl, privateKey.Public(), privateKey)
var caPassword string
if flagGenerateCAPKCS12Password != "" {
caPassword = flagGenerateCAPKCS12Password
} else {
caPassword = strings.ToUpper(hex.EncodeToString(skid[:4]))
}
caTpl.Raw = publicDer
p12Bytes, err := pkcs12.Modern.Encode(privateKey, caTpl, nil, caPassword)
if err != nil {
return err
}
privateDer, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return err
}
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".pem"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".private.pem"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer}), 0o644)
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".crt"), publicDer, 0o644)
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".p12"), p12Bytes, 0o644)
var tlsDecryptionOptions option.TLSDecryptionOptions
tlsDecryptionOptions.Enabled = true
tlsDecryptionOptions.KeyPair = base64.StdEncoding.EncodeToString(p12Bytes)
tlsDecryptionOptions.KeyPairPassword = caPassword
var certificateOptions option.CertificateOptions
certificateOptions.TLSDecryption = &tlsDecryptionOptions
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(certificateOptions)
}

View File

@@ -5,7 +5,6 @@ import (
"context" "context"
"io" "io"
"os" "os"
"path/filepath"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
@@ -57,14 +56,6 @@ func ruleSetMatch(sourcePath string, domain string) error {
if err != nil { if err != nil {
return E.Cause(err, "read rule-set") return E.Cause(err, "read rule-set")
} }
if flagRuleSetMatchFormat == "" {
switch filepath.Ext(sourcePath) {
case ".json":
flagRuleSetMatchFormat = C.RuleSetFormatSource
case ".srs":
flagRuleSetMatchFormat = C.RuleSetFormatBinary
}
}
var ruleSet option.PlainRuleSetCompat var ruleSet option.PlainRuleSetCompat
switch flagRuleSetMatchFormat { switch flagRuleSetMatchFormat {
case C.RuleSetFormatSource: case C.RuleSetFormatSource:

View File

@@ -1,13 +1,6 @@
package main package main
import ( import (
"errors"
"os"
"github.com/sagernet/sing-box"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -19,36 +12,5 @@ var commandTools = &cobra.Command{
} }
func init() { func init() {
commandTools.PersistentFlags().StringVarP(&commandToolsFlagOutbound, "outbound", "o", "", "Use specified tag instead of default outbound")
mainCommand.AddCommand(commandTools) mainCommand.AddCommand(commandTools)
} }
func createPreStartedClient() (*box.Box, error) {
options, err := readConfigAndMerge()
if err != nil {
if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" {
return nil, err
}
}
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
if err != nil {
return nil, E.Cause(err, "create service")
}
err = instance.PreStart()
if err != nil {
return nil, E.Cause(err, "start service")
}
return instance, nil
}
func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) {
if outboundTag == "" {
return instance.Outbound().Default(), nil
} else {
outbound, loaded := instance.Outbound().Outbound(outboundTag)
if !loaded {
return nil, E.New("outbound not found: ", outboundTag)
}
return outbound, nil
}
}

View File

@@ -1,73 +0,0 @@
package main
import (
"context"
"os"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/task"
"github.com/spf13/cobra"
)
var commandConnectFlagNetwork string
var commandConnect = &cobra.Command{
Use: "connect <address>",
Short: "Connect to an address",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := connect(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandConnect.Flags().StringVarP(&commandConnectFlagNetwork, "network", "n", "tcp", "network type")
commandTools.AddCommand(commandConnect)
}
func connect(address string) error {
switch N.NetworkName(commandConnectFlagNetwork) {
case N.NetworkTCP, N.NetworkUDP:
default:
return E.Cause(N.ErrUnknownNetwork, commandConnectFlagNetwork)
}
instance, err := createPreStartedClient()
if err != nil {
return err
}
defer instance.Close()
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return err
}
conn, err := dialer.DialContext(context.Background(), commandConnectFlagNetwork, M.ParseSocksaddr(address))
if err != nil {
return E.Cause(err, "connect to server")
}
var group task.Group
group.Append("upload", func(ctx context.Context) error {
return common.Error(bufio.Copy(conn, os.Stdin))
})
group.Append("download", func(ctx context.Context) error {
return common.Error(bufio.Copy(os.Stdout, conn))
})
group.Cleanup(func() {
conn.Close()
})
err = group.Run(context.Background())
if E.IsClosed(err) {
log.Info(err)
} else {
log.Error(err)
}
return nil
}

View File

@@ -1,115 +0,0 @@
package main
import (
"context"
"errors"
"io"
"net"
"net/http"
"net/url"
"os"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/spf13/cobra"
)
var commandFetch = &cobra.Command{
Use: "fetch",
Short: "Fetch an URL",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := fetch(args)
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandTools.AddCommand(commandFetch)
}
var (
httpClient *http.Client
http3Client *http.Client
)
func fetch(args []string) error {
instance, err := createPreStartedClient()
if err != nil {
return err
}
defer instance.Close()
httpClient = &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return nil, err
}
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
ForceAttemptHTTP2: true,
},
}
defer httpClient.CloseIdleConnections()
if C.WithQUIC {
err = initializeHTTP3Client(instance)
if err != nil {
return err
}
defer http3Client.CloseIdleConnections()
}
for _, urlString := range args {
var parsedURL *url.URL
parsedURL, err = url.Parse(urlString)
if err != nil {
return err
}
switch parsedURL.Scheme {
case "":
parsedURL.Scheme = "http"
fallthrough
case "http", "https":
err = fetchHTTP(httpClient, parsedURL)
if err != nil {
return err
}
case "http3":
if !C.WithQUIC {
return C.ErrQUICNotIncluded
}
parsedURL.Scheme = "https"
err = fetchHTTP(http3Client, parsedURL)
if err != nil {
return err
}
default:
return E.New("unsupported scheme: ", parsedURL.Scheme)
}
}
return nil
}
func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
request, err := http.NewRequest("GET", parsedURL.String(), nil)
if err != nil {
return err
}
request.Header.Add("User-Agent", "curl/7.88.0")
response, err := httpClient.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
_, err = bufio.Copy(os.Stdout, response.Body)
if errors.Is(err, io.EOF) {
return nil
}
return err
}

View File

@@ -1,36 +0,0 @@
//go:build with_quic
package main
import (
"context"
"crypto/tls"
"net/http"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
box "github.com/sagernet/sing-box"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func initializeHTTP3Client(instance *box.Box) error {
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return err
}
http3Client = &http.Client{
Transport: &http3.Transport{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
destination := M.ParseSocksaddr(addr)
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
if dErr != nil {
return nil, dErr
}
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
},
},
}
return nil
}

View File

@@ -1,18 +0,0 @@
//go:build !with_quic
package main
import (
"net/url"
"os"
box "github.com/sagernet/sing-box"
)
func initializeHTTP3Client(instance *box.Box) error {
return os.ErrInvalid
}
func fetchHTTP3(parsedURL *url.URL) error {
return os.ErrInvalid
}

View File

@@ -0,0 +1,108 @@
package main
import (
"encoding/pem"
"errors"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/shell"
"github.com/spf13/cobra"
)
var commandInstallCACertificate = &cobra.Command{
Use: "install-ca <path to certificate>",
Short: "Install CA certificate to system",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
err := installCACertificate(args[0])
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandTools.AddCommand(commandInstallCACertificate)
}
func installCACertificate(path string) error {
switch runtime.GOOS {
case "windows":
return shell.Exec("powershell", "-Command", "Import-Certificate -FilePath \""+path+"\" -CertStoreLocation Cert:\\LocalMachine\\Root").Attach().Run()
case "darwin":
return shell.Exec("sudo", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", path).Attach().Run()
case "linux":
updateCertPath, updateCertPathNotFoundErr := exec.LookPath("update-ca-certificates")
if updateCertPathNotFoundErr == nil {
publicDer, err := os.ReadFile(path)
if err != nil {
return err
}
err = os.MkdirAll("/usr/local/share/ca-certificates", 0o755)
if err != nil {
if errors.Is(err, os.ErrPermission) {
log.Info("Try running with sudo")
return shell.Exec("sudo", os.Args...).Attach().Run()
}
return err
}
fileName := filepath.Base(updateCertPath)
if !strings.HasSuffix(fileName, ".crt") {
fileName = fileName + ".crt"
}
filePath, _ := filepath.Abs(filepath.Join("/usr/local/share/ca-certificates", fileName))
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
if err != nil {
if errors.Is(err, os.ErrPermission) {
log.Info("Try running with sudo")
return shell.Exec("sudo", os.Args...).Attach().Run()
}
return err
}
log.Info("certificate written to " + filePath + "\n")
err = shell.Exec(updateCertPath).Attach().Run()
if err != nil {
return err
}
log.Info("certificate installed")
return nil
}
updateTrustPath, updateTrustPathNotFoundErr := exec.LookPath("update-ca-trust")
if updateTrustPathNotFoundErr == nil {
publicDer, err := os.ReadFile(path)
if err != nil {
return err
}
fileName := filepath.Base(updateTrustPath)
fileExt := filepath.Ext(path)
if fileExt != "" {
fileName = fileName[:len(fileName)-len(fileExt)]
}
filePath, _ := filepath.Abs(filepath.Join("/etc/pki/ca-trust/source/anchors/", fileName+".pem"))
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
if err != nil {
if errors.Is(err, os.ErrPermission) {
log.Info("Try running with sudo")
return shell.Exec("sudo", os.Args...).Attach().Run()
}
return err
}
log.Info("certificate written to " + filePath + "\n")
err = shell.Exec(updateTrustPath, "extract").Attach().Run()
if err != nil {
return err
}
log.Info("certificate installed")
}
return E.New("update-ca-certificates or update-ca-trust not found")
default:
return E.New("unsupported operating system: ", runtime.GOOS)
}
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
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"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -39,20 +40,11 @@ func init() {
} }
func syncTime() error { func syncTime() error {
instance, err := createPreStartedClient()
if err != nil {
return err
}
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return err
}
defer instance.Close()
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer) serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
if serverAddress.Port == 0 { if serverAddress.Port == 0 {
serverAddress.Port = 123 serverAddress.Port = 123
} }
response, err := ntp.Exchange(context.Background(), dialer, serverAddress) response, err := ntp.Exchange(context.Background(), N.SystemDialer, serverAddress)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -7,8 +7,7 @@ import (
_ "unsafe" _ "unsafe"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/utls"
"github.com/metacubex/utls"
) )
func init() { func init() {
@@ -25,8 +24,8 @@ func init() {
}) })
} }
//go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord //go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord
func utlsReadRecord(c *tls.Conn) error func utlsReadRecord(c *tls.Conn) error
//go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage //go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage
func utlsHandlePostHandshakeMessage(c *tls.Conn) error func utlsHandlePostHandshakeMessage(c *tls.Conn) error

View File

@@ -3,6 +3,7 @@ package certificate
import ( import (
"context" "context"
"crypto/x509" "crypto/x509"
"encoding/base64"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
@@ -16,6 +17,8 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"software.sslmate.com/src/go-pkcs12"
) )
var _ adapter.CertificateStore = (*Store)(nil) var _ adapter.CertificateStore = (*Store)(nil)
@@ -27,6 +30,9 @@ type Store struct {
certificatePaths []string certificatePaths []string
certificateDirectoryPaths []string certificateDirectoryPaths []string
watcher *fswatch.Watcher watcher *fswatch.Watcher
tlsDecryptionEnabled bool
tlsDecryptionPrivateKey any
tlsDecryptionCertificate *x509.Certificate
} }
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) { func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
@@ -90,6 +96,19 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
if err != nil { if err != nil {
return nil, E.Cause(err, "initializing certificate store") return nil, E.Cause(err, "initializing certificate store")
} }
if options.TLSDecryption != nil && options.TLSDecryption.Enabled {
pfxBytes, err := base64.StdEncoding.DecodeString(options.TLSDecryption.KeyPair)
if err != nil {
return nil, E.Cause(err, "decode key pair base64 bytes")
}
privateKey, certificate, err := pkcs12.Decode(pfxBytes, options.TLSDecryption.KeyPairPassword)
if err != nil {
return nil, E.Cause(err, "decode key pair")
}
store.tlsDecryptionEnabled = true
store.tlsDecryptionPrivateKey = privateKey
store.tlsDecryptionCertificate = certificate
}
return store, nil return store, nil
} }
@@ -183,3 +202,15 @@ func isSameDirSymlink(f fs.DirEntry, dir string) bool {
target, err := os.Readlink(filepath.Join(dir, f.Name())) target, err := os.Readlink(filepath.Join(dir, f.Name()))
return err == nil && !strings.Contains(target, "/") return err == nil && !strings.Contains(target, "/")
} }
func (s *Store) TLSDecryptionEnabled() bool {
return s.tlsDecryptionEnabled
}
func (s *Store) TLSDecryptionCertificate() *x509.Certificate {
return s.tlsDecryptionCertificate
}
func (s *Store) TLSDecryptionPrivateKey() any {
return s.tlsDecryptionPrivateKey
}

View File

@@ -71,8 +71,18 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} }
if options.RoutingMark > 0 { if options.RoutingMark > 0 {
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false)) dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(options.RoutingMark)))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false)) listener.Control = control.Append(listener.Control, control.RoutingMark(uint32(options.RoutingMark)))
}
if networkManager != nil {
autoRedirectOutputMark := networkManager.AutoRedirectOutputMark()
if autoRedirectOutputMark > 0 {
if options.RoutingMark > 0 {
return nil, E.New("`routing_mark` is conflict with `tun.auto_redirect` with `tun.route_[_exclude]_address_set")
}
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
}
} }
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
if disableDefaultBind || options.TCPFastOpen { if disableDefaultBind || options.TCPFastOpen {
@@ -117,8 +127,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} }
} }
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 { if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true)) dialer.Control = control.Append(dialer.Control, control.RoutingMark(defaultOptions.RoutingMark))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true)) listener.Control = control.Append(listener.Control, control.RoutingMark(defaultOptions.RoutingMark))
} }
} }
if options.ReuseAddr { if options.ReuseAddr {
@@ -200,22 +210,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
}, nil }, nil
} }
func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefault bool) control.Func {
if networkManager == nil {
return control.RoutingMark(mark)
}
return func(network, address string, conn syscall.RawConn) error {
if networkManager.AutoRedirectOutputMark() != 0 {
if isDefault {
return E.New("`route.default_mark` is conflict with `tun.auto_redirect`")
} else {
return E.New("`routing_mark` is conflict with `tun.auto_redirect`")
}
}
return control.RoutingMark(mark)(network, address, conn)
}
}
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) { func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
if !address.IsValid() { if !address.IsValid() {
return nil, E.New("invalid address") return nil, E.New("invalid address")
@@ -341,17 +335,7 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
} }
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) { func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
udpListener := d.udpListener return d.udpListener.ListenPacket(context.Background(), network, address)
udpListener.Control = control.Append(udpListener.Control, func(network, address string, conn syscall.RawConn) error {
for _, wgControlFn := range WgControlFns {
err := wgControlFn(network, address, conn)
if err != nil {
return err
}
}
return nil
})
return udpListener.ListenPacket(context.Background(), network, address)
} }
func trackConn(conn net.Conn, err error) (net.Conn, error) { func trackConn(conn net.Conn, err error) (net.Conn, error) {

View File

@@ -24,7 +24,6 @@ type Options struct {
ResolverOnDetour bool ResolverOnDetour bool
NewDialer bool NewDialer bool
LegacyDNSDialer bool LegacyDNSDialer bool
DirectOutbound bool
} }
// TODO: merge with NewWithOptions // TODO: merge with NewWithOptions
@@ -83,7 +82,6 @@ func NewWithOptions(options Options) (N.Dialer, error) {
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) { dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
//nolint:staticcheck //nolint:staticcheck
strategy = C.DomainStrategy(dialOptions.DomainStrategy) strategy = C.DomainStrategy(dialOptions.DomainStrategy)
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
} }
server = dialOptions.DomainResolver.Server server = dialOptions.DomainResolver.Server
dnsQueryOptions = adapter.DNSQueryOptions{ dnsQueryOptions = adapter.DNSQueryOptions{
@@ -96,31 +94,22 @@ func NewWithOptions(options Options) (N.Dialer, error) {
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.DirectResolver { } else if options.DirectResolver {
return nil, E.New("missing domain resolver for domain server address") return nil, E.New("missing domain resolver for domain server address")
} else { } else if defaultOptions.DomainResolver != "" {
if defaultOptions.DomainResolver != "" { dnsQueryOptions = defaultOptions.DomainResolveOptions
dnsQueryOptions = defaultOptions.DomainResolveOptions transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver) if !loaded {
if !loaded { return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
}
dnsQueryOptions.Transport = transport
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else {
transports := dnsTransport.Transports()
if len(transports) < 2 {
dnsQueryOptions.Transport = dnsTransport.Default()
} else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address")
} else if !options.DirectOutbound {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
}
} }
if dnsQueryOptions.Transport = transport
//nolint:staticcheck resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) { } else if options.NewDialer {
//nolint:staticcheck return nil, E.New("missing domain resolver for domain server address")
dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy) } else {
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions) transports := dnsTransport.Transports()
if len(transports) < 2 {
dnsQueryOptions.Transport = dnsTransport.Default()
} else {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
} }
} }
dialer = NewResolveDialer( dialer = NewResolveDialer(

158
common/humanize/bytes.go Normal file
View File

@@ -0,0 +1,158 @@
package humanize
import (
"fmt"
"math"
"strconv"
"strings"
"unicode"
)
// IEC Sizes.
// kibis of bits
const (
Byte = 1 << (iota * 10)
KiByte
MiByte
GiByte
TiByte
PiByte
EiByte
)
// SI Sizes.
const (
IByte = 1
KByte = IByte * 1000
MByte = KByte * 1000
GByte = MByte * 1000
TByte = GByte * 1000
PByte = TByte * 1000
EByte = PByte * 1000
)
var defaultSizeTable = map[string]uint64{
"b": Byte,
"kib": KiByte,
"kb": KByte,
"mib": MiByte,
"mb": MByte,
"gib": GiByte,
"gb": GByte,
"tib": TiByte,
"tb": TByte,
"pib": PiByte,
"pb": PByte,
"eib": EiByte,
"eb": EByte,
// Without suffix
"": Byte,
"ki": KiByte,
"k": KByte,
"mi": MiByte,
"m": MByte,
"gi": GiByte,
"g": GByte,
"ti": TiByte,
"t": TByte,
"pi": PiByte,
"p": PByte,
"ei": EiByte,
"e": EByte,
}
var memorysSizeTable = map[string]uint64{
"b": Byte,
"kb": KiByte,
"mb": MiByte,
"gb": GiByte,
"tb": TiByte,
"pb": PiByte,
"eb": EiByte,
"": Byte,
"k": KiByte,
"m": MiByte,
"g": GiByte,
"t": TiByte,
"p": PiByte,
"e": EiByte,
}
var (
defaultSizes = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
iSizes = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
)
func Bytes(s uint64) string {
return humanateBytes(s, 1000, defaultSizes)
}
func MemoryBytes(s uint64) string {
return humanateBytes(s, 1024, defaultSizes)
}
func IBytes(s uint64) string {
return humanateBytes(s, 1024, iSizes)
}
func logn(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}
func humanateBytes(s uint64, base float64, sizes []string) string {
if s < 10 {
return fmt.Sprintf("%d B", s)
}
e := math.Floor(logn(float64(s), base))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
func ParseBytes(s string) (uint64, error) {
return parseBytes0(s, defaultSizeTable)
}
func ParseMemoryBytes(s string) (uint64, error) {
return parseBytes0(s, memorysSizeTable)
}
func parseBytes0(s string, sizeTable map[string]uint64) (uint64, error) {
lastDigit := 0
hasComma := false
for _, r := range s {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
if r == ',' {
hasComma = true
}
lastDigit++
}
num := s[:lastDigit]
if hasComma {
num = strings.Replace(num, ",", "", -1)
}
f, err := strconv.ParseFloat(num, 64)
if err != nil {
return 0, err
}
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
if m, ok := sizeTable[extra]; ok {
f *= float64(m)
if f >= math.MaxUint64 {
return 0, fmt.Errorf("too large: %v", s)
}
return uint64(f), nil
}
return 0, fmt.Errorf("unhandled size name: %v", extra)
}

View File

@@ -37,12 +37,6 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
return udpConn, err return udpConn, err
} }
func (l *Listener) DialContext(dialer net.Dialer, ctx context.Context, network string, address string) (net.Conn, error) {
return ListenNetworkNamespace[net.Conn](l.listenOptions.NetNs, func() (net.Conn, error) {
return dialer.DialContext(ctx, network, address)
})
}
func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) { func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) {
return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) { return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
return listenConfig.ListenPacket(ctx, network, address) return listenConfig.ListenPacket(ctx, network, address)

View File

@@ -9,7 +9,6 @@ import (
"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"
E "github.com/sagernet/sing/common/exceptions"
) )
const ( const (
@@ -24,25 +23,20 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R
var first byte var first byte
err := binary.Read(reader, binary.BigEndian, &first) err := binary.Read(reader, binary.BigEndian, &first)
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
if first != 19 { if first != 19 {
return os.ErrInvalid return os.ErrInvalid
} }
const header = "BitTorrent protocol"
var protocol [19]byte var protocol [19]byte
var n int _, err = reader.Read(protocol[:])
n, err = reader.Read(protocol[:])
if string(protocol[:n]) != header[:n] {
return os.ErrInvalid
}
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
if n < 19 { if string(protocol[:]) != "BitTorrent protocol" {
return ErrNeedMoreData return os.ErrInvalid
} }
metadata.Protocol = C.ProtocolBitTorrent metadata.Protocol = C.ProtocolBitTorrent
@@ -73,9 +67,7 @@ func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) err
if err != nil { if err != nil {
return err return err
} }
if extension > 0x04 {
return os.ErrInvalid
}
var length byte var length byte
err = binary.Read(reader, binary.BigEndian, &length) err = binary.Read(reader, binary.BigEndian, &length)
if err != nil { if err != nil {

View File

@@ -32,27 +32,6 @@ func TestSniffBittorrent(t *testing.T) {
} }
} }
func TestSniffIncompleteBittorrent(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("13426974546f7272656e74")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotBittorrent(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("13426974546f7272656e75")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffUTP(t *testing.T) { func TestSniffUTP(t *testing.T) {
t.Parallel() t.Parallel()
@@ -92,19 +71,3 @@ func TestSniffUDPTracker(t *testing.T) {
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol) require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
} }
} }
func TestSniffNotUTP(t *testing.T) {
t.Parallel()
packets := []string{
"0102736470696e674958d580121500000000000079aaed6717a39c27b07c0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
}
for _, pkt := range packets {
pkt, err := hex.DecodeString(pkt)
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.UTP(context.TODO(), &metadata, pkt)
require.Error(t, err)
}
}

View File

@@ -5,11 +5,14 @@ import (
"encoding/binary" "encoding/binary"
"io" "io"
"os" "os"
"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/common"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/task"
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
) )
@@ -18,40 +21,35 @@ func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundCon
var length uint16 var length uint16
err := binary.Read(reader, binary.BigEndian, &length) err := binary.Read(reader, binary.BigEndian, &length)
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return os.ErrInvalid
} }
if length < 12 { if length == 0 {
return os.ErrInvalid return os.ErrInvalid
} }
buffer := buf.NewSize(int(length)) buffer := buf.NewSize(int(length))
defer buffer.Release() defer buffer.Release()
var n int readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
n, err = buffer.ReadFullFrom(reader, buffer.FreeLen()) var readTask task.Group
packet := buffer.Bytes() readTask.Append0(func(ctx context.Context) error {
if n > 2 && packet[2]&0x80 != 0 { // QR return common.Error(buffer.ReadFullFrom(reader, buffer.FreeLen()))
return os.ErrInvalid })
} err = readTask.Run(readCtx)
if n > 5 && packet[4] == 0 && packet[5] == 0 { // QDCOUNT cancel()
return os.ErrInvalid
}
for i := 6; i < 10; i++ {
// ANCOUNT, NSCOUNT
if n > i && packet[i] != 0 {
return os.ErrInvalid
}
}
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
return DomainNameQuery(readCtx, metadata, packet) return DomainNameQuery(readCtx, metadata, buffer.Bytes())
} }
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
var msg mDNS.Msg var msg mDNS.Msg
err := msg.Unpack(packet) err := msg.Unpack(packet)
if err != nil || msg.Response || len(msg.Question) == 0 || len(msg.Answer) > 0 || len(msg.Ns) > 0 { if err != nil {
return err return err
} }
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
return os.ErrInvalid
}
metadata.Protocol = C.ProtocolDNS metadata.Protocol = C.ProtocolDNS
return nil return nil
} }

View File

@@ -1,53 +0,0 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
"github.com/stretchr/testify/require"
)
func TestSniffDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("740701000001000000000000012a06676f6f676c6503636f6d0000010001")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.DomainNameQuery(context.TODO(), &metadata, query)
require.NoError(t, err)
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
}
func TestSniffStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000001000000000000012a06676f6f676c6503636f6d0000010001")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.NoError(t, err)
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
}
func TestSniffIncompleteStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000001000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000000000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}

View File

@@ -3,12 +3,10 @@ package sniff
import ( import (
std_bufio "bufio" std_bufio "bufio"
"context" "context"
"errors"
"io" "io"
"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"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/http"
) )
@@ -16,13 +14,10 @@ import (
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
request, err := http.ReadRequest(std_bufio.NewReader(reader)) request, err := http.ReadRequest(std_bufio.NewReader(reader))
if err != nil { if err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) { return err
return E.Cause1(ErrNeedMoreData, err)
} else {
return err
}
} }
metadata.Protocol = C.ProtocolHTTP metadata.Protocol = C.ProtocolHTTP
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString() metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
metadata.HTTPRequest = request
return nil return nil
} }

View File

@@ -20,6 +20,8 @@ import (
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
) )
var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection")
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error { func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
reader := bytes.NewReader(packet) reader := bytes.NewReader(packet)
typeByte, err := reader.ReadByte() typeByte, err := reader.ReadByte()
@@ -306,7 +308,7 @@ find:
metadata.Protocol = C.ProtocolQUIC metadata.Protocol = C.ProtocolQUIC
metadata.Client = C.ClientChromium metadata.Client = C.ClientChromium
metadata.SniffContext = fragments metadata.SniffContext = fragments
return E.Cause1(ErrNeedMoreData, err) return ErrClientHelloFragmented
} }
metadata.Domain = fingerprint.ServerName metadata.Domain = fingerprint.ServerName
for metadata.Client == "" { for metadata.Client == "" {

View File

@@ -20,11 +20,11 @@ func TestSniffQUICChromeNew(t *testing.T) {
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.Equal(t, metadata.Client, C.ClientChromium) require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrNeedMoreData) require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894") pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
require.NoError(t, err) require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
require.ErrorIs(t, err, sniff.ErrNeedMoreData) require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f") pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f")
require.NoError(t, err) require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
@@ -40,7 +40,7 @@ func TestSniffQUICChromium(t *testing.T) {
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.Equal(t, metadata.Client, C.ClientChromium) require.Equal(t, metadata.Client, C.ClientChromium)
require.ErrorIs(t, err, sniff.ErrNeedMoreData) require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28") pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
require.NoError(t, err) require.NoError(t, err)
err = sniff.QUICClientHello(context.Background(), &metadata, pkt) err = sniff.QUICClientHello(context.Background(), &metadata, pkt)

View File

@@ -8,7 +8,6 @@ import (
"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"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/rw"
) )
@@ -16,7 +15,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktVersion uint8 var tpktVersion uint8
err := binary.Read(reader, binary.BigEndian, &tpktVersion) err := binary.Read(reader, binary.BigEndian, &tpktVersion)
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
if tpktVersion != 0x03 { if tpktVersion != 0x03 {
return os.ErrInvalid return os.ErrInvalid
@@ -25,7 +24,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktReserved uint8 var tpktReserved uint8
err = binary.Read(reader, binary.BigEndian, &tpktReserved) err = binary.Read(reader, binary.BigEndian, &tpktReserved)
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
if tpktReserved != 0x00 { if tpktReserved != 0x00 {
return os.ErrInvalid return os.ErrInvalid
@@ -34,7 +33,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var tpktLength uint16 var tpktLength uint16
err = binary.Read(reader, binary.BigEndian, &tpktLength) err = binary.Read(reader, binary.BigEndian, &tpktLength)
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
if tpktLength != 19 { if tpktLength != 19 {
@@ -44,7 +43,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var cotpLength uint8 var cotpLength uint8
err = binary.Read(reader, binary.BigEndian, &cotpLength) err = binary.Read(reader, binary.BigEndian, &cotpLength)
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
if cotpLength != 14 { if cotpLength != 14 {
@@ -54,7 +53,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var cotpTpduType uint8 var cotpTpduType uint8
err = binary.Read(reader, binary.BigEndian, &cotpTpduType) err = binary.Read(reader, binary.BigEndian, &cotpTpduType)
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
if cotpTpduType != 0xE0 { if cotpTpduType != 0xE0 {
return os.ErrInvalid return os.ErrInvalid
@@ -62,13 +61,13 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
err = rw.SkipN(reader, 5) err = rw.SkipN(reader, 5)
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
var rdpType uint8 var rdpType uint8
err = binary.Read(reader, binary.BigEndian, &rdpType) err = binary.Read(reader, binary.BigEndian, &rdpType)
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
if rdpType != 0x01 { if rdpType != 0x01 {
return os.ErrInvalid return os.ErrInvalid
@@ -76,12 +75,12 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
var rdpFlags uint8 var rdpFlags uint8
err = binary.Read(reader, binary.BigEndian, &rdpFlags) err = binary.Read(reader, binary.BigEndian, &rdpFlags)
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
var rdpLength uint8 var rdpLength uint8
err = binary.Read(reader, binary.BigEndian, &rdpLength) err = binary.Read(reader, binary.BigEndian, &rdpLength)
if err != nil { if err != nil {
return E.Cause1(ErrNeedMoreData, err) return err
} }
if rdpLength != 8 { if rdpLength != 8 {
return os.ErrInvalid return os.ErrInvalid

View File

@@ -3,7 +3,6 @@ package sniff
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"io" "io"
"net" "net"
"time" "time"
@@ -20,8 +19,6 @@ type (
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
) )
var ErrNeedMoreData = E.New("need more data")
func Skip(metadata *adapter.InboundContext) bool { func Skip(metadata *adapter.InboundContext) bool {
// skip server first protocols // skip server first protocols
switch metadata.Destination.Port { switch metadata.Destination.Port {
@@ -43,7 +40,7 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
timeout = C.ReadPayloadTimeout timeout = C.ReadPayloadTimeout
} }
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
var sniffError error var errors []error
for i := 0; ; i++ { for i := 0; ; i++ {
err := conn.SetReadDeadline(deadline) err := conn.SetReadDeadline(deadline)
if err != nil { if err != nil {
@@ -57,7 +54,7 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
} }
return E.Cause(err, "read payload") return E.Cause(err, "read payload")
} }
sniffError = nil errors = nil
for _, sniffer := range sniffers { for _, sniffer := range sniffers {
reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader { reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader {
return bytes.NewReader(it.Bytes()) return bytes.NewReader(it.Bytes())
@@ -66,23 +63,20 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
if err == nil { if err == nil {
return nil return nil
} }
sniffError = E.Errors(sniffError, err) errors = append(errors, err)
}
if !errors.Is(sniffError, ErrNeedMoreData) {
break
} }
} }
return sniffError return E.Errors(errors...)
} }
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error { func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
var sniffError []error var errors []error
for _, sniffer := range sniffers { for _, sniffer := range sniffers {
err := sniffer(ctx, metadata, packet) err := sniffer(ctx, metadata, packet)
if err == nil { if err == nil {
return nil return nil
} }
sniffError = append(sniffError, err) errors = append(errors, err)
} }
return E.Errors(sniffError...) return E.Errors(errors...)
} }

View File

@@ -5,27 +5,22 @@ import (
"context" "context"
"io" "io"
"os" "os"
"strings"
"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"
E "github.com/sagernet/sing/common/exceptions"
) )
func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error { func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
const sshPrefix = "SSH-2.0-" scanner := bufio.NewScanner(reader)
bReader := bufio.NewReader(reader) if !scanner.Scan() {
prefix, err := bReader.Peek(len(sshPrefix))
if string(prefix[:]) != sshPrefix[:len(prefix)] {
return os.ErrInvalid return os.ErrInvalid
} }
if err != nil { fistLine := scanner.Text()
return E.Cause1(ErrNeedMoreData, err) if !strings.HasPrefix(fistLine, "SSH-2.0-") {
} return os.ErrInvalid
fistLine, _, err := bReader.ReadLine()
if err != nil {
return err
} }
metadata.Protocol = C.ProtocolSSH metadata.Protocol = C.ProtocolSSH
metadata.Client = string(fistLine)[8:] metadata.Client = fistLine[8:]
return nil return nil
} }

View File

@@ -24,24 +24,3 @@ func TestSniffSSH(t *testing.T) {
require.Equal(t, C.ProtocolSSH, metadata.Protocol) require.Equal(t, C.ProtocolSSH, metadata.Protocol)
require.Equal(t, "dropbear", metadata.Client) require.Equal(t, "dropbear", metadata.Client)
} }
func TestSniffIncompleteSSH(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("5353482d322e30")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotSSH(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("5353482d322e31")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}

View File

@@ -3,13 +3,11 @@ package sniff
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"io" "io"
"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/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
) )
func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error { func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
@@ -23,11 +21,8 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
if clientHello != nil { if clientHello != nil {
metadata.Protocol = C.ProtocolTLS metadata.Protocol = C.ProtocolTLS
metadata.Domain = clientHello.ServerName metadata.Domain = clientHello.ServerName
metadata.ClientHello = clientHello
return nil return nil
} }
if errors.Is(err, io.ErrUnexpectedEOF) { return err
return E.Cause1(ErrNeedMoreData, err)
} else {
return err
}
} }

View File

@@ -18,7 +18,6 @@ type (
STDConfig = tls.Config STDConfig = tls.Config
STDConn = tls.Conn STDConn = tls.Conn
ConnectionState = tls.ConnectionState ConnectionState = tls.ConnectionState
CurveID = tls.CurveID
) )
func ParseTLSVersion(version string) (uint16, error) { func ParseTLSVersion(version string) (uint16, error) {

View File

@@ -10,8 +10,6 @@ import (
"net" "net"
"os" "os"
"strings" "strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
@@ -48,10 +46,7 @@ func parseECHClientConfig(ctx context.Context, options option.OutboundTLSOptions
tlsConfig.EncryptedClientHelloConfigList = block.Bytes tlsConfig.EncryptedClientHelloConfigList = block.Bytes
return &STDClientConfig{tlsConfig}, nil return &STDClientConfig{tlsConfig}, nil
} else { } else {
return &STDECHClientConfig{ return &STDECHClientConfig{STDClientConfig{tlsConfig}, service.FromContext[adapter.DNSRouter](ctx)}, nil
STDClientConfig: STDClientConfig{tlsConfig},
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
}, nil
} }
} }
@@ -104,28 +99,11 @@ func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
type STDECHClientConfig struct { type STDECHClientConfig struct {
STDClientConfig STDClientConfig
access sync.Mutex dnsRouter adapter.DNSRouter
dnsRouter adapter.DNSRouter
lastTTL time.Duration
lastUpdate time.Time
} }
func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) { func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
tlsConn, err := s.fetchAndHandshake(ctx, conn) if len(s.config.EncryptedClientHelloConfigList) == 0 {
if err != nil {
return nil, err
}
err = tlsConn.HandshakeContext(ctx)
if err != nil {
return nil, err
}
return tlsConn, nil
}
func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
s.access.Lock()
defer s.access.Unlock()
if len(s.config.EncryptedClientHelloConfigList) == 0 || s.lastTTL == 0 || time.Now().Sub(s.lastUpdate) > s.lastTTL {
message := &mDNS.Msg{ message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{ MsgHdr: mDNS.MsgHdr{
RecursionDesired: true, RecursionDesired: true,
@@ -145,7 +123,6 @@ func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Con
if response.Rcode != mDNS.RcodeSuccess { if response.Rcode != mDNS.RcodeSuccess {
return nil, E.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list") return nil, E.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list")
} }
match:
for _, rr := range response.Answer { for _, rr := range response.Answer {
switch resource := rr.(type) { switch resource := rr.(type) {
case *mDNS.HTTPS: case *mDNS.HTTPS:
@@ -155,23 +132,26 @@ func (s *STDECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Con
if err != nil { if err != nil {
return nil, E.Cause(err, "decode ECH config") return nil, E.Cause(err, "decode ECH config")
} }
s.lastTTL = time.Duration(rr.Header().Ttl) * time.Second
s.lastUpdate = time.Now()
s.config.EncryptedClientHelloConfigList = echConfigList s.config.EncryptedClientHelloConfigList = echConfigList
break match
} }
} }
} }
} }
if len(s.config.EncryptedClientHelloConfigList) == 0 { return nil, E.New("no ECH config found in DNS records")
return nil, E.New("no ECH config found in DNS records")
}
} }
return s.Client(conn) tlsConn, err := s.Client(conn)
if err != nil {
return nil, err
}
err = tlsConn.HandshakeContext(ctx)
if err != nil {
return nil, err
}
return tlsConn, nil
} }
func (s *STDECHClientConfig) Clone() Config { func (s *STDECHClientConfig) Clone() Config {
return &STDECHClientConfig{STDClientConfig: STDClientConfig{s.config.Clone()}, dnsRouter: s.dnsRouter, lastUpdate: s.lastUpdate} return &STDECHClientConfig{STDClientConfig{s.config.Clone()}, s.dnsRouter}
} }
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) { func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {

View File

@@ -1,5 +0,0 @@
//go:build with_ech
package tls
var _ int = "Due to the migration to stdlib, the separate `with_ech` build tag has been deprecated and is no longer needed, please update your build configuration."

View File

@@ -8,7 +8,10 @@ import (
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/pem" "encoding/pem"
"math/big" "math/big"
"net"
"time" "time"
M "github.com/sagernet/sing/common/metadata"
) )
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) { func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
@@ -35,17 +38,30 @@ func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func(
if err != nil { if err != nil {
return return
} }
template := &x509.Certificate{ var template *x509.Certificate
SerialNumber: serialNumber, if serverAddress := M.ParseAddr(serverName); serverAddress.IsValid() {
NotBefore: timeFunc().Add(time.Hour * -1), template = &x509.Certificate{
NotAfter: expire, SerialNumber: serialNumber,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, IPAddresses: []net.IP{serverAddress.AsSlice()},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, NotBefore: timeFunc().Add(time.Hour * -1),
BasicConstraintsValid: true, NotAfter: expire,
Subject: pkix.Name{ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
CommonName: serverName, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}, BasicConstraintsValid: true,
DNSNames: []string{serverName}, }
} else {
template = &x509.Certificate{
SerialNumber: serialNumber,
NotBefore: timeFunc().Add(time.Hour * -1),
NotAfter: expire,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
Subject: pkix.Name{
CommonName: serverName,
},
DNSNames: []string{serverName},
}
} }
if parent == nil { if parent == nil {
parent = template parent = template

View File

@@ -33,8 +33,8 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
aTLS "github.com/sagernet/sing/common/tls" aTLS "github.com/sagernet/sing/common/tls"
utls "github.com/sagernet/utls"
utls "github.com/metacubex/utls"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@@ -148,13 +148,9 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
if err != nil { if err != nil {
return nil, err return nil, err
} }
keyShareKeys := uConn.HandshakeState.State13.KeyShareKeys ecdheKey := uConn.HandshakeState.State13.EcdheKey
if keyShareKeys == nil {
return nil, E.New("nil KeyShareKeys")
}
ecdheKey := keyShareKeys.Ecdhe
if ecdheKey == nil { if ecdheKey == nil {
return nil, E.New("nil ecdheKey") return nil, E.New("nil ecdhe_key")
} }
authKey, err := ecdheKey.ECDH(publicKey) authKey, err := ecdheKey.ECDH(publicKey)
if err != nil { if err != nil {
@@ -218,6 +214,10 @@ func realityClientFallback(ctx context.Context, uConn net.Conn, serverName strin
response.Body.Close() response.Body.Close()
} }
func (e *RealityClientConfig) SetSessionIDGenerator(generator func(clientHello []byte, sessionID []byte) error) {
e.uClient.config.SessionIDGenerator = generator
}
func (e *RealityClientConfig) Clone() Config { func (e *RealityClientConfig) Clone() Config {
return &RealityClientConfig{ return &RealityClientConfig{
e.ctx, e.ctx,

View File

@@ -1,4 +1,4 @@
//go:build with_utls //go:build with_reality_server
package tls package tls
@@ -10,25 +10,25 @@ import (
"net" "net"
"time" "time"
"github.com/sagernet/reality"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/debug"
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"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
utls "github.com/metacubex/utls"
) )
var _ ServerConfigCompat = (*RealityServerConfig)(nil) var _ ServerConfigCompat = (*RealityServerConfig)(nil)
type RealityServerConfig struct { type RealityServerConfig struct {
config *utls.RealityConfig config *reality.Config
} }
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) { func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) {
var tlsConfig utls.RealityConfig var tlsConfig reality.Config
if options.ACME != nil && len(options.ACME.Domain) > 0 { if options.ACME != nil && len(options.ACME.Domain) > 0 {
return nil, E.New("acme is unavailable in reality") return nil, E.New("acme is unavailable in reality")
@@ -89,20 +89,16 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference) tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference)
tlsConfig.ShortIds = make(map[[8]byte]bool) tlsConfig.ShortIds = make(map[[8]byte]bool)
if len(options.Reality.ShortID) == 0 { for i, shortIDString := range options.Reality.ShortID {
tlsConfig.ShortIds[[8]byte{0}] = true var shortID [8]byte
} else { decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
for i, shortIDString := range options.Reality.ShortID { if err != nil {
var shortID [8]byte return nil, E.Cause(err, "decode short_id[", i, "]: ", shortIDString)
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
if err != nil {
return nil, E.Cause(err, "decode short_id[", i, "]: ", shortIDString)
}
if decodedLen > 8 {
return nil, E.New("invalid short_id[", i, "]: ", shortIDString)
}
tlsConfig.ShortIds[shortID] = true
} }
if decodedLen > 8 {
return nil, E.New("invalid short_id[", i, "]: ", shortIDString)
}
tlsConfig.ShortIds[shortID] = true
} }
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions, options.Reality.Handshake.ServerIsDomain()) handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions, options.Reality.Handshake.ServerIsDomain())
@@ -113,6 +109,10 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
} }
if debug.Enabled {
tlsConfig.Show = true
}
return &RealityServerConfig{&tlsConfig}, nil return &RealityServerConfig{&tlsConfig}, nil
} }
@@ -153,7 +153,7 @@ func (c *RealityServerConfig) Server(conn net.Conn) (Conn, error) {
} }
func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error) { func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (Conn, error) {
tlsConn, err := utls.RealityServer(ctx, conn, c.config) tlsConn, err := reality.Server(ctx, conn, c.config)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -169,7 +169,7 @@ func (c *RealityServerConfig) Clone() Config {
var _ Conn = (*realityConnWrapper)(nil) var _ Conn = (*realityConnWrapper)(nil)
type realityConnWrapper struct { type realityConnWrapper struct {
*utls.Conn *reality.Conn
} }
func (c *realityConnWrapper) ConnectionState() ConnectionState { func (c *realityConnWrapper) ConnectionState() ConnectionState {

View File

@@ -1,5 +1,15 @@
//go:build with_reality_server //go:build !with_reality_server
package tls package tls
var _ int = "The separate `with_reality_server` build tag has been merged into `with_utls` and is no longer needed, please update your build configuration." import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
return nil, E.New(`reality server is not included in this build, rebuild with -tags with_reality_server`)
}

View File

@@ -6,7 +6,6 @@ import (
"net" "net"
"os" "os"
"strings" "strings"
"time"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -234,12 +233,8 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
key = content key = content
} }
if certificate == nil && key == nil && options.Insecure { if certificate == nil && key == nil && options.Insecure {
timeFunc := ntp.TimeFuncFromContext(ctx)
if timeFunc == nil {
timeFunc = time.Now
}
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return GenerateKeyPair(nil, nil, timeFunc, info.ServerName) return GenerateKeyPair(nil, nil, ntp.TimeFuncFromContext(ctx), info.ServerName)
} }
} else { } else {
if certificate == nil { if certificate == nil {

View File

@@ -16,8 +16,8 @@ import (
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
utls "github.com/sagernet/utls"
utls "github.com/metacubex/utls"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )

View File

@@ -14,9 +14,5 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
} }
func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`) return nil, E.New(`uTLS, which is required by reality client is not included in this build, rebuild with -tags with_utls`)
}
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
} }

7
constant/script.go Normal file
View File

@@ -0,0 +1,7 @@
package constant
const (
ScriptTypeSurge = "surge"
ScriptSourceTypeLocal = "local"
ScriptSourceTypeRemote = "remote"
)

View File

@@ -24,9 +24,9 @@ func applyDebugOptions(options option.DebugOptions) {
if options.TraceBack != "" { if options.TraceBack != "" {
debug.SetTraceback(options.TraceBack) debug.SetTraceback(options.TraceBack)
} }
if options.MemoryLimit.Value() != 0 { if options.MemoryLimit != 0 {
debug.SetMemoryLimit(int64(float64(options.MemoryLimit.Value()) / 1.5)) debug.SetMemoryLimit(int64(float64(options.MemoryLimit) / 1.5))
conntrack.MemoryLimit = options.MemoryLimit.Value() conntrack.MemoryLimit = uint64(options.MemoryLimit)
} }
if options.OOMKiller != nil { if options.OOMKiller != nil {
conntrack.KillerEnabled = *options.OOMKiller conntrack.KillerEnabled = *options.OOMKiller

View File

@@ -7,9 +7,9 @@ import (
"runtime/debug" "runtime/debug"
"strings" "strings"
"github.com/sagernet/sing-box/common/humanize"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/byteformats"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badjson"
@@ -38,9 +38,9 @@ func applyDebugListenOption(options option.DebugOptions) {
runtime.ReadMemStats(&memStats) runtime.ReadMemStats(&memStats)
var memObject badjson.JSONObject var memObject badjson.JSONObject
memObject.Put("heap", byteformats.FormatMemoryBytes(memStats.HeapInuse)) memObject.Put("heap", humanize.MemoryBytes(memStats.HeapInuse))
memObject.Put("stack", byteformats.FormatMemoryBytes(memStats.StackInuse)) memObject.Put("stack", humanize.MemoryBytes(memStats.StackInuse))
memObject.Put("idle", byteformats.FormatMemoryBytes(memStats.HeapIdle-memStats.HeapReleased)) memObject.Put("idle", humanize.MemoryBytes(memStats.HeapIdle-memStats.HeapReleased))
memObject.Put("goroutines", runtime.NumGoroutine()) memObject.Put("goroutines", runtime.NumGoroutine())
memObject.Put("rss", rusageMaxRSS()) memObject.Put("rss", rusageMaxRSS())

View File

@@ -232,20 +232,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
record.Header().Ttl = timeToLive record.Header().Ttl = timeToLive
} }
} }
response.Id = messageId
if !disableCache { if !disableCache {
c.storeCache(transport, question, response, timeToLive) c.storeCache(transport, question, response, timeToLive)
} }
response.Id = messageId
requestEDNSOpt := message.IsEdns0()
responseEDNSOpt := response.IsEdns0()
if responseEDNSOpt != nil && (requestEDNSOpt == nil || requestEDNSOpt.Version() < responseEDNSOpt.Version()) {
response.Extra = common.Filter(response.Extra, func(it dns.RR) bool {
return it.Header().Rrtype != dns.TypeOPT
})
if requestEDNSOpt != nil {
response.SetEdns0(responseEDNSOpt.UDPSize(), responseEDNSOpt.Do())
}
}
logExchangedResponse(c.logger, ctx, response, timeToLive) logExchangedResponse(c.logger, ctx, response, timeToLive)
return response, err return response, err
} }
@@ -493,7 +483,7 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
} }
func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) { func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
if response.Rcode != dns.RcodeSuccess { if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
return nil, RcodeError(response.Rcode) return nil, RcodeError(response.Rcode)
} }
addresses := make([]netip.Addr, 0, len(response.Answer)) addresses := make([]netip.Addr, 0, len(response.Answer))

View File

@@ -15,8 +15,6 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
} }
responseLen := response.Len() responseLen := response.Len()
if responseLen > maxLen { if responseLen > maxLen {
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

@@ -323,9 +323,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
err error err error
) )
printResult := func() { printResult := func() {
if err == nil && len(responseAddrs) == 0 {
err = E.New("empty result")
}
if err != nil { if err != nil {
if errors.Is(err, ErrResponseRejectedCached) { if errors.Is(err, ErrResponseRejectedCached) {
r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)") r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
@@ -334,15 +331,15 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
} else { } else {
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
} }
} } else if len(responseAddrs) == 0 {
if err != nil { r.logger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
err = E.Cause(err, "lookup ", domain) err = RcodeNameError
} }
} }
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy) responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
if cached { if cached {
if len(responseAddrs) == 0 { if len(responseAddrs) == 0 {
return nil, E.New("lookup ", domain, ": empty result (cached)") return nil, RcodeNameError
} }
return responseAddrs, nil return responseAddrs, nil
} }

View File

@@ -96,9 +96,6 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
if serverAddr.Port == 0 { if serverAddr.Port == 0 {
serverAddr.Port = 443 serverAddr.Port = 443
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return NewHTTPSRaw( return NewHTTPSRaw(
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions), dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
logger, logger,

View File

@@ -3,7 +3,6 @@ package local
import ( import (
"context" "context"
"math/rand" "math/rand"
"net/netip"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -36,7 +35,6 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
} }
return &Transport{ return &Transport{
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options), TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
ctx: ctx,
hosts: hosts.NewFile(hosts.DefaultPath), hosts: hosts.NewFile(hosts.DefaultPath),
dialer: transportDialer, dialer: transportDialer,
}, nil }, nil
@@ -59,7 +57,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
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()
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, domain) return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
} else { } else {
@@ -91,9 +89,8 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
startRacer := func(ctx context.Context, fqdn string) { startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, systemConfig, fqdn, message) response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
if err == nil { if err == nil {
var addresses []netip.Addr addresses, _ := dns.MessageToAddresses(response)
addresses, err = dns.MessageToAddresses(response) if len(addresses) == 0 {
if err == nil && len(addresses) == 0 {
err = E.New(fqdn, ": empty result") err = E.New(fqdn, ": empty result")
} }
} }

View File

@@ -1,7 +1,6 @@
package local package local
import ( import (
"context"
"os" "os"
"runtime" "runtime"
"strings" "strings"
@@ -24,21 +23,19 @@ type resolverConfig struct {
var resolvConf resolverConfig var resolvConf resolverConfig
func getSystemDNSConfig(ctx context.Context) *dnsConfig { func getSystemDNSConfig() *dnsConfig {
resolvConf.tryUpdate(ctx, "/etc/resolv.conf") resolvConf.tryUpdate("/etc/resolv.conf")
return resolvConf.dnsConfig.Load() return resolvConf.dnsConfig.Load()
} }
func (conf *resolverConfig) init(ctx context.Context) { func (conf *resolverConfig) init() {
conf.dnsConfig.Store(dnsReadConfig(ctx, "/etc/resolv.conf")) conf.dnsConfig.Store(dnsReadConfig("/etc/resolv.conf"))
conf.lastChecked = time.Now() conf.lastChecked = time.Now()
conf.ch = make(chan struct{}, 1) conf.ch = make(chan struct{}, 1)
} }
func (conf *resolverConfig) tryUpdate(ctx context.Context, name string) { func (conf *resolverConfig) tryUpdate(name string) {
conf.initOnce.Do(func() { conf.initOnce.Do(conf.init)
conf.init(ctx)
})
if conf.dnsConfig.Load().noReload { if conf.dnsConfig.Load().noReload {
return return
@@ -62,7 +59,7 @@ func (conf *resolverConfig) tryUpdate(ctx context.Context, name string) {
return return
} }
} }
dnsConf := dnsReadConfig(ctx, name) dnsConf := dnsReadConfig(name)
conf.dnsConfig.Store(dnsConf) conf.dnsConfig.Store(dnsConf)
} }

View File

@@ -11,7 +11,6 @@ package local
import "C" import "C"
import ( import (
"context"
"time" "time"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -19,7 +18,7 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func dnsReadConfig(_ context.Context, _ string) *dnsConfig { func dnsReadConfig(_ string) *dnsConfig {
if C.res_init() != 0 { if C.res_init() != 0 {
return &dnsConfig{ return &dnsConfig{
servers: defaultNS, servers: defaultNS,

View File

@@ -4,7 +4,6 @@ package local
import ( import (
"bufio" "bufio"
"context"
"net" "net"
"net/netip" "net/netip"
"os" "os"
@@ -14,7 +13,7 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
) )
func dnsReadConfig(_ context.Context, name string) *dnsConfig { func dnsReadConfig(name string) *dnsConfig {
conf := &dnsConfig{ conf := &dnsConfig{
ndots: 1, ndots: 1,
timeout: 5 * time.Second, timeout: 5 * time.Second,

View File

@@ -1,7 +1,6 @@
package local package local
import ( import (
"context"
"net" "net"
"net/netip" "net/netip"
"os" "os"
@@ -9,13 +8,10 @@ import (
"time" "time"
"unsafe" "unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/service"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
func dnsReadConfig(ctx context.Context, _ string) *dnsConfig { func dnsReadConfig(_ string) *dnsConfig {
conf := &dnsConfig{ conf := &dnsConfig{
ndots: 1, ndots: 1,
timeout: 5 * time.Second, timeout: 5 * time.Second,
@@ -26,35 +22,35 @@ func dnsReadConfig(ctx context.Context, _ string) *dnsConfig {
conf.servers = defaultNS conf.servers = defaultNS
} }
}() }()
addresses, err := adapterAddresses() aas, err := adapterAddresses()
if err != nil { if err != nil {
return nil return nil
} }
var dnsAddresses []struct {
ifName string for _, aa := range aas {
netip.Addr // Only take interfaces whose OperStatus is IfOperStatusUp(0x01) into DNS configs.
} if aa.OperStatus != windows.IfOperStatusUp {
for _, address := range addresses {
if address.OperStatus != windows.IfOperStatusUp {
continue continue
} }
if address.IfType == windows.IF_TYPE_TUNNEL {
// Only take interfaces which have at least one gateway
if aa.FirstGatewayAddress == nil {
continue continue
} }
if address.FirstGatewayAddress == nil {
continue for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next {
} sa, err := dns.Address.Sockaddr.Sockaddr()
for dnsServerAddress := address.FirstDnsServerAddress; dnsServerAddress != nil; dnsServerAddress = dnsServerAddress.Next {
rawSockaddr, err := dnsServerAddress.Address.Sockaddr.Sockaddr()
if err != nil { if err != nil {
continue continue
} }
var dnsServerAddr netip.Addr var ip netip.Addr
switch sockaddr := rawSockaddr.(type) { switch sa := sa.(type) {
case *syscall.SockaddrInet4: case *syscall.SockaddrInet4:
dnsServerAddr = netip.AddrFrom4(sockaddr.Addr) ip = netip.AddrFrom4([4]byte{sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3]})
case *syscall.SockaddrInet6: case *syscall.SockaddrInet6:
if sockaddr.Addr[0] == 0xfe && sockaddr.Addr[1] == 0xc0 { var addr16 [16]byte
copy(addr16[:], sa.Addr[:])
if addr16[0] == 0xfe && addr16[1] == 0xc0 {
// fec0/10 IPv6 addresses are site local anycast DNS // fec0/10 IPv6 addresses are site local anycast DNS
// addresses Microsoft sets by default if no other // addresses Microsoft sets by default if no other
// IPv6 DNS address is set. Site local anycast is // IPv6 DNS address is set. Site local anycast is
@@ -62,27 +58,14 @@ func dnsReadConfig(ctx context.Context, _ string) *dnsConfig {
// https://datatracker.ietf.org/doc/html/rfc3879 // https://datatracker.ietf.org/doc/html/rfc3879
continue continue
} }
dnsServerAddr = netip.AddrFrom16(sockaddr.Addr) ip = netip.AddrFrom16(addr16)
default: default:
// Unexpected type. // Unexpected type.
continue continue
} }
dnsAddresses = append(dnsAddresses, struct { conf.servers = append(conf.servers, net.JoinHostPort(ip.String(), "53"))
ifName string
netip.Addr
}{ifName: windows.UTF16PtrToString(address.FriendlyName), Addr: dnsServerAddr})
} }
} }
var myInterface string
if networkManager := service.FromContext[adapter.NetworkManager](ctx); networkManager != nil {
myInterface = networkManager.InterfaceMonitor().MyInterface()
}
for _, address := range dnsAddresses {
if address.ifName == myInterface {
continue
}
conf.servers = append(conf.servers, net.JoinHostPort(address.String(), "53"))
}
return conf return conf
} }

View File

@@ -92,9 +92,6 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
if serverAddr.Port == 0 { if serverAddr.Port == 0 {
serverAddr.Port = 443 serverAddr.Port = 443
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &HTTP3Transport{ return &HTTP3Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions), TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
logger: logger, logger: logger,

View File

@@ -16,7 +16,6 @@ import (
sQUIC "github.com/sagernet/sing-quic" sQUIC "github.com/sagernet/sing-quic"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@@ -59,9 +58,6 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
if serverAddr.Port == 0 { if serverAddr.Port == 0 {
serverAddr.Port = 853 serverAddr.Port = 853
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &Transport{ return &Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions), TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
ctx: ctx, ctx: ctx,
@@ -144,12 +140,12 @@ func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn quic.C
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer stream.Close()
defer stream.CancelRead(0)
err = transport.WriteMessage(stream, 0, message) err = transport.WriteMessage(stream, 0, message)
if err != nil { if err != nil {
stream.Close()
return nil, err return nil, err
} }
stream.Close()
return transport.ReadMessage(stream) return transport.ReadMessage(stream)
} }

View File

@@ -13,7 +13,6 @@ import (
"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/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
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"
@@ -41,9 +40,6 @@ func NewTCP(ctx context.Context, logger log.ContextLogger, tag string, options o
if serverAddr.Port == 0 { if serverAddr.Port == 0 {
serverAddr.Port = 53 serverAddr.Port = 53
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &TCPTransport{ return &TCPTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTCP, tag, options), TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTCP, tag, options),
dialer: transportDialer, dialer: transportDialer,

View File

@@ -57,9 +57,6 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
if serverAddr.Port == 0 { if serverAddr.Port == 0 {
serverAddr.Port = 853 serverAddr.Port = 853
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &TLSTransport{ return &TLSTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions), TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions),
logger: logger, logger: logger,

View File

@@ -13,7 +13,6 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@@ -48,9 +47,6 @@ func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options o
if serverAddr.Port == 0 { if serverAddr.Port == 0 {
serverAddr.Port = 53 serverAddr.Port = 53
} }
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil
} }
@@ -121,7 +117,7 @@ func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.M
conn.access.Unlock() conn.access.Unlock()
defer func() { defer func() {
conn.access.Lock() conn.access.Lock()
delete(conn.callbacks, exMessage.Id) delete(conn.callbacks, messageId)
conn.access.Unlock() conn.access.Unlock()
}() }()
rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes()) rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
@@ -216,8 +212,8 @@ type dnsConnection struct {
func (c *dnsConnection) Close(err error) { func (c *dnsConnection) Close(err error) {
c.closeOnce.Do(func() { c.closeOnce.Do(func() {
c.err = err
close(c.done) close(c.done)
c.err = err
}) })
c.Conn.Close() c.Conn.Close()
} }

View File

@@ -2,58 +2,6 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.12.0-beta.9
* Update quic-go to v0.51.0
* Fixes and improvements
### 1.11.9
* Fixes and improvements
_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)._
#### 1.12.0-beta.5
* Fixes and improvements
### 1.11.8
* Improve `auto_redirect` **1**
* Fixes and improvements
**1**:
Now `auto_redirect` fixes compatibility issues between TUN and Docker bridge networks,
see [Tun](/configuration/inbound/tun/#auto_redirect).
_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)._
#### 1.12.0-beta.3
* Fixes and improvements
### 1.11.7
* Fixes and improvements
_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)._
#### 1.12.0-beta.1
* Fixes and improvements
**1**:
Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks,
see [Tun](/configuration/inbound/tun/#auto_redirect).
### 1.11.6
* Fixes and improvements
_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)._
#### 1.12.0-alpha.19 #### 1.12.0-alpha.19
* Update gVisor to 20250319.0 * Update gVisor to 20250319.0

View File

@@ -9,10 +9,6 @@ and the data generated by the software is always on your device.
## Android ## Android
The broad package (App) visibility (QUERY_ALL_PACKAGES) permission
is used to provide per-application proxy features for VPN,
sing-box will not collect your app list.
If your configuration contains `wifi_ssid` or `wifi_bssid` routing rules, If your configuration contains `wifi_ssid` or `wifi_bssid` routing rules,
sing-box uses the location permission in the background sing-box uses the location permission in the background
to get information about the connected Wi-Fi network to make them work. to get information about the connected Wi-Fi network to make them work.

View File

@@ -27,20 +27,19 @@ icon: material/alert-decagram
The type of the DNS server. The type of the DNS server.
| Type | Format | | Type | Format |
|-----------------|---------------------------| |-----------------|-----------------------------|
| empty (default) | [Legacy](./legacy/) | | empty (default) | [Legacy](./legacy/) |
| `local` | [Local](./local/) | | `tcp` | [TCP](./tcp/) |
| `hosts` | [Hosts](./hosts/) | | `udp` | [UDP](./udp/) |
| `tcp` | [TCP](./tcp/) | | `tls` | [TLS](./tls/) |
| `udp` | [UDP](./udp/) | | `https` | [HTTPS](./https/) |
| `tls` | [TLS](./tls/) | | `quic` | [QUIC](./quic/) |
| `quic` | [QUIC](./quic/) | | `h3` | [HTTP/3](./http3/) |
| `https` | [HTTPS](./https/) | | `predefined` | [Predefined](./predefined/) |
| `h3` | [HTTP/3](./http3/) | | `dhcp` | [DHCP](./dhcp/) |
| `dhcp` | [DHCP](./dhcp/) | | `fakeip` | [Fake IP](./fakeip/) |
| `fakeip` | [Fake IP](./fakeip/) | | `tailscale` | [Tailscale](./tailscale/) |
| `tailscale` | [Tailscale](./tailscale/) |
#### tag #### tag

View File

@@ -27,20 +27,19 @@ icon: material/alert-decagram
DNS 服务器的类型。 DNS 服务器的类型。
| 类型 | 格式 | | 类型 | 格式 |
|-----------------|---------------------------| |-----------------|-----------------------------|
| empty (default) | [Legacy](./legacy/) | | empty (default) | [Legacy](./legacy/) |
| `local` | [Local](./local/) | | `tcp` | [TCP](./tcp/) |
| `hosts` | [Hosts](./hosts/) | | `udp` | [UDP](./udp/) |
| `tcp` | [TCP](./tcp/) | | `tls` | [TLS](./tls/) |
| `udp` | [UDP](./udp/) | | `https` | [HTTPS](./https/) |
| `tls` | [TLS](./tls/) | | `quic` | [QUIC](./quic/) |
| `quic` | [QUIC](./quic/) | | `h3` | [HTTP/3](./http3/) |
| `https` | [HTTPS](./https/) | | `predefined` | [Predefined](./predefined/) |
| `h3` | [HTTP/3](./http3/) | | `dhcp` | [DHCP](./dhcp/) |
| `dhcp` | [DHCP](./dhcp/) | | `fakeip` | [Fake IP](./fakeip/) |
| `fakeip` | [Fake IP](./fakeip/) | | `tailscale` | [Tailscale](./tailscale/) |
| `tailscale` | [Tailscale](./tailscale/) |
#### tag #### tag

View File

@@ -42,18 +42,16 @@ AnyTLS padding scheme line array.
Default padding scheme: Default padding scheme:
```json ```
[ stop=8
"stop=8", 0=34-120
"0=30-30", 1=100-400
"1=100-400", 2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
"2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000", 3=500-1000
"3=9-9,500-1000", 4=500-1000
"4=500-1000", 5=500-1000
"5=500-1000", 6=500-1000
"6=500-1000", 7=500-1000
"7=500-1000"
]
``` ```
#### tls #### tls

View File

@@ -42,18 +42,16 @@ AnyTLS 填充方案行数组。
默认填充方案: 默认填充方案:
```json ```
[ stop=8
"stop=8", 0=34-120
"0=30-30", 1=100-400
"1=100-400", 2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
"2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000", 3=500-1000
"3=9-9,500-1000", 4=500-1000
"4=500-1000", 5=500-1000
"5=500-1000", 6=500-1000
"6=500-1000", 7=500-1000
"7=500-1000"
]
``` ```
#### tls #### tls

View File

@@ -211,10 +211,6 @@ Set the default route to the Tun.
By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`. By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`.
!!! note "Also enable `auto_redirect`"
`auto_redirect` is always recommended on Linux, it provides better routing, higher performance (better than tproxy), and avoids conflicts between TUN and Docker bridge networks.
#### iproute2_table_index #### iproute2_table_index
!!! question "Since sing-box 1.10.0" !!! question "Since sing-box 1.10.0"
@@ -239,29 +235,22 @@ Linux iproute2 rule start index generated by `auto_route`.
Only supported on Linux with `auto_route` enabled. Only supported on Linux with `auto_route` enabled.
Improve TUN routing and performance using nftables. Automatically configure iptables/nftables to redirect connections.
`auto_redirect` is always recommended on Linux, it provides better routing, *In Android*
higher performance (better than tproxy),
and avoids conflicts between TUN and Docker bridge networks.
Note that `auto_redirect` also works on Android, Only local IPv4 connections are forwarded. To share your VPN connection over hotspot or repeater,
but due to the lack of `nftables` and `ip6tables`,
only simple IPv4 TCP forwarding is performed.
To share your VPN connection over hotspot or repeater on Android,
use [VPNHotspot](https://github.com/Mygod/VPNHotspot). use [VPNHotspot](https://github.com/Mygod/VPNHotspot).
`auto_redirect` also automatically inserts compatibility rules *In Linux*:
into the OpenWrt fw4 table, i.e.
it will work on routers without any extra configuration.
Conflict with `route.default_mark` and `[dialOptions].routing_mark`. `auto_route` with `auto_redirect` works as expected on routers **without intervention**.
#### auto_redirect_input_mark #### auto_redirect_input_mark
!!! question "Since sing-box 1.10.0" !!! question "Since sing-box 1.10.0"
Connection input mark used by `auto_redirect`. Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`.
`0x2023` is used by default. `0x2023` is used by default.
@@ -269,7 +258,7 @@ Connection input mark used by `auto_redirect`.
!!! question "Since sing-box 1.10.0" !!! question "Since sing-box 1.10.0"
Connection output mark used by `auto_redirect`. Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`.
`0x2024` is used by default. `0x2024` is used by default.
@@ -280,15 +269,17 @@ Enforce strict routing rules when `auto_route` is enabled:
*In Linux*: *In Linux*:
* Let unsupported network unreachable * Let unsupported network unreachable
* For legacy reasons, when neither `strict_route` nor `auto_redirect` are enabled, all ICMP traffic will not go through TUN. * Make ICMP traffic route to tun instead of upstream interfaces
* Route all connections to tun
It prevents IP address leaks and makes DNS hijacking work on Android.
*In Windows*: *In Windows*:
* Let unsupported network unreachable * Add firewall rules to prevent DNS leak caused by
* prevent DNS leak caused by
Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29)
It may prevent some Windows applications (such as VirtualBox) from working properly in certain situations. It may prevent some applications (such as VirtualBox) from working properly in certain situations.
#### route_address #### route_address
@@ -377,6 +368,8 @@ Exclude custom routes when `auto_route` is enabled.
Add the destination IP CIDR rules in the specified rule-sets to the firewall. Add the destination IP CIDR rules in the specified rule-sets to the firewall.
Matched traffic will bypass the sing-box routes. Matched traffic will bypass the sing-box routes.
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
=== "Without `auto_redirect` enabled" === "Without `auto_redirect` enabled"
!!! question "Since sing-box 1.11.0" !!! question "Since sing-box 1.11.0"

View File

@@ -215,10 +215,6 @@ tun 接口的 IPv6 前缀。
VPN 默认优先于 tun。要使 tun 经过 VPN启用 `route.override_android_vpn` VPN 默认优先于 tun。要使 tun 经过 VPN启用 `route.override_android_vpn`
!!! note "也启用 `auto_redirect`"
在 Linux 上始终推荐使用 `auto_redirect`,它提供更好的路由, 更高的性能(优于 tproxy 并避免 TUN 与 Docker 桥接网络冲突。
#### iproute2_table_index #### iproute2_table_index
!!! question "自 sing-box 1.10.0 起" !!! question "自 sing-box 1.10.0 起"
@@ -243,22 +239,21 @@ tun 接口的 IPv6 前缀。
仅支持 Linux且需要 `auto_route` 已启用。 仅支持 Linux且需要 `auto_route` 已启用。
通过使用 nftables 改善 TUN 路由和性能 自动配置 iptables/nftables 以重定向连接
在 Linux 上始终推荐使用 `auto_redirect`,它提供更好的路由、更高的性能(优于 tproxy并避免了 TUN 和 Docker 桥接网络之间的冲突。 *在 Android 中*
请注意,`auto_redirect` 也适用于 Android但由于缺少 `nftables``ip6tables`,仅执行简单的 IPv4 TCP 转发。 仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
若要在 Android 上通过热点或中继器共享 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
`auto_redirect` 还会自动将兼容性规则插入 OpenWrt 的 fw4 表中,即无需额外配置即可在路由器上工作。 *在 Linux 中*:
`route.default_mark``[dialOptions].routing_mark` 冲突 带有 `auto_redirect ``auto_route` 可以在路由器上按预期工作,**无需干预**
#### auto_redirect_input_mark #### auto_redirect_input_mark
!!! question "自 sing-box 1.10.0 起" !!! question "自 sing-box 1.10.0 起"
`auto_redirect` 使用的连接输入标记。 `route_address_set``route_exclude_address_set` 使用的连接输入标记。
默认使用 `0x2023` 默认使用 `0x2023`
@@ -266,25 +261,29 @@ tun 接口的 IPv6 前缀。
!!! question "自 sing-box 1.10.0 起" !!! question "自 sing-box 1.10.0 起"
`auto_redirect` 使用的连接输出标记。 `route_address_set``route_exclude_address_set` 使用的连接输出标记。
默认使用 `0x2024` 默认使用 `0x2024`
#### strict_route #### strict_route
启用 `auto_route`,强制执行严格的路由规则 启用 `auto_route` 时执行严格的路由规则
*在 Linux 中* *在 Linux 中*:
* 使不支持的网络不可达。 * 不支持的网络无法到达
* 出于历史遗留原因,当未启用 `strict_route``auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。 * 使 ICMP 流量路由到 tun 而不是上游接口
* 将所有连接路由到 tun
*在 Windows 中* 它可以防止 IP 地址泄漏,并使 DNS 劫持在 Android 上工作。
* 使不支持的网络不可达。 *在 Windows 中*:
* 阻止 Windows 的 [普通多宿主 DNS 解析行为](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29) 造成的 DNS 泄露
它可能会使某些 Windows 应用程序(如 VirtualBox在某些情况下无法正常工作。 * 添加防火墙规则以阻止 Windows
的 [普通多宿主 DNS 解析行为](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29)
造成的 DNS 泄露
它可能会使某些应用程序(如 VirtualBox在某些情况下无法正常工作。
#### route_address #### route_address
@@ -343,6 +342,8 @@ tun 接口的 IPv6 前缀。
将指定规则集中的目标 IP CIDR 规则添加到防火墙。 将指定规则集中的目标 IP CIDR 规则添加到防火墙。
不匹配的流量将绕过 sing-box 路由。 不匹配的流量将绕过 sing-box 路由。
`route.default_mark``[dialOptions].routing_mark` 冲突。
=== "`auto_redirect` 未启用" === "`auto_redirect` 未启用"
!!! question "自 sing-box 1.11.0 起" !!! question "自 sing-box 1.11.0 起"

View File

@@ -26,7 +26,7 @@ If enabled in the inbound, the protocol and domain name (if present) of by the c
| QUIC Client | Type | | QUIC Client | Type |
|:------------------------:|:----------:| |:------------------------:|:----------:|
| Chromium/Cronet | `chromium` | | Chromium/Cronet | `chrimium` |
| Safari/Apple Network API | `safari` | | Safari/Apple Network API | `safari` |
| Firefox / uquic firefox | `firefox` | | Firefox / uquic firefox | `firefox` |
| quic-go / uquic chrome | `quic-go` | | quic-go / uquic chrome | `quic-go` |

View File

@@ -26,7 +26,7 @@
| QUIC 客户端 | 类型 | | QUIC 客户端 | 类型 |
|:------------------------:|:----------:| |:------------------------:|:----------:|
| Chromium/Cronet | `chromium` | | Chromium/Cronet | `chrimium` |
| Safari/Apple Network API | `safari` | | Safari/Apple Network API | `safari` |
| Firefox / uquic firefox | `firefox` | | Firefox / uquic firefox | `firefox` |
| quic-go / uquic chrome | `quic-go` | | quic-go / uquic chrome | `quic-go` |

View File

@@ -80,8 +80,6 @@ List of [Headless Rule](./headless-rule/).
Format of rule-set file, `source` or `binary`. Format of rule-set file, `source` or `binary`.
Optional when `path` or `url` uses `json` or `srs` as extension.
### Local Fields ### Local Fields
#### path #### path

View File

@@ -80,8 +80,6 @@
规则集格式, `source` 或 `binary`。 规则集格式, `source` 或 `binary`。
当 `path` 或 `url` 使用 `json` 或 `srs` 作为扩展名时可选。
### 本地字段 ### 本地字段
#### path #### path

View File

@@ -206,7 +206,7 @@ Only take effect when `domain_strategy` or `network_strategy` is set.
!!! failure "Deprecated in sing-box 1.12.0" !!! failure "Deprecated in sing-box 1.12.0"
`domain_strategy` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-outbound-domain-strategy-option-to-domain-resolver). `domain_strategy` is merged to [domain_resolver](#domain_resolver) in sing-box 1.12.0.
Available values: `prefer_ipv4`, `prefer_ipv6`, `ipv4_only`, `ipv6_only`. Available values: `prefer_ipv4`, `prefer_ipv6`, `ipv4_only`, `ipv6_only`.

View File

@@ -194,10 +194,6 @@ icon: material/new-box
#### domain_strategy #### domain_strategy
!!! failure "已在 sing-box 1.12.0 废弃"
`domain_strategy` 已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-domain-strategy-option-to-domain-resolver)。
可选值:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only` 可选值:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`
如果设置,域名将在请求发出之前解析为 IP。 如果设置,域名将在请求发出之前解析为 IP。

View File

@@ -52,6 +52,7 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). | | `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). |
| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). | | `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). |
| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). | | `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). |
| `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls/). |
| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | | `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). |
| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | | `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). |
| `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | | `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |

View File

@@ -56,6 +56,7 @@ go build -tags "tag_a tag_b" ./cmd/sing-box
| `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). | | `with_dhcp` | :material-check: | Build with DHCP support, see [DHCP DNS transport](/configuration/dns/server/). |
| `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). | | `with_wireguard` | :material-check: | Build with WireGuard support, see [WireGuard outbound](/configuration/outbound/wireguard/). |
| `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). | | `with_utls` | :material-check: | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](/configuration/shared/tls#utls). |
| `with_reality_server` | :material-check: | Build with reality TLS server support, see [TLS](/configuration/shared/tls/). |
| `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). | | `with_acme` | :material-check: | Build with ACME TLS certificate issuer support, see [TLS](/configuration/shared/tls/). |
| `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). | | `with_clash_api` | :material-check: | Build with Clash API support, see [Experimental](/configuration/experimental#clash-api-fields). |
| `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). | | `with_v2ray_api` | :material-close: | Build with V2Ray API support, see [Experimental](/configuration/experimental#v2ray-api-fields). |

View File

@@ -9,56 +9,43 @@ icon: material/package
=== ":material-debian: Debian / APT" === ":material-debian: Debian / APT"
```bash ```bash
sudo mkdir -p /etc/apt/keyrings && sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc
sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc && sudo chmod a+r /etc/apt/keyrings/sagernet.asc
sudo chmod a+r /etc/apt/keyrings/sagernet.asc && echo "deb [arch=`dpkg --print-architecture` signed-by=/etc/apt/keyrings/sagernet.asc] https://deb.sagernet.org/ * *" | \
echo ' sudo tee /etc/apt/sources.list.d/sagernet.list > /dev/null
Types: deb sudo apt-get update
URIs: https://deb.sagernet.org/ sudo apt-get install sing-box # or sing-box-beta
Suites: *
Components: *
Enabled: yes
Signed-By: /etc/apt/keyrings/sagernet.asc
' | sudo tee /etc/apt/sources.list.d/sagernet.sources &&
sudo apt-get update &&
sudo apt-get install sing-box # or sing-box-beta
``` ```
=== ":material-redhat: Redhat / DNF 5" === ":material-redhat: Redhat / DNF"
```bash ```bash
sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo && sudo dnf -y install dnf-plugins-core
sudo dnf install sing-box # or sing-box-beta sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo
```
=== ":material-redhat: Redhat / DNF 4"
```bash
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo &&
sudo dnf -y install dnf-plugins-core &&
sudo dnf install sing-box # or sing-box-beta sudo dnf install sing-box # or sing-box-beta
``` ```
(This applies to any distribution that uses `dnf` as the package manager: Fedora, CentOS, even OpenSUSE with DNF installed.)
## :material-download-box: Manual Installation ## :material-download-box: Manual Installation
The script download and install the latest package from GitHub releases === ":material-debian: Debian / DEB"
for deb or rpm based Linux distributions, ArchLinux and OpenWrt.
```shell ```bash
curl -fsSL https://sing-box.app/install.sh | sh bash <(curl -fsSL https://sing-box.app/deb-install.sh)
``` ```
or latest beta: === ":material-redhat: Redhat / RPM"
```shell ```bash
curl -fsSL https://sing-box.app/install.sh | sh -s -- --beta bash <(curl -fsSL https://sing-box.app/rpm-install.sh)
``` ```
(This applies to any distribution that uses `rpm` and `systemd`. Because of how `rpm` defines dependencies, if it installs, it probably works.)
or specific version: === ":simple-archlinux: Archlinux / PKG"
```shell ```bash
curl -fsSL https://sing-box.app/install.sh | sh -s -- --version <version> bash <(curl -fsSL https://sing-box.app/arch-install.sh)
``` ```
## :material-book-lock-open: Managed Installation ## :material-book-lock-open: Managed Installation

View File

@@ -9,55 +9,43 @@ icon: material/package
=== ":material-debian: Debian / APT" === ":material-debian: Debian / APT"
```bash ```bash
sudo mkdir -p /etc/apt/keyrings && sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc
sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc && sudo chmod a+r /etc/apt/keyrings/sagernet.asc
sudo chmod a+r /etc/apt/keyrings/sagernet.asc && echo "deb [arch=`dpkg --print-architecture` signed-by=/etc/apt/keyrings/sagernet.asc] https://deb.sagernet.org/ * *" | \
echo ' sudo tee /etc/apt/sources.list.d/sagernet.list > /dev/null
Types: deb sudo apt-get update
URIs: https://deb.sagernet.org/ sudo apt-get install sing-box # or sing-box-beta
Suites: *
Components: *
Enabled: yes
Signed-By: /etc/apt/keyrings/sagernet.asc
' | sudo tee /etc/apt/sources.list.d/sagernet.sources &&
sudo apt-get update &&
sudo apt-get install sing-box # or sing-box-beta
``` ```
=== ":material-redhat: Redhat / DNF 5" === ":material-redhat: Redhat / DNF"
```bash ```bash
sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo && sudo dnf -y install dnf-plugins-core
sudo dnf install sing-box # or sing-box-beta sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo
```
=== ":material-redhat: Redhat / DNF 4"
```bash
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo &&
sudo dnf -y install dnf-plugins-core &&
sudo dnf install sing-box # or sing-box-beta sudo dnf install sing-box # or sing-box-beta
``` ```
(这适用于任何使用 `dnf` 作为包管理器的发行版Fedora、CentOS甚至安装了 DNF 的 OpenSUSE。
## :material-download-box: 手动安装 ## :material-download-box: 手动安装
该脚本从 GitHub 发布中下载并安装最新的软件包,适用于基于 deb 或 rpm 的 Linux 发行版、ArchLinux 和 OpenWrt。 === ":material-debian: Debian / DEB"
```shell ```bash
curl -fsSL https://sing-box.app/install.sh | sh bash <(curl -fsSL https://sing-box.app/deb-install.sh)
``` ```
或最新测试版: === ":material-redhat: Redhat / RPM"
```shell ```bash
curl -fsSL https://sing-box.app/install.sh | sh -s -- --beta bash <(curl -fsSL https://sing-box.app/rpm-install.sh)
``` ```
(这适用于任何使用 `rpm` 和 `systemd` 的发行版。由于 `rpm` 定义依赖关系的方式,如果安装成功,就多半能用。)
或指定版本: === ":simple-archlinux: Archlinux / PKG"
```shell ```bash
curl -fsSL https://sing-box.app/install.sh | sh -s -- --version <version> bash <(curl -fsSL https://sing-box.app/arch-install.sh)
``` ```
## :material-book-lock-open: 托管安装 ## :material-book-lock-open: 托管安装

View File

@@ -1,116 +0,0 @@
#!/bin/sh
download_beta=false
download_version=""
while [ $# -gt 0 ]; do
case "$1" in
--beta)
download_beta=true
shift
;;
--version)
shift
if [ $# -eq 0 ]; then
echo "Missing argument for --version"
echo "Usage: $0 [--beta] [--version <version>]"
exit 1
fi
download_version="$1"
shift
;;
*)
echo "Unknown argument: $1"
echo "Usage: $0 [--beta] [--version <version>]"
exit 1
;;
esac
done
if command -v pacman >/dev/null 2>&1; then
os="linux"
arch=$(uname -m)
package_suffix=".pkg.tar.zst"
package_install="pacman -U --noconfirm"
elif command -v dpkg >/dev/null 2>&1; then
os="linux"
arch=$(dpkg --print-architecture)
package_suffix=".deb"
package_install="dpkg -i"
elif command -v dnf >/dev/null 2>&1; then
os="linux"
arch=$(uname -m)
package_suffix=".rpm"
package_install="dnf install -y"
elif command -v rpm >/dev/null 2>&1; then
os="linux"
arch=$(uname -m)
package_suffix=".rpm"
package_install="rpm -i"
elif command -v opkg >/dev/null 2>&1; then
os="openwrt"
. /etc/os-release
arch="$OPENWRT_ARCH"
package_suffix=".ipk"
package_install="opkg update && opkg install"
else
echo "Missing supported package manager."
exit 1
fi
if [ -z "$download_version" ]; then
if [ "$download_beta" != "true" ]; then
if [ -n "$GITHUB_TOKEN" ]; then
latest_release=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases/latest)
else
latest_release=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest)
fi
curl_exit_status=$?
if [ $curl_exit_status -ne 0 ]; then
exit $curl_exit_status
fi
if [ "$(echo "$latest_release" | grep tag_name | wc -l)" -eq 0 ]; then
echo "$latest_release"
exit 1
fi
download_version=$(echo "$latest_release" | grep tag_name | head -n 1 | awk -F: '{print $2}' | sed 's/[", v]//g')
else
if [ -n "$GITHUB_TOKEN" ]; then
latest_release=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases)
else
latest_release=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases)
fi
curl_exit_status=$?
if [ $curl_exit_status -ne 0 ]; then
exit $curl_exit_status
fi
if [ "$(echo "$latest_release" | grep tag_name | wc -l)" -eq 0 ]; then
echo "$latest_release"
exit 1
fi
download_version=$(echo "$latest_release" | grep tag_name | head -n 1 | awk -F: '{print $2}' | sed 's/[", v]//g')
fi
fi
package_name="sing-box_${download_version}_${os}_${arch}${package_suffix}"
package_url="https://github.com/SagerNet/sing-box/releases/download/v${download_version}/${package_name}"
echo "Downloading $package_url"
if [ -n "$GITHUB_TOKEN" ]; then
curl --fail -Lo "$package_name" -H "Authorization: token ${GITHUB_TOKEN}" "$package_url"
else
curl --fail -Lo "$package_name" "$package_url"
fi
curl_exit_status=$?
if [ $curl_exit_status -ne 0 ]; then
exit $curl_exit_status
fi
if command -v sudo >/dev/null 2>&1; then
package_install="sudo $package_install"
fi
echo "$package_install $package_name"
sh -c "$package_install \"$package_name\""
rm -f "$package_name"

View File

@@ -292,7 +292,7 @@ DNS servers are refactored for better performance and scalability.
} }
], ],
"fakeip": { "fakeip": {
"enabled": true, "enable": true,
"inet4_range": "198.18.0.0/15", "inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18" "inet6_range": "fc00::/18"
} }
@@ -556,8 +556,7 @@ The legacy outbound DNS rules are deprecated and can be replaced by new domain r
"dns": { "dns": {
"servers": [ "servers": [
{ {
"type": "local", "type": "local"
"tag": "local"
} }
] ]
}, },
@@ -587,58 +586,6 @@ The legacy outbound DNS rules are deprecated and can be replaced by new domain r
} }
``` ```
### Migrate outbound domain strategy option to domain resolver
!!! info "References"
[Dial Fields](/configuration/shared/dial/#domain_strategy)
The `domain_strategy` option in Dial Fields has been deprecated and can be replaced with the new domain resolver option.
Note that due to the use of Dial Fields by some of the new DNS servers introduced in sing-box 1.12,
some people mistakenly believe that `domain_strategy` is the same feature as in the legacy DNS servers.
=== ":material-card-remove: Deprecated"
```json
{
"outbounds": [
{
"type": "socks",
"server": "example.org",
"server_port": 2080,
"domain_strategy": "prefer_ipv4",
}
]
}
```
=== ":material-card-multiple: New"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
}
]
},
"outbounds": [
{
"type": "socks",
"server": "example.org",
"server_port": 2080,
"domain_resolver": {
"server": "local",
"strategy": "prefer_ipv4"
}
}
]
}
```
## 1.11.0 ## 1.11.0
### Migrate legacy special outbounds to rule actions ### Migrate legacy special outbounds to rule actions

View File

@@ -292,7 +292,7 @@ DNS 服务器已经重构。
} }
], ],
"fakeip": { "fakeip": {
"enabled": true, "enable": true,
"inet4_range": "198.18.0.0/15", "inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18" "inet6_range": "fc00::/18"
} }
@@ -556,8 +556,7 @@ DNS 服务器已经重构。
"dns": { "dns": {
"servers": [ "servers": [
{ {
"type": "local", "type": "local"
"tag": "local"
} }
] ]
}, },
@@ -587,57 +586,6 @@ DNS 服务器已经重构。
} }
``` ```
### 迁移出站域名策略选项到域名解析器
拨号字段中的 `domain_strategy` 选项已被弃用,可以用新的域名解析器选项替代。
请注意,由于 sing-box 1.12 中引入的一些新 DNS 服务器使用了拨号字段,一些人错误地认为 `domain_strategy` 与旧 DNS 服务器中的功能相同。
!!! info "参考"
[拨号字段](/configuration/shared/dial/#domain_strategy)
=== ":material-card-remove: 弃用的"
```json
{
"outbounds": [
{
"type": "socks",
"server": "example.org",
"server_port": 2080,
"domain_strategy": "prefer_ipv4",
}
]
}
```
=== ":material-card-multiple: 新的"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
}
]
},
"outbounds": [
{
"type": "socks",
"server": "example.org",
"server_port": 2080,
"domain_resolver": {
"server": "local",
"strategy": "prefer_ipv4"
}
}
]
}
```
## 1.11.0 ## 1.11.0
### 迁移旧的特殊出站到规则动作 ### 迁移旧的特殊出站到规则动作

View File

@@ -19,10 +19,12 @@ import (
) )
var ( var (
bucketSelected = []byte("selected") bucketSelected = []byte("selected")
bucketExpand = []byte("group_expand") bucketExpand = []byte("group_expand")
bucketMode = []byte("clash_mode") bucketMode = []byte("clash_mode")
bucketRuleSet = []byte("rule_set") bucketRuleSet = []byte("rule_set")
bucketScript = []byte("script")
bucketSgPersistentStore = []byte("sg_persistent_store")
bucketNameList = []string{ bucketNameList = []string{
string(bucketSelected), string(bucketSelected),
@@ -316,3 +318,70 @@ func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error {
return bucket.Put([]byte(tag), setBinary) return bucket.Put([]byte(tag), setBinary)
}) })
} }
func (c *CacheFile) LoadScript(tag string) *adapter.SavedBinary {
var savedSet adapter.SavedBinary
err := c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketScript)
if bucket == nil {
return os.ErrNotExist
}
scriptBinary := bucket.Get([]byte(tag))
if len(scriptBinary) == 0 {
return os.ErrInvalid
}
return savedSet.UnmarshalBinary(scriptBinary)
})
if err != nil {
return nil
}
return &savedSet
}
func (c *CacheFile) SaveScript(tag string, set *adapter.SavedBinary) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketScript)
if err != nil {
return err
}
scriptBinary, err := set.MarshalBinary()
if err != nil {
return err
}
return bucket.Put([]byte(tag), scriptBinary)
})
}
func (c *CacheFile) SurgePersistentStoreRead(key string) string {
var value string
_ = c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketSgPersistentStore)
if bucket == nil {
return nil
}
valueBinary := bucket.Get([]byte(key))
if len(valueBinary) > 0 {
value = string(valueBinary)
}
return nil
})
return value
}
func (c *CacheFile) SurgePersistentStoreWrite(key string, value string) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
if value != "" {
bucket, err := c.createBucket(t, bucketSgPersistentStore)
if err != nil {
return err
}
return bucket.Put([]byte(key), []byte(value))
} else {
bucket := c.bucket(t, bucketSgPersistentStore)
if bucket == nil {
return nil
}
return bucket.Delete([]byte(key))
}
})
}

View File

@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"net" "net"
"net/http" "net/http"
"runtime/debug"
"time" "time"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
@@ -13,23 +12,14 @@ import (
"github.com/sagernet/ws/wsutil" "github.com/sagernet/ws/wsutil"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render" "github.com/go-chi/render"
) )
// API created by Clash.Meta // API created by Clash.Meta
func (s *Server) setupMetaAPI(r chi.Router) { func (s *Server) setupMetaAPI(r chi.Router) {
if s.logDebug {
r := chi.NewRouter()
r.Put("/gc", func(w http.ResponseWriter, r *http.Request) {
debug.FreeOSMemory()
})
r.Mount("/", middleware.Profiler())
}
r.Get("/memory", memory(s.trafficManager)) r.Get("/memory", memory(s.trafficManager))
r.Mount("/group", groupRouter(s)) r.Mount("/group", groupRouter(s))
r.Mount("/upgrade", upgradeRouter(s))
} }
type Memory struct { type Memory struct {

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