mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-12 01:57:18 +10:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aff12ff671 | ||
|
|
101fb88255 | ||
|
|
8b489354e4 | ||
|
|
7dea6eb7a6 | ||
|
|
af1bfe4e3e | ||
|
|
d574e9eb52 | ||
|
|
2d7df1e1f2 | ||
|
|
1c0ffcf5b1 | ||
|
|
348cc39975 | ||
|
|
987899f94a | ||
|
|
d8b2d5142f | ||
|
|
134802d1ee | ||
|
|
e5e81b4de1 | ||
|
|
300c961efa | ||
|
|
7c7f512405 | ||
|
|
03e8d029c2 | ||
|
|
787b5f1931 | ||
|
|
56a7624618 | ||
|
|
3a84acf122 | ||
|
|
f600e02e47 | ||
|
|
e6d19de58a | ||
|
|
f2bbf6b2aa | ||
|
|
c54d50fd36 | ||
|
|
6a051054db | ||
|
|
49498f6439 | ||
|
|
144a890c71 | ||
|
|
afb4993445 | ||
|
|
4c9455b944 | ||
|
|
5fdc051a08 | ||
|
|
cb68a40c43 | ||
|
|
023218e6e7 | ||
|
|
2a24b94b8d | ||
|
|
c6531cf184 | ||
|
|
d4fa0ed349 | ||
|
|
10874d2dc4 | ||
|
|
5adaf1ac75 | ||
|
|
9668ea69b8 | ||
|
|
ae9bc7acf1 | ||
|
|
594ee480a2 | ||
|
|
a15b5a2463 | ||
|
|
991e755789 | ||
|
|
97d41ffde8 | ||
|
|
24af0766ac | ||
|
|
af17eaa537 | ||
|
|
3adc10a797 | ||
|
|
5eeef6b28e | ||
|
|
f4c29840c3 | ||
|
|
47fc3ebda4 | ||
|
|
9774a659b0 | ||
|
|
2e4a6de4e7 | ||
|
|
a530e424e9 | ||
|
|
0bfd487ee9 | ||
|
|
6aae834493 | ||
|
|
f56131f38e | ||
|
|
273a11d550 | ||
|
|
ae8ce75e41 | ||
|
|
d6d94b689f | ||
|
|
30d785f1ee | ||
|
|
db5ec3cdfc | ||
|
|
9aca54d039 | ||
|
|
d55d5009c2 | ||
|
|
4f3ee61104 | ||
|
|
96eb98c00a | ||
|
|
68ce9577c6 | ||
|
|
3ae036e997 | ||
|
|
5da2d1d470 | ||
|
|
8e2baf40f1 | ||
|
|
c24c40dfee | ||
|
|
32e52ce1ed | ||
|
|
ed46438359 | ||
|
|
0b5490d5a3 | ||
|
|
2d73ef511d | ||
|
|
63e6c85f6f | ||
|
|
8946a6d2d0 | ||
|
|
d3132645fb | ||
|
|
373f158fe0 | ||
|
|
ce36835fab | ||
|
|
619fa671d7 | ||
|
|
eb07c7a79e | ||
|
|
7eb3535094 | ||
|
|
93b68312cf | ||
|
|
97ce666e43 | ||
|
|
4000e1e66d | ||
|
|
270740e859 | ||
|
|
6cad142cfe | ||
|
|
093013687c | ||
|
|
ff31c469a0 | ||
|
|
fbe390268c | ||
|
|
07ac01dcb7 | ||
|
|
badfdb62cd | ||
|
|
986a410b30 | ||
|
|
9db2d58545 | ||
|
|
4eed46ac59 | ||
|
|
abc38d1dab | ||
|
|
8d6c4f1289 | ||
|
|
a2d40eb8b8 | ||
|
|
17b502bb4b | ||
|
|
a0d4421085 | ||
|
|
0d443072d1 | ||
|
|
c9fb99b799 | ||
|
|
92d245ad04 | ||
|
|
0908627297 |
30
.fpm_openwrt
Normal file
30
.fpm_openwrt
Normal file
@@ -0,0 +1,30 @@
|
||||
-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
|
||||
21
.fpm_systemd
Normal file
21
.fpm_systemd
Normal file
@@ -0,0 +1,21 @@
|
||||
-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>"
|
||||
--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/sing-box.service=/usr/lib/systemd/system/sing-box.service
|
||||
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
|
||||
|
||||
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
Executable file
28
.github/deb2ipk.sh
vendored
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/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
|
||||
349
.github/workflows/build.yml
vendored
349
.github/workflows/build.yml
vendored
@@ -46,16 +46,16 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
go-version: ^1.24
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
echo "version=${{ inputs.version }}"
|
||||
echo "version=${{ inputs.version }}"
|
||||
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Calculate version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/read_tag --nightly
|
||||
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||
- name: Set outputs
|
||||
id: outputs
|
||||
run: |-
|
||||
@@ -69,141 +69,208 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: linux_386
|
||||
goos: linux
|
||||
goarch: 386
|
||||
- name: linux_amd64
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- name: linux_arm64
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
- name: linux_arm
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
- name: linux_arm_v7
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
- name: linux_s390x
|
||||
goos: linux
|
||||
goarch: s390x
|
||||
- name: linux_riscv64
|
||||
goos: linux
|
||||
goarch: riscv64
|
||||
- name: linux_mips64le
|
||||
goos: linux
|
||||
goarch: mips64le
|
||||
- name: windows_amd64
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
require_legacy_go: true
|
||||
- name: windows_386
|
||||
goos: windows
|
||||
goarch: 386
|
||||
require_legacy_go: true
|
||||
- name: windows_arm64
|
||||
goos: windows
|
||||
goarch: arm64
|
||||
- name: darwin_arm64
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
- name: darwin_amd64
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
require_legacy_go: true
|
||||
- name: android_arm64
|
||||
goos: android
|
||||
goarch: arm64
|
||||
- name: android_arm
|
||||
goos: android
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
- name: android_amd64
|
||||
goos: android
|
||||
goarch: amd64
|
||||
- name: android_386
|
||||
goos: android
|
||||
goarch: 386
|
||||
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
|
||||
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
|
||||
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
|
||||
- { 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: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
|
||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
|
||||
- { 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: 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: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
|
||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
||||
|
||||
- { 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: arm64 }
|
||||
|
||||
- { os: darwin, arch: amd64 }
|
||||
- { os: darwin, arch: amd64, legacy_go: true }
|
||||
- { os: darwin, arch: arm64 }
|
||||
|
||||
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
|
||||
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
|
||||
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
|
||||
- { os: android, arch: "386", ndk: "i686-linux-android21" }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
if: matrix.legacy_go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
- name: Cache legacy Go
|
||||
if: matrix.require_legacy_go
|
||||
id: cache-legacy-go
|
||||
uses: actions/cache@v4
|
||||
go-version: ~1.20
|
||||
- name: Setup Go
|
||||
if: ${{ ! matrix.legacy_go }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
path: |
|
||||
~/go/go1.20.14
|
||||
key: go120
|
||||
- name: Setup legacy Go
|
||||
if: matrix.require_legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
||||
run: |-
|
||||
wget https://dl.google.com/go/go1.20.14.linux-amd64.tar.gz
|
||||
tar -xzf go1.20.14.linux-amd64.tar.gz
|
||||
mv go $HOME/go/go1.20.14
|
||||
go-version: ^1.24
|
||||
- name: Setup Android NDK
|
||||
if: matrix.goos == 'android'
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28-beta2
|
||||
ndk-version: r28
|
||||
local-cache: true
|
||||
- name: Setup Goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: 2.5.1
|
||||
install-only: true
|
||||
- name: Extract signing key
|
||||
run: |-
|
||||
mkdir -p $HOME/.gnupg
|
||||
cat > $HOME/.gnupg/sagernet.key <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
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}"
|
||||
- name: Build
|
||||
if: matrix.goos != 'android'
|
||||
run: |-
|
||||
goreleaser release --clean --split
|
||||
if: matrix.os != 'android'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOPATH: ${{ env.HOME }}/go
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GO386: ${{ matrix.go386 }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GOMIPS: ${{ matrix.gomips }}
|
||||
GOMIPS64: ${{ matrix.gomips }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
- name: Build Android
|
||||
if: matrix.goos == 'android'
|
||||
run: |-
|
||||
if: matrix.os == 'android'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
go install -v ./cmd/internal/build
|
||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build goreleaser release --clean --split
|
||||
export CC='${{ matrix.ndk }}-clang'
|
||||
export CXX="${CC}++"
|
||||
mkdir -p dist
|
||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
BUILD_GOOS: ${{ matrix.goos }}
|
||||
BUILD_GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
CGO_ENABLED: "1"
|
||||
BUILD_GOOS: ${{ matrix.os }}
|
||||
BUILD_GOARCH: ${{ matrix.arch }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
- name: Set name
|
||||
run: |-
|
||||
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
if [[ -n "${{ matrix.goarm }}" ]]; then
|
||||
DIR_NAME="${DIR_NAME}v${{ matrix.goarm }}"
|
||||
elif [[ -n "${{ matrix.go386 }}" && "${{ matrix.go386 }}" != 'sse2' ]]; then
|
||||
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}"
|
||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||
- name: Package DEB
|
||||
if: matrix.debian != ''
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y debsigs
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t deb \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.debian }}.deb" \
|
||||
--architecture ${{ matrix.debian }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||
rm -rf $HOME/.gnupg
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||
- name: Package RPM
|
||||
if: matrix.rpm != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t rpm \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.rpm }}.rpm" \
|
||||
--architecture ${{ matrix.rpm }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
cat > $HOME/.rpmmacros <<EOF
|
||||
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||
EOF
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
rpmsign --addsign dist/*.rpm
|
||||
- name: Package Pacman
|
||||
if: matrix.pacman != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libarchive-tools
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t pacman \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
||||
--architecture ${{ matrix.pacman }} \
|
||||
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
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
cd dist
|
||||
mkdir -p "${DIR_NAME}"
|
||||
cp ../LICENSE "${DIR_NAME}"
|
||||
if [ '${{ matrix.os }}' = 'windows' ]; then
|
||||
cp sing-box "${DIR_NAME}/sing-box.exe"
|
||||
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
|
||||
else
|
||||
cp sing-box "${DIR_NAME}"
|
||||
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
|
||||
fi
|
||||
rm -r "${DIR_NAME}"
|
||||
- name: Cleanup
|
||||
run: rm dist/sing-box
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-${{ matrix.name }}
|
||||
path: 'dist'
|
||||
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' || '' }}
|
||||
path: "dist"
|
||||
build_android:
|
||||
name: Build Android
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
||||
@@ -219,12 +286,12 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
go-version: ^1.24
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28-beta3
|
||||
ndk-version: r28
|
||||
- name: Setup OpenJDK
|
||||
run: |-
|
||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||
@@ -256,9 +323,16 @@ jobs:
|
||||
with:
|
||||
path: ~/.gradle
|
||||
key: gradle-${{ hashFiles('**/*.gradle') }}
|
||||
- name: Build
|
||||
- name: Update version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_android_version --ci
|
||||
- name: Update nightly version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/update_android_version --ci --nightly
|
||||
- name: Build
|
||||
run: |-
|
||||
mkdir clients/android/app/libs
|
||||
cp libbox.aar clients/android/app/libs
|
||||
cd clients/android
|
||||
@@ -268,13 +342,11 @@ jobs:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||
- name: Prepare upload
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
mkdir -p dist/release
|
||||
cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release
|
||||
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release
|
||||
mkdir -p dist
|
||||
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
|
||||
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-android-apks
|
||||
@@ -294,12 +366,12 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
go-version: ^1.24
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28-beta3
|
||||
ndk-version: r28
|
||||
- name: Setup OpenJDK
|
||||
run: |-
|
||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||
@@ -392,7 +464,7 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
go-version: ^1.24
|
||||
- name: Setup Xcode stable
|
||||
if: matrix.if && github.ref == 'refs/heads/main-next'
|
||||
run: |-
|
||||
@@ -432,19 +504,19 @@ jobs:
|
||||
|
||||
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
|
||||
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
|
||||
|
||||
|
||||
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
mkdir -p "$PROFILES_PATH"
|
||||
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
|
||||
|
||||
|
||||
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
|
||||
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
|
||||
|
||||
|
||||
xcrun notarytool store-credentials "notarytool-password" \
|
||||
--key $ASC_KEY_PATH \
|
||||
--key-id $ASC_KEY_ID \
|
||||
--issuer $ASC_KEY_ISSUER_ID
|
||||
|
||||
|
||||
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
|
||||
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
|
||||
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
|
||||
@@ -520,10 +592,10 @@ jobs:
|
||||
cd "${{ matrix.archive }}"
|
||||
zip -r SFM.dSYMs.zip dSYMs
|
||||
popd
|
||||
|
||||
mkdir -p dist/release
|
||||
cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg"
|
||||
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip"
|
||||
|
||||
mkdir -p dist
|
||||
cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg"
|
||||
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip"
|
||||
- name: Upload image
|
||||
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -544,12 +616,6 @@ jobs:
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: 2.5.1
|
||||
install-only: true
|
||||
- name: Cache ghr
|
||||
uses: actions/cache@v4
|
||||
id: cache-ghr
|
||||
@@ -574,26 +640,17 @@ jobs:
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Merge builds
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
||||
run: |-
|
||||
goreleaser continue --merge --skip publish
|
||||
mkdir -p dist/release
|
||||
mv dist/*/sing-box*{tar.gz,zip,deb,rpm,_amd64.pkg.tar.zst,_arm64.pkg.tar.zst} dist/release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
- name: Upload builds
|
||||
if: ${{ env.PUBLISHED == 'false' }}
|
||||
run: |-
|
||||
export PATH="$PATH:$HOME/go/bin"
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Replace builds
|
||||
if: ${{ env.PUBLISHED != 'false' }}
|
||||
run: |-
|
||||
export PATH="$PATH:$HOME/go/bin"
|
||||
ghr --replace -p 5 "v${VERSION}" dist/release
|
||||
ghr --replace -p 5 "v${VERSION}" dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
5
.github/workflows/lint.yml
vendored
5
.github/workflows/lint.yml
vendored
@@ -28,10 +28,11 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
go-version: ^1.24
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=30m
|
||||
install-mode: binary
|
||||
install-mode: binary
|
||||
verify: false
|
||||
|
||||
185
.github/workflows/linux.yml
vendored
185
.github/workflows/linux.yml
vendored
@@ -1,13 +1,22 @@
|
||||
name: Release to Linux repository
|
||||
name: Build Linux Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version name"
|
||||
required: true
|
||||
type: string
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
build:
|
||||
calculate_version:
|
||||
name: Calculate version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.outputs.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
@@ -16,23 +25,163 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23
|
||||
- name: Extract signing key
|
||||
go-version: ^1.24
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
mkdir -p $HOME/.gnupg
|
||||
cat > $HOME/.gnupg/sagernet.key <<EOF
|
||||
echo "version=${{ inputs.version }}"
|
||||
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Calculate version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/read_tag --ci --nightly
|
||||
- name: Set outputs
|
||||
id: outputs
|
||||
run: |-
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
build:
|
||||
name: Build binary
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
|
||||
- { os: linux, arch: "386", debian: i386, rpm: i386 }
|
||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
||||
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
||||
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
|
||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
|
||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24
|
||||
- name: Setup Android NDK
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28
|
||||
local-cache: true
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
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}"
|
||||
- name: Build
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set mtime
|
||||
run: |-
|
||||
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||
- name: Set name
|
||||
if: ${{ ! contains(needs.calculate_version.outputs.version, '-') }}
|
||||
run: |-
|
||||
echo "NAME=sing-box" >> "$GITHUB_ENV"
|
||||
- name: Set beta name
|
||||
if: contains(needs.calculate_version.outputs.version, '-')
|
||||
run: |-
|
||||
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
|
||||
if: matrix.debian != ''
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get install -y debsigs
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t deb \
|
||||
--name "${NAME}" \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
||||
--architecture ${{ matrix.debian }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||
rm -rf $HOME/.gnupg
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
||||
- name: Publish release
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||
- name: Package RPM
|
||||
if: matrix.rpm != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t rpm \
|
||||
--name "${NAME}" \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
||||
--architecture ${{ matrix.rpm }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
cat > $HOME/.rpmmacros <<EOF
|
||||
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||
EOF
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
rpmsign --addsign dist/*.rpm
|
||||
- name: Cleanup
|
||||
run: rm dist/sing-box
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release -f .goreleaser.fury.yaml --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
|
||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||
path: "dist"
|
||||
upload:
|
||||
name: Upload builds
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
- build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Download builds
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Publish packages
|
||||
run: |-
|
||||
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/
|
||||
|
||||
@@ -6,9 +6,7 @@ builds:
|
||||
- -v
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
||||
- -s
|
||||
- -buildid=
|
||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
||||
tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
@@ -50,7 +48,7 @@ nfpms:
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: config
|
||||
type: "config|noreplace"
|
||||
|
||||
- src: release/config/sing-box.service
|
||||
dst: /usr/lib/systemd/system/sing-box.service
|
||||
|
||||
@@ -95,10 +95,12 @@ archives:
|
||||
builds:
|
||||
- main
|
||||
- android
|
||||
format: tar.gz
|
||||
formats:
|
||||
- tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
formats:
|
||||
- zip
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
@@ -128,7 +130,7 @@ nfpms:
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
dst: /etc/sing-box/config.json
|
||||
type: config
|
||||
type: "config|noreplace"
|
||||
|
||||
- src: release/config/sing-box.service
|
||||
dst: /usr/lib/systemd/system/sing-box.service
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
|
||||
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
COPY . /go/src/github.com/sagernet/sing-box
|
||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||
|
||||
12
Makefile
12
Makefile
@@ -10,7 +10,7 @@ GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||
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="
|
||||
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
|
||||
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
||||
MAIN = ./cmd/sing-box
|
||||
PREFIX ?= $(shell go env GOPATH)
|
||||
|
||||
@@ -28,7 +28,7 @@ ci_build:
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
generate_completions:
|
||||
go run -v --tags $(TAGS),generate,generate_completions $(MAIN)
|
||||
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
|
||||
|
||||
install:
|
||||
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
||||
@@ -61,12 +61,6 @@ proto_install:
|
||||
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
|
||||
update_public_suffix:
|
||||
go generate common/tlsfragment/public_suffix.go
|
||||
|
||||
update_certificates:
|
||||
go run ./cmd/internal/update_certificates
|
||||
|
||||
release:
|
||||
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
||||
mkdir dist/release
|
||||
@@ -253,4 +247,4 @@ clean:
|
||||
update:
|
||||
git fetch
|
||||
git reset FETCH_HEAD --hard
|
||||
git clean -fdx
|
||||
git clean -fdx
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type CertificateStore interface {
|
||||
LifecycleService
|
||||
Pool() *x509.CertPool
|
||||
}
|
||||
|
||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||
store := service.FromContext[CertificateStore](ctx)
|
||||
if store == nil {
|
||||
return nil
|
||||
}
|
||||
return store.Pool()
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type DNSRouter interface {
|
||||
Lifecycle
|
||||
Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, domain string, options DNSQueryOptions) ([]netip.Addr, error)
|
||||
ClearCache()
|
||||
LookupReverseMapping(ip netip.Addr) (string, bool)
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
type DNSClient interface {
|
||||
Start()
|
||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
|
||||
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
|
||||
ClearCache()
|
||||
}
|
||||
|
||||
type DNSQueryOptions struct {
|
||||
Transport DNSTransport
|
||||
Strategy C.DomainStrategy
|
||||
DisableCache bool
|
||||
RewriteTTL *uint32
|
||||
ClientSubnet netip.Prefix
|
||||
}
|
||||
|
||||
type RDRCStore interface {
|
||||
LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
|
||||
SaveRDRC(transportName string, qName string, qType uint16) error
|
||||
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
||||
}
|
||||
|
||||
type DNSTransport interface {
|
||||
Type() string
|
||||
Tag() string
|
||||
Dependencies() []string
|
||||
Reset()
|
||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||
}
|
||||
|
||||
type LegacyDNSTransport interface {
|
||||
LegacyStrategy() C.DomainStrategy
|
||||
LegacyClientSubnet() netip.Prefix
|
||||
}
|
||||
|
||||
type DNSTransportRegistry interface {
|
||||
option.DNSTransportOptionsRegistry
|
||||
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
||||
}
|
||||
|
||||
type DNSTransportManager interface {
|
||||
Lifecycle
|
||||
Transports() []DNSTransport
|
||||
Transport(tag string) (DNSTransport, bool)
|
||||
Default() DNSTransport
|
||||
FakeIP() FakeIPTransport
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
)
|
||||
|
||||
@@ -14,20 +16,7 @@ type ClashServer interface {
|
||||
ConnectionTracker
|
||||
Mode() string
|
||||
ModeList() []string
|
||||
HistoryStorage() URLTestHistoryStorage
|
||||
}
|
||||
|
||||
type URLTestHistory struct {
|
||||
Time time.Time `json:"time"`
|
||||
Delay uint16 `json:"delay"`
|
||||
}
|
||||
|
||||
type URLTestHistoryStorage interface {
|
||||
SetHook(hook chan<- struct{})
|
||||
LoadURLTestHistory(tag string) *URLTestHistory
|
||||
DeleteURLTestHistory(tag string)
|
||||
StoreURLTestHistory(tag string, history *URLTestHistory)
|
||||
Close() error
|
||||
HistoryStorage() *urltest.HistoryStorage
|
||||
}
|
||||
|
||||
type V2RayServer interface {
|
||||
@@ -42,7 +31,7 @@ type CacheFile interface {
|
||||
FakeIPStorage
|
||||
|
||||
StoreRDRC() bool
|
||||
RDRCStore
|
||||
dns.RDRCStore
|
||||
|
||||
LoadMode() string
|
||||
StoreMode(mode string) error
|
||||
@@ -52,10 +41,6 @@ type CacheFile interface {
|
||||
StoreGroupExpand(group string, expand bool) error
|
||||
LoadRuleSet(tag string) *SavedBinary
|
||||
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 {
|
||||
|
||||
@@ -3,6 +3,7 @@ package adapter
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
@@ -26,6 +27,6 @@ type FakeIPStorage interface {
|
||||
}
|
||||
|
||||
type FakeIPTransport interface {
|
||||
DNSTransport
|
||||
dns.Transport
|
||||
Store() FakeIPStore
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
@@ -55,12 +53,11 @@ type InboundContext struct {
|
||||
|
||||
// sniffer
|
||||
|
||||
Protocol string
|
||||
Domain string
|
||||
Client string
|
||||
SniffContext any
|
||||
HTTPRequest *http.Request
|
||||
ClientHello *tls.ClientHelloInfo
|
||||
Protocol string
|
||||
Domain string
|
||||
Client string
|
||||
SniffContext any
|
||||
PacketSniffError error
|
||||
|
||||
// cache
|
||||
|
||||
@@ -75,15 +72,14 @@ type InboundContext struct {
|
||||
UDPDisableDomainUnmapping bool
|
||||
UDPConnect bool
|
||||
UDPTimeout time.Duration
|
||||
TLSFragment bool
|
||||
TLSFragmentFallbackDelay time.Duration
|
||||
MITM *option.MITMRouteOptions
|
||||
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
FallbackDelay time.Duration
|
||||
|
||||
DNSServer string
|
||||
|
||||
DestinationAddresses []netip.Addr
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
import E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
type StartStage uint8
|
||||
|
||||
@@ -47,9 +45,6 @@ type LifecycleService interface {
|
||||
|
||||
func Start(stage StartStage, services ...Lifecycle) error {
|
||||
for _, service := range services {
|
||||
if service == nil {
|
||||
continue
|
||||
}
|
||||
err := service.Start(stage)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type MITMEngine interface {
|
||||
Lifecycle
|
||||
ExportCertificate() *x509.Certificate
|
||||
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
@@ -25,17 +25,16 @@ type NetworkManager interface {
|
||||
PackageManager() tun.PackageManager
|
||||
WIFIState() WIFIState
|
||||
ResetNetwork()
|
||||
UpdateWIFIState()
|
||||
}
|
||||
|
||||
type NetworkOptions struct {
|
||||
BindInterface string
|
||||
RoutingMark uint32
|
||||
DomainResolver string
|
||||
DomainResolveOptions DNSQueryOptions
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
FallbackDelay time.Duration
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
FallbackDelay time.Duration
|
||||
BindInterface string
|
||||
RoutingMark uint32
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
|
||||
@@ -23,7 +23,7 @@ type Manager struct {
|
||||
registry adapter.OutboundRegistry
|
||||
endpoint adapter.EndpointManager
|
||||
defaultTag string
|
||||
access sync.RWMutex
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
outbounds []adapter.Outbound
|
||||
@@ -169,15 +169,15 @@ func (m *Manager) Close() error {
|
||||
}
|
||||
|
||||
func (m *Manager) Outbounds() []adapter.Outbound {
|
||||
m.access.RLock()
|
||||
defer m.access.RUnlock()
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.outbounds
|
||||
}
|
||||
|
||||
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
||||
m.access.RLock()
|
||||
m.access.Lock()
|
||||
outbound, found := m.outboundByTag[tag]
|
||||
m.access.RUnlock()
|
||||
m.access.Unlock()
|
||||
if found {
|
||||
return outbound, true
|
||||
}
|
||||
@@ -185,8 +185,8 @@ func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
||||
}
|
||||
|
||||
func (m *Manager) Default() adapter.Outbound {
|
||||
m.access.RLock()
|
||||
defer m.access.RUnlock()
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.defaultOutbound != nil {
|
||||
return m.defaultOutbound
|
||||
} else {
|
||||
@@ -196,9 +196,9 @@ func (m *Manager) Default() adapter.Outbound {
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
outbound, found := m.outboundByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.outboundByTag, tag)
|
||||
@@ -232,6 +232,7 @@ func (m *Manager) Remove(tag string) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return common.Close(outbound)
|
||||
}
|
||||
|
||||
@@ -2,29 +2,44 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/common/geoip"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-dns"
|
||||
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/x/list"
|
||||
|
||||
mdns "github.com/miekg/dns"
|
||||
"go4.org/netipx"
|
||||
)
|
||||
|
||||
type Router interface {
|
||||
Lifecycle
|
||||
|
||||
FakeIPStore() FakeIPStore
|
||||
|
||||
ConnectionRouter
|
||||
PreMatch(metadata InboundContext) error
|
||||
ConnectionRouterEx
|
||||
|
||||
GeoIPReader() *geoip.Reader
|
||||
LoadGeosite(code string) (Rule, error)
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
NeedWIFIState() bool
|
||||
|
||||
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
||||
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
||||
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
||||
ClearDNSCache()
|
||||
Rules() []Rule
|
||||
SetTracker(tracker ConnectionTracker)
|
||||
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
@@ -68,14 +83,12 @@ type RuleSetMetadata struct {
|
||||
ContainsIPCIDRRule bool
|
||||
}
|
||||
type HTTPStartContext struct {
|
||||
ctx context.Context
|
||||
access sync.Mutex
|
||||
httpClientCache map[string]*http.Client
|
||||
}
|
||||
|
||||
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||
func NewHTTPStartContext() *HTTPStartContext {
|
||||
return &HTTPStartContext{
|
||||
ctx: ctx,
|
||||
httpClientCache: make(map[string]*http.Client),
|
||||
}
|
||||
}
|
||||
@@ -93,10 +106,6 @@ func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Clie
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||
RootCAs: RootPoolFromContext(c.ctx),
|
||||
},
|
||||
},
|
||||
}
|
||||
c.httpClientCache[detour] = httpClient
|
||||
|
||||
@@ -13,6 +13,7 @@ type Rule interface {
|
||||
HeadlessRule
|
||||
Service
|
||||
Type() string
|
||||
UpdateGeosite() error
|
||||
Action() RuleAction
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
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
|
||||
}
|
||||
165
box.go
165
box.go
@@ -12,22 +12,17 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
"github.com/sagernet/sing-box/common/certificate"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport/local"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/mitm"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
"github.com/sagernet/sing-box/script"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
@@ -39,21 +34,17 @@ import (
|
||||
var _ adapter.Service = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
script *script.Manager
|
||||
mitm adapter.MITMEngine //*mitm.Engine
|
||||
services []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
services []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
@@ -67,7 +58,6 @@ func Context(
|
||||
inboundRegistry adapter.InboundRegistry,
|
||||
outboundRegistry adapter.OutboundRegistry,
|
||||
endpointRegistry adapter.EndpointRegistry,
|
||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||
) context.Context {
|
||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||
@@ -84,10 +74,6 @@ func Context(
|
||||
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
||||
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
||||
}
|
||||
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
||||
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -102,7 +88,6 @@ func New(options Options) (*Box, error) {
|
||||
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||
|
||||
if endpointRegistry == nil {
|
||||
return nil, E.New("missing endpoint registry in context")
|
||||
@@ -146,73 +131,33 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "create log factory")
|
||||
}
|
||||
|
||||
var services []adapter.LifecycleService
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
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)
|
||||
}
|
||||
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
}
|
||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||
connectionManager := route.NewConnectionManager(ctx, logFactory.NewLogger("connection"))
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||
service.MustRegister[adapter.Router](ctx, router)
|
||||
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
||||
router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize router")
|
||||
}
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
|
||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
if ntpOptions.Enabled {
|
||||
timeService = new(tls.TimeServiceWrapper)
|
||||
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||
}
|
||||
for i, transportOptions := range dnsOptions.Servers {
|
||||
var tag string
|
||||
if transportOptions.Tag != "" {
|
||||
tag = transportOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = dnsTransportManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
transportOptions.Type,
|
||||
transportOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize DNS server[", i, "]")
|
||||
}
|
||||
}
|
||||
err = dnsRouter.Initialize(dnsOptions.Rules)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize dns router")
|
||||
}
|
||||
|
||||
for i, endpointOptions := range options.Endpoints {
|
||||
var tag string
|
||||
if endpointOptions.Tag != "" {
|
||||
@@ -220,8 +165,15 @@ func New(options Options) (*Box, error) {
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
endpointCtx := ctx
|
||||
if tag != "" {
|
||||
// TODO: remove this
|
||||
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
|
||||
Outbound: tag,
|
||||
})
|
||||
}
|
||||
err = endpointManager.Create(
|
||||
ctx,
|
||||
endpointCtx,
|
||||
router,
|
||||
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
@@ -229,7 +181,7 @@ func New(options Options) (*Box, error) {
|
||||
endpointOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize endpoint[", i, "]")
|
||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, inboundOptions := range options.Inbounds {
|
||||
@@ -286,24 +238,13 @@ func New(options Options) (*Box, error) {
|
||||
option.DirectOutboundOptions{},
|
||||
),
|
||||
))
|
||||
dnsTransportManager.Initialize(common.Must1(
|
||||
local.NewTransport(
|
||||
ctx,
|
||||
logFactory.NewLogger("dns/local"),
|
||||
"local",
|
||||
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 {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize platform interface")
|
||||
}
|
||||
}
|
||||
var services []adapter.LifecycleService
|
||||
if needCacheFile {
|
||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||
@@ -316,7 +257,7 @@ func New(options Options) (*Box, error) {
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create clash-server")
|
||||
}
|
||||
router.SetTracker(clashServer)
|
||||
router.AppendTracker(clashServer)
|
||||
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||
services = append(services, clashServer)
|
||||
}
|
||||
@@ -326,13 +267,13 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "create v2ray-server")
|
||||
}
|
||||
if v2rayServer.StatsService() != nil {
|
||||
router.SetTracker(v2rayServer.StatsService())
|
||||
router.AppendTracker(v2rayServer.StatsService())
|
||||
services = append(services, v2rayServer)
|
||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||
}
|
||||
}
|
||||
if ntpOptions.Enabled {
|
||||
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
||||
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create NTP service")
|
||||
}
|
||||
@@ -347,32 +288,18 @@ func New(options Options) (*Box, error) {
|
||||
timeService.TimeService = ntpService
|
||||
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{
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
script: scriptManager,
|
||||
mitm: mitmEngine,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
services: services,
|
||||
done: make(chan struct{}),
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
services: services,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -426,11 +353,11 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router, s.script, s.mitm)
|
||||
err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.connection, s.router)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -454,7 +381,7 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.connection, s.router, s.inbound, s.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -462,7 +389,7 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -481,7 +408,7 @@ func (s *Box) Close() error {
|
||||
close(s.done)
|
||||
}
|
||||
err := common.Close(
|
||||
s.inbound, s.outbound, s.endpoint, s.mitm, s.script, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.network,
|
||||
)
|
||||
for _, lifecycleService := range s.services {
|
||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||
|
||||
Submodule clients/android updated: 599d8cecac...9dd336679d
Submodule clients/apple updated: e43e3a3f64...ae5818ee5a
@@ -48,7 +48,7 @@ func FindSDK() {
|
||||
}
|
||||
|
||||
func findNDK() bool {
|
||||
const fixedVersion = "28.0.12916984"
|
||||
const fixedVersion = "28.0.13004108"
|
||||
const versionFile = "source.properties"
|
||||
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
|
||||
androidNDKPath = fixedPath
|
||||
|
||||
@@ -5,40 +5,49 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||
"github.com/sagernet/sing-box/common/badversion"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
var nightly bool
|
||||
var (
|
||||
flagRunInCI bool
|
||||
flagRunNightly bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&nightly, "nightly", false, "Print nightly tag")
|
||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if nightly {
|
||||
version, err := build_shared.ReadTagVersionRev()
|
||||
var (
|
||||
versionStr string
|
||||
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 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var versionStr string
|
||||
if version.PreReleaseIdentifier != "" {
|
||||
versionStr = version.VersionString() + "-nightly"
|
||||
} else {
|
||||
version.Patch++
|
||||
versionStr = version.VersionString() + "-nightly"
|
||||
}
|
||||
err = setGitHubEnv("version", versionStr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
tag, err := build_shared.ReadTag()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
os.Stdout.WriteString("unknown\n")
|
||||
} else {
|
||||
os.Stdout.WriteString(tag + "\n")
|
||||
os.Stdout.WriteString(versionStr + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,14 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
var flagRunInCI bool
|
||||
var (
|
||||
flagRunInCI bool
|
||||
flagRunNightly bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -46,21 +50,23 @@ func main() {
|
||||
switch propPair[0] {
|
||||
case "VERSION_NAME":
|
||||
if propPair[1] != newVersion {
|
||||
log.Info("updated version from ", propPair[1], " to ", newVersion)
|
||||
versionUpdated = true
|
||||
propPair[1] = newVersion
|
||||
log.Info("updated version to ", newVersion)
|
||||
}
|
||||
case "GO_VERSION":
|
||||
if propPair[1] != runtime.Version() {
|
||||
log.Info("updated Go version from ", propPair[1], " to ", runtime.Version())
|
||||
goVersionUpdated = true
|
||||
propPair[1] = runtime.Version()
|
||||
log.Info("updated Go version to ", runtime.Version())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !(versionUpdated || goVersionUpdated) {
|
||||
log.Info("version not changed")
|
||||
return
|
||||
} else if flagRunInCI && !flagRunNightly {
|
||||
log.Fatal("version changed, commit changes first.")
|
||||
}
|
||||
for _, propPair := range propsList {
|
||||
switch propPair[0] {
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := updateMozillaIncludedRootCAs()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func updateMozillaIncludedRootCAs() error {
|
||||
response, err := http.Get("https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
reader := csv.NewReader(response.Body)
|
||||
header, err := reader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
geoIndex := slices.Index(header, "Geographic Focus")
|
||||
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||
certIndex := slices.Index(header, "PEM Info")
|
||||
|
||||
generated := strings.Builder{}
|
||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||
|
||||
package certificate
|
||||
|
||||
import "crypto/x509"
|
||||
|
||||
var mozillaIncluded *x509.CertPool
|
||||
|
||||
func init() {
|
||||
mozillaIncluded = x509.NewCertPool()
|
||||
`)
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if record[geoIndex] == "China" {
|
||||
continue
|
||||
}
|
||||
generated.WriteString("\n // ")
|
||||
generated.WriteString(record[nameIndex])
|
||||
generated.WriteString("\n")
|
||||
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||
generated.WriteString(record[certIndex])
|
||||
generated.WriteString("`))\n")
|
||||
}
|
||||
generated.WriteString("}\n")
|
||||
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||
}
|
||||
@@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) {
|
||||
configPaths = append(configPaths, "config.json")
|
||||
}
|
||||
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
|
||||
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry())
|
||||
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry())
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
@@ -56,6 +57,14 @@ func ruleSetMatch(sourcePath string, domain string) error {
|
||||
if err != nil {
|
||||
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
|
||||
switch flagRuleSetMatchFormat {
|
||||
case C.RuleSetFormatSource:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,184 +0,0 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/fswatch"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ adapter.CertificateStore = (*Store)(nil)
|
||||
|
||||
type Store struct {
|
||||
systemPool *x509.CertPool
|
||||
currentPool *x509.CertPool
|
||||
certificate string
|
||||
certificatePaths []string
|
||||
certificateDirectoryPaths []string
|
||||
watcher *fswatch.Watcher
|
||||
}
|
||||
|
||||
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
||||
var systemPool *x509.CertPool
|
||||
switch options.Store {
|
||||
case C.CertificateStoreSystem, "":
|
||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||
systemCertificates := platformInterface.SystemCertificates()
|
||||
if len(systemCertificates) > 0 {
|
||||
systemPool = x509.NewCertPool()
|
||||
for _, cert := range systemCertificates {
|
||||
if !systemPool.AppendCertsFromPEM([]byte(cert)) {
|
||||
return nil, E.New("invalid system certificate PEM: ", cert)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
certPool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
systemPool = certPool
|
||||
}
|
||||
case C.CertificateStoreMozilla:
|
||||
systemPool = mozillaIncluded
|
||||
case C.CertificateStoreNone:
|
||||
systemPool = nil
|
||||
default:
|
||||
return nil, E.New("unknown certificate store: ", options.Store)
|
||||
}
|
||||
store := &Store{
|
||||
systemPool: systemPool,
|
||||
certificate: strings.Join(options.Certificate, "\n"),
|
||||
certificatePaths: options.CertificatePath,
|
||||
certificateDirectoryPaths: options.CertificateDirectoryPath,
|
||||
}
|
||||
var watchPaths []string
|
||||
for _, target := range options.CertificatePath {
|
||||
watchPaths = append(watchPaths, target)
|
||||
}
|
||||
for _, target := range options.CertificateDirectoryPath {
|
||||
watchPaths = append(watchPaths, target)
|
||||
}
|
||||
if len(watchPaths) > 0 {
|
||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||
Path: watchPaths,
|
||||
Logger: logger,
|
||||
Callback: func(_ string) {
|
||||
err := store.update()
|
||||
if err != nil {
|
||||
logger.Error(E.Cause(err, "reload certificates"))
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "fswatch: create fsnotify watcher")
|
||||
}
|
||||
store.watcher = watcher
|
||||
}
|
||||
err := store.update()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initializing certificate store")
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (s *Store) Name() string {
|
||||
return "certificate"
|
||||
}
|
||||
|
||||
func (s *Store) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
if s.watcher != nil {
|
||||
return s.watcher.Start()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) Close() error {
|
||||
if s.watcher != nil {
|
||||
return s.watcher.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) Pool() *x509.CertPool {
|
||||
return s.currentPool
|
||||
}
|
||||
|
||||
func (s *Store) update() error {
|
||||
var currentPool *x509.CertPool
|
||||
if s.systemPool == nil {
|
||||
currentPool = x509.NewCertPool()
|
||||
} else {
|
||||
currentPool = s.systemPool.Clone()
|
||||
}
|
||||
if s.certificate != "" {
|
||||
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||
return E.New("invalid certificate PEM strings")
|
||||
}
|
||||
}
|
||||
for _, path := range s.certificatePaths {
|
||||
pemContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||
return E.New("invalid certificate PEM file: ", path)
|
||||
}
|
||||
}
|
||||
var firstErr error
|
||||
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||
directoryEntries, err := readUniqueDirectoryEntries(directoryPath)
|
||||
if err != nil {
|
||||
if firstErr == nil && !os.IsNotExist(err) {
|
||||
firstErr = E.Cause(err, "invalid certificate directory: ", directoryPath)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, directoryEntry := range directoryEntries {
|
||||
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||
if err == nil {
|
||||
currentPool.AppendCertsFromPEM(pemContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
if firstErr != nil {
|
||||
return firstErr
|
||||
}
|
||||
s.currentPool = currentPool
|
||||
return nil
|
||||
}
|
||||
|
||||
func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) {
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uniq := files[:0]
|
||||
for _, f := range files {
|
||||
if !isSameDirSymlink(f, dir) {
|
||||
uniq = append(uniq, f)
|
||||
}
|
||||
}
|
||||
return uniq, nil
|
||||
}
|
||||
|
||||
func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
||||
if f.Type()&fs.ModeSymlink == 0 {
|
||||
return false
|
||||
}
|
||||
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||
return err == nil && !strings.Contains(target, "/")
|
||||
}
|
||||
@@ -35,6 +35,7 @@ type DefaultDialer struct {
|
||||
udpListener net.ListenConfig
|
||||
udpAddr4 string
|
||||
udpAddr6 string
|
||||
isWireGuardListener bool
|
||||
networkManager adapter.NetworkManager
|
||||
networkStrategy *C.NetworkStrategy
|
||||
defaultNetworkStrategy bool
|
||||
@@ -182,6 +183,11 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
}
|
||||
setMultiPathTCP(&dialer4)
|
||||
}
|
||||
if options.IsWireGuardListener {
|
||||
for _, controlFn := range WgControlFns {
|
||||
listener.Control = control.Append(listener.Control, controlFn)
|
||||
}
|
||||
}
|
||||
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -198,6 +204,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
udpListener: listener,
|
||||
udpAddr4: udpAddr4,
|
||||
udpAddr6: udpAddr6,
|
||||
isWireGuardListener: options.IsWireGuardListener,
|
||||
networkManager: networkManager,
|
||||
networkStrategy: networkStrategy,
|
||||
defaultNetworkStrategy: defaultNetworkStrategy,
|
||||
@@ -326,7 +333,17 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
|
||||
return d.udpListener.ListenPacket(context.Background(), network, address)
|
||||
udpListener := d.udpListener
|
||||
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) {
|
||||
|
||||
@@ -18,6 +18,7 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
|
||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||
return nil, false, E.New("no available network interface")
|
||||
}
|
||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = N.DefaultFallbackDelay
|
||||
}
|
||||
@@ -31,7 +32,9 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
|
||||
results := make(chan dialResult) // unbuffered
|
||||
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
||||
perNetDialer := dialer
|
||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||
if defaultInterface == nil || iif.Index != defaultInterface.Index {
|
||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||
}
|
||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
select {
|
||||
@@ -89,6 +92,7 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
|
||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||
return nil, false, E.New("no available network interface")
|
||||
}
|
||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = N.DefaultFallbackDelay
|
||||
}
|
||||
@@ -103,7 +107,9 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
|
||||
results := make(chan dialResult) // unbuffered
|
||||
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
||||
perNetDialer := dialer
|
||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||
if defaultInterface == nil || iif.Index != defaultInterface.Index {
|
||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||
}
|
||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
select {
|
||||
@@ -149,10 +155,13 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
|
||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||
return nil, E.New("no available network interface")
|
||||
}
|
||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
||||
var errors []error
|
||||
for _, primaryInterface := range primaryInterfaces {
|
||||
perNetListener := listener
|
||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index))
|
||||
if defaultInterface == nil || primaryInterface.Index != defaultInterface.Index {
|
||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index))
|
||||
}
|
||||
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
@@ -161,7 +170,9 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
|
||||
}
|
||||
for _, fallbackInterface := range fallbackInterfaces {
|
||||
perNetListener := listener
|
||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index))
|
||||
if defaultInterface == nil || fallbackInterface.Index != defaultInterface.Index {
|
||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index))
|
||||
}
|
||||
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
|
||||
@@ -29,18 +29,16 @@ func (d *DetourDialer) Start() error {
|
||||
}
|
||||
|
||||
func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
||||
d.initOnce.Do(d.init)
|
||||
d.initOnce.Do(func() {
|
||||
var loaded bool
|
||||
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
}
|
||||
})
|
||||
return d.dialer, d.initErr
|
||||
}
|
||||
|
||||
func (d *DetourDialer) init() {
|
||||
var loaded bool
|
||||
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
dialer, err := d.Dialer()
|
||||
if err != nil {
|
||||
|
||||
@@ -8,113 +8,68 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-dns"
|
||||
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/service"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Context context.Context
|
||||
Options option.DialerOptions
|
||||
RemoteIsDomain bool
|
||||
DirectResolver bool
|
||||
ResolverOnDetour bool
|
||||
}
|
||||
|
||||
// TODO: merge with NewWithOptions
|
||||
func New(ctx context.Context, options option.DialerOptions, remoteIsDomain bool) (N.Dialer, error) {
|
||||
return NewWithOptions(Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
RemoteIsDomain: remoteIsDomain,
|
||||
})
|
||||
}
|
||||
|
||||
func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
dialOptions := options.Options
|
||||
func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) {
|
||||
if options.IsWireGuardListener {
|
||||
return NewDefault(ctx, options)
|
||||
}
|
||||
var (
|
||||
dialer N.Dialer
|
||||
err error
|
||||
)
|
||||
if dialOptions.Detour != "" {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDetour(outboundManager, dialOptions.Detour)
|
||||
} else {
|
||||
dialer, err = NewDefault(options.Context, dialOptions)
|
||||
if options.Detour == "" {
|
||||
dialer, err = NewDefault(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
outboundManager := service.FromContext[adapter.OutboundManager](ctx)
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDetour(outboundManager, options.Detour)
|
||||
}
|
||||
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour) {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||
var defaultOptions adapter.NetworkOptions
|
||||
if networkManager != nil {
|
||||
defaultOptions = networkManager.DefaultOptions()
|
||||
if options.Detour == "" {
|
||||
router := service.FromContext[adapter.Router](ctx)
|
||||
if router != nil {
|
||||
dialer = NewResolveDialer(
|
||||
router,
|
||||
dialer,
|
||||
options.Detour == "" && !options.TCPFastOpen,
|
||||
dns.DomainStrategy(options.DomainStrategy),
|
||||
time.Duration(options.FallbackDelay))
|
||||
}
|
||||
var (
|
||||
server string
|
||||
dnsQueryOptions adapter.DNSQueryOptions
|
||||
resolveFallbackDelay time.Duration
|
||||
)
|
||||
if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" {
|
||||
var transport adapter.DNSTransport
|
||||
if !options.DirectResolver {
|
||||
var loaded bool
|
||||
transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server)
|
||||
if !loaded {
|
||||
return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server)
|
||||
}
|
||||
}
|
||||
var strategy C.DomainStrategy
|
||||
if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||
strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy)
|
||||
} else if
|
||||
//nolint:staticcheck
|
||||
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
||||
//nolint:staticcheck
|
||||
strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
||||
}
|
||||
server = dialOptions.DomainResolver.Server
|
||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||
Transport: transport,
|
||||
Strategy: strategy,
|
||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||
}
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else if options.DirectResolver {
|
||||
return nil, E.New("missing domain resolver for domain server address")
|
||||
} else if defaultOptions.DomainResolver != "" {
|
||||
dnsQueryOptions = defaultOptions.DomainResolveOptions
|
||||
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
|
||||
if !loaded {
|
||||
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
|
||||
}
|
||||
dnsQueryOptions.Transport = transport
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else {
|
||||
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||
}
|
||||
dialer = NewResolveDialer(
|
||||
options.Context,
|
||||
dialer,
|
||||
dialOptions.Detour == "" && !dialOptions.TCPFastOpen,
|
||||
server,
|
||||
dnsQueryOptions,
|
||||
resolveFallbackDelay,
|
||||
)
|
||||
}
|
||||
return dialer, nil
|
||||
}
|
||||
|
||||
func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInterfaceDialer, error) {
|
||||
if options.Detour != "" {
|
||||
return nil, E.New("`detour` is not supported in direct context")
|
||||
}
|
||||
if options.IsWireGuardListener {
|
||||
return NewDefault(ctx, options)
|
||||
}
|
||||
dialer, err := NewDefault(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewResolveParallelInterfaceDialer(
|
||||
service.FromContext[adapter.Router](ctx),
|
||||
dialer,
|
||||
true,
|
||||
dns.DomainStrategy(options.DomainStrategy),
|
||||
time.Duration(options.FallbackDelay),
|
||||
), nil
|
||||
}
|
||||
|
||||
type ParallelInterfaceDialer interface {
|
||||
N.Dialer
|
||||
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||
|
||||
@@ -3,17 +3,16 @@ package dialer
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"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/service"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -21,37 +20,21 @@ var (
|
||||
_ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil)
|
||||
)
|
||||
|
||||
type ResolveDialer interface {
|
||||
N.Dialer
|
||||
QueryOptions() adapter.DNSQueryOptions
|
||||
}
|
||||
|
||||
type ParallelInterfaceResolveDialer interface {
|
||||
ParallelInterfaceDialer
|
||||
QueryOptions() adapter.DNSQueryOptions
|
||||
}
|
||||
|
||||
type resolveDialer struct {
|
||||
transport adapter.DNSTransportManager
|
||||
router adapter.DNSRouter
|
||||
dialer N.Dialer
|
||||
parallel bool
|
||||
server string
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
queryOptions adapter.DNSQueryOptions
|
||||
router adapter.Router
|
||||
strategy dns.DomainStrategy
|
||||
fallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer {
|
||||
func NewResolveDialer(router adapter.Router, dialer N.Dialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) N.Dialer {
|
||||
return &resolveDialer{
|
||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||
router: service.FromContext[adapter.DNSRouter](ctx),
|
||||
dialer: dialer,
|
||||
parallel: parallel,
|
||||
server: server,
|
||||
queryOptions: queryOptions,
|
||||
fallbackDelay: fallbackDelay,
|
||||
dialer,
|
||||
parallel,
|
||||
router,
|
||||
strategy,
|
||||
fallbackDelay,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,68 +43,59 @@ type resolveParallelNetworkDialer struct {
|
||||
dialer ParallelInterfaceDialer
|
||||
}
|
||||
|
||||
func NewResolveParallelInterfaceDialer(ctx context.Context, dialer ParallelInterfaceDialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ParallelInterfaceResolveDialer {
|
||||
func NewResolveParallelInterfaceDialer(router adapter.Router, dialer ParallelInterfaceDialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) ParallelInterfaceDialer {
|
||||
return &resolveParallelNetworkDialer{
|
||||
resolveDialer{
|
||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||
router: service.FromContext[adapter.DNSRouter](ctx),
|
||||
dialer: dialer,
|
||||
parallel: parallel,
|
||||
server: server,
|
||||
queryOptions: queryOptions,
|
||||
fallbackDelay: fallbackDelay,
|
||||
dialer,
|
||||
parallel,
|
||||
router,
|
||||
strategy,
|
||||
fallbackDelay,
|
||||
},
|
||||
dialer,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *resolveDialer) initialize() error {
|
||||
d.initOnce.Do(d.initServer)
|
||||
return d.initErr
|
||||
}
|
||||
|
||||
func (d *resolveDialer) initServer() {
|
||||
if d.server == "" {
|
||||
return
|
||||
}
|
||||
transport, loaded := d.transport.Transport(d.server)
|
||||
if !loaded {
|
||||
d.initErr = E.New("domain resolver not found: " + d.server)
|
||||
return
|
||||
}
|
||||
d.queryOptions.Transport = transport
|
||||
}
|
||||
|
||||
func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
err := d.initialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
return d.dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||
metadata.Destination = destination
|
||||
metadata.Domain = ""
|
||||
var addresses []netip.Addr
|
||||
var err error
|
||||
if d.strategy == dns.DomainStrategyAsIS {
|
||||
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
||||
} else {
|
||||
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.parallel {
|
||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||
} else {
|
||||
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
err := d.initialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
return d.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||
metadata.Destination = destination
|
||||
metadata.Domain = ""
|
||||
var addresses []netip.Addr
|
||||
var err error
|
||||
if d.strategy == dns.DomainStrategyAsIS {
|
||||
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
||||
} else {
|
||||
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -132,24 +106,21 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||
}
|
||||
|
||||
func (d *resolveDialer) QueryOptions() adapter.DNSQueryOptions {
|
||||
return d.queryOptions
|
||||
}
|
||||
|
||||
func (d *resolveDialer) Upstream() any {
|
||||
return d.dialer
|
||||
}
|
||||
|
||||
func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||
err := d.initialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
return d.dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||
metadata.Destination = destination
|
||||
metadata.Domain = ""
|
||||
var addresses []netip.Addr
|
||||
var err error
|
||||
if d.strategy == dns.DomainStrategyAsIS {
|
||||
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
||||
} else {
|
||||
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -157,28 +128,30 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
|
||||
fallbackDelay = d.fallbackDelay
|
||||
}
|
||||
if d.parallel {
|
||||
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
} else {
|
||||
return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||
err := d.initialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
return d.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
||||
metadata.Destination = destination
|
||||
metadata.Domain = ""
|
||||
var addresses []netip.Addr
|
||||
var err error
|
||||
if d.strategy == dns.DomainStrategyAsIS {
|
||||
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
||||
} else {
|
||||
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fallbackDelay == 0 {
|
||||
fallbackDelay = d.fallbackDelay
|
||||
}
|
||||
conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -186,10 +159,6 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
|
||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||
}
|
||||
|
||||
func (d *resolveParallelNetworkDialer) QueryOptions() adapter.DNSQueryOptions {
|
||||
return d.queryOptions
|
||||
}
|
||||
|
||||
func (d *resolveParallelNetworkDialer) Upstream() any {
|
||||
func (d *resolveDialer) Upstream() any {
|
||||
return d.dialer
|
||||
}
|
||||
|
||||
@@ -7,27 +7,24 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type DefaultOutboundDialer struct {
|
||||
outbound adapter.OutboundManager
|
||||
outboundManager adapter.OutboundManager
|
||||
}
|
||||
|
||||
func NewDefaultOutbound(ctx context.Context) N.Dialer {
|
||||
return &DefaultOutboundDialer{
|
||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||
}
|
||||
func NewDefaultOutbound(outboundManager adapter.OutboundManager) N.Dialer {
|
||||
return &DefaultOutboundDialer{outboundManager: outboundManager}
|
||||
}
|
||||
|
||||
func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
return d.outbound.Default().DialContext(ctx, network, destination)
|
||||
return d.outboundManager.Default().DialContext(ctx, network, destination)
|
||||
}
|
||||
|
||||
func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return d.outbound.Default().ListenPacket(ctx, destination)
|
||||
return d.outboundManager.Default().ListenPacket(ctx, destination)
|
||||
}
|
||||
|
||||
func (d *DefaultOutboundDialer) Upstream() any {
|
||||
return d.outbound.Default()
|
||||
return d.outboundManager.Default()
|
||||
}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -23,7 +23,6 @@ type Config struct {
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
ProcessID uint32
|
||||
ProcessPath string
|
||||
PackageName string
|
||||
User string
|
||||
|
||||
@@ -2,11 +2,14 @@ package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/winiphlpapi"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
@@ -23,39 +26,201 @@ func NewSearcher(_ Config) (Searcher, error) {
|
||||
return &windowsSearcher{}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
|
||||
procGetExtendedTcpTable = modiphlpapi.NewProc("GetExtendedTcpTable")
|
||||
procGetExtendedUdpTable = modiphlpapi.NewProc("GetExtendedUdpTable")
|
||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW")
|
||||
)
|
||||
|
||||
func initWin32API() error {
|
||||
return winiphlpapi.LoadExtendedTable()
|
||||
err := modiphlpapi.Load()
|
||||
if err != nil {
|
||||
return E.Cause(err, "load iphlpapi.dll")
|
||||
}
|
||||
|
||||
err = procGetExtendedTcpTable.Find()
|
||||
if err != nil {
|
||||
return E.Cause(err, "load iphlpapi::GetExtendedTcpTable")
|
||||
}
|
||||
|
||||
err = procGetExtendedUdpTable.Find()
|
||||
if err != nil {
|
||||
return E.Cause(err, "load iphlpapi::GetExtendedUdpTable")
|
||||
}
|
||||
|
||||
err = modkernel32.Load()
|
||||
if err != nil {
|
||||
return E.Cause(err, "load kernel32.dll")
|
||||
}
|
||||
|
||||
err = procQueryFullProcessImageNameW.Find()
|
||||
if err != nil {
|
||||
return E.Cause(err, "load kernel32::QueryFullProcessImageNameW")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
pid, err := winiphlpapi.FindPid(network, source)
|
||||
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err := getProcessPath(pid)
|
||||
if err != nil {
|
||||
return &Info{ProcessID: pid, UserId: -1}, err
|
||||
}
|
||||
return &Info{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
|
||||
return &Info{ProcessPath: processName, UserId: -1}, nil
|
||||
}
|
||||
|
||||
func getProcessPath(pid uint32) (string, error) {
|
||||
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
|
||||
family := windows.AF_INET
|
||||
if ip.Is6() {
|
||||
family = windows.AF_INET6
|
||||
}
|
||||
|
||||
const (
|
||||
tcpTablePidConn = 4
|
||||
udpTablePid = 1
|
||||
)
|
||||
|
||||
var class int
|
||||
var fn uintptr
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
fn = procGetExtendedTcpTable.Addr()
|
||||
class = tcpTablePidConn
|
||||
case N.NetworkUDP:
|
||||
fn = procGetExtendedUdpTable.Addr()
|
||||
class = udpTablePid
|
||||
default:
|
||||
return "", os.ErrInvalid
|
||||
}
|
||||
|
||||
buf, err := getTransportTable(fn, family, class)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
s := newSearcher(family == windows.AF_INET, network == N.NetworkTCP)
|
||||
|
||||
pid, err := s.Search(buf, ip, uint16(srcPort))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return getExecPathFromPID(pid)
|
||||
}
|
||||
|
||||
type searcher struct {
|
||||
itemSize int
|
||||
port int
|
||||
ip int
|
||||
ipSize int
|
||||
pid int
|
||||
tcpState int
|
||||
}
|
||||
|
||||
func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) {
|
||||
n := int(readNativeUint32(b[:4]))
|
||||
itemSize := s.itemSize
|
||||
for i := 0; i < n; i++ {
|
||||
row := b[4+itemSize*i : 4+itemSize*(i+1)]
|
||||
|
||||
// according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian.
|
||||
// this field can be illustrated as follows depends on different machine endianess:
|
||||
// little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB)
|
||||
// big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB)
|
||||
// so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32
|
||||
srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4])))
|
||||
if srcPort != port {
|
||||
continue
|
||||
}
|
||||
|
||||
srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize])
|
||||
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
|
||||
if ip != srcIP && (!srcIP.IsUnspecified()) {
|
||||
continue
|
||||
}
|
||||
|
||||
pid := readNativeUint32(row[s.pid : s.pid+4])
|
||||
return pid, nil
|
||||
}
|
||||
return 0, ErrNotFound
|
||||
}
|
||||
|
||||
func newSearcher(isV4, isTCP bool) *searcher {
|
||||
var itemSize, port, ip, ipSize, pid int
|
||||
tcpState := -1
|
||||
switch {
|
||||
case isV4 && isTCP:
|
||||
// struct MIB_TCPROW_OWNER_PID
|
||||
itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0
|
||||
case isV4 && !isTCP:
|
||||
// struct MIB_UDPROW_OWNER_PID
|
||||
itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8
|
||||
case !isV4 && isTCP:
|
||||
// struct MIB_TCP6ROW_OWNER_PID
|
||||
itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48
|
||||
case !isV4 && !isTCP:
|
||||
// struct MIB_UDP6ROW_OWNER_PID
|
||||
itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24
|
||||
}
|
||||
|
||||
return &searcher{
|
||||
itemSize: itemSize,
|
||||
port: port,
|
||||
ip: ip,
|
||||
ipSize: ipSize,
|
||||
pid: pid,
|
||||
tcpState: tcpState,
|
||||
}
|
||||
}
|
||||
|
||||
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
|
||||
for size, buf := uint32(8), make([]byte, 8); ; {
|
||||
ptr := unsafe.Pointer(&buf[0])
|
||||
err, _, _ := syscall.SyscallN(fn, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
|
||||
|
||||
switch err {
|
||||
case 0:
|
||||
return buf, nil
|
||||
case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER):
|
||||
buf = make([]byte, size)
|
||||
default:
|
||||
return nil, fmt.Errorf("syscall error: %d", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readNativeUint32(b []byte) uint32 {
|
||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||
}
|
||||
|
||||
func getExecPathFromPID(pid uint32) (string, error) {
|
||||
// kernel process starts with a colon in order to distinguish with normal processes
|
||||
switch pid {
|
||||
case 0:
|
||||
// reserved pid for system idle process
|
||||
return ":System Idle Process", nil
|
||||
case 4:
|
||||
// reserved pid for windows kernel image
|
||||
return ":System", nil
|
||||
}
|
||||
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer windows.CloseHandle(handle)
|
||||
size := uint32(syscall.MAX_LONG_PATH)
|
||||
defer windows.CloseHandle(h)
|
||||
|
||||
buf := make([]uint16, syscall.MAX_LONG_PATH)
|
||||
err = windows.QueryFullProcessImageName(handle, 0, &buf[0], &size)
|
||||
if err != nil {
|
||||
size := uint32(len(buf))
|
||||
r1, _, err := syscall.SyscallN(
|
||||
procQueryFullProcessImageNameW.Addr(),
|
||||
uintptr(h),
|
||||
uintptr(0),
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
uintptr(unsafe.Pointer(&size)),
|
||||
)
|
||||
if r1 == 0 {
|
||||
return "", err
|
||||
}
|
||||
return windows.UTF16ToString(buf[:size]), nil
|
||||
return syscall.UTF16ToString(buf[:size]), nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -23,21 +24,26 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R
|
||||
var first byte
|
||||
err := binary.Read(reader, binary.BigEndian, &first)
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
|
||||
if first != 19 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
const header = "BitTorrent protocol"
|
||||
var protocol [19]byte
|
||||
_, err = reader.Read(protocol[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(protocol[:]) != "BitTorrent protocol" {
|
||||
var n int
|
||||
n, err = reader.Read(protocol[:])
|
||||
if string(protocol[:n]) != header[:n] {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
if err != nil {
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
if n < 19 {
|
||||
return ErrNeedMoreData
|
||||
}
|
||||
|
||||
metadata.Protocol = C.ProtocolBitTorrent
|
||||
return nil
|
||||
@@ -67,7 +73,9 @@ func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if extension > 0x04 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
var length byte
|
||||
err = binary.Read(reader, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
|
||||
@@ -32,6 +32,27 @@ 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) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -71,3 +92,19 @@ func TestSniffUDPTracker(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,11 @@ import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/task"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
@@ -21,35 +18,40 @@ func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundCon
|
||||
var length uint16
|
||||
err := binary.Read(reader, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return os.ErrInvalid
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
if length == 0 {
|
||||
if length < 12 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
buffer := buf.NewSize(int(length))
|
||||
defer buffer.Release()
|
||||
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
|
||||
var readTask task.Group
|
||||
readTask.Append0(func(ctx context.Context) error {
|
||||
return common.Error(buffer.ReadFullFrom(reader, buffer.FreeLen()))
|
||||
})
|
||||
err = readTask.Run(readCtx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
var n int
|
||||
n, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
|
||||
packet := buffer.Bytes()
|
||||
if n > 2 && packet[2]&0x80 != 0 { // QR
|
||||
return os.ErrInvalid
|
||||
}
|
||||
return DomainNameQuery(readCtx, metadata, buffer.Bytes())
|
||||
if n > 5 && packet[4] == 0 && packet[5] == 0 { // QDCOUNT
|
||||
return os.ErrInvalid
|
||||
}
|
||||
for i := 6; i < 10; i++ {
|
||||
// ANCOUNT, NSCOUNT
|
||||
if n > i && packet[i] != 0 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
return DomainNameQuery(readCtx, metadata, packet)
|
||||
}
|
||||
|
||||
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||
var msg mDNS.Msg
|
||||
err := msg.Unpack(packet)
|
||||
if err != nil {
|
||||
if err != nil || msg.Response || len(msg.Question) == 0 || len(msg.Answer) > 0 || len(msg.Ns) > 0 {
|
||||
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
|
||||
return nil
|
||||
}
|
||||
|
||||
53
common/sniff/dns_test.go
Normal file
53
common/sniff/dns_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
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)
|
||||
}
|
||||
@@ -3,10 +3,12 @@ package sniff
|
||||
import (
|
||||
std_bufio "bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/protocol/http"
|
||||
)
|
||||
@@ -14,10 +16,13 @@ import (
|
||||
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||
request, err := http.ReadRequest(std_bufio.NewReader(reader))
|
||||
if err != nil {
|
||||
return err
|
||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
metadata.Protocol = C.ProtocolHTTP
|
||||
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||
metadata.HTTPRequest = request
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ import (
|
||||
"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 {
|
||||
reader := bytes.NewReader(packet)
|
||||
typeByte, err := reader.ReadByte()
|
||||
@@ -308,7 +306,7 @@ find:
|
||||
metadata.Protocol = C.ProtocolQUIC
|
||||
metadata.Client = C.ClientChromium
|
||||
metadata.SniffContext = fragments
|
||||
return ErrClientHelloFragmented
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
metadata.Domain = fingerprint.ServerName
|
||||
for metadata.Client == "" {
|
||||
|
||||
@@ -12,6 +12,26 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSniffQUICChromeNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
pkt, err := hex.DecodeString("ca0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489ad89c322f75f9a383c90d126a0b21104cb519c2bb32e6a134e86896452e942b26c519b8c7ac9e4c99fae5e1f65cf08fb98443b30e4567932e8fb0789820d8f33037b59ac8113530258c9467dfb52489396dae01f099d28b234efa107fa411f2a1ffa2abe74988e03d662d4296024e95ce0fe1671724937157f77b84990478a2d4060676cf0827b4e8c600654111750414dafa0cccb332f3020c2922a015f445df5edc9c7d2d1ceea9fddcc9ff821c9183aa39a70da20fcc057579e1051c1c899148d6cf9d08b4919822082d040d1ce03ca4f216be6cb7ef03db6df0993ef1ccce5c8c648980554f41704526e1809d2545739f5872e75ec797db1c99f5682e2eda9363cb32aa367b7b363c782ddbacf874183cc15c8a2db068dd4093eebdd096ad33832a7939deb0a872279744f5a56dc001ba62fac973bf680f3b362bdd336add4dd102f462b773bf70bfce1921070a802a92025273a177186d1a643081b42175eb789ccddadb71033ef4feacbf6fd282ab622cf61669d73cda559e411c6ccdd8f003443b6933b7729b7a357aa4aa2fba0f365f829a4d497afb5dc2648a53bc9f3e786d955069d0a4781088a5463747dfe9958ea19ea444eae947ec6a67640955f710f93640084f3fbb8ad259b68dbc0ee0b7fab2d81bffd83ed8a6d33522dbfef43bec0a0fb4bdf1cb712dc4ced0680c0687fa240fd157baa232b1c84e14adce6421cf9270f9b3972f98fc67b344b8a4f1fb551e26f7f76d484ed9f8197f231dc5d9a44cc0ddce73d7f810a620851f4e97eb5037ab5135d7c3be5b80cc32d19910b8387aca64c93c02dc3e35238b78e6aff470722078982e58802844932b6041446bfdcc97ba640cbb86721bcd0f40f27b77aa6287ce5674ec1720134b9302875482c3269787e004b9edb483d44f326eef38c0e83cb46af96488c2e696bc2524567fb29c1e8edcd5a73615496d172d46a9d29e0505c0018b7bbb00165eca0389e09c4b1d73b6cc4a2f735a720650134a2e98e8105e20695cf231b92586237dfe0f99c897414e51c21627496276535f07abb53fb2b554376fe520fa45a3e944fd91dfe7a72aead08842b6b63d8edf861fb911954c83bd9a896eb9da4af5eff646455069d747facd4e77c254096843bff7c3e9031dbdf8dc37ea45f1122922fcbc322ec1378f3c7c1af0da62e1052e6210f1b23073f93a82d90e14cb20bc4501d487a1c848674d57a7c269b13590b3a99d8b8b4f6d0dfbd1d2cbbe7a32c0d5c84ae7ec438b0b19f3862d8fabaa828d06c7e3c6967405cd56a1ae90f38633e2ee0e3ecfca3df399fe12f029e0860a1a30da010300d0c94f0bf56091d00011488c1429928b21c739ebf50ba8be91116315d3173f6d2c56735722478c4d74392ba84d1727036b3d64e8c2263b0f33cb8086be587ca6b3940259c06afa2683868856529303ae12e91d7ca874568be7f2bfaa0656dfab0ed31ed90eaea10fb7f3433ec59a334abe6211d547fa0c825ac45d3691e749d15432008de83e9f6d98f368359137ae803d9189b3386f800c7c0cf4b615d1983cf82d9981a8105b60a80fe66c9b0d439b5ba153dd19e9e7483a01cf3b02b4597540b38e658d4eb8455e030b2bf2690bdd78c23f16fe5")
|
||||
require.NoError(t, err)
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
||||
require.NoError(t, err)
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f")
|
||||
require.NoError(t, err)
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "www.google.com", metadata.Domain)
|
||||
}
|
||||
|
||||
func TestSniffQUICChromium(t *testing.T) {
|
||||
t.Parallel()
|
||||
pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620")
|
||||
@@ -20,7 +40,7 @@ func TestSniffQUICChromium(t *testing.T) {
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
|
||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
||||
require.NoError(t, err)
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
@@ -15,7 +16,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
||||
var tpktVersion uint8
|
||||
err := binary.Read(reader, binary.BigEndian, &tpktVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
if tpktVersion != 0x03 {
|
||||
return os.ErrInvalid
|
||||
@@ -24,7 +25,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
||||
var tpktReserved uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &tpktReserved)
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
if tpktReserved != 0x00 {
|
||||
return os.ErrInvalid
|
||||
@@ -33,7 +34,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
||||
var tpktLength uint16
|
||||
err = binary.Read(reader, binary.BigEndian, &tpktLength)
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
|
||||
if tpktLength != 19 {
|
||||
@@ -43,7 +44,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
||||
var cotpLength uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &cotpLength)
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
|
||||
if cotpLength != 14 {
|
||||
@@ -53,7 +54,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
||||
var cotpTpduType uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &cotpTpduType)
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
if cotpTpduType != 0xE0 {
|
||||
return os.ErrInvalid
|
||||
@@ -61,13 +62,13 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
||||
|
||||
err = rw.SkipN(reader, 5)
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
|
||||
var rdpType uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &rdpType)
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
if rdpType != 0x01 {
|
||||
return os.ErrInvalid
|
||||
@@ -75,12 +76,12 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
||||
var rdpFlags uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &rdpFlags)
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
var rdpLength uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &rdpLength)
|
||||
if err != nil {
|
||||
return err
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
if rdpLength != 8 {
|
||||
return os.ErrInvalid
|
||||
|
||||
@@ -3,12 +3,14 @@ package sniff
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
@@ -18,6 +20,8 @@ type (
|
||||
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
|
||||
)
|
||||
|
||||
var ErrNeedMoreData = E.New("need more data")
|
||||
|
||||
func Skip(metadata *adapter.InboundContext) bool {
|
||||
// skip server first protocols
|
||||
switch metadata.Destination.Port {
|
||||
@@ -34,12 +38,12 @@ func Skip(metadata *adapter.InboundContext) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
|
||||
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffers []*buf.Buffer, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
|
||||
if timeout == 0 {
|
||||
timeout = C.ReadPayloadTimeout
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
var errors []error
|
||||
var sniffError error
|
||||
for i := 0; ; i++ {
|
||||
err := conn.SetReadDeadline(deadline)
|
||||
if err != nil {
|
||||
@@ -53,26 +57,32 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
|
||||
}
|
||||
return E.Cause(err, "read payload")
|
||||
}
|
||||
errors = nil
|
||||
sniffError = nil
|
||||
for _, sniffer := range sniffers {
|
||||
err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes()))
|
||||
reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader {
|
||||
return bytes.NewReader(it.Bytes())
|
||||
})...)
|
||||
err = sniffer(ctx, metadata, reader)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errors = append(errors, err)
|
||||
sniffError = E.Errors(sniffError, err)
|
||||
}
|
||||
if !errors.Is(sniffError, ErrNeedMoreData) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return E.Errors(errors...)
|
||||
return sniffError
|
||||
}
|
||||
|
||||
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
|
||||
var errors []error
|
||||
var sniffError []error
|
||||
for _, sniffer := range sniffers {
|
||||
err := sniffer(ctx, metadata, packet)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
errors = append(errors, err)
|
||||
sniffError = append(sniffError, err)
|
||||
}
|
||||
return E.Errors(errors...)
|
||||
return E.Errors(sniffError...)
|
||||
}
|
||||
|
||||
@@ -5,22 +5,27 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
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 {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
if !scanner.Scan() {
|
||||
const sshPrefix = "SSH-2.0-"
|
||||
bReader := bufio.NewReader(reader)
|
||||
prefix, err := bReader.Peek(len(sshPrefix))
|
||||
if string(prefix[:]) != sshPrefix[:len(prefix)] {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
fistLine := scanner.Text()
|
||||
if !strings.HasPrefix(fistLine, "SSH-2.0-") {
|
||||
return os.ErrInvalid
|
||||
if err != nil {
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
fistLine, _, err := bReader.ReadLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metadata.Protocol = C.ProtocolSSH
|
||||
metadata.Client = fistLine[8:]
|
||||
metadata.Client = string(fistLine)[8:]
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -24,3 +24,24 @@ func TestSniffSSH(t *testing.T) {
|
||||
require.Equal(t, C.ProtocolSSH, metadata.Protocol)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package sniff
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"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 {
|
||||
@@ -21,8 +23,11 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
|
||||
if clientHello != nil {
|
||||
metadata.Protocol = C.ProtocolTLS
|
||||
metadata.Domain = clientHello.ServerName
|
||||
metadata.ClientHello = clientHello
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
|
||||
cftls "github.com/sagernet/cloudflare-tls"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-dns"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/service"
|
||||
@@ -100,7 +100,6 @@ func NewECHClient(ctx context.Context, serverAddress string, options option.Outb
|
||||
|
||||
var tlsConfig cftls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if options.DisableSNI {
|
||||
tlsConfig.ServerName = "127.0.0.1"
|
||||
} else {
|
||||
@@ -216,7 +215,7 @@ func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverNam
|
||||
},
|
||||
},
|
||||
}
|
||||
response, err := service.FromContext[adapter.DNSRouter](ctx).Exchange(ctx, message, adapter.DNSQueryOptions{})
|
||||
response, err := service.FromContext[adapter.Router](ctx).Exchange(ctx, message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func (c *echServerConfig) startWatcher() error {
|
||||
Callback: func(path string) {
|
||||
err := c.credentialsUpdated(path)
|
||||
if err != nil {
|
||||
c.logger.Error(E.Cause(err, "reload credentials"))
|
||||
c.logger.Error(E.Cause(err, "reload credentials from ", path))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -8,10 +8,7 @@ import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
||||
@@ -38,30 +35,17 @@ func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func(
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var template *x509.Certificate
|
||||
if serverAddress := M.ParseAddr(serverName); serverAddress.IsValid() {
|
||||
template = &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
IPAddresses: []net.IP{serverAddress.AsSlice()},
|
||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||
NotAfter: expire,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
} 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},
|
||||
}
|
||||
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 {
|
||||
parent = template
|
||||
|
||||
@@ -27,11 +27,9 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/debug"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
utls "github.com/sagernet/utls"
|
||||
|
||||
@@ -42,7 +40,6 @@ import (
|
||||
var _ ConfigCompat = (*RealityClientConfig)(nil)
|
||||
|
||||
type RealityClientConfig struct {
|
||||
ctx context.Context
|
||||
uClient *UTLSClientConfig
|
||||
publicKey []byte
|
||||
shortID [8]byte
|
||||
@@ -73,7 +70,7 @@ func NewRealityClient(ctx context.Context, serverAddress string, options option.
|
||||
if decodedLen > 8 {
|
||||
return nil, E.New("invalid short_id")
|
||||
}
|
||||
return &RealityClientConfig{ctx, uClient, publicKey, shortID}, nil
|
||||
return &RealityClientConfig{uClient, publicKey, shortID}, nil
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) ServerName() string {
|
||||
@@ -183,24 +180,20 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
|
||||
}
|
||||
|
||||
if !verifier.verified {
|
||||
go realityClientFallback(e.ctx, uConn, e.uClient.ServerName(), e.uClient.id)
|
||||
go realityClientFallback(uConn, e.uClient.ServerName(), e.uClient.id)
|
||||
return nil, E.New("reality verification failed")
|
||||
}
|
||||
|
||||
return &realityClientConnWrapper{uConn}, nil
|
||||
}
|
||||
|
||||
func realityClientFallback(ctx context.Context, uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
|
||||
func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
|
||||
defer uConn.Close()
|
||||
client := &http.Client{
|
||||
Transport: &http2.Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, config *tls.Config) (net.Conn, error) {
|
||||
return uConn, nil
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
Time: ntp.TimeFuncFromContext(ctx),
|
||||
RootCAs: adapter.RootPoolFromContext(ctx),
|
||||
},
|
||||
},
|
||||
}
|
||||
request, _ := http.NewRequest("GET", "https://"+serverName, nil)
|
||||
@@ -220,7 +213,6 @@ func (e *RealityClientConfig) SetSessionIDGenerator(generator func(clientHello [
|
||||
|
||||
func (e *RealityClientConfig) Clone() Config {
|
||||
return &RealityClientConfig{
|
||||
e.ctx,
|
||||
e.uClient.Clone().(*UTLSClientConfig),
|
||||
e.publicKey,
|
||||
e.shortID,
|
||||
|
||||
@@ -89,19 +89,23 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
|
||||
tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference)
|
||||
|
||||
tlsConfig.ShortIds = make(map[[8]byte]bool)
|
||||
for i, shortIDString := range options.Reality.ShortID {
|
||||
var shortID [8]byte
|
||||
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode short_id[", i, "]: ", shortIDString)
|
||||
if len(options.Reality.ShortID) == 0 {
|
||||
tlsConfig.ShortIds[[8]byte{0}] = true
|
||||
} else {
|
||||
for i, shortIDString := range options.Reality.ShortID {
|
||||
var shortID [8]byte
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
@@ -51,7 +51,9 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
} else if serverAddress != "" {
|
||||
serverName = serverAddress
|
||||
if _, err := netip.ParseAddr(serverName); err != nil {
|
||||
serverName = serverAddress
|
||||
}
|
||||
}
|
||||
if serverName == "" && !options.Insecure {
|
||||
return nil, E.New("missing server_name or insecure=true")
|
||||
@@ -59,7 +61,6 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
||||
|
||||
var tlsConfig tls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if options.DisableSNI {
|
||||
tlsConfig.ServerName = "127.0.0.1"
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/fswatch"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -99,7 +100,7 @@ func (c *STDServerConfig) startWatcher() error {
|
||||
Callback: func(path string) {
|
||||
err := c.certificateUpdated(path)
|
||||
if err != nil {
|
||||
c.logger.Error(E.Cause(err, "reload certificate"))
|
||||
c.logger.Error(err)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -221,8 +222,12 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
||||
key = content
|
||||
}
|
||||
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) {
|
||||
return GenerateKeyPair(nil, nil, ntp.TimeFuncFromContext(ctx), info.ServerName)
|
||||
return GenerateKeyPair(nil, nil, timeFunc, info.ServerName)
|
||||
}
|
||||
} else {
|
||||
if certificate == nil {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
@@ -131,7 +130,6 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
|
||||
|
||||
var tlsConfig utls.Config
|
||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||
if options.DisableSNI {
|
||||
tlsConfig.ServerName = "127.0.0.1"
|
||||
} else {
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package tf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
net.Conn
|
||||
tcpConn *net.TCPConn
|
||||
ctx context.Context
|
||||
firstPacketWritten bool
|
||||
fallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func NewConn(conn net.Conn, ctx context.Context, fallbackDelay time.Duration) (*Conn, error) {
|
||||
tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
|
||||
return &Conn{
|
||||
Conn: conn,
|
||||
tcpConn: tcpConn,
|
||||
ctx: ctx,
|
||||
fallbackDelay: fallbackDelay,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Write(b []byte) (n int, err error) {
|
||||
if !c.firstPacketWritten {
|
||||
defer func() {
|
||||
c.firstPacketWritten = true
|
||||
}()
|
||||
serverName := indexTLSServerName(b)
|
||||
if serverName != nil {
|
||||
if c.tcpConn != nil {
|
||||
err = c.tcpConn.SetNoDelay(true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
splits := strings.Split(serverName.ServerName, ".")
|
||||
currentIndex := serverName.Index
|
||||
if publicSuffix := publicsuffix.List.PublicSuffix(serverName.ServerName); publicSuffix != "" {
|
||||
splits = splits[:len(splits)-strings.Count(serverName.ServerName, ".")]
|
||||
}
|
||||
if len(splits) > 1 && splits[0] == "..." {
|
||||
currentIndex += len(splits[0]) + 1
|
||||
splits = splits[1:]
|
||||
}
|
||||
var splitIndexes []int
|
||||
for i, split := range splits {
|
||||
splitAt := rand.Intn(len(split))
|
||||
splitIndexes = append(splitIndexes, currentIndex+splitAt)
|
||||
currentIndex += len(split)
|
||||
if i != len(splits)-1 {
|
||||
currentIndex++
|
||||
}
|
||||
}
|
||||
for i := 0; i <= len(splitIndexes); i++ {
|
||||
var payload []byte
|
||||
if i == 0 {
|
||||
payload = b[:splitIndexes[i]]
|
||||
} else if i == len(splitIndexes) {
|
||||
payload = b[splitIndexes[i-1]:]
|
||||
} else {
|
||||
payload = b[splitIndexes[i-1]:splitIndexes[i]]
|
||||
}
|
||||
if c.tcpConn != nil && i != len(splitIndexes) {
|
||||
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err = c.Conn.Write(payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.tcpConn != nil {
|
||||
err = c.tcpConn.SetNoDelay(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
}
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
|
||||
func (c *Conn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Conn) WriterReplaceable() bool {
|
||||
return c.firstPacketWritten
|
||||
}
|
||||
|
||||
func (c *Conn) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package tf
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
const (
|
||||
recordLayerHeaderLen int = 5
|
||||
handshakeHeaderLen int = 6
|
||||
randomDataLen int = 32
|
||||
sessionIDHeaderLen int = 1
|
||||
cipherSuiteHeaderLen int = 2
|
||||
compressMethodHeaderLen int = 1
|
||||
extensionsHeaderLen int = 2
|
||||
extensionHeaderLen int = 4
|
||||
sniExtensionHeaderLen int = 5
|
||||
contentType uint8 = 22
|
||||
handshakeType uint8 = 1
|
||||
sniExtensionType uint16 = 0
|
||||
sniNameDNSHostnameType uint8 = 0
|
||||
tlsVersionBitmask uint16 = 0xFFFC
|
||||
tls13 uint16 = 0x0304
|
||||
)
|
||||
|
||||
type myServerName struct {
|
||||
Index int
|
||||
Length int
|
||||
ServerName string
|
||||
}
|
||||
|
||||
func indexTLSServerName(payload []byte) *myServerName {
|
||||
if len(payload) < recordLayerHeaderLen || payload[0] != contentType {
|
||||
return nil
|
||||
}
|
||||
segmentLen := binary.BigEndian.Uint16(payload[3:5])
|
||||
if len(payload) < recordLayerHeaderLen+int(segmentLen) {
|
||||
return nil
|
||||
}
|
||||
serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)])
|
||||
if serverName == nil {
|
||||
return nil
|
||||
}
|
||||
serverName.Length += recordLayerHeaderLen
|
||||
return serverName
|
||||
}
|
||||
|
||||
func indexTLSServerNameFromHandshake(hs []byte) *myServerName {
|
||||
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
|
||||
return nil
|
||||
}
|
||||
if hs[0] != handshakeType {
|
||||
return nil
|
||||
}
|
||||
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
|
||||
if len(hs[4:]) != int(handshakeLen) {
|
||||
return nil
|
||||
}
|
||||
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
|
||||
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
|
||||
return nil
|
||||
}
|
||||
sessionIDLen := hs[38]
|
||||
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
|
||||
return nil
|
||||
}
|
||||
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
|
||||
if len(cs) < cipherSuiteHeaderLen {
|
||||
return nil
|
||||
}
|
||||
csLen := uint16(cs[0])<<8 | uint16(cs[1])
|
||||
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
|
||||
return nil
|
||||
}
|
||||
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
|
||||
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
|
||||
return nil
|
||||
}
|
||||
currentIndex := cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen)
|
||||
serverName := indexTLSServerNameFromExtensions(cs[currentIndex:])
|
||||
if serverName == nil {
|
||||
return nil
|
||||
}
|
||||
serverName.Index += currentIndex
|
||||
return serverName
|
||||
}
|
||||
|
||||
func indexTLSServerNameFromExtensions(exs []byte) *myServerName {
|
||||
if len(exs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(exs) < extensionsHeaderLen {
|
||||
return nil
|
||||
}
|
||||
exsLen := uint16(exs[0])<<8 | uint16(exs[1])
|
||||
exs = exs[extensionsHeaderLen:]
|
||||
if len(exs) < int(exsLen) {
|
||||
return nil
|
||||
}
|
||||
for currentIndex := extensionsHeaderLen; len(exs) > 0; {
|
||||
if len(exs) < extensionHeaderLen {
|
||||
return nil
|
||||
}
|
||||
exType := uint16(exs[0])<<8 | uint16(exs[1])
|
||||
exLen := uint16(exs[2])<<8 | uint16(exs[3])
|
||||
if len(exs) < extensionHeaderLen+int(exLen) {
|
||||
return nil
|
||||
}
|
||||
sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)]
|
||||
|
||||
switch exType {
|
||||
case sniExtensionType:
|
||||
if len(sex) < sniExtensionHeaderLen {
|
||||
return nil
|
||||
}
|
||||
sniType := sex[2]
|
||||
if sniType != sniNameDNSHostnameType {
|
||||
return nil
|
||||
}
|
||||
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
|
||||
sex = sex[sniExtensionHeaderLen:]
|
||||
return &myServerName{
|
||||
Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen,
|
||||
Length: int(sniLen),
|
||||
ServerName: string(sex),
|
||||
}
|
||||
}
|
||||
exs = exs[4+exLen:]
|
||||
currentIndex += 4 + int(exLen)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package tf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/control"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
/*
|
||||
const tcpMaxNotifyAck = 10
|
||||
|
||||
type tcpNotifyAckID uint32
|
||||
|
||||
type tcpNotifyAckComplete struct {
|
||||
NotifyPending uint32
|
||||
NotifyCompleteCount uint32
|
||||
NotifyCompleteID [tcpMaxNotifyAck]tcpNotifyAckID
|
||||
}
|
||||
|
||||
var sizeOfTCPNotifyAckComplete = int(unsafe.Sizeof(tcpNotifyAckComplete{}))
|
||||
|
||||
func getsockoptTCPNotifyAckComplete(fd, level, opt int) (*tcpNotifyAckComplete, error) {
|
||||
var value tcpNotifyAckComplete
|
||||
vallen := uint32(sizeOfTCPNotifyAckComplete)
|
||||
err := getsockopt(fd, level, opt, unsafe.Pointer(&value), &vallen)
|
||||
return &value, err
|
||||
}
|
||||
|
||||
//go:linkname getsockopt golang.org/x/sys/unix.getsockopt
|
||||
func getsockopt(s int, level int, name int, val unsafe.Pointer, vallen *uint32) error
|
||||
|
||||
func waitAck(ctx context.Context, conn *net.TCPConn, _ time.Duration) error {
|
||||
const TCP_NOTIFY_ACKNOWLEDGEMENT = 0x212
|
||||
return control.Conn(conn, func(fd uintptr) error {
|
||||
err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT, 1)
|
||||
if err != nil {
|
||||
if errors.Is(err, unix.EINVAL) {
|
||||
return waitAckFallback(ctx, conn, 0)
|
||||
}
|
||||
return err
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
var ackComplete *tcpNotifyAckComplete
|
||||
ackComplete, err = getsockoptTCPNotifyAckComplete(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ackComplete.NotifyPending == 0 {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
||||
_, err := conn.Write(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return control.Conn(conn, func(fd uintptr) error {
|
||||
start := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
unacked, err := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_NWRITE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unacked == 0 {
|
||||
if time.Since(start) <= 20*time.Millisecond {
|
||||
// under transparent proxy
|
||||
time.Sleep(fallbackDelay)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package tf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/control"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
||||
_, err := conn.Write(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return control.Conn(conn, func(fd uintptr) error {
|
||||
start := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
tcpInfo, err := unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tcpInfo.Unacked == 0 {
|
||||
if time.Since(start) <= 20*time.Millisecond {
|
||||
// under transparent proxy
|
||||
time.Sleep(fallbackDelay)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
//go:build !(linux || darwin || windows)
|
||||
|
||||
package tf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
||||
time.Sleep(fallbackDelay)
|
||||
return nil
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package tf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/winiphlpapi"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
||||
start := time.Now()
|
||||
err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload)
|
||||
if err != nil {
|
||||
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||
time.Sleep(fallbackDelay)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if time.Since(start) <= 20*time.Millisecond {
|
||||
time.Sleep(fallbackDelay)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,32 +2,32 @@ package urltest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
)
|
||||
|
||||
var _ adapter.URLTestHistoryStorage = (*HistoryStorage)(nil)
|
||||
type History struct {
|
||||
Time time.Time `json:"time"`
|
||||
Delay uint16 `json:"delay"`
|
||||
}
|
||||
|
||||
type HistoryStorage struct {
|
||||
access sync.RWMutex
|
||||
delayHistory map[string]*adapter.URLTestHistory
|
||||
delayHistory map[string]*History
|
||||
updateHook chan<- struct{}
|
||||
}
|
||||
|
||||
func NewHistoryStorage() *HistoryStorage {
|
||||
return &HistoryStorage{
|
||||
delayHistory: make(map[string]*adapter.URLTestHistory),
|
||||
delayHistory: make(map[string]*History),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func (s *HistoryStorage) SetHook(hook chan<- struct{}) {
|
||||
s.updateHook = hook
|
||||
}
|
||||
|
||||
func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory {
|
||||
func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
||||
s.notifyUpdated()
|
||||
}
|
||||
|
||||
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
|
||||
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
|
||||
s.access.Lock()
|
||||
s.delayHistory[tag] = history
|
||||
s.access.Unlock()
|
||||
@@ -110,10 +110,6 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return instance, nil
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
Time: ntp.TimeFuncFromContext(ctx),
|
||||
RootCAs: adapter.RootPoolFromContext(ctx),
|
||||
},
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
CertificateStoreSystem = "system"
|
||||
CertificateStoreMozilla = "mozilla"
|
||||
CertificateStoreNone = "none"
|
||||
)
|
||||
@@ -1,34 +1,5 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
DefaultDNSTTL = 600
|
||||
)
|
||||
|
||||
type DomainStrategy = uint8
|
||||
|
||||
const (
|
||||
DomainStrategyAsIS DomainStrategy = iota
|
||||
DomainStrategyPreferIPv4
|
||||
DomainStrategyPreferIPv6
|
||||
DomainStrategyIPv4Only
|
||||
DomainStrategyIPv6Only
|
||||
)
|
||||
|
||||
const (
|
||||
DNSTypeLegacy = "legacy"
|
||||
DNSTypeUDP = "udp"
|
||||
DNSTypeTCP = "tcp"
|
||||
DNSTypeTLS = "tls"
|
||||
DNSTypeHTTPS = "https"
|
||||
DNSTypeQUIC = "quic"
|
||||
DNSTypeHTTP3 = "h3"
|
||||
DNSTypeHosts = "hosts"
|
||||
DNSTypeLocal = "local"
|
||||
DNSTypePreDefined = "predefined"
|
||||
DNSTypeFakeIP = "fakeip"
|
||||
DNSTypeDHCP = "dhcp"
|
||||
)
|
||||
|
||||
const (
|
||||
DNSProviderAliDNS = "alidns"
|
||||
DNSProviderCloudflare = "cloudflare"
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
ScriptTypeSurge = "surge"
|
||||
ScriptSourceTypeLocal = "local"
|
||||
ScriptSourceTypeRemote = "remote"
|
||||
)
|
||||
@@ -16,7 +16,6 @@ const (
|
||||
StopTimeout = 5 * time.Second
|
||||
FatalStopTimeout = 10 * time.Second
|
||||
FakeIPMetadataSaveInterval = 10 * time.Second
|
||||
TLSFragmentFallbackDelay = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
var PortProtocols = map[uint16]string{
|
||||
|
||||
6
debug.go
6
debug.go
@@ -24,9 +24,9 @@ func applyDebugOptions(options option.DebugOptions) {
|
||||
if options.TraceBack != "" {
|
||||
debug.SetTraceback(options.TraceBack)
|
||||
}
|
||||
if options.MemoryLimit != 0 {
|
||||
debug.SetMemoryLimit(int64(float64(options.MemoryLimit) / 1.5))
|
||||
conntrack.MemoryLimit = uint64(options.MemoryLimit)
|
||||
if options.MemoryLimit.Value() != 0 {
|
||||
debug.SetMemoryLimit(int64(float64(options.MemoryLimit.Value()) / 1.5))
|
||||
conntrack.MemoryLimit = options.MemoryLimit.Value()
|
||||
}
|
||||
if options.OOMKiller != nil {
|
||||
conntrack.KillerEnabled = *options.OOMKiller
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/humanize"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
@@ -38,9 +38,9 @@ func applyDebugListenOption(options option.DebugOptions) {
|
||||
runtime.ReadMemStats(&memStats)
|
||||
|
||||
var memObject badjson.JSONObject
|
||||
memObject.Put("heap", humanize.MemoryBytes(memStats.HeapInuse))
|
||||
memObject.Put("stack", humanize.MemoryBytes(memStats.StackInuse))
|
||||
memObject.Put("idle", humanize.MemoryBytes(memStats.HeapIdle-memStats.HeapReleased))
|
||||
memObject.Put("heap", byteformats.FormatMemoryBytes(memStats.HeapInuse))
|
||||
memObject.Put("stack", byteformats.FormatMemoryBytes(memStats.StackInuse))
|
||||
memObject.Put("idle", byteformats.FormatMemoryBytes(memStats.HeapIdle-memStats.HeapReleased))
|
||||
memObject.Put("goroutines", runtime.NumGoroutine())
|
||||
memObject.Put("rss", rusageMaxRSS())
|
||||
|
||||
|
||||
563
dns/client.go
563
dns/client.go
@@ -1,563 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/task"
|
||||
"github.com/sagernet/sing/contrab/freelru"
|
||||
"github.com/sagernet/sing/contrab/maphash"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoRawSupport = E.New("no raw query support by current transport")
|
||||
ErrNotCached = E.New("not cached")
|
||||
ErrResponseRejected = E.New("response rejected")
|
||||
ErrResponseRejectedCached = E.Extend(ErrResponseRejected, "cached")
|
||||
)
|
||||
|
||||
var _ adapter.DNSClient = (*Client)(nil)
|
||||
|
||||
type Client struct {
|
||||
timeout time.Duration
|
||||
disableCache bool
|
||||
disableExpire bool
|
||||
independentCache bool
|
||||
rdrc adapter.RDRCStore
|
||||
initRDRCFunc func() adapter.RDRCStore
|
||||
logger logger.ContextLogger
|
||||
cache freelru.Cache[dns.Question, *dns.Msg]
|
||||
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Timeout time.Duration
|
||||
DisableCache bool
|
||||
DisableExpire bool
|
||||
IndependentCache bool
|
||||
CacheCapacity uint32
|
||||
RDRC func() adapter.RDRCStore
|
||||
Logger logger.ContextLogger
|
||||
}
|
||||
|
||||
func NewClient(options ClientOptions) *Client {
|
||||
client := &Client{
|
||||
timeout: options.Timeout,
|
||||
disableCache: options.DisableCache,
|
||||
disableExpire: options.DisableExpire,
|
||||
independentCache: options.IndependentCache,
|
||||
initRDRCFunc: options.RDRC,
|
||||
logger: options.Logger,
|
||||
}
|
||||
if client.timeout == 0 {
|
||||
client.timeout = C.DNSTimeout
|
||||
}
|
||||
cacheCapacity := options.CacheCapacity
|
||||
if cacheCapacity < 1024 {
|
||||
cacheCapacity = 1024
|
||||
}
|
||||
if !client.disableCache {
|
||||
if !client.independentCache {
|
||||
client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32))
|
||||
} else {
|
||||
client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32))
|
||||
}
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
type transportCacheKey struct {
|
||||
dns.Question
|
||||
transportTag string
|
||||
}
|
||||
|
||||
func (c *Client) Start() {
|
||||
if c.initRDRCFunc != nil {
|
||||
c.rdrc = c.initRDRCFunc()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
|
||||
if len(message.Question) == 0 {
|
||||
if c.logger != nil {
|
||||
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
||||
}
|
||||
responseMessage := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: message.Id,
|
||||
Response: true,
|
||||
Rcode: dns.RcodeFormatError,
|
||||
},
|
||||
Question: message.Question,
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
question := message.Question[0]
|
||||
if options.ClientSubnet.IsValid() {
|
||||
message = SetClientSubnet(message, options.ClientSubnet, true)
|
||||
}
|
||||
isSimpleRequest := len(message.Question) == 1 &&
|
||||
len(message.Ns) == 0 &&
|
||||
len(message.Extra) == 0 &&
|
||||
!options.ClientSubnet.IsValid()
|
||||
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
||||
if !disableCache {
|
||||
response, ttl := c.loadResponse(question, transport)
|
||||
if response != nil {
|
||||
logCachedResponse(c.logger, ctx, response, ttl)
|
||||
response.Id = message.Id
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only {
|
||||
responseMessage := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: message.Id,
|
||||
Response: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
}
|
||||
if c.logger != nil {
|
||||
c.logger.DebugContext(ctx, "strategy rejected")
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
messageId := message.Id
|
||||
contextTransport, clientSubnetLoaded := transportTagFromContext(ctx)
|
||||
if clientSubnetLoaded && transport.Tag() == contextTransport {
|
||||
return nil, E.New("DNS query loopback in transport[", contextTransport, "]")
|
||||
}
|
||||
ctx = contextWithTransportTag(ctx, transport.Tag())
|
||||
if responseChecker != nil && c.rdrc != nil {
|
||||
rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype)
|
||||
if rejected {
|
||||
return nil, ErrResponseRejectedCached
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
response, err := transport.Exchange(ctx, message)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
|
||||
validResponse := response
|
||||
loop:
|
||||
for {
|
||||
var (
|
||||
addresses int
|
||||
queryCNAME string
|
||||
)
|
||||
for _, rawRR := range validResponse.Answer {
|
||||
switch rr := rawRR.(type) {
|
||||
case *dns.A:
|
||||
break loop
|
||||
case *dns.AAAA:
|
||||
break loop
|
||||
case *dns.CNAME:
|
||||
queryCNAME = rr.Target
|
||||
}
|
||||
}
|
||||
if queryCNAME == "" {
|
||||
break
|
||||
}
|
||||
exMessage := *message
|
||||
exMessage.Question = []dns.Question{{
|
||||
Name: queryCNAME,
|
||||
Qtype: question.Qtype,
|
||||
}}
|
||||
validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if validResponse != response {
|
||||
response.Answer = append(response.Answer, validResponse.Answer...)
|
||||
}
|
||||
}*/
|
||||
if responseChecker != nil {
|
||||
addr, addrErr := MessageToAddresses(response)
|
||||
if addrErr != nil || !responseChecker(addr) {
|
||||
if c.rdrc != nil {
|
||||
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
|
||||
}
|
||||
logRejectedResponse(c.logger, ctx, response)
|
||||
return response, ErrResponseRejected
|
||||
}
|
||||
}
|
||||
if question.Qtype == dns.TypeHTTPS {
|
||||
if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only {
|
||||
for _, rr := range response.Answer {
|
||||
https, isHTTPS := rr.(*dns.HTTPS)
|
||||
if !isHTTPS {
|
||||
continue
|
||||
}
|
||||
content := https.SVCB
|
||||
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
|
||||
if options.Strategy == C.DomainStrategyIPv4Only {
|
||||
return it.Key() != dns.SVCB_IPV6HINT
|
||||
} else {
|
||||
return it.Key() != dns.SVCB_IPV4HINT
|
||||
}
|
||||
})
|
||||
https.SVCB = content
|
||||
}
|
||||
}
|
||||
}
|
||||
var timeToLive uint32
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||
timeToLive = record.Header().Ttl
|
||||
}
|
||||
}
|
||||
}
|
||||
if options.RewriteTTL != nil {
|
||||
timeToLive = *options.RewriteTTL
|
||||
}
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
record.Header().Ttl = timeToLive
|
||||
}
|
||||
}
|
||||
response.Id = messageId
|
||||
if !disableCache {
|
||||
c.storeCache(transport, question, response, timeToLive)
|
||||
}
|
||||
logExchangedResponse(c.logger, ctx, response, timeToLive)
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||
domain = FqdnToDomain(domain)
|
||||
dnsName := dns.Fqdn(domain)
|
||||
if options.Strategy == C.DomainStrategyIPv4Only {
|
||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
|
||||
} else if options.Strategy == C.DomainStrategyIPv6Only {
|
||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
|
||||
}
|
||||
var response4 []netip.Addr
|
||||
var response6 []netip.Addr
|
||||
var group task.Group
|
||||
group.Append("exchange4", func(ctx context.Context) error {
|
||||
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response4 = response
|
||||
return nil
|
||||
})
|
||||
group.Append("exchange6", func(ctx context.Context) error {
|
||||
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response6 = response
|
||||
return nil
|
||||
})
|
||||
err := group.Run(ctx)
|
||||
if len(response4) == 0 && len(response6) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return sortAddresses(response4, response6, options.Strategy), nil
|
||||
}
|
||||
|
||||
func (c *Client) ClearCache() {
|
||||
if c.cache != nil {
|
||||
c.cache.Purge()
|
||||
}
|
||||
if c.transportCache != nil {
|
||||
c.transportCache.Purge()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool) {
|
||||
if c.disableCache || c.independentCache {
|
||||
return nil, false
|
||||
}
|
||||
if dns.IsFqdn(domain) {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
dnsName := dns.Fqdn(domain)
|
||||
if strategy == C.DomainStrategyIPv4Only {
|
||||
response, err := c.questionCache(dns.Question{
|
||||
Name: dnsName,
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
}, nil)
|
||||
if err != ErrNotCached {
|
||||
return response, true
|
||||
}
|
||||
} else if strategy == C.DomainStrategyIPv6Only {
|
||||
response, err := c.questionCache(dns.Question{
|
||||
Name: dnsName,
|
||||
Qtype: dns.TypeAAAA,
|
||||
Qclass: dns.ClassINET,
|
||||
}, nil)
|
||||
if err != ErrNotCached {
|
||||
return response, true
|
||||
}
|
||||
} else {
|
||||
response4, _ := c.questionCache(dns.Question{
|
||||
Name: dnsName,
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
}, nil)
|
||||
response6, _ := c.questionCache(dns.Question{
|
||||
Name: dnsName,
|
||||
Qtype: dns.TypeAAAA,
|
||||
Qclass: dns.ClassINET,
|
||||
}, nil)
|
||||
if len(response4) > 0 || len(response6) > 0 {
|
||||
return sortAddresses(response4, response6, strategy), true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *Client) ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool) {
|
||||
if c.disableCache || c.independentCache || len(message.Question) != 1 {
|
||||
return nil, false
|
||||
}
|
||||
question := message.Question[0]
|
||||
response, ttl := c.loadResponse(question, nil)
|
||||
if response == nil {
|
||||
return nil, false
|
||||
}
|
||||
logCachedResponse(c.logger, ctx, response, ttl)
|
||||
response.Id = message.Id
|
||||
return response, true
|
||||
}
|
||||
|
||||
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
|
||||
if strategy == C.DomainStrategyPreferIPv6 {
|
||||
return append(response6, response4...)
|
||||
} else {
|
||||
return append(response4, response6...)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, timeToLive uint32) {
|
||||
if timeToLive == 0 {
|
||||
return
|
||||
}
|
||||
if c.disableExpire {
|
||||
if !c.independentCache {
|
||||
c.cache.Add(question, message)
|
||||
} else {
|
||||
c.transportCache.Add(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
}, message)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !c.independentCache {
|
||||
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
|
||||
} else {
|
||||
c.transportCache.AddWithLifetime(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
}, message, time.Second*time.Duration(timeToLive))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||
question := dns.Question{
|
||||
Name: name,
|
||||
Qtype: qType,
|
||||
Qclass: dns.ClassINET,
|
||||
}
|
||||
disableCache := c.disableCache || options.DisableCache
|
||||
if !disableCache {
|
||||
cachedAddresses, err := c.questionCache(question, transport)
|
||||
if err != ErrNotCached {
|
||||
return cachedAddresses, err
|
||||
}
|
||||
}
|
||||
message := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
}
|
||||
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return MessageToAddresses(response)
|
||||
}
|
||||
|
||||
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
|
||||
response, _ := c.loadResponse(question, transport)
|
||||
if response == nil {
|
||||
return nil, ErrNotCached
|
||||
}
|
||||
return MessageToAddresses(response)
|
||||
}
|
||||
|
||||
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) {
|
||||
var (
|
||||
response *dns.Msg
|
||||
loaded bool
|
||||
)
|
||||
if c.disableExpire {
|
||||
if !c.independentCache {
|
||||
response, loaded = c.cache.Get(question)
|
||||
} else {
|
||||
response, loaded = c.transportCache.Get(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
})
|
||||
}
|
||||
if !loaded {
|
||||
return nil, 0
|
||||
}
|
||||
return response.Copy(), 0
|
||||
} else {
|
||||
var expireAt time.Time
|
||||
if !c.independentCache {
|
||||
response, expireAt, loaded = c.cache.GetWithLifetime(question)
|
||||
} else {
|
||||
response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
})
|
||||
}
|
||||
if !loaded {
|
||||
return nil, 0
|
||||
}
|
||||
timeNow := time.Now()
|
||||
if timeNow.After(expireAt) {
|
||||
if !c.independentCache {
|
||||
c.cache.Remove(question)
|
||||
} else {
|
||||
c.transportCache.Remove(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
})
|
||||
}
|
||||
return nil, 0
|
||||
}
|
||||
var originTTL int
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
|
||||
originTTL = int(record.Header().Ttl)
|
||||
}
|
||||
}
|
||||
}
|
||||
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
||||
if nowTTL < 0 {
|
||||
nowTTL = 0
|
||||
}
|
||||
response = response.Copy()
|
||||
if originTTL > 0 {
|
||||
duration := uint32(originTTL - nowTTL)
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
record.Header().Ttl = record.Header().Ttl - duration
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
record.Header().Ttl = uint32(nowTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
return response, nowTTL
|
||||
}
|
||||
}
|
||||
|
||||
func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
|
||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||
return nil, RCodeError(response.Rcode)
|
||||
}
|
||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||
for _, rawAnswer := range response.Answer {
|
||||
switch answer := rawAnswer.(type) {
|
||||
case *dns.A:
|
||||
addresses = append(addresses, M.AddrFromIP(answer.A))
|
||||
case *dns.AAAA:
|
||||
addresses = append(addresses, M.AddrFromIP(answer.AAAA))
|
||||
case *dns.HTTPS:
|
||||
for _, value := range answer.SVCB.Value {
|
||||
if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT {
|
||||
addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return addresses, nil
|
||||
}
|
||||
|
||||
func wrapError(err error) error {
|
||||
switch dnsErr := err.(type) {
|
||||
case *net.DNSError:
|
||||
if dnsErr.IsNotFound {
|
||||
return RCodeNameError
|
||||
}
|
||||
case *net.AddrError:
|
||||
return RCodeNameError
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type transportKey struct{}
|
||||
|
||||
func contextWithTransportTag(ctx context.Context, transportTag string) context.Context {
|
||||
return context.WithValue(ctx, transportKey{}, transportTag)
|
||||
}
|
||||
|
||||
func transportTagFromContext(ctx context.Context) (string, bool) {
|
||||
value, loaded := ctx.Value(transportKey{}).(string)
|
||||
return value, loaded
|
||||
}
|
||||
|
||||
func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg {
|
||||
response := dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: id,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Response: true,
|
||||
},
|
||||
Question: []dns.Question{question},
|
||||
}
|
||||
for _, address := range addresses {
|
||||
if address.Is4() {
|
||||
response.Answer = append(response.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: timeToLive,
|
||||
},
|
||||
A: address.AsSlice(),
|
||||
})
|
||||
} else {
|
||||
response.Answer = append(response.Answer, &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: timeToLive,
|
||||
},
|
||||
AAAA: address.AsSlice(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return &response
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func logCachedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl int) {
|
||||
if logger == nil || len(response.Question) == 0 {
|
||||
return
|
||||
}
|
||||
domain := FqdnToDomain(response.Question[0].Name)
|
||||
logger.DebugContext(ctx, "cached ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl)
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
logger.InfoContext(ctx, "cached ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
|
||||
if logger == nil || len(response.Question) == 0 {
|
||||
return
|
||||
}
|
||||
domain := FqdnToDomain(response.Question[0].Name)
|
||||
logger.DebugContext(ctx, "exchanged ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl)
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
logger.InfoContext(ctx, "exchanged ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logRejectedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) {
|
||||
if logger == nil || len(response.Question) == 0 {
|
||||
return
|
||||
}
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
logger.InfoContext(ctx, "rejected ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FqdnToDomain(fqdn string) string {
|
||||
if dns.IsFqdn(fqdn) {
|
||||
return fqdn[:len(fqdn)-1]
|
||||
}
|
||||
return fqdn
|
||||
}
|
||||
|
||||
func FormatQuestion(string string) string {
|
||||
for strings.HasPrefix(string, ";") {
|
||||
string = string[1:]
|
||||
}
|
||||
string = strings.ReplaceAll(string, "\t", " ")
|
||||
string = strings.ReplaceAll(string, "\n", " ")
|
||||
string = strings.ReplaceAll(string, ";; ", " ")
|
||||
string = strings.ReplaceAll(string, "; ", " ")
|
||||
|
||||
for strings.Contains(string, " ") {
|
||||
string = strings.ReplaceAll(string, " ", " ")
|
||||
}
|
||||
return strings.TrimSpace(string)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf.Buffer, error) {
|
||||
maxLen := 512
|
||||
if edns0Option := request.IsEdns0(); edns0Option != nil {
|
||||
if udpSize := int(edns0Option.UDPSize()); udpSize > 512 {
|
||||
maxLen = udpSize
|
||||
}
|
||||
}
|
||||
responseLen := response.Len()
|
||||
if responseLen > maxLen {
|
||||
response.Truncate(maxLen)
|
||||
}
|
||||
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
||||
buffer.Resize(headroom, 0)
|
||||
rawMessage, err := response.PackBuffer(buffer.FreeBytes())
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
buffer.Truncate(len(rawMessage))
|
||||
return buffer, nil
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func SetClientSubnet(message *dns.Msg, clientSubnet netip.Prefix, override bool) *dns.Msg {
|
||||
var (
|
||||
optRecord *dns.OPT
|
||||
subnetOption *dns.EDNS0_SUBNET
|
||||
)
|
||||
findExists:
|
||||
for _, record := range message.Extra {
|
||||
var isOPTRecord bool
|
||||
if optRecord, isOPTRecord = record.(*dns.OPT); isOPTRecord {
|
||||
for _, option := range optRecord.Option {
|
||||
var isEDNS0Subnet bool
|
||||
subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET)
|
||||
if isEDNS0Subnet {
|
||||
if !override {
|
||||
return message
|
||||
}
|
||||
break findExists
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if optRecord == nil {
|
||||
exMessage := *message
|
||||
message = &exMessage
|
||||
optRecord = &dns.OPT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: ".",
|
||||
Rrtype: dns.TypeOPT,
|
||||
},
|
||||
}
|
||||
message.Extra = append(message.Extra, optRecord)
|
||||
} else {
|
||||
message = message.Copy()
|
||||
}
|
||||
if subnetOption == nil {
|
||||
subnetOption = new(dns.EDNS0_SUBNET)
|
||||
optRecord.Option = append(optRecord.Option, subnetOption)
|
||||
}
|
||||
subnetOption.Code = dns.EDNS0SUBNET
|
||||
if clientSubnet.Addr().Is4() {
|
||||
subnetOption.Family = 1
|
||||
} else {
|
||||
subnetOption.Family = 2
|
||||
}
|
||||
subnetOption.SourceNetmask = uint8(clientSubnet.Bits())
|
||||
subnetOption.Address = clientSubnet.Addr().AsSlice()
|
||||
return message
|
||||
}
|
||||
33
dns/rcode.go
33
dns/rcode.go
@@ -1,33 +0,0 @@
|
||||
package dns
|
||||
|
||||
import F "github.com/sagernet/sing/common/format"
|
||||
|
||||
const (
|
||||
RCodeSuccess RCodeError = 0 // NoError
|
||||
RCodeFormatError RCodeError = 1 // FormErr
|
||||
RCodeServerFailure RCodeError = 2 // ServFail
|
||||
RCodeNameError RCodeError = 3 // NXDomain
|
||||
RCodeNotImplemented RCodeError = 4 // NotImp
|
||||
RCodeRefused RCodeError = 5 // Refused
|
||||
)
|
||||
|
||||
type RCodeError uint16
|
||||
|
||||
func (e RCodeError) Error() string {
|
||||
switch e {
|
||||
case RCodeSuccess:
|
||||
return "success"
|
||||
case RCodeFormatError:
|
||||
return "format error"
|
||||
case RCodeServerFailure:
|
||||
return "server failure"
|
||||
case RCodeNameError:
|
||||
return "name error"
|
||||
case RCodeNotImplemented:
|
||||
return "not implemented"
|
||||
case RCodeRefused:
|
||||
return "refused"
|
||||
default:
|
||||
return F.ToString("unknown error: ", uint16(e))
|
||||
}
|
||||
}
|
||||
437
dns/router.go
437
dns/router.go
@@ -1,437 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
R "github.com/sagernet/sing-box/route/rule"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/contrab/freelru"
|
||||
"github.com/sagernet/sing/contrab/maphash"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ adapter.DNSRouter = (*Router)(nil)
|
||||
|
||||
type Router struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
transport adapter.DNSTransportManager
|
||||
outbound adapter.OutboundManager
|
||||
client adapter.DNSClient
|
||||
rules []adapter.DNSRule
|
||||
defaultDomainStrategy C.DomainStrategy
|
||||
dnsReverseMapping freelru.Cache[netip.Addr, string]
|
||||
platformInterface platform.Interface
|
||||
}
|
||||
|
||||
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
|
||||
router := &Router{
|
||||
ctx: ctx,
|
||||
logger: logFactory.NewLogger("dns"),
|
||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
|
||||
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
|
||||
}
|
||||
router.client = NewClient(ClientOptions{
|
||||
DisableCache: options.DNSClientOptions.DisableCache,
|
||||
DisableExpire: options.DNSClientOptions.DisableExpire,
|
||||
IndependentCache: options.DNSClientOptions.IndependentCache,
|
||||
CacheCapacity: options.DNSClientOptions.CacheCapacity,
|
||||
RDRC: func() adapter.RDRCStore {
|
||||
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
||||
if cacheFile == nil {
|
||||
return nil
|
||||
}
|
||||
if !cacheFile.StoreRDRC() {
|
||||
return nil
|
||||
}
|
||||
return cacheFile
|
||||
},
|
||||
Logger: router.logger,
|
||||
})
|
||||
if options.ReverseMapping {
|
||||
router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32))
|
||||
}
|
||||
return router
|
||||
}
|
||||
|
||||
func (r *Router) Initialize(rules []option.DNSRule) error {
|
||||
for i, ruleOptions := range rules {
|
||||
dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse dns rule[", i, "]")
|
||||
}
|
||||
r.rules = append(r.rules, dnsRule)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) Start(stage adapter.StartStage) error {
|
||||
monitor := taskmonitor.New(r.logger, C.StartTimeout)
|
||||
switch stage {
|
||||
case adapter.StartStateStart:
|
||||
monitor.Start("initialize DNS client")
|
||||
r.client.Start()
|
||||
monitor.Finish()
|
||||
|
||||
for i, rule := range r.rules {
|
||||
monitor.Start("initialize DNS rule[", i, "]")
|
||||
err := rule.Start()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize DNS rule[", i, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) Close() error {
|
||||
monitor := taskmonitor.New(r.logger, C.StopTimeout)
|
||||
var err error
|
||||
for i, rule := range r.rules {
|
||||
monitor.Start("close dns rule[", i, "]")
|
||||
err = E.Append(err, rule.Close(), func(err error) error {
|
||||
return E.Cause(err, "close dns rule[", i, "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
|
||||
metadata := adapter.ContextFrom(ctx)
|
||||
if metadata == nil {
|
||||
panic("no context")
|
||||
}
|
||||
var currentRuleIndex int
|
||||
if ruleIndex != -1 {
|
||||
currentRuleIndex = ruleIndex + 1
|
||||
}
|
||||
for ; currentRuleIndex < len(r.rules); currentRuleIndex++ {
|
||||
currentRule := r.rules[currentRuleIndex]
|
||||
if currentRule.WithAddressLimit() && !isAddressQuery {
|
||||
continue
|
||||
}
|
||||
metadata.ResetRuleCache()
|
||||
if currentRule.Match(metadata) {
|
||||
displayRuleIndex := currentRuleIndex
|
||||
if displayRuleIndex != -1 {
|
||||
displayRuleIndex += displayRuleIndex + 1
|
||||
}
|
||||
ruleDescription := currentRule.String()
|
||||
if ruleDescription != "" {
|
||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action())
|
||||
} else {
|
||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
||||
}
|
||||
switch action := currentRule.Action().(type) {
|
||||
case *R.RuleActionDNSRoute:
|
||||
transport, loaded := r.transport.Transport(action.Server)
|
||||
if !loaded {
|
||||
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
|
||||
continue
|
||||
}
|
||||
isFakeIP := transport.Type() == C.DNSTypeFakeIP
|
||||
if isFakeIP && !allowFakeIP {
|
||||
continue
|
||||
}
|
||||
if action.Strategy != C.DomainStrategyAsIS {
|
||||
options.Strategy = action.Strategy
|
||||
}
|
||||
if isFakeIP || action.DisableCache {
|
||||
options.DisableCache = true
|
||||
}
|
||||
if action.RewriteTTL != nil {
|
||||
options.RewriteTTL = action.RewriteTTL
|
||||
}
|
||||
if action.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = action.ClientSubnet
|
||||
}
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = legacyTransport.LegacyStrategy()
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
}
|
||||
}
|
||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
||||
return transport, currentRule, currentRuleIndex
|
||||
case *R.RuleActionDNSRouteOptions:
|
||||
if action.Strategy != C.DomainStrategyAsIS {
|
||||
options.Strategy = action.Strategy
|
||||
}
|
||||
if action.DisableCache {
|
||||
options.DisableCache = true
|
||||
}
|
||||
if action.RewriteTTL != nil {
|
||||
options.RewriteTTL = action.RewriteTTL
|
||||
}
|
||||
if action.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = action.ClientSubnet
|
||||
}
|
||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
||||
case *R.RuleActionReject:
|
||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
||||
return nil, currentRule, currentRuleIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.transport.Default(), nil, -1
|
||||
}
|
||||
|
||||
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
|
||||
if len(message.Question) != 1 {
|
||||
r.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
||||
responseMessage := mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Response: true,
|
||||
Rcode: mDNS.RcodeFormatError,
|
||||
},
|
||||
Question: message.Question,
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
||||
var (
|
||||
transport adapter.DNSTransport
|
||||
err error
|
||||
)
|
||||
response, cached := r.client.ExchangeCache(ctx, message)
|
||||
if !cached {
|
||||
var metadata *adapter.InboundContext
|
||||
ctx, metadata = adapter.ExtendContext(ctx)
|
||||
metadata.Destination = M.Socksaddr{}
|
||||
metadata.QueryType = message.Question[0].Qtype
|
||||
switch metadata.QueryType {
|
||||
case mDNS.TypeA:
|
||||
metadata.IPVersion = 4
|
||||
case mDNS.TypeAAAA:
|
||||
metadata.IPVersion = 6
|
||||
}
|
||||
metadata.Domain = FqdnToDomain(message.Question[0].Name)
|
||||
if options.Transport != nil {
|
||||
transport = options.Transport
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = legacyTransport.LegacyStrategy()
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
}
|
||||
}
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
||||
} else {
|
||||
var (
|
||||
rule adapter.DNSRule
|
||||
ruleIndex int
|
||||
)
|
||||
ruleIndex = -1
|
||||
for {
|
||||
dnsCtx := adapter.OverrideContext(ctx)
|
||||
dnsOptions := options
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
||||
if rule != nil {
|
||||
switch action := rule.Action().(type) {
|
||||
case *R.RuleActionReject:
|
||||
switch action.Method {
|
||||
case C.RuleActionRejectMethodDefault:
|
||||
return FixedResponse(message.Id, message.Question[0], nil, 0), nil
|
||||
case C.RuleActionRejectMethodDrop:
|
||||
return nil, tun.ErrDrop
|
||||
}
|
||||
}
|
||||
}
|
||||
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||
if rule != nil && rule.WithAddressLimit() {
|
||||
responseCheck = func(responseAddrs []netip.Addr) bool {
|
||||
metadata.DestinationAddresses = responseAddrs
|
||||
return rule.MatchAddressLimit(metadata)
|
||||
}
|
||||
}
|
||||
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
|
||||
var rejected bool
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrResponseRejectedCached) {
|
||||
rejected = true
|
||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
|
||||
} else if errors.Is(err, ErrResponseRejected) {
|
||||
rejected = true
|
||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
||||
} else if len(message.Question) > 0 {
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
||||
} else {
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
|
||||
}
|
||||
}
|
||||
if responseCheck != nil && rejected {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 {
|
||||
if transport.Type() != C.DNSTypeFakeIP {
|
||||
for _, answer := range response.Answer {
|
||||
switch record := answer.(type) {
|
||||
case *mDNS.A:
|
||||
r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.A), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second)
|
||||
case *mDNS.AAAA:
|
||||
r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.AAAA), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||
var (
|
||||
responseAddrs []netip.Addr
|
||||
cached bool
|
||||
err error
|
||||
)
|
||||
printResult := func() {
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrResponseRejectedCached) {
|
||||
r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
|
||||
} else if errors.Is(err, ErrResponseRejected) {
|
||||
r.logger.DebugContext(ctx, "response rejected for ", domain)
|
||||
} else {
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
|
||||
}
|
||||
} else if len(responseAddrs) == 0 {
|
||||
r.logger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
|
||||
err = RCodeNameError
|
||||
}
|
||||
}
|
||||
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
|
||||
if cached {
|
||||
if len(responseAddrs) == 0 {
|
||||
return nil, RCodeNameError
|
||||
}
|
||||
return responseAddrs, nil
|
||||
}
|
||||
r.logger.DebugContext(ctx, "lookup domain ", domain)
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
metadata.Destination = M.Socksaddr{}
|
||||
metadata.Domain = domain
|
||||
if options.Transport != nil {
|
||||
transport := options.Transport
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
}
|
||||
}
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil)
|
||||
} else {
|
||||
var (
|
||||
transport adapter.DNSTransport
|
||||
rule adapter.DNSRule
|
||||
ruleIndex int
|
||||
)
|
||||
ruleIndex = -1
|
||||
for {
|
||||
dnsCtx := adapter.OverrideContext(ctx)
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &options)
|
||||
if rule != nil {
|
||||
switch action := rule.Action().(type) {
|
||||
case *R.RuleActionReject:
|
||||
switch action.Method {
|
||||
case C.RuleActionRejectMethodDefault:
|
||||
return nil, nil
|
||||
case C.RuleActionRejectMethodDrop:
|
||||
return nil, tun.ErrDrop
|
||||
}
|
||||
}
|
||||
}
|
||||
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||
if rule != nil && rule.WithAddressLimit() {
|
||||
responseCheck = func(responseAddrs []netip.Addr) bool {
|
||||
metadata.DestinationAddresses = responseAddrs
|
||||
return rule.MatchAddressLimit(metadata)
|
||||
}
|
||||
}
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, options, responseCheck)
|
||||
if responseCheck == nil || err == nil {
|
||||
break
|
||||
}
|
||||
printResult()
|
||||
}
|
||||
}
|
||||
printResult()
|
||||
if len(responseAddrs) > 0 {
|
||||
r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " "))
|
||||
}
|
||||
return responseAddrs, err
|
||||
}
|
||||
|
||||
func isAddressQuery(message *mDNS.Msg) bool {
|
||||
for _, question := range message.Question {
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA || question.Qtype == mDNS.TypeHTTPS {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *Router) ClearCache() {
|
||||
r.client.ClearCache()
|
||||
if r.platformInterface != nil {
|
||||
r.platformInterface.ClearDNSCache()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) {
|
||||
if r.dnsReverseMapping == nil {
|
||||
return "", false
|
||||
}
|
||||
domain, loaded := r.dnsReverseMapping.Get(ip)
|
||||
return domain, loaded
|
||||
}
|
||||
|
||||
func (r *Router) ResetNetwork() {
|
||||
r.ClearCache()
|
||||
for _, transport := range r.transport.Transports() {
|
||||
transport.Reset()
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package fakeip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.FakeIPDNSServerOptions](registry, C.DNSTypeFakeIP, NewTransport)
|
||||
}
|
||||
|
||||
var _ adapter.FakeIPTransport = (*Transport)(nil)
|
||||
|
||||
type Transport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
store adapter.FakeIPStore
|
||||
}
|
||||
|
||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
store := NewStore(ctx, logger, options.Inet4Range.Build(netip.Prefix{}), options.Inet6Range.Build(netip.Prefix{}))
|
||||
return &Transport{
|
||||
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil),
|
||||
logger: logger,
|
||||
store: store,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Start() error {
|
||||
return t.store.Start()
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
return t.store.Close()
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
question := message.Question[0]
|
||||
if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA {
|
||||
return nil, E.New("only IP queries are supported by fakeip")
|
||||
}
|
||||
address, err := t.store.Create(question.Name, question.Qtype == mDNS.TypeAAAA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dns.FixedResponse(message.Id, question, []netip.Addr{address}, C.DefaultDNSTTL), nil
|
||||
}
|
||||
|
||||
func (t *Transport) Store() adapter.FakeIPStore {
|
||||
return t.store
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package hosts
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.HostsDNSServerOptions](registry, C.DNSTypeHosts, NewTransport)
|
||||
}
|
||||
|
||||
var _ adapter.DNSTransport = (*Transport)(nil)
|
||||
|
||||
type Transport struct {
|
||||
dns.TransportAdapter
|
||||
files []*File
|
||||
}
|
||||
|
||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.HostsDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
var files []*File
|
||||
if len(options.Path) == 0 {
|
||||
files = append(files, NewFile(DefaultPath))
|
||||
} else {
|
||||
for _, path := range options.Path {
|
||||
files = append(files, NewFile(path))
|
||||
}
|
||||
}
|
||||
return &Transport{
|
||||
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeHosts, tag, nil),
|
||||
files: files,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
question := message.Question[0]
|
||||
domain := dns.FqdnToDomain(question.Name)
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
for _, file := range t.files {
|
||||
addresses := file.Lookup(domain)
|
||||
if len(addresses) > 0 {
|
||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Rcode: mDNS.RcodeNameError,
|
||||
Response: true,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
}, nil
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package hosts
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const cacheMaxAge = 5 * time.Second
|
||||
|
||||
type File struct {
|
||||
path string
|
||||
access sync.Mutex
|
||||
byName map[string][]netip.Addr
|
||||
expire time.Time
|
||||
modTime time.Time
|
||||
size int64
|
||||
}
|
||||
|
||||
func NewFile(path string) *File {
|
||||
return &File{
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *File) Lookup(name string) []netip.Addr {
|
||||
f.access.Lock()
|
||||
defer f.access.Unlock()
|
||||
f.update()
|
||||
return f.byName[name]
|
||||
}
|
||||
|
||||
func (f *File) update() {
|
||||
now := time.Now()
|
||||
if now.Before(f.expire) && len(f.byName) > 0 {
|
||||
return
|
||||
}
|
||||
stat, err := os.Stat(f.path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if f.modTime.Equal(stat.ModTime()) && f.size == stat.Size() {
|
||||
f.expire = now.Add(cacheMaxAge)
|
||||
return
|
||||
}
|
||||
byName := make(map[string][]netip.Addr)
|
||||
file, err := os.Open(f.path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
reader := bufio.NewReader(file)
|
||||
var (
|
||||
prefix []byte
|
||||
line []byte
|
||||
isPrefix bool
|
||||
)
|
||||
for {
|
||||
line, isPrefix, err = reader.ReadLine()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
if isPrefix {
|
||||
prefix = append(prefix, line...)
|
||||
continue
|
||||
} else if len(prefix) > 0 {
|
||||
line = append(prefix, line...)
|
||||
prefix = nil
|
||||
}
|
||||
commentIndex := strings.IndexRune(string(line), '#')
|
||||
if commentIndex != -1 {
|
||||
line = line[:commentIndex]
|
||||
}
|
||||
fields := strings.Fields(string(line))
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
var addr netip.Addr
|
||||
addr, err = netip.ParseAddr(fields[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for index := 1; index < len(fields); index++ {
|
||||
canonicalName := dns.CanonicalName(fields[index])
|
||||
byName[canonicalName] = append(byName[canonicalName], addr)
|
||||
}
|
||||
}
|
||||
f.expire = now.Add(cacheMaxAge)
|
||||
f.modTime = stat.ModTime()
|
||||
f.size = stat.Size()
|
||||
f.byName = byName
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package hosts_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/dns/transport/hosts"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHosts(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, hosts.NewFile("testdata/hosts").Lookup("localhost."))
|
||||
require.NotEmpty(t, hosts.NewFile(hosts.DefaultPath).Lookup("localhost."))
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
//go:build !windows
|
||||
|
||||
package hosts
|
||||
|
||||
var DefaultPath = "/etc/hosts"
|
||||
@@ -1,17 +0,0 @@
|
||||
package hosts
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var DefaultPath string
|
||||
|
||||
func init() {
|
||||
systemDirectory, err := windows.GetSystemDirectory()
|
||||
if err != nil {
|
||||
systemDirectory = "C:\\Windows\\System32"
|
||||
}
|
||||
DefaultPath = filepath.Join(systemDirectory, "Drivers/etc/hosts")
|
||||
}
|
||||
2
dns/transport/hosts/testdata/hosts
vendored
2
dns/transport/hosts/testdata/hosts
vendored
@@ -1,2 +0,0 @@
|
||||
127.0.0.1 localhost
|
||||
::1 localhost
|
||||
@@ -1,204 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const MimeType = "application/dns-message"
|
||||
|
||||
var _ adapter.DNSTransport = (*HTTPSTransport)(nil)
|
||||
|
||||
func RegisterHTTPS(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTPS, NewHTTPS)
|
||||
}
|
||||
|
||||
type HTTPSTransport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
transport *http.Transport
|
||||
}
|
||||
|
||||
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
||||
tlsOptions.Enabled = true
|
||||
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if common.Error(tlsConfig.Config()) == nil && !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), http2.NextProtoTLS))
|
||||
}
|
||||
if !common.Contains(tlsConfig.NextProtos(), "http/1.1") {
|
||||
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), "http/1.1"))
|
||||
}
|
||||
headers := options.Headers.Build()
|
||||
host := headers.Get("Host")
|
||||
if host != "" {
|
||||
headers.Del("Host")
|
||||
} else {
|
||||
if tlsConfig.ServerName() != "" {
|
||||
host = tlsConfig.ServerName()
|
||||
} else {
|
||||
host = options.Server
|
||||
}
|
||||
}
|
||||
destinationURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: host,
|
||||
}
|
||||
if destinationURL.Host == "" {
|
||||
destinationURL.Host = options.Server
|
||||
}
|
||||
if options.ServerPort != 0 && options.ServerPort != 443 {
|
||||
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
|
||||
}
|
||||
path := options.Path
|
||||
if path == "" {
|
||||
path = "/dns-query"
|
||||
}
|
||||
err = sHTTP.URLSetPath(&destinationURL, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
}
|
||||
return NewHTTPSRaw(
|
||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
|
||||
logger,
|
||||
transportDialer,
|
||||
&destinationURL,
|
||||
headers,
|
||||
serverAddr,
|
||||
tlsConfig,
|
||||
), nil
|
||||
}
|
||||
|
||||
func NewHTTPSRaw(
|
||||
adapter dns.TransportAdapter,
|
||||
logger log.ContextLogger,
|
||||
dialer N.Dialer,
|
||||
destination *url.URL,
|
||||
headers http.Header,
|
||||
serverAddr M.Socksaddr,
|
||||
tlsConfig tls.Config,
|
||||
) *HTTPSTransport {
|
||||
var transport *http.Transport
|
||||
if tlsConfig != nil {
|
||||
transport = &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
tcpConn, hErr := dialer.DialContext(ctx, network, serverAddr)
|
||||
if hErr != nil {
|
||||
return nil, hErr
|
||||
}
|
||||
tlsConn, hErr := aTLS.ClientHandshake(ctx, tcpConn, tlsConfig)
|
||||
if hErr != nil {
|
||||
tcpConn.Close()
|
||||
return nil, hErr
|
||||
}
|
||||
return tlsConn, nil
|
||||
},
|
||||
}
|
||||
} else {
|
||||
transport = &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, serverAddr)
|
||||
},
|
||||
}
|
||||
}
|
||||
return &HTTPSTransport{
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: dialer,
|
||||
destination: destination,
|
||||
headers: headers,
|
||||
transport: transport,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Reset() {
|
||||
t.transport.CloseIdleConnections()
|
||||
t.transport = t.transport.Clone()
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
exMessage := *message
|
||||
exMessage.Id = 0
|
||||
exMessage.Compress = true
|
||||
requestBuffer := buf.NewSize(1 + message.Len())
|
||||
rawMessage, err := exMessage.PackBuffer(requestBuffer.FreeBytes())
|
||||
if err != nil {
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
|
||||
if err != nil {
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
request.Header = t.headers.Clone()
|
||||
request.Header.Set("Content-Type", MimeType)
|
||||
request.Header.Set("Accept", MimeType)
|
||||
response, err := t.transport.RoundTrip(request)
|
||||
requestBuffer.Release()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, E.New("unexpected status: ", response.Status)
|
||||
}
|
||||
var responseMessage mDNS.Msg
|
||||
if response.ContentLength > 0 {
|
||||
responseBuffer := buf.NewSize(int(response.ContentLength))
|
||||
_, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = responseMessage.Unpack(responseBuffer.Bytes())
|
||||
responseBuffer.Release()
|
||||
} else {
|
||||
rawMessage, err = io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = responseMessage.Unpack(rawMessage)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport/hosts"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport)
|
||||
}
|
||||
|
||||
var _ adapter.DNSTransport = (*Transport)(nil)
|
||||
|
||||
type Transport struct {
|
||||
dns.TransportAdapter
|
||||
hosts *hosts.File
|
||||
dialer N.Dialer
|
||||
}
|
||||
|
||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewLocalDialer(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Transport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
|
||||
hosts: hosts.NewFile(hosts.DefaultPath),
|
||||
dialer: transportDialer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
question := message.Question[0]
|
||||
domain := dns.FqdnToDomain(question.Name)
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
addresses := t.hosts.Lookup(domain)
|
||||
if len(addresses) > 0 {
|
||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||
}
|
||||
}
|
||||
systemConfig := getSystemDNSConfig()
|
||||
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
|
||||
return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
|
||||
} else {
|
||||
return t.exchangeParallel(ctx, systemConfig, message, domain)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||
var lastErr error
|
||||
for _, fqdn := range systemConfig.nameList(domain) {
|
||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
type queryResult struct {
|
||||
response *mDNS.Msg
|
||||
err error
|
||||
}
|
||||
results := make(chan queryResult)
|
||||
startRacer := func(ctx context.Context, fqdn string) {
|
||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
||||
addresses, _ := dns.MessageToAddresses(response)
|
||||
if len(addresses) == 0 {
|
||||
err = E.New(fqdn, ": empty result")
|
||||
}
|
||||
select {
|
||||
case results <- queryResult{response, err}:
|
||||
case <-returned:
|
||||
}
|
||||
}
|
||||
queryCtx, queryCancel := context.WithCancel(ctx)
|
||||
defer queryCancel()
|
||||
var nameCount int
|
||||
for _, fqdn := range systemConfig.nameList(domain) {
|
||||
nameCount++
|
||||
go startRacer(queryCtx, fqdn)
|
||||
}
|
||||
var errors []error
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case result := <-results:
|
||||
if result.err == nil {
|
||||
return result.response, nil
|
||||
}
|
||||
errors = append(errors, result.err)
|
||||
if len(errors) == nameCount {
|
||||
return nil, E.Errors(errors...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
serverOffset := config.serverOffset()
|
||||
sLen := uint32(len(config.servers))
|
||||
var lastErr error
|
||||
for i := 0; i < config.attempts; i++ {
|
||||
for j := uint32(0); j < sLen; j++ {
|
||||
server := config.servers[(serverOffset+j)%sLen]
|
||||
question := message.Question[0]
|
||||
question.Name = fqdn
|
||||
response, err := t.exchangeOne(ctx, M.ParseSocksaddr(server), question, config.timeout, config.useTCP, config.trustAD)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
return nil, E.Cause(lastErr, fqdn)
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) {
|
||||
var networks []string
|
||||
if useTCP {
|
||||
networks = []string{N.NetworkTCP}
|
||||
} else {
|
||||
networks = []string{N.NetworkUDP, N.NetworkTCP}
|
||||
}
|
||||
request := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: uint16(rand.Uint32()),
|
||||
RecursionDesired: true,
|
||||
AuthenticatedData: ad,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
Compress: true,
|
||||
}
|
||||
request.SetEdns0(maxDNSPacketSize, false)
|
||||
buffer := buf.Get(buf.UDPBufferSize)
|
||||
defer buf.Put(buffer)
|
||||
for _, network := range networks {
|
||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
|
||||
defer cancel()
|
||||
conn, err := t.dialer.DialContext(ctx, network, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||
conn.SetDeadline(deadline)
|
||||
}
|
||||
rawMessage, err := request.PackBuffer(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "pack request")
|
||||
}
|
||||
_, err = conn.Write(rawMessage)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read response")
|
||||
}
|
||||
var response mDNS.Msg
|
||||
err = response.Unpack(buffer[:n])
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack response")
|
||||
}
|
||||
if response.Truncated && network == N.NetworkUDP {
|
||||
continue
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
panic("unexpected")
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// net.maxDNSPacketSize
|
||||
maxDNSPacketSize = 1232
|
||||
)
|
||||
|
||||
type resolverConfig struct {
|
||||
initOnce sync.Once
|
||||
ch chan struct{}
|
||||
lastChecked time.Time
|
||||
dnsConfig atomic.Pointer[dnsConfig]
|
||||
}
|
||||
|
||||
var resolvConf resolverConfig
|
||||
|
||||
func getSystemDNSConfig() *dnsConfig {
|
||||
resolvConf.tryUpdate("/etc/resolv.conf")
|
||||
return resolvConf.dnsConfig.Load()
|
||||
}
|
||||
|
||||
func (conf *resolverConfig) init() {
|
||||
conf.dnsConfig.Store(dnsReadConfig("/etc/resolv.conf"))
|
||||
conf.lastChecked = time.Now()
|
||||
conf.ch = make(chan struct{}, 1)
|
||||
}
|
||||
|
||||
func (conf *resolverConfig) tryUpdate(name string) {
|
||||
conf.initOnce.Do(conf.init)
|
||||
|
||||
if conf.dnsConfig.Load().noReload {
|
||||
return
|
||||
}
|
||||
if !conf.tryAcquireSema() {
|
||||
return
|
||||
}
|
||||
defer conf.releaseSema()
|
||||
|
||||
now := time.Now()
|
||||
if conf.lastChecked.After(now.Add(-5 * time.Second)) {
|
||||
return
|
||||
}
|
||||
conf.lastChecked = now
|
||||
if runtime.GOOS != "windows" {
|
||||
var mtime time.Time
|
||||
if fi, err := os.Stat(name); err == nil {
|
||||
mtime = fi.ModTime()
|
||||
}
|
||||
if mtime.Equal(conf.dnsConfig.Load().mtime) {
|
||||
return
|
||||
}
|
||||
}
|
||||
dnsConf := dnsReadConfig(name)
|
||||
conf.dnsConfig.Store(dnsConf)
|
||||
}
|
||||
|
||||
func (conf *resolverConfig) tryAcquireSema() bool {
|
||||
select {
|
||||
case conf.ch <- struct{}{}:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (conf *resolverConfig) releaseSema() {
|
||||
<-conf.ch
|
||||
}
|
||||
|
||||
type dnsConfig struct {
|
||||
servers []string
|
||||
search []string
|
||||
ndots int
|
||||
timeout time.Duration
|
||||
attempts int
|
||||
rotate bool
|
||||
unknownOpt bool
|
||||
lookup []string
|
||||
err error
|
||||
mtime time.Time
|
||||
soffset uint32
|
||||
singleRequest bool
|
||||
useTCP bool
|
||||
trustAD bool
|
||||
noReload bool
|
||||
}
|
||||
|
||||
func (c *dnsConfig) serverOffset() uint32 {
|
||||
if c.rotate {
|
||||
return atomic.AddUint32(&c.soffset, 1) - 1 // return 0 to start
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (conf *dnsConfig) nameList(name string) []string {
|
||||
l := len(name)
|
||||
rooted := l > 0 && name[l-1] == '.'
|
||||
if l > 254 || l == 254 && !rooted {
|
||||
return nil
|
||||
}
|
||||
|
||||
if rooted {
|
||||
if avoidDNS(name) {
|
||||
return nil
|
||||
}
|
||||
return []string{name}
|
||||
}
|
||||
|
||||
hasNdots := strings.Count(name, ".") >= conf.ndots
|
||||
name += "."
|
||||
// l++
|
||||
|
||||
names := make([]string, 0, 1+len(conf.search))
|
||||
if hasNdots && !avoidDNS(name) {
|
||||
names = append(names, name)
|
||||
}
|
||||
for _, suffix := range conf.search {
|
||||
fqdn := name + suffix
|
||||
if !avoidDNS(fqdn) && len(fqdn) <= 254 {
|
||||
names = append(names, fqdn)
|
||||
}
|
||||
}
|
||||
if !hasNdots && !avoidDNS(name) {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func avoidDNS(name string) bool {
|
||||
if name == "" {
|
||||
return true
|
||||
}
|
||||
if name[len(name)-1] == '.' {
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
return strings.HasSuffix(name, ".onion")
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
//go:build !windows
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
func dnsReadConfig(name string) *dnsConfig {
|
||||
conf := &dnsConfig{
|
||||
ndots: 1,
|
||||
timeout: 5 * time.Second,
|
||||
attempts: 2,
|
||||
}
|
||||
file, err := os.Open(name)
|
||||
if err != nil {
|
||||
conf.servers = defaultNS
|
||||
conf.search = dnsDefaultSearch()
|
||||
conf.err = err
|
||||
return conf
|
||||
}
|
||||
defer file.Close()
|
||||
fi, err := file.Stat()
|
||||
if err == nil {
|
||||
conf.mtime = fi.ModTime()
|
||||
} else {
|
||||
conf.servers = defaultNS
|
||||
conf.search = dnsDefaultSearch()
|
||||
conf.err = err
|
||||
return conf
|
||||
}
|
||||
reader := bufio.NewReader(file)
|
||||
var (
|
||||
prefix []byte
|
||||
line []byte
|
||||
isPrefix bool
|
||||
)
|
||||
for {
|
||||
line, isPrefix, err = reader.ReadLine()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if isPrefix {
|
||||
prefix = append(prefix, line...)
|
||||
continue
|
||||
} else if len(prefix) > 0 {
|
||||
line = append(prefix, line...)
|
||||
prefix = nil
|
||||
}
|
||||
if len(line) > 0 && (line[0] == ';' || line[0] == '#') {
|
||||
continue
|
||||
}
|
||||
f := strings.Fields(string(line))
|
||||
if len(f) < 1 {
|
||||
continue
|
||||
}
|
||||
switch f[0] {
|
||||
case "nameserver":
|
||||
if len(f) > 1 && len(conf.servers) < 3 {
|
||||
if _, err := netip.ParseAddr(f[1]); err == nil {
|
||||
conf.servers = append(conf.servers, net.JoinHostPort(f[1], "53"))
|
||||
}
|
||||
}
|
||||
case "domain":
|
||||
if len(f) > 1 {
|
||||
conf.search = []string{ensureRooted(f[1])}
|
||||
}
|
||||
|
||||
case "search":
|
||||
conf.search = make([]string, 0, len(f)-1)
|
||||
for i := 1; i < len(f); i++ {
|
||||
name := ensureRooted(f[i])
|
||||
if name == "." {
|
||||
continue
|
||||
}
|
||||
conf.search = append(conf.search, name)
|
||||
}
|
||||
|
||||
case "options":
|
||||
for _, s := range f[1:] {
|
||||
switch {
|
||||
case strings.HasPrefix(s, "ndots:"):
|
||||
n, _, _ := dtoi(s[6:])
|
||||
if n < 0 {
|
||||
n = 0
|
||||
} else if n > 15 {
|
||||
n = 15
|
||||
}
|
||||
conf.ndots = n
|
||||
case strings.HasPrefix(s, "timeout:"):
|
||||
n, _, _ := dtoi(s[8:])
|
||||
if n < 1 {
|
||||
n = 1
|
||||
}
|
||||
conf.timeout = time.Duration(n) * time.Second
|
||||
case strings.HasPrefix(s, "attempts:"):
|
||||
n, _, _ := dtoi(s[9:])
|
||||
if n < 1 {
|
||||
n = 1
|
||||
}
|
||||
conf.attempts = n
|
||||
case s == "rotate":
|
||||
conf.rotate = true
|
||||
case s == "single-request" || s == "single-request-reopen":
|
||||
conf.singleRequest = true
|
||||
case s == "use-vc" || s == "usevc" || s == "tcp":
|
||||
conf.useTCP = true
|
||||
case s == "trust-ad":
|
||||
conf.trustAD = true
|
||||
case s == "edns0":
|
||||
case s == "no-reload":
|
||||
conf.noReload = true
|
||||
default:
|
||||
conf.unknownOpt = true
|
||||
}
|
||||
}
|
||||
|
||||
case "lookup":
|
||||
conf.lookup = f[1:]
|
||||
|
||||
default:
|
||||
conf.unknownOpt = true
|
||||
}
|
||||
}
|
||||
if len(conf.servers) == 0 {
|
||||
conf.servers = defaultNS
|
||||
}
|
||||
if len(conf.search) == 0 {
|
||||
conf.search = dnsDefaultSearch()
|
||||
}
|
||||
return conf
|
||||
}
|
||||
|
||||
//go:linkname defaultNS net.defaultNS
|
||||
var defaultNS []string
|
||||
|
||||
func dnsDefaultSearch() []string {
|
||||
hn, err := os.Hostname()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if i := strings.IndexRune(hn, '.'); i >= 0 && i < len(hn)-1 {
|
||||
return []string{ensureRooted(hn[i+1:])}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureRooted(s string) string {
|
||||
if len(s) > 0 && s[len(s)-1] == '.' {
|
||||
return s
|
||||
}
|
||||
return s + "."
|
||||
}
|
||||
|
||||
const big = 0xFFFFFF
|
||||
|
||||
func dtoi(s string) (n int, i int, ok bool) {
|
||||
n = 0
|
||||
for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
|
||||
n = n*10 + int(s[i]-'0')
|
||||
if n >= big {
|
||||
return big, i, false
|
||||
}
|
||||
}
|
||||
if i == 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return n, i, true
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func dnsReadConfig(_ string) *dnsConfig {
|
||||
conf := &dnsConfig{
|
||||
ndots: 1,
|
||||
timeout: 5 * time.Second,
|
||||
attempts: 2,
|
||||
}
|
||||
defer func() {
|
||||
if len(conf.servers) == 0 {
|
||||
conf.servers = defaultNS
|
||||
}
|
||||
}()
|
||||
aas, err := adapterAddresses()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, aa := range aas {
|
||||
// Only take interfaces whose OperStatus is IfOperStatusUp(0x01) into DNS configs.
|
||||
if aa.OperStatus != windows.IfOperStatusUp {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only take interfaces which have at least one gateway
|
||||
if aa.FirstGatewayAddress == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next {
|
||||
sa, err := dns.Address.Sockaddr.Sockaddr()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var ip netip.Addr
|
||||
switch sa := sa.(type) {
|
||||
case *syscall.SockaddrInet4:
|
||||
ip = netip.AddrFrom4([4]byte{sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3]})
|
||||
case *syscall.SockaddrInet6:
|
||||
var addr16 [16]byte
|
||||
copy(addr16[:], sa.Addr[:])
|
||||
if addr16[0] == 0xfe && addr16[1] == 0xc0 {
|
||||
// fec0/10 IPv6 addresses are site local anycast DNS
|
||||
// addresses Microsoft sets by default if no other
|
||||
// IPv6 DNS address is set. Site local anycast is
|
||||
// deprecated since 2004, see
|
||||
// https://datatracker.ietf.org/doc/html/rfc3879
|
||||
continue
|
||||
}
|
||||
ip = netip.AddrFrom16(addr16)
|
||||
default:
|
||||
// Unexpected type.
|
||||
continue
|
||||
}
|
||||
conf.servers = append(conf.servers, net.JoinHostPort(ip.String(), "53"))
|
||||
}
|
||||
}
|
||||
return conf
|
||||
}
|
||||
|
||||
//go:linkname defaultNS net.defaultNS
|
||||
var defaultNS []string
|
||||
|
||||
func adapterAddresses() ([]*windows.IpAdapterAddresses, error) {
|
||||
var b []byte
|
||||
l := uint32(15000) // recommended initial size
|
||||
for {
|
||||
b = make([]byte, l)
|
||||
const flags = windows.GAA_FLAG_INCLUDE_PREFIX | windows.GAA_FLAG_INCLUDE_GATEWAYS
|
||||
err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, flags, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l)
|
||||
if err == nil {
|
||||
if l == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW {
|
||||
return nil, os.NewSyscallError("getadaptersaddresses", err)
|
||||
}
|
||||
if l <= uint32(len(b)) {
|
||||
return nil, os.NewSyscallError("getadaptersaddresses", err)
|
||||
}
|
||||
}
|
||||
var aas []*windows.IpAdapterAddresses
|
||||
for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next {
|
||||
aas = append(aas, aa)
|
||||
}
|
||||
return aas, nil
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ adapter.DNSTransport = (*PredefinedTransport)(nil)
|
||||
|
||||
func RegisterPredefined(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.PredefinedDNSServerOptions](registry, C.DNSTypePreDefined, NewPredefined)
|
||||
}
|
||||
|
||||
type PredefinedTransport struct {
|
||||
dns.TransportAdapter
|
||||
responses []*predefinedResponse
|
||||
}
|
||||
|
||||
type predefinedResponse struct {
|
||||
questions []mDNS.Question
|
||||
answer *mDNS.Msg
|
||||
}
|
||||
|
||||
func NewPredefined(ctx context.Context, logger log.ContextLogger, tag string, options option.PredefinedDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
var responses []*predefinedResponse
|
||||
for _, response := range options.Responses {
|
||||
questions, msg, err := response.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responses = append(responses, &predefinedResponse{
|
||||
questions: questions,
|
||||
answer: msg,
|
||||
})
|
||||
}
|
||||
if len(responses) == 0 {
|
||||
return nil, E.New("empty predefined responses")
|
||||
}
|
||||
return &PredefinedTransport{
|
||||
TransportAdapter: dns.NewTransportAdapter(C.DNSTypePreDefined, tag, nil),
|
||||
responses: responses,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *PredefinedTransport) Reset() {
|
||||
}
|
||||
|
||||
func (t *PredefinedTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
for _, response := range t.responses {
|
||||
for _, question := range response.questions {
|
||||
if func() bool {
|
||||
if question.Name == "" && question.Qtype == mDNS.TypeNone {
|
||||
return true
|
||||
} else if question.Name == "" {
|
||||
return common.Any(message.Question, func(it mDNS.Question) bool {
|
||||
return it.Qtype == question.Qtype
|
||||
})
|
||||
} else if question.Qtype == mDNS.TypeNone {
|
||||
return common.Any(message.Question, func(it mDNS.Question) bool {
|
||||
return it.Name == question.Name
|
||||
})
|
||||
} else {
|
||||
return common.Contains(message.Question, question)
|
||||
}
|
||||
}() {
|
||||
copyAnswer := *response.answer
|
||||
copyAnswer.Id = message.Id
|
||||
return ©Answer, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, dns.RCodeNameError
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ adapter.DNSTransport = (*HTTP3Transport)(nil)
|
||||
|
||||
func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTP3, NewHTTP3)
|
||||
}
|
||||
|
||||
type HTTP3Transport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
destination *url.URL
|
||||
headers http.Header
|
||||
transport *http3.Transport
|
||||
}
|
||||
|
||||
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
||||
tlsOptions.Enabled = true
|
||||
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdConfig, err := tlsConfig.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers := options.Headers.Build()
|
||||
host := headers.Get("Host")
|
||||
if host != "" {
|
||||
headers.Del("Host")
|
||||
} else {
|
||||
if tlsConfig.ServerName() != "" {
|
||||
host = tlsConfig.ServerName()
|
||||
} else {
|
||||
host = options.Server
|
||||
}
|
||||
}
|
||||
destinationURL := url.URL{
|
||||
Scheme: "HTTP3",
|
||||
Host: host,
|
||||
}
|
||||
if destinationURL.Host == "" {
|
||||
destinationURL.Host = options.Server
|
||||
}
|
||||
if options.ServerPort != 0 && options.ServerPort != 443 {
|
||||
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
|
||||
}
|
||||
path := options.Path
|
||||
if path == "" {
|
||||
path = "/dns-query"
|
||||
}
|
||||
err = sHTTP.URLSetPath(&destinationURL, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 443
|
||||
}
|
||||
return &HTTP3Transport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
|
||||
logger: logger,
|
||||
dialer: transportDialer,
|
||||
destination: &destinationURL,
|
||||
headers: headers,
|
||||
transport: &http3.Transport{
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
destinationAddr := M.ParseSocksaddr(addr)
|
||||
conn, dialErr := transportDialer.DialContext(ctx, N.NetworkUDP, destinationAddr)
|
||||
if dialErr != nil {
|
||||
return nil, dialErr
|
||||
}
|
||||
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsCfg, cfg)
|
||||
},
|
||||
TLSClientConfig: stdConfig,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *HTTP3Transport) Reset() {
|
||||
t.transport.Close()
|
||||
}
|
||||
|
||||
func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
exMessage := *message
|
||||
exMessage.Id = 0
|
||||
exMessage.Compress = true
|
||||
requestBuffer := buf.NewSize(1 + message.Len())
|
||||
rawMessage, err := exMessage.PackBuffer(requestBuffer.FreeBytes())
|
||||
if err != nil {
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
|
||||
if err != nil {
|
||||
requestBuffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
request.Header = t.headers.Clone()
|
||||
request.Header.Set("Content-Type", transport.MimeType)
|
||||
request.Header.Set("Accept", transport.MimeType)
|
||||
response, err := t.transport.RoundTrip(request)
|
||||
requestBuffer.Release()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, E.New("unexpected status: ", response.Status)
|
||||
}
|
||||
var responseMessage mDNS.Msg
|
||||
if response.ContentLength > 0 {
|
||||
responseBuffer := buf.NewSize(int(response.ContentLength))
|
||||
_, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = responseMessage.Unpack(responseBuffer.Bytes())
|
||||
responseBuffer.Release()
|
||||
} else {
|
||||
rawMessage, err = io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = responseMessage.Unpack(rawMessage)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
sQUIC "github.com/sagernet/sing-quic"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ adapter.DNSTransport = (*Transport)(nil)
|
||||
|
||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeQUIC, NewQUIC)
|
||||
}
|
||||
|
||||
type Transport struct {
|
||||
dns.TransportAdapter
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig tls.Config
|
||||
access sync.Mutex
|
||||
connection quic.EarlyConnection
|
||||
}
|
||||
|
||||
func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
||||
tlsOptions.Enabled = true
|
||||
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tlsConfig.NextProtos()) == 0 {
|
||||
tlsConfig.SetNextProtos([]string{"doq"})
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 853
|
||||
}
|
||||
return &Transport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
dialer: transportDialer,
|
||||
serverAddr: serverAddr,
|
||||
tlsConfig: tlsConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
t.access.Lock()
|
||||
defer t.access.Unlock()
|
||||
connection := t.connection
|
||||
if connection != nil {
|
||||
connection.CloseWithError(0, "")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
var (
|
||||
conn quic.Connection
|
||||
err error
|
||||
response *mDNS.Msg
|
||||
)
|
||||
for i := 0; i < 2; i++ {
|
||||
conn, err = t.openConnection()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err = t.exchange(ctx, message, conn)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
} else if !isQUICRetryError(err) {
|
||||
return nil, err
|
||||
} else {
|
||||
conn.CloseWithError(quic.ApplicationErrorCode(0), "")
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (t *Transport) openConnection() (quic.EarlyConnection, error) {
|
||||
connection := t.connection
|
||||
if connection != nil && !common.Done(connection.Context()) {
|
||||
return connection, nil
|
||||
}
|
||||
t.access.Lock()
|
||||
defer t.access.Unlock()
|
||||
connection = t.connection
|
||||
if connection != nil && !common.Done(connection.Context()) {
|
||||
return connection, nil
|
||||
}
|
||||
conn, err := t.dialer.DialContext(t.ctx, N.NetworkUDP, t.serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
earlyConnection, err := sQUIC.DialEarly(
|
||||
t.ctx,
|
||||
bufio.NewUnbindPacketConn(conn),
|
||||
t.serverAddr.UDPAddr(),
|
||||
t.tlsConfig,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.connection = earlyConnection
|
||||
return earlyConnection, nil
|
||||
}
|
||||
|
||||
func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn quic.Connection) (*mDNS.Msg, error) {
|
||||
stream, err := conn.OpenStreamSync(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stream.Close()
|
||||
defer stream.CancelRead(0)
|
||||
err = transport.WriteMessage(stream, 0, message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transport.ReadMessage(stream)
|
||||
}
|
||||
|
||||
// https://github.com/AdguardTeam/dnsproxy/blob/fd1868577652c639cce3da00e12ca548f421baf1/upstream/upstream_quic.go#L394
|
||||
func isQUICRetryError(err error) (ok bool) {
|
||||
var qAppErr *quic.ApplicationError
|
||||
if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
var qIdleErr *quic.IdleTimeoutError
|
||||
if errors.As(err, &qIdleErr) {
|
||||
return true
|
||||
}
|
||||
|
||||
var resetErr *quic.StatelessResetError
|
||||
if errors.As(err, &resetErr) {
|
||||
return true
|
||||
}
|
||||
|
||||
var qTransportError *quic.TransportError
|
||||
if errors.As(err, &qTransportError) && qTransportError.ErrorCode == quic.NoError {
|
||||
return true
|
||||
}
|
||||
|
||||
if errors.Is(err, quic.Err0RTTRejected) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ adapter.DNSTransport = (*TCPTransport)(nil)
|
||||
|
||||
func RegisterTCP(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeTCP, NewTCP)
|
||||
}
|
||||
|
||||
type TCPTransport struct {
|
||||
dns.TransportAdapter
|
||||
dialer N.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
}
|
||||
|
||||
func NewTCP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 53
|
||||
}
|
||||
return &TCPTransport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTCP, tag, options),
|
||||
dialer: transportDialer,
|
||||
serverAddr: serverAddr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *TCPTransport) Reset() {
|
||||
}
|
||||
|
||||
func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
err = WriteMessage(conn, 0, message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ReadMessage(conn)
|
||||
}
|
||||
|
||||
func ReadMessage(reader io.Reader) (*mDNS.Msg, error) {
|
||||
var responseLen uint16
|
||||
err := binary.Read(reader, binary.BigEndian, &responseLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if responseLen < 10 {
|
||||
return nil, mDNS.ErrShortRead
|
||||
}
|
||||
buffer := buf.NewSize(int(responseLen))
|
||||
defer buffer.Release()
|
||||
_, err = buffer.ReadFullFrom(reader, int(responseLen))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var message mDNS.Msg
|
||||
err = message.Unpack(buffer.Bytes())
|
||||
return &message, err
|
||||
}
|
||||
|
||||
func WriteMessage(writer io.Writer, messageId uint16, message *mDNS.Msg) error {
|
||||
requestLen := message.Len()
|
||||
buffer := buf.NewSize(3 + requestLen)
|
||||
defer buffer.Release()
|
||||
common.Must(binary.Write(buffer, binary.BigEndian, uint16(requestLen)))
|
||||
exMessage := *message
|
||||
exMessage.Id = messageId
|
||||
exMessage.Compress = true
|
||||
rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buffer.Truncate(2 + len(rawMessage))
|
||||
return common.Error(writer.Write(buffer.Bytes()))
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ adapter.DNSTransport = (*TLSTransport)(nil)
|
||||
|
||||
func RegisterTLS(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeTLS, NewTLS)
|
||||
}
|
||||
|
||||
type TLSTransport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
tlsConfig tls.Config
|
||||
access sync.Mutex
|
||||
connections list.List[*tlsDNSConn]
|
||||
}
|
||||
|
||||
type tlsDNSConn struct {
|
||||
tls.Conn
|
||||
queryId uint16
|
||||
}
|
||||
|
||||
func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
||||
tlsOptions.Enabled = true
|
||||
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 853
|
||||
}
|
||||
return &TLSTransport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions),
|
||||
logger: logger,
|
||||
dialer: transportDialer,
|
||||
serverAddr: serverAddr,
|
||||
tlsConfig: tlsConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *TLSTransport) Reset() {
|
||||
t.access.Lock()
|
||||
defer t.access.Unlock()
|
||||
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
|
||||
connection.Value.Close()
|
||||
}
|
||||
t.connections.Init()
|
||||
}
|
||||
|
||||
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
t.access.Lock()
|
||||
conn := t.connections.PopFront()
|
||||
t.access.Unlock()
|
||||
if conn != nil {
|
||||
response, err := t.exchange(message, conn)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
tcpConn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConn, err := tls.ClientHandshake(ctx, tcpConn, t.tlsConfig)
|
||||
if err != nil {
|
||||
tcpConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return t.exchange(message, &tlsDNSConn{Conn: tlsConn})
|
||||
}
|
||||
|
||||
func (t *TLSTransport) exchange(message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) {
|
||||
conn.queryId++
|
||||
err := WriteMessage(conn, conn.queryId, message)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
response, err := ReadMessage(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, E.Cause(err, "read response")
|
||||
}
|
||||
t.access.Lock()
|
||||
t.connections.PushBack(conn)
|
||||
t.access.Unlock()
|
||||
return response, nil
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ adapter.DNSTransport = (*UDPTransport)(nil)
|
||||
|
||||
func RegisterUDP(registry *dns.TransportRegistry) {
|
||||
dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeUDP, NewUDP)
|
||||
}
|
||||
|
||||
type UDPTransport struct {
|
||||
dns.TransportAdapter
|
||||
logger logger.ContextLogger
|
||||
dialer N.Dialer
|
||||
serverAddr M.Socksaddr
|
||||
udpSize int
|
||||
tcpTransport *TCPTransport
|
||||
access sync.Mutex
|
||||
conn *dnsConnection
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
transportDialer, err := dns.NewRemoteDialer(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serverAddr := options.ServerOptions.Build()
|
||||
if serverAddr.Port == 0 {
|
||||
serverAddr.Port = 53
|
||||
}
|
||||
return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil
|
||||
}
|
||||
|
||||
func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr) *UDPTransport {
|
||||
return &UDPTransport{
|
||||
TransportAdapter: adapter,
|
||||
logger: logger,
|
||||
dialer: dialer,
|
||||
serverAddr: serverAddr,
|
||||
udpSize: 512,
|
||||
tcpTransport: &TCPTransport{
|
||||
dialer: dialer,
|
||||
serverAddr: serverAddr,
|
||||
},
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *UDPTransport) Reset() {
|
||||
t.access.Lock()
|
||||
defer t.access.Unlock()
|
||||
close(t.done)
|
||||
t.done = make(chan struct{})
|
||||
}
|
||||
|
||||
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
response, err := t.exchange(ctx, message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if response.Truncated {
|
||||
t.logger.InfoContext(ctx, "response truncated, retrying with TCP")
|
||||
return t.tcpTransport.Exchange(ctx, message)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
conn, err := t.open(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if edns0Opt := message.IsEdns0(); edns0Opt != nil {
|
||||
if udpSize := int(edns0Opt.UDPSize()); udpSize > t.udpSize {
|
||||
t.udpSize = udpSize
|
||||
}
|
||||
}
|
||||
buffer := buf.NewSize(1 + message.Len())
|
||||
defer buffer.Release()
|
||||
exMessage := *message
|
||||
exMessage.Compress = true
|
||||
messageId := message.Id
|
||||
callback := &dnsCallback{
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
conn.access.Lock()
|
||||
conn.queryId++
|
||||
exMessage.Id = conn.queryId
|
||||
conn.callbacks[exMessage.Id] = callback
|
||||
conn.access.Unlock()
|
||||
defer func() {
|
||||
conn.access.Lock()
|
||||
delete(conn.callbacks, messageId)
|
||||
conn.access.Unlock()
|
||||
callback.access.Lock()
|
||||
select {
|
||||
case <-callback.done:
|
||||
default:
|
||||
close(callback.done)
|
||||
}
|
||||
callback.access.Unlock()
|
||||
}()
|
||||
rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = conn.Write(rawMessage)
|
||||
if err != nil {
|
||||
conn.Close(err)
|
||||
return nil, err
|
||||
}
|
||||
select {
|
||||
case <-callback.done:
|
||||
callback.message.Id = messageId
|
||||
return callback.message, nil
|
||||
case <-conn.done:
|
||||
return nil, conn.err
|
||||
case <-t.done:
|
||||
return nil, os.ErrClosed
|
||||
case <-ctx.Done():
|
||||
conn.Close(ctx.Err())
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *UDPTransport) open(ctx context.Context) (*dnsConnection, error) {
|
||||
t.access.Lock()
|
||||
defer t.access.Unlock()
|
||||
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dnsConn := &dnsConnection{
|
||||
Conn: conn,
|
||||
done: make(chan struct{}),
|
||||
callbacks: make(map[uint16]*dnsCallback),
|
||||
}
|
||||
go t.recvLoop(dnsConn)
|
||||
return dnsConn, nil
|
||||
}
|
||||
|
||||
func (t *UDPTransport) recvLoop(conn *dnsConnection) {
|
||||
for {
|
||||
buffer := buf.NewSize(t.udpSize)
|
||||
_, err := buffer.ReadOnceFrom(conn)
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
conn.Close(err)
|
||||
return
|
||||
}
|
||||
var message mDNS.Msg
|
||||
err = message.Unpack(buffer.Bytes())
|
||||
buffer.Release()
|
||||
if err != nil {
|
||||
conn.Close(err)
|
||||
return
|
||||
}
|
||||
conn.access.RLock()
|
||||
callback, loaded := conn.callbacks[message.Id]
|
||||
conn.access.RUnlock()
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
callback.access.Lock()
|
||||
select {
|
||||
case <-callback.done:
|
||||
default:
|
||||
callback.message = &message
|
||||
close(callback.done)
|
||||
}
|
||||
callback.access.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
type dnsConnection struct {
|
||||
net.Conn
|
||||
access sync.RWMutex
|
||||
done chan struct{}
|
||||
closeOnce sync.Once
|
||||
err error
|
||||
queryId uint16
|
||||
callbacks map[uint16]*dnsCallback
|
||||
}
|
||||
|
||||
func (c *dnsConnection) Close(err error) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
c.closeOnce.Do(func() {
|
||||
close(c.done)
|
||||
c.err = err
|
||||
})
|
||||
c.Conn.Close()
|
||||
}
|
||||
|
||||
type dnsCallback struct {
|
||||
access sync.Mutex
|
||||
message *mDNS.Msg
|
||||
done chan struct{}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
var _ adapter.LegacyDNSTransport = (*TransportAdapter)(nil)
|
||||
|
||||
type TransportAdapter struct {
|
||||
transportType string
|
||||
transportTag string
|
||||
dependencies []string
|
||||
strategy C.DomainStrategy
|
||||
clientSubnet netip.Prefix
|
||||
}
|
||||
|
||||
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
|
||||
return TransportAdapter{
|
||||
transportType: transportType,
|
||||
transportTag: transportTag,
|
||||
dependencies: dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTransportAdapterWithLocalOptions(transportType string, transportTag string, localOptions option.LocalDNSServerOptions) TransportAdapter {
|
||||
var dependencies []string
|
||||
if localOptions.DomainResolver != nil && localOptions.DomainResolver.Server != "" {
|
||||
dependencies = append(dependencies, localOptions.DomainResolver.Server)
|
||||
}
|
||||
return TransportAdapter{
|
||||
transportType: transportType,
|
||||
transportTag: transportTag,
|
||||
dependencies: dependencies,
|
||||
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
|
||||
clientSubnet: localOptions.LegacyClientSubnet,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTransportAdapterWithRemoteOptions(transportType string, transportTag string, remoteOptions option.RemoteDNSServerOptions) TransportAdapter {
|
||||
var dependencies []string
|
||||
if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" {
|
||||
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
|
||||
}
|
||||
if remoteOptions.LegacyAddressResolver != "" {
|
||||
dependencies = append(dependencies, remoteOptions.LegacyAddressResolver)
|
||||
}
|
||||
return TransportAdapter{
|
||||
transportType: transportType,
|
||||
transportTag: transportTag,
|
||||
dependencies: dependencies,
|
||||
strategy: C.DomainStrategy(remoteOptions.LegacyStrategy),
|
||||
clientSubnet: remoteOptions.LegacyClientSubnet,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *TransportAdapter) Type() string {
|
||||
return a.transportType
|
||||
}
|
||||
|
||||
func (a *TransportAdapter) Tag() string {
|
||||
return a.transportTag
|
||||
}
|
||||
|
||||
func (a *TransportAdapter) Dependencies() []string {
|
||||
return a.dependencies
|
||||
}
|
||||
|
||||
func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy {
|
||||
return a.strategy
|
||||
}
|
||||
|
||||
func (a *TransportAdapter) LegacyClientSubnet() netip.Prefix {
|
||||
return a.clientSubnet
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user