mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-07-07 17:01:36 +03:00
Compare commits
41 Commits
v1.11.11-e
...
dev-go124-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
658d4a180b | ||
|
|
93966bdd36 | ||
|
|
14a4b0f922 | ||
|
|
919b08e64c | ||
|
|
bdd2472065 | ||
|
|
705c23866a | ||
|
|
ec310170cc | ||
|
|
7405b22dc8 | ||
|
|
cd2cf2450e | ||
|
|
a525f139dc | ||
|
|
cc8ba050dd | ||
|
|
987556fd3d | ||
|
|
e6dd7d279d | ||
|
|
b1cbf141c7 | ||
|
|
ab616d8510 | ||
|
|
097818dfef | ||
|
|
47af425e45 | ||
|
|
5d49685683 | ||
|
|
15624a648c | ||
|
|
1bfedf5332 | ||
|
|
10d15259b6 | ||
|
|
ee11ca4935 | ||
|
|
e83331c2d9 | ||
|
|
b74df53a9c | ||
|
|
f64107f040 | ||
|
|
7db0e712b6 | ||
|
|
edfdf1d4f3 | ||
|
|
eb3023d66c | ||
|
|
2c39c4d19c | ||
|
|
aa3fcefb72 | ||
|
|
ce503fd682 | ||
|
|
017fa5e298 | ||
|
|
94d187397d | ||
|
|
1d497b08d7 | ||
|
|
e52ab781bb | ||
|
|
2c1e398c78 | ||
|
|
d4ad7ff638 | ||
|
|
ee0f3ba739 | ||
|
|
b365b1369d | ||
|
|
69f8d7fa72 | ||
|
|
746f63a9ac |
30
.fpm_openwrt
30
.fpm_openwrt
@@ -1,30 +0,0 @@
|
|||||||
-s dir
|
|
||||||
--name sing-box
|
|
||||||
--category net
|
|
||||||
--license GPL-3.0-or-later
|
|
||||||
--description "The universal proxy platform."
|
|
||||||
--url "https://sing-box.sagernet.org/"
|
|
||||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
|
||||||
--no-deb-generate-changes
|
|
||||||
|
|
||||||
--config-files /etc/config/sing-box
|
|
||||||
--config-files /etc/sing-box/config.json
|
|
||||||
|
|
||||||
--depends ca-bundle
|
|
||||||
--depends kmod-inet-diag
|
|
||||||
--depends kmod-tun
|
|
||||||
--depends firewall4
|
|
||||||
|
|
||||||
--before-remove release/config/openwrt.prerm
|
|
||||||
|
|
||||||
release/config/config.json=/etc/sing-box/config.json
|
|
||||||
|
|
||||||
release/config/openwrt.conf=/etc/config/sing-box
|
|
||||||
release/config/openwrt.init=/etc/init.d/sing-box
|
|
||||||
release/config/openwrt.keep=/lib/upgrade/keep.d/sing-box
|
|
||||||
|
|
||||||
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
|
||||||
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
|
||||||
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
|
||||||
|
|
||||||
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
|
||||||
21
.fpm_systemd
21
.fpm_systemd
@@ -1,21 +0,0 @@
|
|||||||
-s dir
|
|
||||||
--name sing-box
|
|
||||||
--category net
|
|
||||||
--license GPL-3.0-or-later
|
|
||||||
--description "The universal proxy platform."
|
|
||||||
--url "https://sing-box.sagernet.org/"
|
|
||||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
|
||||||
--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
28
.github/deb2ipk.sh
vendored
@@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683
|
|
||||||
|
|
||||||
set -e -o pipefail
|
|
||||||
|
|
||||||
PROJECT=$(dirname "$0")/../..
|
|
||||||
TMP_PATH=`mktemp -d`
|
|
||||||
cp $2 $TMP_PATH
|
|
||||||
pushd $TMP_PATH
|
|
||||||
|
|
||||||
DEB_NAME=`ls *.deb`
|
|
||||||
ar x $DEB_NAME
|
|
||||||
|
|
||||||
mkdir control
|
|
||||||
pushd control
|
|
||||||
tar xf ../control.tar.gz
|
|
||||||
rm md5sums
|
|
||||||
sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i
|
|
||||||
cat control
|
|
||||||
tar czf ../control.tar.gz ./*
|
|
||||||
popd
|
|
||||||
|
|
||||||
DEB_NAME=${DEB_NAME%.deb}
|
|
||||||
tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary
|
|
||||||
popd
|
|
||||||
|
|
||||||
cp $TMP_PATH/$DEB_NAME.ipk $3
|
|
||||||
rm -r $TMP_PATH
|
|
||||||
481
.github/workflows/build.yml
vendored
481
.github/workflows/build.yml
vendored
@@ -7,6 +7,11 @@ on:
|
|||||||
description: "Version name"
|
description: "Version name"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
prerelease:
|
||||||
|
description: "Is prerelease"
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
build:
|
build:
|
||||||
description: "Build type"
|
description: "Build type"
|
||||||
required: true
|
required: true
|
||||||
@@ -23,6 +28,10 @@ on:
|
|||||||
- tvOS
|
- tvOS
|
||||||
- macOS-standalone
|
- macOS-standalone
|
||||||
- publish-android
|
- publish-android
|
||||||
|
macos_project_version:
|
||||||
|
description: "macOS project version"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main-next
|
- main-next
|
||||||
@@ -38,6 +47,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.outputs.outputs.version }}
|
version: ${{ steps.outputs.outputs.version }}
|
||||||
|
prerelease: ${{ steps.outputs.outputs.prerelease }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
@@ -46,20 +56,23 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24
|
go-version: ^1.23
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
echo "version=${{ inputs.version }}"
|
echo "version=${{ inputs.version }}"
|
||||||
|
echo "prerelease=${{ inputs.prerelease }}"
|
||||||
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
echo "prerelease=${{ inputs.prerelease }}" >> "$GITHUB_ENV"
|
||||||
- name: Calculate version
|
- name: Calculate version
|
||||||
if: github.event_name != 'workflow_dispatch'
|
if: github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
go run -v ./cmd/internal/read_tag --ci --nightly
|
go run -v ./cmd/internal/read_tag --nightly
|
||||||
- name: Set outputs
|
- name: Set outputs
|
||||||
id: outputs
|
id: outputs
|
||||||
run: |-
|
run: |-
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
|
||||||
build:
|
build:
|
||||||
name: Build binary
|
name: Build binary
|
||||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
||||||
@@ -69,208 +82,140 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64, openwrt: "x86_64" }
|
- name: linux_386
|
||||||
- { os: linux, arch: "386", go386: sse2, debian: i386, rpm: i386, openwrt: "i386_pentium4" }
|
goos: linux
|
||||||
- { os: linux, arch: "386", go386: softfloat, openwrt: "i386_pentium-mmx" }
|
goarch: 386
|
||||||
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64, openwrt: "aarch64_cortex-a53 aarch64_cortex-a72 aarch64_cortex-a76 aarch64_generic" }
|
- name: linux_amd64
|
||||||
- { os: linux, arch: arm, goarm: "5", openwrt: "arm_arm926ej-s arm_cortex-a7 arm_cortex-a9 arm_fa526 arm_xscale" }
|
goos: linux
|
||||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl, openwrt: "arm_arm1176jzf-s_vfp" }
|
goarch: amd64
|
||||||
- { 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" }
|
- name: linux_arm64
|
||||||
- { os: linux, arch: mips, gomips: softfloat, openwrt: "mips_24kc mips_4kec mips_mips32" }
|
goos: linux
|
||||||
- { os: linux, arch: mipsle, gomips: hardfloat, debian: mipsel, rpm: mipsel, openwrt: "mipsel_24kc_24kf" }
|
goarch: arm64
|
||||||
- { os: linux, arch: mipsle, gomips: softfloat, openwrt: "mipsel_24kc mipsel_74kc mipsel_mips32" }
|
- name: linux_arm
|
||||||
- { os: linux, arch: mips64, gomips: softfloat, openwrt: "mips64_mips64r2 mips64_octeonplus" }
|
goos: linux
|
||||||
- { os: linux, arch: mips64le, gomips: hardfloat, debian: mips64el, rpm: mips64el }
|
goarch: arm
|
||||||
- { os: linux, arch: mips64le, gomips: softfloat, openwrt: "mips64el_mips64r2" }
|
goarm: 6
|
||||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
- name: linux_arm_v7
|
||||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
goos: linux
|
||||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64, openwrt: "riscv64_generic" }
|
goarch: arm
|
||||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
|
goarm: 7
|
||||||
|
- name: linux_s390x
|
||||||
- { os: windows, arch: amd64 }
|
goos: linux
|
||||||
- { os: windows, arch: amd64, legacy_go: true }
|
goarch: s390x
|
||||||
- { os: windows, arch: "386" }
|
- name: linux_riscv64
|
||||||
- { os: windows, arch: "386", legacy_go: true }
|
goos: linux
|
||||||
- { os: windows, arch: arm64 }
|
goarch: riscv64
|
||||||
|
- name: linux_mips64le
|
||||||
- { os: darwin, arch: amd64 }
|
goos: linux
|
||||||
- { os: darwin, arch: amd64, legacy_go: true }
|
goarch: mips64le
|
||||||
- { os: darwin, arch: arm64 }
|
- name: windows_amd64
|
||||||
|
goos: windows
|
||||||
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
|
goarch: amd64
|
||||||
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
|
require_legacy_go: true
|
||||||
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
|
- name: windows_386
|
||||||
- { os: android, arch: "386", ndk: "i686-linux-android21" }
|
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
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
if: matrix.legacy_go
|
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.20
|
go-version: ^1.23
|
||||||
- name: Setup Go
|
- name: Cache legacy Go
|
||||||
if: ${{ ! matrix.legacy_go }}
|
if: matrix.require_legacy_go
|
||||||
uses: actions/setup-go@v5
|
id: cache-legacy-go
|
||||||
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24
|
path: |
|
||||||
|
~/go/go1.20.14
|
||||||
|
key: go120
|
||||||
|
- name: Setup legacy Go
|
||||||
|
if: matrix.require_legacy_go == 'true' && 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
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
if: matrix.os == 'android'
|
if: matrix.goos == 'android'
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
with:
|
with:
|
||||||
ndk-version: r28
|
ndk-version: r28-beta2
|
||||||
local-cache: true
|
local-cache: true
|
||||||
|
- name: Setup Goreleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
distribution: goreleaser-pro
|
||||||
|
version: latest
|
||||||
|
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
|
- name: Set tag
|
||||||
run: |-
|
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 }}
|
||||||
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
|
- name: Build
|
||||||
if: matrix.os != 'android'
|
if: matrix.goos != 'android'
|
||||||
run: |
|
run: |-
|
||||||
set -xeuo pipefail
|
goreleaser release --clean --split
|
||||||
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:
|
env:
|
||||||
CGO_ENABLED: "0"
|
GOOS: ${{ matrix.goos }}
|
||||||
GOOS: ${{ matrix.os }}
|
GOARCH: ${{ matrix.goarch }}
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOPATH: ${{ env.HOME }}/go
|
||||||
GO386: ${{ matrix.go386 }}
|
|
||||||
GOARM: ${{ matrix.goarm }}
|
GOARM: ${{ matrix.goarm }}
|
||||||
GOMIPS: ${{ matrix.gomips }}
|
|
||||||
GOMIPS64: ${{ matrix.gomips }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||||
|
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||||
|
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
- name: Build Android
|
- name: Build Android
|
||||||
if: matrix.os == 'android'
|
if: matrix.goos == 'android'
|
||||||
run: |
|
run: |-
|
||||||
set -xeuo pipefail
|
|
||||||
go install -v ./cmd/internal/build
|
go install -v ./cmd/internal/build
|
||||||
export CC='${{ matrix.ndk }}-clang'
|
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build goreleaser release --clean --split
|
||||||
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:
|
env:
|
||||||
CGO_ENABLED: "1"
|
BUILD_GOOS: ${{ matrix.goos }}
|
||||||
BUILD_GOOS: ${{ matrix.os }}
|
BUILD_GOARCH: ${{ matrix.goarch }}
|
||||||
BUILD_GOARCH: ${{ matrix.arch }}
|
GOARM: ${{ matrix.goarm }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Set name
|
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||||
run: |-
|
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||||
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}"
|
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
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
|
- name: Upload artifact
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.go386 && format('_{0}', matrix.go386) }}${{ matrix.gomips && format('_{0}', matrix.gomips) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
name: binary-${{ matrix.name }}
|
||||||
path: "dist"
|
path: 'dist'
|
||||||
build_android:
|
build_android:
|
||||||
name: Build Android
|
name: Build Android
|
||||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
||||||
@@ -286,20 +231,19 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24
|
go-version: ^1.23
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
with:
|
with:
|
||||||
ndk-version: r28
|
ndk-version: r28-beta2
|
||||||
- name: Setup OpenJDK
|
- name: Setup OpenJDK
|
||||||
run: |-
|
run: |-
|
||||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||||
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
run: |-
|
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 }}
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
|
||||||
- name: Build library
|
- name: Build library
|
||||||
run: |-
|
run: |-
|
||||||
make lib_install
|
make lib_install
|
||||||
@@ -309,12 +253,12 @@ jobs:
|
|||||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
if: needs.calculate_version.outputs.prerelease == 'false'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: github.ref == 'refs/heads/dev-next'
|
if: needs.calculate_version.outputs.prerelease == 'true'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -323,16 +267,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: ~/.gradle
|
path: ~/.gradle
|
||||||
key: gradle-${{ hashFiles('**/*.gradle') }}
|
key: gradle-${{ hashFiles('**/*.gradle') }}
|
||||||
- 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
|
- name: Build
|
||||||
run: |-
|
run: |-
|
||||||
|
go run -v ./cmd/internal/update_android_version --ci
|
||||||
mkdir clients/android/app/libs
|
mkdir clients/android/app/libs
|
||||||
cp libbox.aar clients/android/app/libs
|
cp libbox.aar clients/android/app/libs
|
||||||
cd clients/android
|
cd clients/android
|
||||||
@@ -342,11 +279,13 @@ jobs:
|
|||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||||
- name: Prepare upload
|
- name: Prepare upload
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
mkdir -p dist
|
mkdir -p dist/release
|
||||||
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
|
cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release
|
||||||
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
|
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: binary-android-apks
|
name: binary-android-apks
|
||||||
@@ -366,20 +305,19 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24
|
go-version: ^1.23
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
with:
|
with:
|
||||||
ndk-version: r28
|
ndk-version: r28-beta2
|
||||||
- name: Setup OpenJDK
|
- name: Setup OpenJDK
|
||||||
run: |-
|
run: |-
|
||||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||||
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
run: |-
|
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 }}
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
|
||||||
- name: Build library
|
- name: Build library
|
||||||
run: |-
|
run: |-
|
||||||
make lib_install
|
make lib_install
|
||||||
@@ -389,12 +327,12 @@ jobs:
|
|||||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
if: needs.calculate_version.outputs.prerelease == 'false'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: github.ref == 'refs/heads/dev-next'
|
if: needs.calculate_version.outputs.prerelease == 'true'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -416,38 +354,67 @@ jobs:
|
|||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||||
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
|
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
|
||||||
|
build_apple_library:
|
||||||
|
name: Build Apple library
|
||||||
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone'
|
||||||
|
runs-on: macos-15
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: 'recursive'
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.23
|
||||||
|
- name: Setup Xcode
|
||||||
|
run: |-
|
||||||
|
sudo xcode-select -s /Applications/Xcode_16.2_beta_3.app
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }}
|
||||||
|
- name: Build library
|
||||||
|
run: |-
|
||||||
|
make lib_install
|
||||||
|
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||||
|
make lib_ios
|
||||||
|
- name: Upload library
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: library-apple
|
||||||
|
path: 'Libbox.xcframework'
|
||||||
build_apple:
|
build_apple:
|
||||||
name: Build Apple clients
|
name: Build Apple clients
|
||||||
runs-on: macos-15
|
runs-on: macos-15
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
|
- build_apple_library
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- name: iOS
|
- name: iOS
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'iOS' }}
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'iOS' }}
|
||||||
platform: ios
|
|
||||||
scheme: SFI
|
scheme: SFI
|
||||||
destination: 'generic/platform=iOS'
|
destination: 'generic/platform=iOS'
|
||||||
archive: build/SFI.xcarchive
|
archive: build/SFI.xcarchive
|
||||||
upload: SFI/Upload.plist
|
upload: SFI/Upload.plist
|
||||||
- name: macOS
|
- name: macOS
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'macOS' }}
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'macOS' }}
|
||||||
platform: macos
|
|
||||||
scheme: SFM
|
scheme: SFM
|
||||||
destination: 'generic/platform=macOS'
|
destination: 'generic/platform=macOS'
|
||||||
archive: build/SFM.xcarchive
|
archive: build/SFM.xcarchive
|
||||||
upload: SFI/Upload.plist
|
upload: SFI/Upload.plist
|
||||||
- name: tvOS
|
- name: tvOS
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'tvOS' }}
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'tvOS' }}
|
||||||
platform: tvos
|
|
||||||
scheme: SFT
|
scheme: SFT
|
||||||
destination: 'generic/platform=tvOS'
|
destination: 'generic/platform=tvOS'
|
||||||
archive: build/SFT.xcarchive
|
archive: build/SFT.xcarchive
|
||||||
upload: SFI/Upload.plist
|
upload: SFI/Upload.plist
|
||||||
- name: macOS-standalone
|
- name: macOS-standalone
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone' }}
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone' }}
|
||||||
platform: macos
|
|
||||||
scheme: SFM.System
|
scheme: SFM.System
|
||||||
destination: 'generic/platform=macOS'
|
destination: 'generic/platform=macOS'
|
||||||
archive: build/SFM.System.xcarchive
|
archive: build/SFM.System.xcarchive
|
||||||
@@ -464,28 +431,23 @@ jobs:
|
|||||||
if: matrix.if
|
if: matrix.if
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24
|
go-version: ^1.23
|
||||||
- name: Setup Xcode stable
|
- name: Setup Xcode
|
||||||
if: matrix.if && github.ref == 'refs/heads/main-next'
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
sudo xcode-select -s /Applications/Xcode_16.2.app
|
sudo xcode-select -s /Applications/Xcode_16.2_beta_3.app
|
||||||
- name: Setup Xcode beta
|
|
||||||
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
|
||||||
run: |-
|
|
||||||
sudo xcode-select -s /Applications/Xcode_16.2.app
|
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
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 }}
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
|
||||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: matrix.if && github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
if: matrix.if && needs.calculate_version.outputs.prerelease == 'false'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
if: matrix.if && needs.calculate_version.outputs.prerelease == 'true'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -504,22 +466,18 @@ jobs:
|
|||||||
|
|
||||||
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
|
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
|
||||||
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
|
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
|
||||||
|
|
||||||
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
|
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||||
mkdir -p "$PROFILES_PATH"
|
mkdir -p "$PROFILES_PATH"
|
||||||
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
|
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
|
||||||
|
|
||||||
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
|
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
|
||||||
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
|
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
|
||||||
|
|
||||||
xcrun notarytool store-credentials "notarytool-password" \
|
xcrun notarytool store-credentials "notarytool-password" \
|
||||||
--key $ASC_KEY_PATH \
|
--key $ASC_KEY_PATH \
|
||||||
--key-id $ASC_KEY_ID \
|
--key-id $ASC_KEY_ID \
|
||||||
--issuer $ASC_KEY_ISSUER_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"
|
|
||||||
env:
|
env:
|
||||||
CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }}
|
CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }}
|
||||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
@@ -528,19 +486,12 @@ jobs:
|
|||||||
ASC_KEY: ${{ secrets.ASC_KEY }}
|
ASC_KEY: ${{ secrets.ASC_KEY }}
|
||||||
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||||
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
|
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
|
||||||
- name: Build library
|
- name: Download library
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
uses: actions/download-artifact@v4
|
||||||
make lib_install
|
with:
|
||||||
export PATH="$PATH:$(go env GOPATH)/bin"
|
name: library-apple
|
||||||
go run ./cmd/internal/build_libbox -target apple -platform ${{ matrix.platform }}
|
path: clients/apple/Libbox.xcframework
|
||||||
mv Libbox.xcframework clients/apple
|
|
||||||
- name: Update macOS version
|
|
||||||
if: matrix.if && matrix.name == 'macOS' && github.event_name == 'workflow_dispatch'
|
|
||||||
run: |-
|
|
||||||
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
|
|
||||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
|
|
||||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
@@ -552,25 +503,27 @@ jobs:
|
|||||||
-destination "${{ matrix.destination }}" \
|
-destination "${{ matrix.destination }}" \
|
||||||
-archivePath "${{ matrix.archive }}" \
|
-archivePath "${{ matrix.archive }}" \
|
||||||
-allowProvisioningUpdates \
|
-allowProvisioningUpdates \
|
||||||
-authenticationKeyPath $ASC_KEY_PATH \
|
-authenticationKeyPath $RUNNER_TEMP/Key.p12 \
|
||||||
-authenticationKeyID $ASC_KEY_ID \
|
-authenticationKeyID $ASC_KEY_ID \
|
||||||
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||||
|
env:
|
||||||
|
MACOS_PROJECT_VERSION: ${{ inputs.macos_project_version }}
|
||||||
|
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||||
|
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
|
||||||
- name: Upload to App Store Connect
|
- name: Upload to App Store Connect
|
||||||
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
go run -v ./cmd/internal/app_store_connect cancel_app_store ${{ matrix.platform }}
|
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
xcodebuild -exportArchive \
|
xcodebuild -exportArchive \
|
||||||
-archivePath "${{ matrix.archive }}" \
|
-archivePath "${{ matrix.archive }}" \
|
||||||
-exportOptionsPlist ${{ matrix.upload }} \
|
-exportOptionsPlist ${{ matrix.upload }} \
|
||||||
-allowProvisioningUpdates \
|
-allowProvisioningUpdates \
|
||||||
-authenticationKeyPath $ASC_KEY_PATH \
|
-authenticationKeyPath $RUNNER_TEMP/Key.p12 \
|
||||||
-authenticationKeyID $ASC_KEY_ID \
|
-authenticationKeyID $ASC_KEY_ID \
|
||||||
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||||
- name: Publish to TestFlight
|
env:
|
||||||
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/dev-next'
|
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||||
run: |-
|
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
|
||||||
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
|
|
||||||
- name: Build image
|
- name: Build image
|
||||||
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -592,10 +545,10 @@ jobs:
|
|||||||
cd "${{ matrix.archive }}"
|
cd "${{ matrix.archive }}"
|
||||||
zip -r SFM.dSYMs.zip dSYMs
|
zip -r SFM.dSYMs.zip dSYMs
|
||||||
popd
|
popd
|
||||||
|
|
||||||
mkdir -p dist
|
mkdir -p dist/release
|
||||||
cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg"
|
cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg"
|
||||||
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip"
|
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip"
|
||||||
- name: Upload image
|
- name: Upload image
|
||||||
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
@@ -604,7 +557,7 @@ jobs:
|
|||||||
path: 'dist'
|
path: 'dist'
|
||||||
upload:
|
upload:
|
||||||
name: Upload builds
|
name: Upload builds
|
||||||
if: always() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')
|
if: always() && github.event_name == 'workflow_dispatch' && inputs.build != 'publish-android'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
@@ -616,6 +569,12 @@ jobs:
|
|||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
- name: Setup Goreleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
distribution: goreleaser-pro
|
||||||
|
version: latest
|
||||||
|
install-only: true
|
||||||
- name: Cache ghr
|
- name: Cache ghr
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
id: cache-ghr
|
id: cache-ghr
|
||||||
@@ -632,25 +591,25 @@ jobs:
|
|||||||
go install -v .
|
go install -v .
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
run: |-
|
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 }}
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
|
||||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
- name: Download builds
|
- name: Download builds
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: dist
|
path: dist
|
||||||
merge-multiple: true
|
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
|
- name: Upload builds
|
||||||
if: ${{ env.PUBLISHED == 'false' }}
|
|
||||||
run: |-
|
run: |-
|
||||||
export PATH="$PATH:$HOME/go/bin"
|
export PATH="$PATH:$HOME/go/bin"
|
||||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist
|
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
5
.github/workflows/lint.yml
vendored
5
.github/workflows/lint.yml
vendored
@@ -28,11 +28,10 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24
|
go-version: ^1.23
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout=30m
|
args: --timeout=30m
|
||||||
install-mode: binary
|
install-mode: binary
|
||||||
verify: false
|
|
||||||
185
.github/workflows/linux.yml
vendored
185
.github/workflows/linux.yml
vendored
@@ -1,63 +1,13 @@
|
|||||||
name: Build Linux Packages
|
name: Release to Linux repository
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "Version name"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
calculate_version:
|
|
||||||
name: Calculate version
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
version: ${{ steps.outputs.outputs.version }}
|
|
||||||
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: Check input version
|
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
run: |-
|
|
||||||
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:
|
build:
|
||||||
name: Build binary
|
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
@@ -66,122 +16,23 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24
|
go-version: ^1.23
|
||||||
- name: Setup Android NDK
|
- name: Extract signing key
|
||||||
if: matrix.os == 'android'
|
|
||||||
uses: nttld/setup-ndk@v1
|
|
||||||
with:
|
|
||||||
ndk-version: r28
|
|
||||||
local-cache: true
|
|
||||||
- name: Set tag
|
|
||||||
run: |-
|
run: |-
|
||||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
mkdir -p $HOME/.gnupg
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
cat > $HOME/.gnupg/sagernet.key <<EOF
|
||||||
- name: Set build tags
|
${{ secrets.GPG_KEY }}
|
||||||
run: |
|
EOF
|
||||||
set -xeuo pipefail
|
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
||||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api'
|
- name: Publish release
|
||||||
if [ ! '${{ matrix.legacy_go }}' = 'true' ]; then
|
uses: goreleaser/goreleaser-action@v6
|
||||||
TAGS="${TAGS},with_ech"
|
with:
|
||||||
fi
|
distribution: goreleaser-pro
|
||||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
version: latest
|
||||||
- name: Build
|
args: release -f .goreleaser.fury.yaml --clean
|
||||||
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:
|
env:
|
||||||
CGO_ENABLED: "0"
|
|
||||||
GOOS: ${{ matrix.os }}
|
|
||||||
GOARCH: ${{ matrix.arch }}
|
|
||||||
GOARM: ${{ matrix.goarm }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Set mtime
|
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||||
run: |-
|
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
|
||||||
TZ=UTC touch -t '197001010000' dist/sing-box
|
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||||
- name: Set name
|
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
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
|
|
||||||
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:
|
|
||||||
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/
|
|
||||||
|
|||||||
@@ -22,16 +22,6 @@ linters-settings:
|
|||||||
|
|
||||||
run:
|
run:
|
||||||
go: "1.23"
|
go: "1.23"
|
||||||
build-tags:
|
|
||||||
- with_gvisor
|
|
||||||
- with_quic
|
|
||||||
- with_dhcp
|
|
||||||
- with_wireguard
|
|
||||||
- with_ech
|
|
||||||
- with_utls
|
|
||||||
- with_reality_server
|
|
||||||
- with_acme
|
|
||||||
- with_clash_api
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-dirs:
|
exclude-dirs:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ nfpms:
|
|||||||
contents:
|
contents:
|
||||||
- src: release/config/config.json
|
- src: release/config/config.json
|
||||||
dst: /etc/sing-box/config.json
|
dst: /etc/sing-box/config.json
|
||||||
type: "config|noreplace"
|
type: config
|
||||||
|
|
||||||
- src: release/config/sing-box.service
|
- src: release/config/sing-box.service
|
||||||
dst: /usr/lib/systemd/system/sing-box.service
|
dst: /usr/lib/systemd/system/sing-box.service
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ builds:
|
|||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GOROOT={{ .Env.GOPATH }}/go1.20.14
|
- GOROOT={{ .Env.GOPATH }}/go1.20.14
|
||||||
tool: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
|
gobinary: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
|
||||||
targets:
|
targets:
|
||||||
- windows_amd64_v1
|
- windows_amd64_v1
|
||||||
- windows_386
|
- windows_386
|
||||||
@@ -95,12 +95,10 @@ archives:
|
|||||||
builds:
|
builds:
|
||||||
- main
|
- main
|
||||||
- android
|
- android
|
||||||
formats:
|
format: tar.gz
|
||||||
- tar.gz
|
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
formats:
|
format: zip
|
||||||
- zip
|
|
||||||
wrap_in_directory: true
|
wrap_in_directory: true
|
||||||
files:
|
files:
|
||||||
- LICENSE
|
- LICENSE
|
||||||
@@ -130,7 +128,7 @@ nfpms:
|
|||||||
contents:
|
contents:
|
||||||
- src: release/config/config.json
|
- src: release/config/config.json
|
||||||
dst: /etc/sing-box/config.json
|
dst: /etc/sing-box/config.json
|
||||||
type: "config|noreplace"
|
type: config
|
||||||
|
|
||||||
- src: release/config/sing-box.service
|
- src: release/config/sing-box.service
|
||||||
dst: /usr/lib/systemd/system/sing-box.service
|
dst: /usr/lib/systemd/system/sing-box.service
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
|
FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS builder
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||||
COPY . /go/src/github.com/sagernet/sing-box
|
COPY . /go/src/github.com/sagernet/sing-box
|
||||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||||
|
|||||||
26
Makefile
26
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)
|
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
|
||||||
|
|
||||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
|
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
|
||||||
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
MAIN_PARAMS = $(PARAMS) -tags $(TAGS)
|
||||||
MAIN = ./cmd/sing-box
|
MAIN = ./cmd/sing-box
|
||||||
PREFIX ?= $(shell go env GOPATH)
|
PREFIX ?= $(shell go env GOPATH)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ ci_build:
|
|||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
generate_completions:
|
generate_completions:
|
||||||
go run -v --tags "$(TAGS),generate,generate_completions" $(MAIN)
|
go run -v --tags generate,generate_completions $(MAIN)
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
||||||
@@ -182,22 +182,10 @@ release_tvos: build_tvos upload_tvos_app_store
|
|||||||
update_apple_version:
|
update_apple_version:
|
||||||
go run ./cmd/internal/update_apple_version
|
go run ./cmd/internal/update_apple_version
|
||||||
|
|
||||||
update_macos_version:
|
|
||||||
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
|
|
||||||
|
|
||||||
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
|
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
|
||||||
|
|
||||||
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
||||||
|
|
||||||
publish_testflight:
|
|
||||||
go run -v ./cmd/internal/app_store_connect publish_testflight
|
|
||||||
|
|
||||||
prepare_app_store:
|
|
||||||
go run -v ./cmd/internal/app_store_connect prepare_app_store
|
|
||||||
|
|
||||||
publish_app_store:
|
|
||||||
go run -v ./cmd/internal/app_store_connect publish_app_store
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test -v ./... && \
|
@go test -v ./... && \
|
||||||
cd test && \
|
cd test && \
|
||||||
@@ -216,11 +204,11 @@ lib_android:
|
|||||||
lib_android_debug:
|
lib_android_debug:
|
||||||
go run ./cmd/internal/build_libbox -target android -debug
|
go run ./cmd/internal/build_libbox -target android -debug
|
||||||
|
|
||||||
lib_apple:
|
|
||||||
go run ./cmd/internal/build_libbox -target apple
|
|
||||||
|
|
||||||
lib_ios:
|
lib_ios:
|
||||||
go run ./cmd/internal/build_libbox -target apple -platform ios -debug
|
go run ./cmd/internal/build_libbox -target ios
|
||||||
|
|
||||||
|
lib_ios_debug:
|
||||||
|
go run ./cmd/internal/build_libbox -target ios -debug
|
||||||
|
|
||||||
lib:
|
lib:
|
||||||
go run ./cmd/internal/build_libbox -target android
|
go run ./cmd/internal/build_libbox -target android
|
||||||
@@ -247,4 +235,4 @@ clean:
|
|||||||
update:
|
update:
|
||||||
git fetch
|
git fetch
|
||||||
git reset FETCH_HEAD --hard
|
git reset FETCH_HEAD --hard
|
||||||
git clean -fdx
|
git clean -fdx
|
||||||
@@ -35,6 +35,7 @@ func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Ma
|
|||||||
|
|
||||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||||
m.access.Lock()
|
m.access.Lock()
|
||||||
|
defer m.access.Unlock()
|
||||||
if m.started && m.stage >= stage {
|
if m.started && m.stage >= stage {
|
||||||
panic("already started")
|
panic("already started")
|
||||||
}
|
}
|
||||||
@@ -42,12 +43,9 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
|||||||
m.stage = stage
|
m.stage = stage
|
||||||
if stage == adapter.StartStateStart {
|
if stage == adapter.StartStateStart {
|
||||||
// started with outbound manager
|
// started with outbound manager
|
||||||
m.access.Unlock()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
endpoints := m.endpoints
|
for _, endpoint := range m.endpoints {
|
||||||
m.access.Unlock()
|
|
||||||
for _, endpoint := range endpoints {
|
|
||||||
err := adapter.LegacyStart(endpoint, stage)
|
err := adapter.LegacyStart(endpoint, stage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||||
|
|||||||
@@ -33,27 +33,23 @@ type CacheFile interface {
|
|||||||
StoreRDRC() bool
|
StoreRDRC() bool
|
||||||
dns.RDRCStore
|
dns.RDRCStore
|
||||||
|
|
||||||
StoreWARPConfig() bool
|
|
||||||
|
|
||||||
LoadMode() string
|
LoadMode() string
|
||||||
StoreMode(mode string) error
|
StoreMode(mode string) error
|
||||||
LoadSelected(group string) string
|
LoadSelected(group string) string
|
||||||
StoreSelected(group string, selected string) error
|
StoreSelected(group string, selected string) error
|
||||||
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
||||||
StoreGroupExpand(group string, expand bool) error
|
StoreGroupExpand(group string, expand bool) error
|
||||||
LoadRuleSet(tag string) *SavedBinary
|
LoadRuleSet(tag string) *SavedRuleSet
|
||||||
SaveRuleSet(tag string, set *SavedBinary) error
|
SaveRuleSet(tag string, set *SavedRuleSet) error
|
||||||
LoadWARPConfig(tag string) *SavedBinary
|
|
||||||
SaveWARPConfig(tag string, set *SavedBinary) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedBinary struct {
|
type SavedRuleSet struct {
|
||||||
Content []byte
|
Content []byte
|
||||||
LastUpdated time.Time
|
LastUpdated time.Time
|
||||||
LastEtag string
|
LastEtag string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,7 +70,7 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
|||||||
return buffer.Bytes(), nil
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
|
||||||
reader := bytes.NewReader(data)
|
reader := bytes.NewReader(data)
|
||||||
var version uint8
|
var version uint8
|
||||||
err := binary.Read(reader, binary.BigEndian, &version)
|
err := binary.Read(reader, binary.BigEndian, &version)
|
||||||
|
|||||||
@@ -53,11 +53,10 @@ type InboundContext struct {
|
|||||||
|
|
||||||
// sniffer
|
// sniffer
|
||||||
|
|
||||||
Protocol string
|
Protocol string
|
||||||
Domain string
|
Domain string
|
||||||
Client string
|
Client string
|
||||||
SniffContext any
|
SniffContext any
|
||||||
PacketSniffError error
|
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|
||||||
@@ -73,7 +72,7 @@ type InboundContext struct {
|
|||||||
UDPConnect bool
|
UDPConnect bool
|
||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
|
|
||||||
NetworkStrategy *C.NetworkStrategy
|
NetworkStrategy C.NetworkStrategy
|
||||||
NetworkType []C.InterfaceType
|
NetworkType []C.InterfaceType
|
||||||
FallbackNetworkType []C.InterfaceType
|
FallbackNetworkType []C.InterfaceType
|
||||||
FallbackDelay time.Duration
|
FallbackDelay time.Duration
|
||||||
|
|||||||
@@ -25,11 +25,10 @@ type NetworkManager interface {
|
|||||||
PackageManager() tun.PackageManager
|
PackageManager() tun.PackageManager
|
||||||
WIFIState() WIFIState
|
WIFIState() WIFIState
|
||||||
ResetNetwork()
|
ResetNetwork()
|
||||||
UpdateWIFIState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type NetworkOptions struct {
|
type NetworkOptions struct {
|
||||||
NetworkStrategy *C.NetworkStrategy
|
NetworkStrategy C.NetworkStrategy
|
||||||
NetworkType []C.InterfaceType
|
NetworkType []C.InterfaceType
|
||||||
FallbackNetworkType []C.InterfaceType
|
FallbackNetworkType []C.InterfaceType
|
||||||
FallbackDelay time.Duration
|
FallbackDelay time.Duration
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ type Router interface {
|
|||||||
ClearDNSCache()
|
ClearDNSCache()
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
|
|
||||||
AppendTracker(tracker ConnectionTracker)
|
SetTracker(tracker ConnectionTracker)
|
||||||
|
|
||||||
ResetNetwork()
|
ResetNetwork()
|
||||||
}
|
}
|
||||||
|
|||||||
35
box.go
35
box.go
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/adapter/outbound"
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/experimental"
|
"github.com/sagernet/sing-box/experimental"
|
||||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||||
@@ -150,14 +149,6 @@ func New(options Options) (*Box, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize router")
|
return nil, E.Cause(err, "initialize router")
|
||||||
}
|
}
|
||||||
|
|
||||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
|
||||||
var timeService *tls.TimeServiceWrapper
|
|
||||||
if ntpOptions.Enabled {
|
|
||||||
timeService = new(tls.TimeServiceWrapper)
|
|
||||||
service.MustRegister[ntp.TimeService](ctx, timeService)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, endpointOptions := range options.Endpoints {
|
for i, endpointOptions := range options.Endpoints {
|
||||||
var tag string
|
var tag string
|
||||||
if endpointOptions.Tag != "" {
|
if endpointOptions.Tag != "" {
|
||||||
@@ -165,15 +156,7 @@ func New(options Options) (*Box, error) {
|
|||||||
} else {
|
} else {
|
||||||
tag = F.ToString(i)
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
endpointCtx := ctx
|
err = endpointManager.Create(ctx,
|
||||||
if tag != "" {
|
|
||||||
// TODO: remove this
|
|
||||||
endpointCtx = adapter.WithContext(endpointCtx, &adapter.InboundContext{
|
|
||||||
Outbound: tag,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
err = endpointManager.Create(
|
|
||||||
endpointCtx,
|
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
||||||
tag,
|
tag,
|
||||||
@@ -191,8 +174,7 @@ func New(options Options) (*Box, error) {
|
|||||||
} else {
|
} else {
|
||||||
tag = F.ToString(i)
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
err = inboundManager.Create(
|
err = inboundManager.Create(ctx,
|
||||||
ctx,
|
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
||||||
tag,
|
tag,
|
||||||
@@ -257,7 +239,7 @@ func New(options Options) (*Box, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create clash-server")
|
return nil, E.Cause(err, "create clash-server")
|
||||||
}
|
}
|
||||||
router.AppendTracker(clashServer)
|
router.SetTracker(clashServer)
|
||||||
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||||
services = append(services, clashServer)
|
services = append(services, clashServer)
|
||||||
}
|
}
|
||||||
@@ -267,17 +249,18 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "create v2ray-server")
|
return nil, E.Cause(err, "create v2ray-server")
|
||||||
}
|
}
|
||||||
if v2rayServer.StatsService() != nil {
|
if v2rayServer.StatsService() != nil {
|
||||||
router.AppendTracker(v2rayServer.StatsService())
|
router.SetTracker(v2rayServer.StatsService())
|
||||||
services = append(services, v2rayServer)
|
services = append(services, v2rayServer)
|
||||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||||
if ntpOptions.Enabled {
|
if ntpOptions.Enabled {
|
||||||
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions)
|
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create NTP service")
|
return nil, E.Cause(err, "create NTP service")
|
||||||
}
|
}
|
||||||
ntpService := ntp.NewService(ntp.Options{
|
timeService := ntp.NewService(ntp.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Dialer: ntpDialer,
|
Dialer: ntpDialer,
|
||||||
Logger: logFactory.NewLogger("ntp"),
|
Logger: logFactory.NewLogger("ntp"),
|
||||||
@@ -285,8 +268,8 @@ func New(options Options) (*Box, error) {
|
|||||||
Interval: time.Duration(ntpOptions.Interval),
|
Interval: time.Duration(ntpOptions.Interval),
|
||||||
WriteToSystem: ntpOptions.WriteToSystem,
|
WriteToSystem: ntpOptions.WriteToSystem,
|
||||||
})
|
})
|
||||||
timeService.TimeService = ntpService
|
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||||
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
services = append(services, adapter.NewLifecycleService(timeService, "ntp service"))
|
||||||
}
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
network: networkManager,
|
network: networkManager,
|
||||||
@@ -408,7 +391,7 @@ func (s *Box) Close() error {
|
|||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
err := common.Close(
|
err := common.Close(
|
||||||
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.network,
|
s.inbound, s.outbound, s.router, s.connection, s.network,
|
||||||
)
|
)
|
||||||
for _, lifecycleService := range s.services {
|
for _, lifecycleService := range s.services {
|
||||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||||
|
|||||||
Submodule clients/android updated: cec05bf693...cff12c57dd
Submodule clients/apple updated: ae5818ee5a...fa107e3b7c
@@ -1,450 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/asc-go/asc"
|
|
||||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
ctx := context.Background()
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "next_macos_project_version":
|
|
||||||
err := fetchMacOSVersion(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
case "publish_testflight":
|
|
||||||
err := publishTestflight(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
case "cancel_app_store":
|
|
||||||
err := cancelAppStore(ctx, os.Args[2])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
case "prepare_app_store":
|
|
||||||
err := prepareAppStore(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
case "publish_app_store":
|
|
||||||
err := publishAppStore(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Fatal("unknown action: ", os.Args[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
appID = "6673731168"
|
|
||||||
groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createClient(expireDuration time.Duration) *asc.Client {
|
|
||||||
privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), expireDuration, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
return asc.NewClient(tokenConfig.Client())
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchMacOSVersion(ctx context.Context) error {
|
|
||||||
client := createClient(time.Minute)
|
|
||||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
|
||||||
FilterPlatform: []string{"MAC_OS"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var versionID string
|
|
||||||
findVersion:
|
|
||||||
for _, version := range versions.Data {
|
|
||||||
switch *version.Attributes.AppStoreState {
|
|
||||||
case asc.AppStoreVersionStateReadyForSale,
|
|
||||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
|
||||||
versionID = version.ID
|
|
||||||
break findVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if versionID == "" {
|
|
||||||
return E.New("no version found")
|
|
||||||
}
|
|
||||||
latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "parse version code")
|
|
||||||
}
|
|
||||||
os.Stdout.WriteString(F.ToString(versionInt+1, "\n"))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func publishTestflight(ctx context.Context) error {
|
|
||||||
tagVersion, err := build_shared.ReadTagVersion()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tag := tagVersion.VersionString()
|
|
||||||
client := createClient(10 * time.Minute)
|
|
||||||
|
|
||||||
log.Info(tag, " list build IDs")
|
|
||||||
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
|
|
||||||
return it.ID
|
|
||||||
})
|
|
||||||
var platforms []asc.Platform
|
|
||||||
if len(os.Args) == 3 {
|
|
||||||
switch os.Args[2] {
|
|
||||||
case "ios":
|
|
||||||
platforms = []asc.Platform{asc.PlatformIOS}
|
|
||||||
case "macos":
|
|
||||||
platforms = []asc.Platform{asc.PlatformMACOS}
|
|
||||||
case "tvos":
|
|
||||||
platforms = []asc.Platform{asc.PlatformTVOS}
|
|
||||||
default:
|
|
||||||
return E.New("unknown platform: ", os.Args[2])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
platforms = []asc.Platform{
|
|
||||||
asc.PlatformIOS,
|
|
||||||
asc.PlatformMACOS,
|
|
||||||
asc.PlatformTVOS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, platform := range platforms {
|
|
||||||
log.Info(string(platform), " list builds")
|
|
||||||
for {
|
|
||||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
|
||||||
FilterApp: []string{appID},
|
|
||||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
build := builds.Data[0]
|
|
||||||
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 5*time.Minute {
|
|
||||||
log.Info(string(platform), " ", tag, " waiting for process")
|
|
||||||
time.Sleep(15 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if *build.Attributes.ProcessingState != "VALID" {
|
|
||||||
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
|
||||||
time.Sleep(15 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " list localizations")
|
|
||||||
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
|
|
||||||
return *it.Attributes.Locale == "en-US"
|
|
||||||
})
|
|
||||||
if localization.ID == "" {
|
|
||||||
log.Fatal(string(platform), " ", tag, " no en-US localization found")
|
|
||||||
}
|
|
||||||
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
|
||||||
log.Info(string(platform), " ", tag, " update localization")
|
|
||||||
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(
|
|
||||||
F.ToString("sing-box ", tagVersion.String()),
|
|
||||||
))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " publish")
|
|
||||||
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
|
|
||||||
if response != nil && response.StatusCode == http.StatusUnprocessableEntity {
|
|
||||||
log.Info("waiting for process")
|
|
||||||
time.Sleep(15 * time.Second)
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " list submissions")
|
|
||||||
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
|
|
||||||
FilterBuild: []string{build.ID},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(betaSubmissions.Data) == 0 {
|
|
||||||
log.Info(string(platform), " ", tag, " create submission")
|
|
||||||
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
|
|
||||||
log.Error(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancelAppStore(ctx context.Context, platform string) error {
|
|
||||||
switch platform {
|
|
||||||
case "ios":
|
|
||||||
platform = string(asc.PlatformIOS)
|
|
||||||
case "macos":
|
|
||||||
platform = string(asc.PlatformMACOS)
|
|
||||||
case "tvos":
|
|
||||||
platform = string(asc.PlatformTVOS)
|
|
||||||
}
|
|
||||||
tag, err := build_shared.ReadTag()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
client := createClient(time.Minute)
|
|
||||||
for {
|
|
||||||
log.Info(platform, " list versions")
|
|
||||||
versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
|
||||||
FilterPlatform: []string{string(platform)},
|
|
||||||
})
|
|
||||||
if isRetryable(response) {
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
|
||||||
return *it.Attributes.VersionString == tag
|
|
||||||
})
|
|
||||||
if version.ID == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
log.Info(platform, " ", tag, " get submission")
|
|
||||||
submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
|
|
||||||
if response != nil && response.StatusCode == http.StatusNotFound {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if isRetryable(response) {
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info(platform, " ", tag, " delete submission")
|
|
||||||
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareAppStore(ctx context.Context) error {
|
|
||||||
tag, err := build_shared.ReadTag()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
client := createClient(time.Minute)
|
|
||||||
for _, platform := range []asc.Platform{
|
|
||||||
asc.PlatformIOS,
|
|
||||||
asc.PlatformMACOS,
|
|
||||||
asc.PlatformTVOS,
|
|
||||||
} {
|
|
||||||
log.Info(string(platform), " list versions")
|
|
||||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
|
||||||
FilterPlatform: []string{string(platform)},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
|
||||||
return *it.Attributes.VersionString == tag
|
|
||||||
})
|
|
||||||
log.Info(string(platform), " ", tag, " list builds")
|
|
||||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
|
||||||
FilterApp: []string{appID},
|
|
||||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(builds.Data) == 0 {
|
|
||||||
log.Fatal(platform, " ", tag, " no build found")
|
|
||||||
}
|
|
||||||
buildID := common.Ptr(builds.Data[0].ID)
|
|
||||||
if version.ID == "" {
|
|
||||||
log.Info(string(platform), " ", tag, " create version")
|
|
||||||
newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
|
|
||||||
Platform: platform,
|
|
||||||
VersionString: tag,
|
|
||||||
}, appID, buildID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version = newVersion.Data
|
|
||||||
|
|
||||||
} else {
|
|
||||||
log.Info(string(platform), " ", tag, " check build")
|
|
||||||
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
|
|
||||||
switch *version.Attributes.AppStoreState {
|
|
||||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
|
||||||
asc.AppStoreVersionStateRejected,
|
|
||||||
asc.AppStoreVersionStateDeveloperRejected:
|
|
||||||
case asc.AppStoreVersionStateWaitingForReview,
|
|
||||||
asc.AppStoreVersionStateInReview,
|
|
||||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
|
||||||
submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if submission != nil {
|
|
||||||
log.Info(string(platform), " ", tag, " delete submission")
|
|
||||||
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " update build")
|
|
||||||
response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if response.StatusCode != http.StatusNoContent {
|
|
||||||
response.Write(os.Stderr)
|
|
||||||
log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch *version.Attributes.AppStoreState {
|
|
||||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
|
||||||
asc.AppStoreVersionStateRejected,
|
|
||||||
asc.AppStoreVersionStateDeveloperRejected:
|
|
||||||
case asc.AppStoreVersionStateWaitingForReview,
|
|
||||||
asc.AppStoreVersionStateInReview,
|
|
||||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " list localization")
|
|
||||||
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
|
|
||||||
return *it.Attributes.Locale == "en-US"
|
|
||||||
})
|
|
||||||
if localization.ID == "" {
|
|
||||||
log.Info(string(platform), " ", tag, " no en-US localization found")
|
|
||||||
}
|
|
||||||
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
|
||||||
log.Info(string(platform), " ", tag, " update localization")
|
|
||||||
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
|
|
||||||
PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
|
|
||||||
WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " create submission")
|
|
||||||
fixSubmit:
|
|
||||||
for {
|
|
||||||
_, response, err := client.Submission.CreateSubmission(ctx, version.ID)
|
|
||||||
if err != nil {
|
|
||||||
switch response.StatusCode {
|
|
||||||
case http.StatusInternalServerError:
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch response.StatusCode {
|
|
||||||
case http.StatusCreated:
|
|
||||||
break fixSubmit
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func publishAppStore(ctx context.Context) error {
|
|
||||||
tag, err := build_shared.ReadTag()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
client := createClient(time.Minute)
|
|
||||||
for _, platform := range []asc.Platform{
|
|
||||||
asc.PlatformIOS,
|
|
||||||
asc.PlatformMACOS,
|
|
||||||
asc.PlatformTVOS,
|
|
||||||
} {
|
|
||||||
log.Info(string(platform), " list versions")
|
|
||||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
|
||||||
FilterPlatform: []string{string(platform)},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
|
||||||
return *it.Attributes.VersionString == tag
|
|
||||||
})
|
|
||||||
switch *version.Attributes.AppStoreState {
|
|
||||||
case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
|
|
||||||
log.Fatal(string(platform), " ", tag, " not submitted")
|
|
||||||
case asc.AppStoreVersionStateWaitingForReview,
|
|
||||||
asc.AppStoreVersionStateInReview:
|
|
||||||
log.Warn(string(platform), " ", tag, " waiting for review")
|
|
||||||
continue
|
|
||||||
case asc.AppStoreVersionStatePendingDeveloperRelease:
|
|
||||||
default:
|
|
||||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
|
||||||
}
|
|
||||||
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isRetryable(response *asc.Response) bool {
|
|
||||||
if response == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch response.StatusCode {
|
|
||||||
case http.StatusInternalServerError, http.StatusUnprocessableEntity:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,13 +18,11 @@ import (
|
|||||||
var (
|
var (
|
||||||
debugEnabled bool
|
debugEnabled bool
|
||||||
target string
|
target string
|
||||||
platform string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
||||||
flag.StringVar(&target, "target", "android", "target platform")
|
flag.StringVar(&target, "target", "android", "target platform")
|
||||||
flag.StringVar(&platform, "platform", "", "specify platform")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -35,8 +33,8 @@ func main() {
|
|||||||
switch target {
|
switch target {
|
||||||
case "android":
|
case "android":
|
||||||
buildAndroid()
|
buildAndroid()
|
||||||
case "apple":
|
case "ios":
|
||||||
buildApple()
|
buildiOS()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,9 +81,7 @@ func buildAndroid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var bindTarget string
|
var bindTarget string
|
||||||
if platform != "" {
|
if debugEnabled {
|
||||||
bindTarget = platform
|
|
||||||
} else if debugEnabled {
|
|
||||||
bindTarget = "android/arm64"
|
bindTarget = "android/arm64"
|
||||||
} else {
|
} else {
|
||||||
bindTarget = "android"
|
bindTarget = "android"
|
||||||
@@ -133,14 +129,12 @@ func buildAndroid() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildApple() {
|
func buildiOS() {
|
||||||
var bindTarget string
|
var bindTarget string
|
||||||
if platform != "" {
|
if debugEnabled {
|
||||||
bindTarget = platform
|
|
||||||
} else if debugEnabled {
|
|
||||||
bindTarget = "ios"
|
bindTarget = "ios"
|
||||||
} else {
|
} else {
|
||||||
bindTarget = "ios,tvos,macos"
|
bindTarget = "ios,iossimulator,tvos,tvossimulator,macos"
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func FindSDK() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findNDK() bool {
|
func findNDK() bool {
|
||||||
const fixedVersion = "28.0.13004108"
|
const fixedVersion = "28.0.12674087"
|
||||||
const versionFile = "source.properties"
|
const versionFile = "source.properties"
|
||||||
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
|
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
|
||||||
androidNDKPath = fixedPath
|
androidNDKPath = fixedPath
|
||||||
|
|||||||
@@ -36,3 +36,11 @@ func ReadTagVersion() (badversion.Version, error) {
|
|||||||
}
|
}
|
||||||
return version, nil
|
return version, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsDevBranch() bool {
|
||||||
|
branch, err := shell.Exec("git", "branch", "--show-current").ReadOutput()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return branch == "dev-next"
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,54 +5,57 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||||
"github.com/sagernet/sing-box/common/badversion"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var nightly bool
|
||||||
flagRunInCI bool
|
|
||||||
flagRunNightly bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
flag.BoolVar(&nightly, "nightly", false, "Print nightly tag")
|
||||||
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
var (
|
if nightly {
|
||||||
versionStr string
|
version, err := build_shared.ReadTagVersionRev()
|
||||||
err error
|
|
||||||
)
|
|
||||||
if flagRunNightly {
|
|
||||||
var version badversion.Version
|
|
||||||
version, err = build_shared.ReadTagVersion()
|
|
||||||
if err == nil {
|
|
||||||
versionStr = version.String()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
versionStr, err = build_shared.ReadTag()
|
|
||||||
}
|
|
||||||
if flagRunInCI {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
err = setGitHubEnv("version", versionStr)
|
var (
|
||||||
|
versionStr string
|
||||||
|
isPrerelease bool
|
||||||
|
)
|
||||||
|
if version.PreReleaseIdentifier != "" {
|
||||||
|
isPrerelease = true
|
||||||
|
versionStr = version.VersionString() + "-nightly"
|
||||||
|
} else {
|
||||||
|
version.Patch++
|
||||||
|
versionStr = version.VersionString() + "-nightly"
|
||||||
|
}
|
||||||
|
if build_shared.IsDevBranch() {
|
||||||
|
isPrerelease = true
|
||||||
|
}
|
||||||
|
err = setGitHubOutput("version", versionStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = setGitHubOutput("prerelease", F.ToString(isPrerelease))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
tag, err := build_shared.ReadTag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
os.Stdout.WriteString("unknown\n")
|
os.Stdout.WriteString("unknown\n")
|
||||||
} else {
|
} else {
|
||||||
os.Stdout.WriteString(versionStr + "\n")
|
os.Stdout.WriteString(tag + "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setGitHubEnv(name string, value string) error {
|
func setGitHubOutput(name string, value string) error {
|
||||||
outputFile, err := os.OpenFile(os.Getenv("GITHUB_ENV"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
outputFile, err := os.OpenFile(os.Getenv("GITHUB_ENV"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -13,14 +13,10 @@ import (
|
|||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var flagRunInCI bool
|
||||||
flagRunInCI bool
|
|
||||||
flagRunNightly bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
||||||
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -50,23 +46,21 @@ func main() {
|
|||||||
switch propPair[0] {
|
switch propPair[0] {
|
||||||
case "VERSION_NAME":
|
case "VERSION_NAME":
|
||||||
if propPair[1] != newVersion {
|
if propPair[1] != newVersion {
|
||||||
log.Info("updated version from ", propPair[1], " to ", newVersion)
|
|
||||||
versionUpdated = true
|
versionUpdated = true
|
||||||
propPair[1] = newVersion
|
propPair[1] = newVersion
|
||||||
|
log.Info("updated version to ", newVersion)
|
||||||
}
|
}
|
||||||
case "GO_VERSION":
|
case "GO_VERSION":
|
||||||
if propPair[1] != runtime.Version() {
|
if propPair[1] != runtime.Version() {
|
||||||
log.Info("updated Go version from ", propPair[1], " to ", runtime.Version())
|
|
||||||
goVersionUpdated = true
|
goVersionUpdated = true
|
||||||
propPair[1] = runtime.Version()
|
propPair[1] = runtime.Version()
|
||||||
|
log.Info("updated Go version to ", runtime.Version())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !(versionUpdated || goVersionUpdated) {
|
if !(versionUpdated || goVersionUpdated) {
|
||||||
log.Info("version not changed")
|
log.Info("version not changed")
|
||||||
return
|
return
|
||||||
} else if flagRunInCI && !flagRunNightly {
|
|
||||||
log.Fatal("version changed, commit changes first.")
|
|
||||||
}
|
}
|
||||||
for _, propPair := range propsList {
|
for _, propPair := range propsList {
|
||||||
switch propPair[0] {
|
switch propPair[0] {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateTLSKeyPair(serverName string) error {
|
func generateTLSKeyPair(serverName string) error {
|
||||||
privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
privateKeyPem, publicKeyPem, err := tls.GenerateKeyPair(time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var commandMerge = &cobra.Command{
|
var commandMerge = &cobra.Command{
|
||||||
Use: "merge <output-path>",
|
Use: "merge <output>",
|
||||||
Short: "Merge configurations",
|
Short: "Merge configurations",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
err := merge(args[0])
|
err := merge(args[0])
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/srs"
|
"github.com/sagernet/sing-box/common/srs"
|
||||||
@@ -57,14 +56,6 @@ func ruleSetMatch(sourcePath string, domain string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "read rule-set")
|
return E.Cause(err, "read rule-set")
|
||||||
}
|
}
|
||||||
if flagRuleSetMatchFormat == "" {
|
|
||||||
switch filepath.Ext(sourcePath) {
|
|
||||||
case ".json":
|
|
||||||
flagRuleSetMatchFormat = C.RuleSetFormatSource
|
|
||||||
case ".srs":
|
|
||||||
flagRuleSetMatchFormat = C.RuleSetFormatBinary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var ruleSet option.PlainRuleSetCompat
|
var ruleSet option.PlainRuleSetCompat
|
||||||
switch flagRuleSetMatchFormat {
|
switch flagRuleSetMatchFormat {
|
||||||
case C.RuleSetFormatSource:
|
case C.RuleSetFormatSource:
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/json"
|
|
||||||
"github.com/sagernet/sing/common/json/badjson"
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ruleSetPaths []string
|
|
||||||
ruleSetDirectories []string
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandRuleSetMerge = &cobra.Command{
|
|
||||||
Use: "merge <output-path>",
|
|
||||||
Short: "Merge rule-set source files",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := mergeRuleSet(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetPaths, "config", "c", nil, "set input rule-set file path")
|
|
||||||
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetDirectories, "config-directory", "C", nil, "set input rule-set directory path")
|
|
||||||
commandRuleSet.AddCommand(commandRuleSetMerge)
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleSetEntry struct {
|
|
||||||
content []byte
|
|
||||||
path string
|
|
||||||
options option.PlainRuleSetCompat
|
|
||||||
}
|
|
||||||
|
|
||||||
func readRuleSetAt(path string) (*RuleSetEntry, error) {
|
|
||||||
var (
|
|
||||||
configContent []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if path == "stdin" {
|
|
||||||
configContent, err = io.ReadAll(os.Stdin)
|
|
||||||
} else {
|
|
||||||
configContent, err = os.ReadFile(path)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read config at ", path)
|
|
||||||
}
|
|
||||||
options, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, configContent)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "decode config at ", path)
|
|
||||||
}
|
|
||||||
return &RuleSetEntry{
|
|
||||||
content: configContent,
|
|
||||||
path: path,
|
|
||||||
options: options,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readRuleSet() ([]*RuleSetEntry, error) {
|
|
||||||
var optionsList []*RuleSetEntry
|
|
||||||
for _, path := range ruleSetPaths {
|
|
||||||
optionsEntry, err := readRuleSetAt(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
optionsList = append(optionsList, optionsEntry)
|
|
||||||
}
|
|
||||||
for _, directory := range ruleSetDirectories {
|
|
||||||
entries, err := os.ReadDir(directory)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read rule-set directory at ", directory)
|
|
||||||
}
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
optionsEntry, err := readRuleSetAt(filepath.Join(directory, entry.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
optionsList = append(optionsList, optionsEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Slice(optionsList, func(i, j int) bool {
|
|
||||||
return optionsList[i].path < optionsList[j].path
|
|
||||||
})
|
|
||||||
return optionsList, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readRuleSetAndMerge() (option.PlainRuleSetCompat, error) {
|
|
||||||
optionsList, err := readRuleSet()
|
|
||||||
if err != nil {
|
|
||||||
return option.PlainRuleSetCompat{}, err
|
|
||||||
}
|
|
||||||
if len(optionsList) == 1 {
|
|
||||||
return optionsList[0].options, nil
|
|
||||||
}
|
|
||||||
var optionVersion uint8
|
|
||||||
for _, options := range optionsList {
|
|
||||||
if optionVersion < options.options.Version {
|
|
||||||
optionVersion = options.options.Version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var mergedMessage json.RawMessage
|
|
||||||
for _, options := range optionsList {
|
|
||||||
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
|
|
||||||
if err != nil {
|
|
||||||
return option.PlainRuleSetCompat{}, E.Cause(err, "merge config at ", options.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mergedOptions, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, mergedMessage)
|
|
||||||
if err != nil {
|
|
||||||
return option.PlainRuleSetCompat{}, E.Cause(err, "unmarshal merged config")
|
|
||||||
}
|
|
||||||
mergedOptions.Version = optionVersion
|
|
||||||
return mergedOptions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeRuleSet(outputPath string) error {
|
|
||||||
mergedOptions, err := readRuleSetAndMerge()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
buffer := new(bytes.Buffer)
|
|
||||||
encoder := json.NewEncoder(buffer)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
err = encoder.Encode(mergedOptions)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "encode config")
|
|
||||||
}
|
|
||||||
if existsContent, err := os.ReadFile(outputPath); err != nil {
|
|
||||||
if string(existsContent) == buffer.String() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = rw.MkdirParent(outputPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
outputPath, _ = filepath.Abs(outputPath)
|
|
||||||
os.Stderr.WriteString(outputPath + "\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -61,15 +61,14 @@ func upgradeRuleSet(sourcePath string) error {
|
|||||||
log.Info("already up-to-date")
|
log.Info("already up-to-date")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
plainRuleSetCompat.Options, err = plainRuleSetCompat.Upgrade()
|
plainRuleSet, err := plainRuleSetCompat.Upgrade()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
plainRuleSetCompat.Version = C.RuleSetVersionCurrent
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
encoder := json.NewEncoder(buffer)
|
encoder := json.NewEncoder(buffer)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
err = encoder.Encode(plainRuleSetCompat)
|
err = encoder.Encode(plainRuleSet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "encode config")
|
return E.Cause(err, "encode config")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func initializeHTTP3Client(instance *box.Box) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
http3Client = &http.Client{
|
http3Client = &http.Client{
|
||||||
Transport: &http3.Transport{
|
Transport: &http3.RoundTripper{
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
destination := M.ParseSocksaddr(addr)
|
destination := M.ParseSocksaddr(addr)
|
||||||
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/settings"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -57,7 +58,7 @@ func syncTime() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if commandSyncTimeWrite {
|
if commandSyncTimeWrite {
|
||||||
err = ntp.SetSystemTime(response.Time)
|
err = settings.SetSystemTime(response.Time)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "write time to system")
|
return E.Cause(err, "write time to system")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
package cloudflare
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CloudflareApi struct {
|
|
||||||
client http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCloudflareApi(opts ...CloudflareApiOption) *CloudflareApi {
|
|
||||||
api := &CloudflareApi{http.Client{Timeout: 30 * time.Second}}
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(api)
|
|
||||||
}
|
|
||||||
return api
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *CloudflareApi) CreateProfile(ctx context.Context, publicKey string) (*CloudflareProfile, error) {
|
|
||||||
request, err := http.NewRequest("POST", "https://api.cloudflareclient.com/v0i1909051800/reg", strings.NewReader(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"{\"install_id\":\"\",\"tos\":\"%s\",\"key\":\"%s\",\"fcm_token\":\"\",\"type\":\"ios\",\"locale\":\"en_US\"}",
|
|
||||||
time.Now().Format("2006-01-02T15:04:05.000Z"),
|
|
||||||
publicKey,
|
|
||||||
),
|
|
||||||
))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
response, err := api.client.Do(request.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
if response.StatusCode != 200 {
|
|
||||||
return nil, fmt.Errorf("status code is not 200")
|
|
||||||
}
|
|
||||||
content, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
profile := new(CloudflareProfile)
|
|
||||||
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *CloudflareApi) GetProfile(ctx context.Context, authToken string, id string) (*CloudflareProfile, error) {
|
|
||||||
request, err := http.NewRequest("GET", "https://api.cloudflareclient.com/v0i1909051800/reg/"+id, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
request.Header.Set("Authorization", "Bearer "+authToken)
|
|
||||||
response, err := api.client.Do(request.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
if response.StatusCode != 200 {
|
|
||||||
return nil, fmt.Errorf("status code is not 200")
|
|
||||||
}
|
|
||||||
content, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
profile := new(CloudflareProfile)
|
|
||||||
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package cloudflare
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CloudflareApiOption func(api *CloudflareApi)
|
|
||||||
|
|
||||||
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) CloudflareApiOption {
|
|
||||||
return func(api *CloudflareApi) {
|
|
||||||
api.client.Transport = &http.Transport{
|
|
||||||
DialContext: dialContext,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package cloudflare
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type CloudflareProfile struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
Account struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
AccountType string `json:"account_type"`
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
Updated time.Time `json:"updated"`
|
|
||||||
PremiumData int `json:"premium_data"`
|
|
||||||
Quota int `json:"quota"`
|
|
||||||
Usage int `json:"usage"`
|
|
||||||
WARPPlus bool `json:"warp_plus"`
|
|
||||||
ReferralCount int `json:"referral_count"`
|
|
||||||
ReferralRenewalCountdown int `json:"referral_renewal_countdown"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
License string `json:"license"`
|
|
||||||
TTL time.Time `json:"ttl"`
|
|
||||||
} `json:"account"`
|
|
||||||
Config struct {
|
|
||||||
ClientID string `json:"client_id"`
|
|
||||||
Interface struct {
|
|
||||||
Addresses struct {
|
|
||||||
V4 string `json:"v4"`
|
|
||||||
V6 string `json:"v6"`
|
|
||||||
} `json:"addresses"`
|
|
||||||
} `json:"interface"`
|
|
||||||
Peers []struct {
|
|
||||||
PublicKey string `json:"public_key"`
|
|
||||||
Endpoint struct {
|
|
||||||
V4 string `json:"v4"`
|
|
||||||
V6 string `json:"v6"`
|
|
||||||
Host string `json:"host"`
|
|
||||||
Ports []int `json:"ports"`
|
|
||||||
} `json:"endpoint"`
|
|
||||||
} `json:"peers"`
|
|
||||||
Services struct {
|
|
||||||
HTTPProxy string `json:"http_proxy"`
|
|
||||||
} `json:"services"`
|
|
||||||
Metrics struct {
|
|
||||||
Ping int `json:"ping"`
|
|
||||||
Report int `json:"report"`
|
|
||||||
} `json:"metrics"`
|
|
||||||
} `json:"config"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
WARPEnabled bool `json:"warp_enabled"`
|
|
||||||
WaitlistEnabled bool `json:"waitlist_enabled"`
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
Updated time.Time `json:"updated"`
|
|
||||||
Tos time.Time `json:"tos"`
|
|
||||||
Place int `json:"place"`
|
|
||||||
Locale string `json:"locale"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
InstallID string `json:"install_id"`
|
|
||||||
FcmToken string `json:"fcm_token"`
|
|
||||||
Policy struct {
|
|
||||||
TunnelProtocol string `json:"tunnel_protocol"`
|
|
||||||
} `json:"policy"`
|
|
||||||
}
|
|
||||||
@@ -2,16 +2,13 @@ package dialer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/conntrack"
|
"github.com/sagernet/sing-box/common/conntrack"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/atomic"
|
"github.com/sagernet/sing/common/atomic"
|
||||||
@@ -19,7 +16,6 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -28,36 +24,31 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DefaultDialer struct {
|
type DefaultDialer struct {
|
||||||
dialer4 tcpDialer
|
dialer4 tcpDialer
|
||||||
dialer6 tcpDialer
|
dialer6 tcpDialer
|
||||||
udpDialer4 net.Dialer
|
udpDialer4 net.Dialer
|
||||||
udpDialer6 net.Dialer
|
udpDialer6 net.Dialer
|
||||||
udpListener net.ListenConfig
|
udpListener net.ListenConfig
|
||||||
udpAddr4 string
|
udpAddr4 string
|
||||||
udpAddr6 string
|
udpAddr6 string
|
||||||
isWireGuardListener bool
|
isWireGuardListener bool
|
||||||
networkManager adapter.NetworkManager
|
networkManager adapter.NetworkManager
|
||||||
networkStrategy *C.NetworkStrategy
|
networkStrategy C.NetworkStrategy
|
||||||
defaultNetworkStrategy bool
|
networkType []C.InterfaceType
|
||||||
networkType []C.InterfaceType
|
fallbackNetworkType []C.InterfaceType
|
||||||
fallbackNetworkType []C.InterfaceType
|
networkFallbackDelay time.Duration
|
||||||
networkFallbackDelay time.Duration
|
networkLastFallback atomic.TypedValue[time.Time]
|
||||||
networkLastFallback atomic.TypedValue[time.Time]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
func NewDefault(networkManager adapter.NetworkManager, options option.DialerOptions) (*DefaultDialer, error) {
|
||||||
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
|
||||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dialer net.Dialer
|
dialer net.Dialer
|
||||||
listener net.ListenConfig
|
listener net.ListenConfig
|
||||||
interfaceFinder control.InterfaceFinder
|
interfaceFinder control.InterfaceFinder
|
||||||
networkStrategy *C.NetworkStrategy
|
networkStrategy C.NetworkStrategy
|
||||||
defaultNetworkStrategy bool
|
networkType []C.InterfaceType
|
||||||
networkType []C.InterfaceType
|
fallbackNetworkType []C.InterfaceType
|
||||||
fallbackNetworkType []C.InterfaceType
|
networkFallbackDelay time.Duration
|
||||||
networkFallbackDelay time.Duration
|
|
||||||
)
|
)
|
||||||
if networkManager != nil {
|
if networkManager != nil {
|
||||||
interfaceFinder = networkManager.InterfaceFinder()
|
interfaceFinder = networkManager.InterfaceFinder()
|
||||||
@@ -83,38 +74,31 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
|
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
|
if C.NetworkStrategy(options.NetworkStrategy) != C.NetworkStrategyDefault {
|
||||||
if disableDefaultBind || options.TCPFastOpen {
|
if options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil {
|
||||||
if options.NetworkStrategy != nil || len(options.NetworkType) > 0 && options.FallbackNetworkType == nil && options.FallbackDelay == 0 {
|
return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address` and `inet6_bind_address`")
|
||||||
return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address`, `inet6_bind_address` and `tcp_fast_open`")
|
}
|
||||||
|
networkStrategy = C.NetworkStrategy(options.NetworkStrategy)
|
||||||
|
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
|
||||||
|
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
|
||||||
|
networkFallbackDelay = time.Duration(options.NetworkFallbackDelay)
|
||||||
|
if networkManager == nil || !networkManager.AutoDetectInterface() {
|
||||||
|
return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if networkManager != nil && options.BindInterface == "" && options.Inet4BindAddress == nil && options.Inet6BindAddress == nil {
|
||||||
if networkManager != nil {
|
|
||||||
defaultOptions := networkManager.DefaultOptions()
|
defaultOptions := networkManager.DefaultOptions()
|
||||||
if !disableDefaultBind {
|
if options.BindInterface == "" {
|
||||||
if defaultOptions.BindInterface != "" {
|
if defaultOptions.BindInterface != "" {
|
||||||
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
} else if networkManager.AutoDetectInterface() {
|
} else if networkManager.AutoDetectInterface() {
|
||||||
if platformInterface != nil {
|
if defaultOptions.NetworkStrategy != C.NetworkStrategyDefault && C.NetworkStrategy(options.NetworkStrategy) == C.NetworkStrategyDefault {
|
||||||
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
|
networkStrategy = defaultOptions.NetworkStrategy
|
||||||
if networkStrategy == nil {
|
networkType = defaultOptions.NetworkType
|
||||||
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
|
fallbackNetworkType = defaultOptions.FallbackNetworkType
|
||||||
defaultNetworkStrategy = true
|
networkFallbackDelay = defaultOptions.FallbackDelay
|
||||||
}
|
|
||||||
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
|
|
||||||
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
|
|
||||||
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
|
|
||||||
networkStrategy = defaultOptions.NetworkStrategy
|
|
||||||
networkType = defaultOptions.NetworkType
|
|
||||||
fallbackNetworkType = defaultOptions.FallbackNetworkType
|
|
||||||
}
|
|
||||||
networkFallbackDelay = time.Duration(options.FallbackDelay)
|
|
||||||
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
|
|
||||||
networkFallbackDelay = defaultOptions.FallbackDelay
|
|
||||||
}
|
|
||||||
bindFunc := networkManager.ProtectFunc()
|
bindFunc := networkManager.ProtectFunc()
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
@@ -188,6 +172,9 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
listener.Control = control.Append(listener.Control, controlFn)
|
listener.Control = control.Append(listener.Control, controlFn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if networkStrategy != C.NetworkStrategyDefault && options.TCPFastOpen {
|
||||||
|
return nil, E.New("`tcp_fast_open` is conflict with `network_strategy` or `route.default_network_strategy`")
|
||||||
|
}
|
||||||
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -197,20 +184,19 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &DefaultDialer{
|
return &DefaultDialer{
|
||||||
dialer4: tcpDialer4,
|
dialer4: tcpDialer4,
|
||||||
dialer6: tcpDialer6,
|
dialer6: tcpDialer6,
|
||||||
udpDialer4: udpDialer4,
|
udpDialer4: udpDialer4,
|
||||||
udpDialer6: udpDialer6,
|
udpDialer6: udpDialer6,
|
||||||
udpListener: listener,
|
udpListener: listener,
|
||||||
udpAddr4: udpAddr4,
|
udpAddr4: udpAddr4,
|
||||||
udpAddr6: udpAddr6,
|
udpAddr6: udpAddr6,
|
||||||
isWireGuardListener: options.IsWireGuardListener,
|
isWireGuardListener: options.IsWireGuardListener,
|
||||||
networkManager: networkManager,
|
networkManager: networkManager,
|
||||||
networkStrategy: networkStrategy,
|
networkStrategy: networkStrategy,
|
||||||
defaultNetworkStrategy: defaultNetworkStrategy,
|
networkType: networkType,
|
||||||
networkType: networkType,
|
fallbackNetworkType: fallbackNetworkType,
|
||||||
fallbackNetworkType: fallbackNetworkType,
|
networkFallbackDelay: networkFallbackDelay,
|
||||||
networkFallbackDelay: networkFallbackDelay,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +204,7 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
|
|||||||
if !address.IsValid() {
|
if !address.IsValid() {
|
||||||
return nil, E.New("invalid address")
|
return nil, E.New("invalid address")
|
||||||
}
|
}
|
||||||
if d.networkStrategy == nil {
|
if d.networkStrategy == C.NetworkStrategyDefault {
|
||||||
switch N.NetworkName(network) {
|
switch N.NetworkName(network) {
|
||||||
case N.NetworkUDP:
|
case N.NetworkUDP:
|
||||||
if !address.IsIPv6() {
|
if !address.IsIPv6() {
|
||||||
@@ -237,21 +223,12 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||||
if strategy == nil {
|
if strategy == C.NetworkStrategyDefault {
|
||||||
strategy = d.networkStrategy
|
|
||||||
}
|
|
||||||
if strategy == nil {
|
|
||||||
return d.DialContext(ctx, network, address)
|
return d.DialContext(ctx, network, address)
|
||||||
}
|
}
|
||||||
if len(interfaceType) == 0 {
|
if !d.networkManager.AutoDetectInterface() {
|
||||||
interfaceType = d.networkType
|
return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`")
|
||||||
}
|
|
||||||
if len(fallbackInterfaceType) == 0 {
|
|
||||||
fallbackInterfaceType = d.fallbackNetworkType
|
|
||||||
}
|
|
||||||
if fallbackDelay == 0 {
|
|
||||||
fallbackDelay = d.networkFallbackDelay
|
|
||||||
}
|
}
|
||||||
var dialer net.Dialer
|
var dialer net.Dialer
|
||||||
if N.NetworkName(network) == N.NetworkTCP {
|
if N.NetworkName(network) == N.NetworkTCP {
|
||||||
@@ -266,18 +243,12 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if !fastFallback {
|
if !fastFallback {
|
||||||
conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
} else {
|
} else {
|
||||||
conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store)
|
conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// bind interface failed on legacy xiaomi systems
|
return nil, err
|
||||||
if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) {
|
|
||||||
d.networkStrategy = nil
|
|
||||||
return d.DialContext(ctx, network, address)
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !fastFallback && !isPrimary {
|
if !fastFallback && !isPrimary {
|
||||||
d.networkLastFallback.Store(time.Now())
|
d.networkLastFallback.Store(time.Now())
|
||||||
@@ -286,7 +257,7 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
if d.networkStrategy == nil {
|
if d.networkStrategy == C.NetworkStrategyDefault {
|
||||||
if destination.IsIPv6() {
|
if destination.IsIPv6() {
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
|
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
|
||||||
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||||
@@ -299,51 +270,22 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||||
if strategy == nil {
|
if strategy == C.NetworkStrategyDefault {
|
||||||
strategy = d.networkStrategy
|
|
||||||
}
|
|
||||||
if strategy == nil {
|
|
||||||
return d.ListenPacket(ctx, destination)
|
return d.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
if len(interfaceType) == 0 {
|
if !d.networkManager.AutoDetectInterface() {
|
||||||
interfaceType = d.networkType
|
return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`")
|
||||||
}
|
|
||||||
if len(fallbackInterfaceType) == 0 {
|
|
||||||
fallbackInterfaceType = d.fallbackNetworkType
|
|
||||||
}
|
|
||||||
if fallbackDelay == 0 {
|
|
||||||
fallbackDelay = d.networkFallbackDelay
|
|
||||||
}
|
}
|
||||||
network := N.NetworkUDP
|
network := N.NetworkUDP
|
||||||
if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||||
network += "4"
|
network += "4"
|
||||||
}
|
}
|
||||||
packetConn, err := d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", *strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return trackPacketConn(d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", strategy, interfaceType, fallbackInterfaceType, fallbackDelay))
|
||||||
if err != nil {
|
|
||||||
// bind interface failed on legacy xiaomi systems
|
|
||||||
if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) {
|
|
||||||
d.networkStrategy = nil
|
|
||||||
return d.ListenPacket(ctx, destination)
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return trackPacketConn(packetConn, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
|
||||||
udpListener := d.udpListener
|
return d.udpListener.ListenPacket(context.Background(), network, address)
|
||||||
udpListener.Control = control.Append(udpListener.Control, func(network, address string, conn syscall.RawConn) error {
|
|
||||||
for _, wgControlFn := range WgControlFns {
|
|
||||||
err := wgControlFn(network, address, conn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return udpListener.ListenPacket(context.Background(), network, address)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
|
|||||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||||
return nil, false, E.New("no available network interface")
|
return nil, false, E.New("no available network interface")
|
||||||
}
|
}
|
||||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
|
||||||
if fallbackDelay == 0 {
|
if fallbackDelay == 0 {
|
||||||
fallbackDelay = N.DefaultFallbackDelay
|
fallbackDelay = N.DefaultFallbackDelay
|
||||||
}
|
}
|
||||||
@@ -32,18 +31,16 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
|
|||||||
results := make(chan dialResult) // unbuffered
|
results := make(chan dialResult) // unbuffered
|
||||||
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
||||||
perNetDialer := dialer
|
perNetDialer := dialer
|
||||||
if defaultInterface == nil || iif.Index != defaultInterface.Index {
|
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
|
||||||
}
|
|
||||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
select {
|
||||||
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}:
|
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Name, ")"), primary: primary}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
select {
|
select {
|
||||||
case results <- dialResult{Conn: conn, primary: primary}:
|
case results <- dialResult{Conn: conn}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
@@ -92,7 +89,6 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
|
|||||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||||
return nil, false, E.New("no available network interface")
|
return nil, false, E.New("no available network interface")
|
||||||
}
|
}
|
||||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
|
||||||
if fallbackDelay == 0 {
|
if fallbackDelay == 0 {
|
||||||
fallbackDelay = N.DefaultFallbackDelay
|
fallbackDelay = N.DefaultFallbackDelay
|
||||||
}
|
}
|
||||||
@@ -107,18 +103,16 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
|
|||||||
results := make(chan dialResult) // unbuffered
|
results := make(chan dialResult) // unbuffered
|
||||||
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) {
|
||||||
perNetDialer := dialer
|
perNetDialer := dialer
|
||||||
if defaultInterface == nil || iif.Index != defaultInterface.Index {
|
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
||||||
perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index))
|
|
||||||
}
|
|
||||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
select {
|
||||||
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}:
|
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Name, ")"), primary: primary}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
select {
|
select {
|
||||||
case results <- dialResult{Conn: conn, primary: primary}:
|
case results <- dialResult{Conn: conn}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
if primary && time.Since(startAt) <= fallbackDelay {
|
if primary && time.Since(startAt) <= fallbackDelay {
|
||||||
resetFastFallback(time.Time{})
|
resetFastFallback(time.Time{})
|
||||||
@@ -155,29 +149,24 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
|
|||||||
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
|
||||||
return nil, E.New("no available network interface")
|
return nil, E.New("no available network interface")
|
||||||
}
|
}
|
||||||
defaultInterface := d.networkManager.InterfaceMonitor().DefaultInterface()
|
|
||||||
var errors []error
|
var errors []error
|
||||||
for _, primaryInterface := range primaryInterfaces {
|
for _, primaryInterface := range primaryInterfaces {
|
||||||
perNetListener := listener
|
perNetListener := listener
|
||||||
if defaultInterface == nil || primaryInterface.Index != defaultInterface.Index {
|
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index))
|
||||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index))
|
|
||||||
}
|
|
||||||
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Index, ")"))
|
errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Name, ")"))
|
||||||
}
|
}
|
||||||
for _, fallbackInterface := range fallbackInterfaces {
|
for _, fallbackInterface := range fallbackInterfaces {
|
||||||
perNetListener := listener
|
perNetListener := listener
|
||||||
if defaultInterface == nil || fallbackInterface.Index != defaultInterface.Index {
|
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index))
|
||||||
perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index))
|
|
||||||
}
|
|
||||||
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
conn, err := perNetListener.ListenPacket(ctx, network, addr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Index, ")"))
|
errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Name, ")"))
|
||||||
}
|
}
|
||||||
return nil, E.Errors(errors...)
|
return nil, E.Errors(errors...)
|
||||||
}
|
}
|
||||||
@@ -188,57 +177,44 @@ func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkS
|
|||||||
case C.NetworkStrategyDefault:
|
case C.NetworkStrategyDefault:
|
||||||
if len(interfaceType) == 0 {
|
if len(interfaceType) == 0 {
|
||||||
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
|
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
|
||||||
if defaultIf != nil {
|
for _, iif := range interfaces {
|
||||||
for _, iif := range interfaces {
|
if iif.Index == defaultIf.Index {
|
||||||
if iif.Index == defaultIf.Index {
|
primaryInterfaces = append(primaryInterfaces, iif)
|
||||||
primaryInterfaces = append(primaryInterfaces, iif)
|
} else {
|
||||||
}
|
fallbackInterfaces = append(fallbackInterfaces, iif)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
primaryInterfaces = interfaces
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
|
||||||
return common.Contains(interfaceType, it.Type)
|
return common.Contains(interfaceType, iif.Type)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case C.NetworkStrategyHybrid:
|
case C.NetworkStrategyHybrid:
|
||||||
if len(interfaceType) == 0 {
|
if len(interfaceType) == 0 {
|
||||||
primaryInterfaces = interfaces
|
primaryInterfaces = interfaces
|
||||||
} else {
|
} else {
|
||||||
primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
|
||||||
return common.Contains(interfaceType, it.Type)
|
return common.Contains(interfaceType, iif.Type)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case C.NetworkStrategyFallback:
|
case C.NetworkStrategyFallback:
|
||||||
if len(interfaceType) == 0 {
|
if len(interfaceType) == 0 {
|
||||||
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
|
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
|
||||||
if defaultIf != nil {
|
for _, iif := range interfaces {
|
||||||
for _, iif := range interfaces {
|
if iif.Index == defaultIf.Index {
|
||||||
if iif.Index == defaultIf.Index {
|
primaryInterfaces = append(primaryInterfaces, iif)
|
||||||
primaryInterfaces = append(primaryInterfaces, iif)
|
} else {
|
||||||
break
|
fallbackInterfaces = append(fallbackInterfaces, iif)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
primaryInterfaces = interfaces
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
|
||||||
return common.Contains(interfaceType, it.Type)
|
return common.Contains(interfaceType, iif.Type)
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(fallbackInterfaceType) == 0 {
|
|
||||||
fallbackInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
|
||||||
return !common.Any(primaryInterfaces, func(iif adapter.NetworkInterface) bool {
|
|
||||||
return it.Index == iif.Index
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
fallbackInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
|
|
||||||
return common.Contains(fallbackInterfaceType, iif.Type)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
fallbackInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
|
||||||
|
return common.Contains(fallbackInterfaceType, iif.Type)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return primaryInterfaces, fallbackInterfaces
|
return primaryInterfaces, fallbackInterfaces
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,7 @@ import (
|
|||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||||
if len(destinationAddresses) == 0 {
|
|
||||||
if !destination.IsIP() {
|
|
||||||
panic("invalid usage")
|
|
||||||
}
|
|
||||||
destinationAddresses = []netip.Addr{destination.Addr}
|
|
||||||
}
|
|
||||||
if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
|
if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
|
||||||
return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
}
|
}
|
||||||
@@ -44,14 +38,7 @@ func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, des
|
|||||||
return nil, E.Errors(errors...)
|
return nil, E.Errors(errors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||||
if len(destinationAddresses) == 0 {
|
|
||||||
if !destination.IsIP() {
|
|
||||||
panic("invalid usage")
|
|
||||||
}
|
|
||||||
destinationAddresses = []netip.Addr{destination.Addr}
|
|
||||||
}
|
|
||||||
|
|
||||||
if fallbackDelay == 0 {
|
if fallbackDelay == 0 {
|
||||||
fallbackDelay = N.DefaultFallbackDelay
|
fallbackDelay = N.DefaultFallbackDelay
|
||||||
}
|
}
|
||||||
@@ -129,13 +116,7 @@ func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, ne
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListenSerialNetworkPacket(ctx context.Context, dialer N.Dialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {
|
func ListenSerialNetworkPacket(ctx context.Context, dialer N.Dialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {
|
||||||
if len(destinationAddresses) == 0 {
|
|
||||||
if !destination.IsIP() {
|
|
||||||
panic("invalid usage")
|
|
||||||
}
|
|
||||||
destinationAddresses = []netip.Addr{destination.Addr}
|
|
||||||
}
|
|
||||||
if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
|
if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
|
||||||
return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,15 +17,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) {
|
func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) {
|
||||||
|
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||||
if options.IsWireGuardListener {
|
if options.IsWireGuardListener {
|
||||||
return NewDefault(ctx, options)
|
return NewDefault(networkManager, options)
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if options.Detour == "" {
|
if options.Detour == "" {
|
||||||
dialer, err = NewDefault(ctx, options)
|
dialer, err = NewDefault(networkManager, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -36,6 +37,9 @@ func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) {
|
|||||||
}
|
}
|
||||||
dialer = NewDetour(outboundManager, options.Detour)
|
dialer = NewDetour(outboundManager, options.Detour)
|
||||||
}
|
}
|
||||||
|
if networkManager == nil {
|
||||||
|
return NewDefault(networkManager, options)
|
||||||
|
}
|
||||||
if options.Detour == "" {
|
if options.Detour == "" {
|
||||||
router := service.FromContext[adapter.Router](ctx)
|
router := service.FromContext[adapter.Router](ctx)
|
||||||
if router != nil {
|
if router != nil {
|
||||||
@@ -54,10 +58,11 @@ func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInter
|
|||||||
if options.Detour != "" {
|
if options.Detour != "" {
|
||||||
return nil, E.New("`detour` is not supported in direct context")
|
return nil, E.New("`detour` is not supported in direct context")
|
||||||
}
|
}
|
||||||
|
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||||
if options.IsWireGuardListener {
|
if options.IsWireGuardListener {
|
||||||
return NewDefault(ctx, options)
|
return NewDefault(networkManager, options)
|
||||||
}
|
}
|
||||||
dialer, err := NewDefault(ctx, options)
|
dialer, err := NewDefault(networkManager, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -72,11 +77,11 @@ func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInter
|
|||||||
|
|
||||||
type ParallelInterfaceDialer interface {
|
type ParallelInterfaceDialer interface {
|
||||||
N.Dialer
|
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)
|
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||||
ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error)
|
ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParallelNetworkDialer interface {
|
type ParallelNetworkDialer interface {
|
||||||
DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||||
ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
|
ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
|||||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
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) {
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
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) {
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
|
|||||||
158
common/humanize/bytes.go
Normal file
158
common/humanize/bytes.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package humanize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IEC Sizes.
|
||||||
|
// kibis of bits
|
||||||
|
const (
|
||||||
|
Byte = 1 << (iota * 10)
|
||||||
|
KiByte
|
||||||
|
MiByte
|
||||||
|
GiByte
|
||||||
|
TiByte
|
||||||
|
PiByte
|
||||||
|
EiByte
|
||||||
|
)
|
||||||
|
|
||||||
|
// SI Sizes.
|
||||||
|
const (
|
||||||
|
IByte = 1
|
||||||
|
KByte = IByte * 1000
|
||||||
|
MByte = KByte * 1000
|
||||||
|
GByte = MByte * 1000
|
||||||
|
TByte = GByte * 1000
|
||||||
|
PByte = TByte * 1000
|
||||||
|
EByte = PByte * 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultSizeTable = map[string]uint64{
|
||||||
|
"b": Byte,
|
||||||
|
"kib": KiByte,
|
||||||
|
"kb": KByte,
|
||||||
|
"mib": MiByte,
|
||||||
|
"mb": MByte,
|
||||||
|
"gib": GiByte,
|
||||||
|
"gb": GByte,
|
||||||
|
"tib": TiByte,
|
||||||
|
"tb": TByte,
|
||||||
|
"pib": PiByte,
|
||||||
|
"pb": PByte,
|
||||||
|
"eib": EiByte,
|
||||||
|
"eb": EByte,
|
||||||
|
// Without suffix
|
||||||
|
"": Byte,
|
||||||
|
"ki": KiByte,
|
||||||
|
"k": KByte,
|
||||||
|
"mi": MiByte,
|
||||||
|
"m": MByte,
|
||||||
|
"gi": GiByte,
|
||||||
|
"g": GByte,
|
||||||
|
"ti": TiByte,
|
||||||
|
"t": TByte,
|
||||||
|
"pi": PiByte,
|
||||||
|
"p": PByte,
|
||||||
|
"ei": EiByte,
|
||||||
|
"e": EByte,
|
||||||
|
}
|
||||||
|
|
||||||
|
var memorysSizeTable = map[string]uint64{
|
||||||
|
"b": Byte,
|
||||||
|
"kb": KiByte,
|
||||||
|
"mb": MiByte,
|
||||||
|
"gb": GiByte,
|
||||||
|
"tb": TiByte,
|
||||||
|
"pb": PiByte,
|
||||||
|
"eb": EiByte,
|
||||||
|
"": Byte,
|
||||||
|
"k": KiByte,
|
||||||
|
"m": MiByte,
|
||||||
|
"g": GiByte,
|
||||||
|
"t": TiByte,
|
||||||
|
"p": PiByte,
|
||||||
|
"e": EiByte,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultSizes = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||||
|
iSizes = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func Bytes(s uint64) string {
|
||||||
|
return humanateBytes(s, 1000, defaultSizes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MemoryBytes(s uint64) string {
|
||||||
|
return humanateBytes(s, 1024, defaultSizes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IBytes(s uint64) string {
|
||||||
|
return humanateBytes(s, 1024, iSizes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logn(n, b float64) float64 {
|
||||||
|
return math.Log(n) / math.Log(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||||
|
if s < 10 {
|
||||||
|
return fmt.Sprintf("%d B", s)
|
||||||
|
}
|
||||||
|
e := math.Floor(logn(float64(s), base))
|
||||||
|
suffix := sizes[int(e)]
|
||||||
|
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||||
|
f := "%.0f %s"
|
||||||
|
if val < 10 {
|
||||||
|
f = "%.1f %s"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(f, val, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseBytes(s string) (uint64, error) {
|
||||||
|
return parseBytes0(s, defaultSizeTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMemoryBytes(s string) (uint64, error) {
|
||||||
|
return parseBytes0(s, memorysSizeTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBytes0(s string, sizeTable map[string]uint64) (uint64, error) {
|
||||||
|
lastDigit := 0
|
||||||
|
hasComma := false
|
||||||
|
for _, r := range s {
|
||||||
|
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if r == ',' {
|
||||||
|
hasComma = true
|
||||||
|
}
|
||||||
|
lastDigit++
|
||||||
|
}
|
||||||
|
|
||||||
|
num := s[:lastDigit]
|
||||||
|
if hasComma {
|
||||||
|
num = strings.Replace(num, ",", "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := strconv.ParseFloat(num, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||||
|
if m, ok := sizeTable[extra]; ok {
|
||||||
|
f *= float64(m)
|
||||||
|
if f >= math.MaxUint64 {
|
||||||
|
return 0, fmt.Errorf("too large: %v", s)
|
||||||
|
}
|
||||||
|
return uint64(f), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||||
|
}
|
||||||
@@ -124,6 +124,14 @@ func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error)
|
|||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
row := b[4+itemSize*i : 4+itemSize*(i+1)]
|
row := b[4+itemSize*i : 4+itemSize*(i+1)]
|
||||||
|
|
||||||
|
if s.tcpState >= 0 {
|
||||||
|
tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4])
|
||||||
|
// MIB_TCP_STATE_ESTAB, only check established connections for TCP
|
||||||
|
if tcpState != 5 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian.
|
// 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:
|
// 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)
|
// little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB)
|
||||||
@@ -136,7 +144,7 @@ func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error)
|
|||||||
|
|
||||||
srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize])
|
srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize])
|
||||||
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
|
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
|
||||||
if ip != srcIP && (!srcIP.IsUnspecified()) {
|
if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
common/settings/time_stub.go
Normal file
12
common/settings/time_stub.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !(windows || linux || darwin)
|
||||||
|
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetSystemTime(nowTime time.Time) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
14
common/settings/time_unix.go
Normal file
14
common/settings/time_unix.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build linux || darwin
|
||||||
|
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetSystemTime(nowTime time.Time) error {
|
||||||
|
timeVal := unix.NsecToTimeval(nowTime.UnixNano())
|
||||||
|
return unix.Settimeofday(&timeVal)
|
||||||
|
}
|
||||||
32
common/settings/time_windows.go
Normal file
32
common/settings/time_windows.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetSystemTime(nowTime time.Time) error {
|
||||||
|
var systemTime windows.Systemtime
|
||||||
|
systemTime.Year = uint16(nowTime.Year())
|
||||||
|
systemTime.Month = uint16(nowTime.Month())
|
||||||
|
systemTime.Day = uint16(nowTime.Day())
|
||||||
|
systemTime.Hour = uint16(nowTime.Hour())
|
||||||
|
systemTime.Minute = uint16(nowTime.Minute())
|
||||||
|
systemTime.Second = uint16(nowTime.Second())
|
||||||
|
systemTime.Milliseconds = uint16(nowTime.UnixMilli() - nowTime.Unix()*1000)
|
||||||
|
|
||||||
|
dllKernel32 := windows.NewLazySystemDLL("kernel32.dll")
|
||||||
|
proc := dllKernel32.NewProc("SetSystemTime")
|
||||||
|
|
||||||
|
_, _, err := proc.Call(
|
||||||
|
uintptr(unsafe.Pointer(&systemTime)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil && err.Error() != "The operation completed successfully." {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -24,25 +23,20 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R
|
|||||||
var first byte
|
var first byte
|
||||||
err := binary.Read(reader, binary.BigEndian, &first)
|
err := binary.Read(reader, binary.BigEndian, &first)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if first != 19 {
|
if first != 19 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
const header = "BitTorrent protocol"
|
|
||||||
var protocol [19]byte
|
var protocol [19]byte
|
||||||
var n int
|
_, err = reader.Read(protocol[:])
|
||||||
n, err = reader.Read(protocol[:])
|
|
||||||
if string(protocol[:n]) != header[:n] {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if n < 19 {
|
if string(protocol[:]) != "BitTorrent protocol" {
|
||||||
return ErrNeedMoreData
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata.Protocol = C.ProtocolBitTorrent
|
metadata.Protocol = C.ProtocolBitTorrent
|
||||||
@@ -73,9 +67,7 @@ func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if extension > 0x04 {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
var length byte
|
var length byte
|
||||||
err = binary.Read(reader, binary.BigEndian, &length)
|
err = binary.Read(reader, binary.BigEndian, &length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -32,27 +32,6 @@ func TestSniffBittorrent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSniffIncompleteBittorrent(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
pkt, err := hex.DecodeString("13426974546f7272656e74")
|
|
||||||
require.NoError(t, err)
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
|
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSniffNotBittorrent(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
pkt, err := hex.DecodeString("13426974546f7272656e75")
|
|
||||||
require.NoError(t, err)
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
|
|
||||||
require.NotEmpty(t, err)
|
|
||||||
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSniffUTP(t *testing.T) {
|
func TestSniffUTP(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -92,19 +71,3 @@ func TestSniffUDPTracker(t *testing.T) {
|
|||||||
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSniffNotUTP(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
packets := []string{
|
|
||||||
"0102736470696e674958d580121500000000000079aaed6717a39c27b07c0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
}
|
|
||||||
for _, pkt := range packets {
|
|
||||||
pkt, err := hex.DecodeString(pkt)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err = sniff.UTP(context.TODO(), &metadata, pkt)
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
"github.com/sagernet/sing/common/task"
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
@@ -18,40 +21,35 @@ func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundCon
|
|||||||
var length uint16
|
var length uint16
|
||||||
err := binary.Read(reader, binary.BigEndian, &length)
|
err := binary.Read(reader, binary.BigEndian, &length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
if length < 12 {
|
if length == 0 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
buffer := buf.NewSize(int(length))
|
buffer := buf.NewSize(int(length))
|
||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
var n int
|
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
|
||||||
n, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
|
var readTask task.Group
|
||||||
packet := buffer.Bytes()
|
readTask.Append0(func(ctx context.Context) error {
|
||||||
if n > 2 && packet[2]&0x80 != 0 { // QR
|
return common.Error(buffer.ReadFullFrom(reader, buffer.FreeLen()))
|
||||||
return os.ErrInvalid
|
})
|
||||||
}
|
err = readTask.Run(readCtx)
|
||||||
if n > 5 && packet[4] == 0 && packet[5] == 0 { // QDCOUNT
|
cancel()
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
for i := 6; i < 10; i++ {
|
|
||||||
// ANCOUNT, NSCOUNT
|
|
||||||
if n > i && packet[i] != 0 {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
return DomainNameQuery(readCtx, metadata, packet)
|
return DomainNameQuery(readCtx, metadata, buffer.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
var msg mDNS.Msg
|
var msg mDNS.Msg
|
||||||
err := msg.Unpack(packet)
|
err := msg.Unpack(packet)
|
||||||
if err != nil || msg.Response || len(msg.Question) == 0 || len(msg.Answer) > 0 || len(msg.Ns) > 0 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
metadata.Protocol = C.ProtocolDNS
|
metadata.Protocol = C.ProtocolDNS
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package sniff_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/hex"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/sniff"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSniffDNS(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
query, err := hex.DecodeString("740701000001000000000000012a06676f6f676c6503636f6d0000010001")
|
|
||||||
require.NoError(t, err)
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err = sniff.DomainNameQuery(context.TODO(), &metadata, query)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSniffStreamDNS(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
query, err := hex.DecodeString("001e740701000001000000000000012a06676f6f676c6503636f6d0000010001")
|
|
||||||
require.NoError(t, err)
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSniffIncompleteStreamDNS(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
query, err := hex.DecodeString("001e740701000001000000000000")
|
|
||||||
require.NoError(t, err)
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
|
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSniffNotStreamDNS(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
query, err := hex.DecodeString("001e740701000000000000000000")
|
|
||||||
require.NoError(t, err)
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
|
|
||||||
require.NotEmpty(t, err)
|
|
||||||
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
|
|
||||||
}
|
|
||||||
@@ -3,12 +3,10 @@ package sniff
|
|||||||
import (
|
import (
|
||||||
std_bufio "bufio"
|
std_bufio "bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/protocol/http"
|
"github.com/sagernet/sing/protocol/http"
|
||||||
)
|
)
|
||||||
@@ -16,11 +14,7 @@ import (
|
|||||||
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
request, err := http.ReadRequest(std_bufio.NewReader(reader))
|
request, err := http.ReadRequest(std_bufio.NewReader(reader))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
return err
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
metadata.Protocol = C.ProtocolHTTP
|
metadata.Protocol = C.ProtocolHTTP
|
||||||
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import (
|
|||||||
"golang.org/x/crypto/hkdf"
|
"golang.org/x/crypto/hkdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection")
|
||||||
|
|
||||||
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
reader := bytes.NewReader(packet)
|
reader := bytes.NewReader(packet)
|
||||||
typeByte, err := reader.ReadByte()
|
typeByte, err := reader.ReadByte()
|
||||||
@@ -306,7 +308,7 @@ find:
|
|||||||
metadata.Protocol = C.ProtocolQUIC
|
metadata.Protocol = C.ProtocolQUIC
|
||||||
metadata.Client = C.ClientChromium
|
metadata.Client = C.ClientChromium
|
||||||
metadata.SniffContext = fragments
|
metadata.SniffContext = fragments
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return ErrClientHelloFragmented
|
||||||
}
|
}
|
||||||
metadata.Domain = fingerprint.ServerName
|
metadata.Domain = fingerprint.ServerName
|
||||||
for metadata.Client == "" {
|
for metadata.Client == "" {
|
||||||
|
|||||||
@@ -12,26 +12,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestSniffQUICChromium(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620")
|
pkt, err := hex.DecodeString("c30000000108f40d654cc09b27f5000044d08a94548e57e43cc5483f129986187c432d58d46674830442988f869566a6e31e2ae37c9f7acbf61cc81621594fab0b3dfdc1635460b32389563dc8e74006315661cd22694114612973c1c45910621713a48b375854f095e8a77ccf3afa64e972f0f7f7002f50e0b014b1b146ea47c07fb20b73ad5587872b51a0b3fafdf1c4cf4fe6f8b112142392efa25d993abe2f42582be145148bdfe12edcd96c3655b65a4781b093e5594ba8e3ae5320f12e8314fc3ca374128cc43381046c322b964681ed4395c813b28534505118201459665a44b8f0abead877de322e9040631d20b05f15b81fa7ff785d4041aecc37c7e2ccdc5d1532787ce566517e8985fd5c200dbfd1e67bc255efaba94cfc07bb52fea4a90887413b134f2715b5643542aa897c6116486f428d82da64d2a2c1e1bdd40bd592558901a554b003d6966ac5a7b8b9413eddbf6ef21f28386c74981e3ce1d724c341e95494907626659692720c81114ca4acea35a14c402cfa3dc2228446e78dc1b81fa4325cf7e314a9cad6a6bdff33b3351dcba74eb15fae67f1227283aa4cdd64bcadf8f19358333f8549b596f4350297b5c65274565869d497398339947b9d3d064e5b06d39d34b436d8a41c1a3880de10bd26c3b1c5b4e2a49b0d4d07b8d90cd9e92bc611564d19ea8ec33099e92033caf21f5307dbeaa4708b99eb313bff99e2081ac25fd12d6a72e8335e0724f6718fe023cd0ad0d6e6a6309f09c9c391eec2bc08e9c3210a043c08e1759f354c121f6517fff4d6e20711a871e41285d48d930352fddffb92c96ba57df045ce99f8bfdfa8edc0969ce68a51e9fbb4f54b956d9df74a9e4af27ed2b27839bce1cffeca8333c0aaee81a570217442f9029ba8fedb84a2cf4be4d910982d891ea00e816c7fb98e8020e896a9c6fdd9106611da0a99dde18df1b7a8f6327acb1eed9ad93314451e48cb0dfb9571728521ca3db2ac0968159d5622556a55d51a422d11995b650949aaefc5d24c16080446dfc4fbc10353f9f93ce161ab513367bb89ab83988e0630b689e174e27bcfcc31996ee7b0bca909e251b82d69a28fee5a5d662e127508cd19dbbe5097b7d5b62a49203d66764197a527e472e2627e44a93d44177dace9d60e7d0e03305ddf4cfe47cdf2362e14de79ef46a6763ce696cd7854a48d9419a0817507a4713ffd4977b906d4f2b5fb6dbe1bd15bc505d5fea582190bf531a45d5ee026da8918547fd5105f15e5d061c7b0cf80a34990366ed8e91e13c2f0d85e5dad537298808d193cf54b7eaac33f10051f74cb6b75e52f81618c36f03d86aef613ba237a1a793ba1539938a38f62ccaf7bd5f6c5e0ce53cde4012fcf2b758214a0422d2faaa798e86e19d7481b42df2b36a73d287ff28c20cce01ce598771fec16a8f1f00305c06010126013a6c1de9f589b4e79d693717cd88ad1c42a2d99fa96617ba0bc6365b68e21a70ebc447904aa27979e1514433cfd83bfec09f137c747d47582cb63eb28f873fb94cf7a59ff764ddfbb687d79a58bb10f85949269f7f72c611a5e0fbb52adfa298ff060ec2eb7216fd7302ea8fb07798cbb3be25cb53ac8161aac2b5bbcfbcfb01c113d28bd1cb0333fb89ac82a95930f7abded0a2f5a623cc6a1f62bf3f38ef1b81c1e50a634f657dbb6770e4af45879e2fb1e00c742e7b52205c8015b5c0f5b1e40186ff9aa7288ab3e01a51fb87761f9bc6837082af109b39cc9f620")
|
||||||
@@ -40,7 +20,7 @@ func TestSniffQUICChromium(t *testing.T) {
|
|||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
|
||||||
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var tpktVersion uint8
|
var tpktVersion uint8
|
||||||
err := binary.Read(reader, binary.BigEndian, &tpktVersion)
|
err := binary.Read(reader, binary.BigEndian, &tpktVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if tpktVersion != 0x03 {
|
if tpktVersion != 0x03 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
@@ -25,7 +24,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var tpktReserved uint8
|
var tpktReserved uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &tpktReserved)
|
err = binary.Read(reader, binary.BigEndian, &tpktReserved)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if tpktReserved != 0x00 {
|
if tpktReserved != 0x00 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
@@ -34,7 +33,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var tpktLength uint16
|
var tpktLength uint16
|
||||||
err = binary.Read(reader, binary.BigEndian, &tpktLength)
|
err = binary.Read(reader, binary.BigEndian, &tpktLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if tpktLength != 19 {
|
if tpktLength != 19 {
|
||||||
@@ -44,7 +43,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var cotpLength uint8
|
var cotpLength uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &cotpLength)
|
err = binary.Read(reader, binary.BigEndian, &cotpLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if cotpLength != 14 {
|
if cotpLength != 14 {
|
||||||
@@ -54,7 +53,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var cotpTpduType uint8
|
var cotpTpduType uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &cotpTpduType)
|
err = binary.Read(reader, binary.BigEndian, &cotpTpduType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if cotpTpduType != 0xE0 {
|
if cotpTpduType != 0xE0 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
@@ -62,13 +61,13 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
|
|
||||||
err = rw.SkipN(reader, 5)
|
err = rw.SkipN(reader, 5)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var rdpType uint8
|
var rdpType uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &rdpType)
|
err = binary.Read(reader, binary.BigEndian, &rdpType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if rdpType != 0x01 {
|
if rdpType != 0x01 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
@@ -76,12 +75,12 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var rdpFlags uint8
|
var rdpFlags uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &rdpFlags)
|
err = binary.Read(reader, binary.BigEndian, &rdpFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
var rdpLength uint8
|
var rdpLength uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &rdpLength)
|
err = binary.Read(reader, binary.BigEndian, &rdpLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if rdpLength != 8 {
|
if rdpLength != 8 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ package sniff
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
@@ -20,8 +18,6 @@ type (
|
|||||||
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
|
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrNeedMoreData = E.New("need more data")
|
|
||||||
|
|
||||||
func Skip(metadata *adapter.InboundContext) bool {
|
func Skip(metadata *adapter.InboundContext) bool {
|
||||||
// skip server first protocols
|
// skip server first protocols
|
||||||
switch metadata.Destination.Port {
|
switch metadata.Destination.Port {
|
||||||
@@ -38,12 +34,12 @@ func Skip(metadata *adapter.InboundContext) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffers []*buf.Buffer, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
|
func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) error {
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
timeout = C.ReadPayloadTimeout
|
timeout = C.ReadPayloadTimeout
|
||||||
}
|
}
|
||||||
deadline := time.Now().Add(timeout)
|
deadline := time.Now().Add(timeout)
|
||||||
var sniffError error
|
var errors []error
|
||||||
for i := 0; ; i++ {
|
for i := 0; ; i++ {
|
||||||
err := conn.SetReadDeadline(deadline)
|
err := conn.SetReadDeadline(deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -57,32 +53,26 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
|
|||||||
}
|
}
|
||||||
return E.Cause(err, "read payload")
|
return E.Cause(err, "read payload")
|
||||||
}
|
}
|
||||||
sniffError = nil
|
errors = nil
|
||||||
for _, sniffer := range sniffers {
|
for _, sniffer := range sniffers {
|
||||||
reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader {
|
err = sniffer(ctx, metadata, bytes.NewReader(buffer.Bytes()))
|
||||||
return bytes.NewReader(it.Bytes())
|
|
||||||
})...)
|
|
||||||
err = sniffer(ctx, metadata, reader)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
sniffError = E.Errors(sniffError, err)
|
errors = append(errors, err)
|
||||||
}
|
|
||||||
if !errors.Is(sniffError, ErrNeedMoreData) {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sniffError
|
return E.Errors(errors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
|
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
|
||||||
var sniffError []error
|
var errors []error
|
||||||
for _, sniffer := range sniffers {
|
for _, sniffer := range sniffers {
|
||||||
err := sniffer(ctx, metadata, packet)
|
err := sniffer(ctx, metadata, packet)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
sniffError = append(sniffError, err)
|
errors = append(errors, err)
|
||||||
}
|
}
|
||||||
return E.Errors(sniffError...)
|
return E.Errors(errors...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,27 +5,22 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
const sshPrefix = "SSH-2.0-"
|
scanner := bufio.NewScanner(reader)
|
||||||
bReader := bufio.NewReader(reader)
|
if !scanner.Scan() {
|
||||||
prefix, err := bReader.Peek(len(sshPrefix))
|
|
||||||
if string(prefix[:]) != sshPrefix[:len(prefix)] {
|
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
if err != nil {
|
fistLine := scanner.Text()
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
if !strings.HasPrefix(fistLine, "SSH-2.0-") {
|
||||||
}
|
return os.ErrInvalid
|
||||||
fistLine, _, err := bReader.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
metadata.Protocol = C.ProtocolSSH
|
metadata.Protocol = C.ProtocolSSH
|
||||||
metadata.Client = string(fistLine)[8:]
|
metadata.Client = fistLine[8:]
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,24 +24,3 @@ func TestSniffSSH(t *testing.T) {
|
|||||||
require.Equal(t, C.ProtocolSSH, metadata.Protocol)
|
require.Equal(t, C.ProtocolSSH, metadata.Protocol)
|
||||||
require.Equal(t, "dropbear", metadata.Client)
|
require.Equal(t, "dropbear", metadata.Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSniffIncompleteSSH(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
pkt, err := hex.DecodeString("5353482d322e30")
|
|
||||||
require.NoError(t, err)
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
|
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSniffNotSSH(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
pkt, err := hex.DecodeString("5353482d322e31")
|
|
||||||
require.NoError(t, err)
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
|
|
||||||
require.NotEmpty(t, err)
|
|
||||||
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ package sniff
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
@@ -25,9 +23,5 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
|
|||||||
metadata.Domain = clientHello.ServerName
|
metadata.Domain = clientHello.ServerName
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
return err
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,14 +30,15 @@ func NewClient(ctx context.Context, serverAddress string, options option.Outboun
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if options.ECH != nil && options.ECH.Enabled {
|
if options.ECH != nil && options.ECH.Enabled {
|
||||||
return NewECHClient(ctx, serverAddress, options)
|
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
|
||||||
|
return NewECHClient(ctx, serverAddress, options)
|
||||||
|
}
|
||||||
} else if options.Reality != nil && options.Reality.Enabled {
|
} else if options.Reality != nil && options.Reality.Enabled {
|
||||||
return NewRealityClient(ctx, serverAddress, options)
|
return NewRealityClient(ctx, serverAddress, options)
|
||||||
} else if options.UTLS != nil && options.UTLS.Enabled {
|
} else if options.UTLS != nil && options.UTLS.Enabled {
|
||||||
return NewUTLSClient(ctx, serverAddress, options)
|
return NewUTLSClient(ctx, serverAddress, options)
|
||||||
} else {
|
|
||||||
return NewSTDClient(ctx, serverAddress, options)
|
|
||||||
}
|
}
|
||||||
|
return NewSTDClient(ctx, serverAddress, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ type echConnWrapper struct {
|
|||||||
|
|
||||||
func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
|
func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
|
||||||
state := c.Conn.ConnectionState()
|
state := c.Conn.ConnectionState()
|
||||||
//nolint:staticcheck
|
|
||||||
return tls.ConnectionState{
|
return tls.ConnectionState{
|
||||||
Version: state.Version,
|
Version: state.Version,
|
||||||
HandshakeComplete: state.HandshakeComplete,
|
HandshakeComplete: state.HandshakeComplete,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
|
||||||
cftls "github.com/sagernet/cloudflare-tls"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
"github.com/cloudflare/circl/hpke"
|
"github.com/cloudflare/circl/hpke"
|
||||||
@@ -59,7 +58,6 @@ func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (config
|
|||||||
|
|
||||||
type echKeyConfigPair struct {
|
type echKeyConfigPair struct {
|
||||||
id uint8
|
id uint8
|
||||||
key cftls.EXP_ECHKey
|
|
||||||
rawKey []byte
|
rawKey []byte
|
||||||
conf myECHKeyConfig
|
conf myECHKeyConfig
|
||||||
rawConf []byte
|
rawConf []byte
|
||||||
@@ -147,23 +145,19 @@ func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite [
|
|||||||
pair.rawConf = b
|
pair.rawConf = b
|
||||||
|
|
||||||
secBuf, err := sec.MarshalBinary()
|
secBuf, err := sec.MarshalBinary()
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "serialize ECH private key")
|
|
||||||
}
|
|
||||||
sk := []byte{}
|
sk := []byte{}
|
||||||
sk = be.AppendUint16(sk, uint16(len(secBuf)))
|
sk = be.AppendUint16(sk, uint16(len(secBuf)))
|
||||||
sk = append(sk, secBuf...)
|
sk = append(sk, secBuf...)
|
||||||
sk = be.AppendUint16(sk, uint16(len(b)))
|
sk = be.AppendUint16(sk, uint16(len(b)))
|
||||||
sk = append(sk, b...)
|
sk = append(sk, b...)
|
||||||
|
|
||||||
cfECHKeys, err := cftls.EXP_UnmarshalECHKeys(sk)
|
cfECHKeys, err := UnmarshalECHKeys(sk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "bug: can't parse generated ECH server key")
|
return nil, E.Cause(err, "bug: can't parse generated ECH server key")
|
||||||
}
|
}
|
||||||
if len(cfECHKeys) != 1 {
|
if len(cfECHKeys) != 1 {
|
||||||
return nil, E.New("bug: unexpected server key count")
|
return nil, E.New("bug: unexpected server key count")
|
||||||
}
|
}
|
||||||
pair.key = cfECHKeys[0]
|
|
||||||
pair.rawKey = sk
|
pair.rawKey = sk
|
||||||
|
|
||||||
pairs = append(pairs, pair)
|
pairs = append(pairs, pair)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, ad
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper {
|
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper {
|
||||||
return &http3.Transport{
|
return &http3.RoundTripper{
|
||||||
TLSClientConfig: c.config,
|
TLSClientConfig: c.config,
|
||||||
QUICConfig: quicConfig,
|
QUICConfig: quicConfig,
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
func GenerateCertificate(timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
||||||
privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour))
|
privateKeyPem, publicKeyPem, err := GenerateKeyPair(timeFunc, serverName, timeFunc().Add(time.Hour))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() ti
|
|||||||
return &certificate, err
|
return &certificate, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
|
func GenerateKeyPair(timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
|
||||||
if timeFunc == nil {
|
if timeFunc == nil {
|
||||||
timeFunc = time.Now
|
timeFunc = time.Now
|
||||||
}
|
}
|
||||||
@@ -47,11 +47,7 @@ func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func(
|
|||||||
},
|
},
|
||||||
DNSNames: []string{serverName},
|
DNSNames: []string{serverName},
|
||||||
}
|
}
|
||||||
if parent == nil {
|
publicDer, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
|
||||||
parent = template
|
|
||||||
parentKey = key
|
|
||||||
}
|
|
||||||
publicDer, err := x509.CreateCertificate(rand.Reader, template, parent, key.Public(), parentKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
|
|||||||
return nil, E.New("reality verification failed")
|
return nil, E.New("reality verification failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &realityClientConnWrapper{uConn}, nil
|
return &utlsConnWrapper{uConn}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
|
func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
|
||||||
@@ -249,36 +249,3 @@ func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChain
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type realityClientConnWrapper struct {
|
|
||||||
*utls.UConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *realityClientConnWrapper) ConnectionState() tls.ConnectionState {
|
|
||||||
state := c.Conn.ConnectionState()
|
|
||||||
//nolint:staticcheck
|
|
||||||
return tls.ConnectionState{
|
|
||||||
Version: state.Version,
|
|
||||||
HandshakeComplete: state.HandshakeComplete,
|
|
||||||
DidResume: state.DidResume,
|
|
||||||
CipherSuite: state.CipherSuite,
|
|
||||||
NegotiatedProtocol: state.NegotiatedProtocol,
|
|
||||||
NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual,
|
|
||||||
ServerName: state.ServerName,
|
|
||||||
PeerCertificates: state.PeerCertificates,
|
|
||||||
VerifiedChains: state.VerifiedChains,
|
|
||||||
SignedCertificateTimestamps: state.SignedCertificateTimestamps,
|
|
||||||
OCSPResponse: state.OCSPResponse,
|
|
||||||
TLSUnique: state.TLSUnique,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *realityClientConnWrapper) Upstream() any {
|
|
||||||
return c.UConn
|
|
||||||
}
|
|
||||||
|
|
||||||
// Due to low implementation quality, the reality server intercepted half close and caused memory leaks.
|
|
||||||
// We fixed it by calling Close() directly.
|
|
||||||
func (c *realityClientConnWrapper) CloseWrite() error {
|
|
||||||
return c.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -89,20 +89,16 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
|
|||||||
tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference)
|
tlsConfig.MaxTimeDiff = time.Duration(options.Reality.MaxTimeDifference)
|
||||||
|
|
||||||
tlsConfig.ShortIds = make(map[[8]byte]bool)
|
tlsConfig.ShortIds = make(map[[8]byte]bool)
|
||||||
if len(options.Reality.ShortID) == 0 {
|
for i, shortIDString := range options.Reality.ShortID {
|
||||||
tlsConfig.ShortIds[[8]byte{0}] = true
|
var shortID [8]byte
|
||||||
} else {
|
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
|
||||||
for i, shortIDString := range options.Reality.ShortID {
|
if err != nil {
|
||||||
var shortID [8]byte
|
return nil, E.Cause(err, "decode short_id[", i, "]: ", shortIDString)
|
||||||
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString))
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "decode short_id[", i, "]: ", shortIDString)
|
|
||||||
}
|
|
||||||
if decodedLen > 8 {
|
|
||||||
return nil, E.New("invalid short_id[", i, "]: ", shortIDString)
|
|
||||||
}
|
|
||||||
tlsConfig.ShortIds[shortID] = true
|
|
||||||
}
|
}
|
||||||
|
if decodedLen > 8 {
|
||||||
|
return nil, E.New("invalid short_id[", i, "]: ", shortIDString)
|
||||||
|
}
|
||||||
|
tlsConfig.ShortIds[shortID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions)
|
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions)
|
||||||
@@ -178,7 +174,6 @@ type realityConnWrapper struct {
|
|||||||
|
|
||||||
func (c *realityConnWrapper) ConnectionState() ConnectionState {
|
func (c *realityConnWrapper) ConnectionState() ConnectionState {
|
||||||
state := c.Conn.ConnectionState()
|
state := c.Conn.ConnectionState()
|
||||||
//nolint:staticcheck
|
|
||||||
return tls.ConnectionState{
|
return tls.ConnectionState{
|
||||||
Version: state.Version,
|
Version: state.Version,
|
||||||
HandshakeComplete: state.HandshakeComplete,
|
HandshakeComplete: state.HandshakeComplete,
|
||||||
@@ -198,9 +193,3 @@ func (c *realityConnWrapper) ConnectionState() ConnectionState {
|
|||||||
func (c *realityConnWrapper) Upstream() any {
|
func (c *realityConnWrapper) Upstream() any {
|
||||||
return c.Conn
|
return c.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Due to low implementation quality, the reality server intercepted half close and caused memory leaks.
|
|
||||||
// We fixed it by calling Close() directly.
|
|
||||||
func (c *realityConnWrapper) CloseWrite() error {
|
|
||||||
return c.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLS
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if options.ECH != nil && options.ECH.Enabled {
|
if options.ECH != nil && options.ECH.Enabled {
|
||||||
return NewECHServer(ctx, logger, options)
|
if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
|
||||||
|
return NewECHServer(ctx, logger, options)
|
||||||
|
}
|
||||||
} else if options.Reality != nil && options.Reality.Enabled {
|
} else if options.Reality != nil && options.Reality.Enabled {
|
||||||
return NewRealityServer(ctx, logger, options)
|
return NewRealityServer(ctx, logger, options)
|
||||||
} else {
|
|
||||||
return NewSTDServer(ctx, logger, options)
|
|
||||||
}
|
}
|
||||||
|
return NewSTDServer(ctx, logger, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
||||||
|
|||||||
@@ -4,16 +4,25 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ ConfigCompat = (*STDClientConfig)(nil)
|
||||||
|
|
||||||
type STDClientConfig struct {
|
type STDClientConfig struct {
|
||||||
config *tls.Config
|
config *tls.Config
|
||||||
}
|
}
|
||||||
@@ -46,6 +55,63 @@ func (s *STDClientConfig) Clone() Config {
|
|||||||
return &STDClientConfig{s.config.Clone()}
|
return &STDClientConfig{s.config.Clone()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type STDECHClientConfig struct {
|
||||||
|
STDClientConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *STDClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||||
|
if len(s.config.EncryptedClientHelloConfigList) == 0 {
|
||||||
|
message := &mDNS.Msg{
|
||||||
|
MsgHdr: mDNS.MsgHdr{
|
||||||
|
RecursionDesired: true,
|
||||||
|
},
|
||||||
|
Question: []mDNS.Question{
|
||||||
|
{
|
||||||
|
Name: mDNS.Fqdn(s.config.ServerName),
|
||||||
|
Qtype: mDNS.TypeHTTPS,
|
||||||
|
Qclass: mDNS.ClassINET,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dnsRouter := service.FromContext[adapter.Router](ctx)
|
||||||
|
response, err := dnsRouter.Exchange(ctx, message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "fetch ECH config list")
|
||||||
|
}
|
||||||
|
if response.Rcode != mDNS.RcodeSuccess {
|
||||||
|
return nil, E.Cause(dns.RCodeError(response.Rcode), "fetch ECH config list")
|
||||||
|
}
|
||||||
|
for _, rr := range response.Answer {
|
||||||
|
switch resource := rr.(type) {
|
||||||
|
case *mDNS.HTTPS:
|
||||||
|
for _, value := range resource.Value {
|
||||||
|
if value.Key().String() == "ech" {
|
||||||
|
echConfigList, err := base64.StdEncoding.DecodeString(value.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "decode ECH config")
|
||||||
|
}
|
||||||
|
s.config.EncryptedClientHelloConfigList = echConfigList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, E.New("no ECH config found in DNS records")
|
||||||
|
}
|
||||||
|
tlsConn, err := s.Client(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = tlsConn.HandshakeContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tlsConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *STDECHClientConfig) Clone() Config {
|
||||||
|
return &STDECHClientConfig{STDClientConfig{s.config.Clone()}}
|
||||||
|
}
|
||||||
|
|
||||||
func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
var serverName string
|
var serverName string
|
||||||
if options.ServerName != "" {
|
if options.ServerName != "" {
|
||||||
@@ -128,5 +194,21 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
|||||||
}
|
}
|
||||||
tlsConfig.RootCAs = certPool
|
tlsConfig.RootCAs = certPool
|
||||||
}
|
}
|
||||||
|
if options.ECH != nil && options.ECH.Enabled {
|
||||||
|
var echConfig []byte
|
||||||
|
if len(options.ECH.Config) > 0 {
|
||||||
|
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
|
||||||
|
} else if options.ECH.ConfigPath != "" {
|
||||||
|
content, err := os.ReadFile(options.ECH.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read ECH config")
|
||||||
|
}
|
||||||
|
echConfig = content
|
||||||
|
}
|
||||||
|
if echConfig != nil {
|
||||||
|
tlsConfig.EncryptedClientHelloConfigList = echConfig
|
||||||
|
}
|
||||||
|
return &STDECHClientConfig{STDClientConfig{&tlsConfig}}, nil
|
||||||
|
}
|
||||||
return &STDClientConfig{&tlsConfig}, nil
|
return &STDClientConfig{&tlsConfig}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package tls
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/pem"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/fswatch"
|
"github.com/sagernet/fswatch"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -15,6 +15,8 @@ import (
|
|||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/cryptobyte"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errInsecureUnused = E.New("tls: insecure unused")
|
var errInsecureUnused = E.New("tls: insecure unused")
|
||||||
@@ -222,12 +224,8 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
|||||||
key = content
|
key = content
|
||||||
}
|
}
|
||||||
if certificate == nil && key == nil && options.Insecure {
|
if certificate == nil && key == nil && options.Insecure {
|
||||||
timeFunc := ntp.TimeFuncFromContext(ctx)
|
|
||||||
if timeFunc == nil {
|
|
||||||
timeFunc = time.Now
|
|
||||||
}
|
|
||||||
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
return GenerateKeyPair(nil, nil, timeFunc, info.ServerName)
|
return GenerateCertificate(ntp.TimeFuncFromContext(ctx), info.ServerName)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if certificate == nil {
|
if certificate == nil {
|
||||||
@@ -243,6 +241,31 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
|||||||
tlsConfig.Certificates = []tls.Certificate{keyPair}
|
tlsConfig.Certificates = []tls.Certificate{keyPair}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if options.ECH != nil && options.ECH.Enabled {
|
||||||
|
var echKey []byte
|
||||||
|
if len(options.ECH.Key) > 0 {
|
||||||
|
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
|
||||||
|
} else if options.ECH.KeyPath != "" {
|
||||||
|
content, err := os.ReadFile(options.ECH.KeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read ECH key")
|
||||||
|
}
|
||||||
|
echKey = content
|
||||||
|
} else {
|
||||||
|
return nil, E.New("missing ECH key")
|
||||||
|
}
|
||||||
|
|
||||||
|
block, rest := pem.Decode(echKey)
|
||||||
|
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
||||||
|
return nil, E.New("invalid ECH keys pem")
|
||||||
|
}
|
||||||
|
|
||||||
|
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse ECH keys")
|
||||||
|
}
|
||||||
|
tlsConfig.EncryptedClientHelloKeys = echKeys
|
||||||
|
}
|
||||||
return &STDServerConfig{
|
return &STDServerConfig{
|
||||||
config: tlsConfig,
|
config: tlsConfig,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
@@ -253,3 +276,22 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
|||||||
keyPath: options.KeyPath,
|
keyPath: options.KeyPath,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {
|
||||||
|
var keys []tls.EncryptedClientHelloKey
|
||||||
|
rawString := cryptobyte.String(raw)
|
||||||
|
for !rawString.Empty() {
|
||||||
|
var key tls.EncryptedClientHelloKey
|
||||||
|
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
|
||||||
|
return nil, E.New("error parsing private key")
|
||||||
|
}
|
||||||
|
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
|
||||||
|
return nil, E.New("error parsing config")
|
||||||
|
}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil, E.New("empty ECH keys")
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/ntp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TimeServiceWrapper struct {
|
|
||||||
ntp.TimeService
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *TimeServiceWrapper) TimeFunc() func() time.Time {
|
|
||||||
if w.TimeService == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return w.TimeService.TimeFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *TimeServiceWrapper) Upstream() any {
|
|
||||||
return w.TimeService
|
|
||||||
}
|
|
||||||
@@ -69,7 +69,6 @@ type utlsConnWrapper struct {
|
|||||||
|
|
||||||
func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState {
|
func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState {
|
||||||
state := c.Conn.ConnectionState()
|
state := c.Conn.ConnectionState()
|
||||||
//nolint:staticcheck
|
|
||||||
return tls.ConnectionState{
|
return tls.ConnectionState{
|
||||||
Version: state.Version,
|
Version: state.Version,
|
||||||
HandshakeComplete: state.HandshakeComplete,
|
HandshakeComplete: state.HandshakeComplete,
|
||||||
|
|||||||
@@ -1,337 +0,0 @@
|
|||||||
package buf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray/bytespool"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/net"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Size of a regular buffer.
|
|
||||||
Size = 8192
|
|
||||||
)
|
|
||||||
|
|
||||||
var zero = [Size * 10]byte{0}
|
|
||||||
|
|
||||||
var pool = bytespool.GetPool(Size)
|
|
||||||
|
|
||||||
// ownership represents the data owner of the buffer.
|
|
||||||
type ownership uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
managed ownership = iota
|
|
||||||
unmanaged
|
|
||||||
bytespools
|
|
||||||
)
|
|
||||||
|
|
||||||
// Buffer is a recyclable allocation of a byte array. Buffer.Release() recycles
|
|
||||||
// the buffer into an internal buffer pool, in order to recreate a buffer more
|
|
||||||
// quickly.
|
|
||||||
type Buffer struct {
|
|
||||||
v []byte
|
|
||||||
start int32
|
|
||||||
end int32
|
|
||||||
ownership ownership
|
|
||||||
UDP *net.Destination
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a Buffer with 0 length and 8K capacity, managed.
|
|
||||||
func New() *Buffer {
|
|
||||||
buf := pool.Get().([]byte)
|
|
||||||
if cap(buf) >= Size {
|
|
||||||
buf = buf[:Size]
|
|
||||||
} else {
|
|
||||||
buf = make([]byte, Size)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Buffer{
|
|
||||||
v: buf,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExisted creates a standard size Buffer with an existed bytearray, managed.
|
|
||||||
func NewExisted(b []byte) *Buffer {
|
|
||||||
if cap(b) < Size {
|
|
||||||
panic("Invalid buffer")
|
|
||||||
}
|
|
||||||
|
|
||||||
oLen := len(b)
|
|
||||||
if oLen < Size {
|
|
||||||
b = b[:Size]
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Buffer{
|
|
||||||
v: b,
|
|
||||||
end: int32(oLen),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromBytes creates a Buffer with an existed bytearray, unmanaged.
|
|
||||||
func FromBytes(b []byte) *Buffer {
|
|
||||||
return &Buffer{
|
|
||||||
v: b,
|
|
||||||
end: int32(len(b)),
|
|
||||||
ownership: unmanaged,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StackNew creates a new Buffer object on stack, managed.
|
|
||||||
// This method is for buffers that is released in the same function.
|
|
||||||
func StackNew() Buffer {
|
|
||||||
buf := pool.Get().([]byte)
|
|
||||||
if cap(buf) >= Size {
|
|
||||||
buf = buf[:Size]
|
|
||||||
} else {
|
|
||||||
buf = make([]byte, Size)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Buffer{
|
|
||||||
v: buf,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWithSize creates a Buffer with 0 length and capacity with at least the given size, bytespool's.
|
|
||||||
func NewWithSize(size int32) *Buffer {
|
|
||||||
return &Buffer{
|
|
||||||
v: bytespool.Alloc(size),
|
|
||||||
ownership: bytespools,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release recycles the buffer into an internal buffer pool.
|
|
||||||
func (b *Buffer) Release() {
|
|
||||||
if b == nil || b.v == nil || b.ownership == unmanaged {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p := b.v
|
|
||||||
b.v = nil
|
|
||||||
b.Clear()
|
|
||||||
|
|
||||||
switch b.ownership {
|
|
||||||
case managed:
|
|
||||||
if cap(p) == Size {
|
|
||||||
pool.Put(p)
|
|
||||||
}
|
|
||||||
case bytespools:
|
|
||||||
bytespool.Free(p)
|
|
||||||
}
|
|
||||||
b.UDP = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear clears the content of the buffer, results an empty buffer with
|
|
||||||
// Len() = 0.
|
|
||||||
func (b *Buffer) Clear() {
|
|
||||||
b.start = 0
|
|
||||||
b.end = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Byte returns the bytes at index.
|
|
||||||
func (b *Buffer) Byte(index int32) byte {
|
|
||||||
return b.v[b.start+index]
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetByte sets the byte value at index.
|
|
||||||
func (b *Buffer) SetByte(index int32, value byte) {
|
|
||||||
b.v[b.start+index] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns the content bytes of this Buffer.
|
|
||||||
func (b *Buffer) Bytes() []byte {
|
|
||||||
return b.v[b.start:b.end]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extend increases the buffer size by n bytes, and returns the extended part.
|
|
||||||
// It panics if result size is larger than buf.Size.
|
|
||||||
func (b *Buffer) Extend(n int32) []byte {
|
|
||||||
end := b.end + n
|
|
||||||
if end > int32(len(b.v)) {
|
|
||||||
panic("extending out of bound")
|
|
||||||
}
|
|
||||||
ext := b.v[b.end:end]
|
|
||||||
b.end = end
|
|
||||||
copy(ext, zero[:])
|
|
||||||
return ext
|
|
||||||
}
|
|
||||||
|
|
||||||
// BytesRange returns a slice of this buffer with given from and to boundary.
|
|
||||||
func (b *Buffer) BytesRange(from, to int32) []byte {
|
|
||||||
if from < 0 {
|
|
||||||
from += b.Len()
|
|
||||||
}
|
|
||||||
if to < 0 {
|
|
||||||
to += b.Len()
|
|
||||||
}
|
|
||||||
return b.v[b.start+from : b.start+to]
|
|
||||||
}
|
|
||||||
|
|
||||||
// BytesFrom returns a slice of this Buffer starting from the given position.
|
|
||||||
func (b *Buffer) BytesFrom(from int32) []byte {
|
|
||||||
if from < 0 {
|
|
||||||
from += b.Len()
|
|
||||||
}
|
|
||||||
return b.v[b.start+from : b.end]
|
|
||||||
}
|
|
||||||
|
|
||||||
// BytesTo returns a slice of this Buffer from start to the given position.
|
|
||||||
func (b *Buffer) BytesTo(to int32) []byte {
|
|
||||||
if to < 0 {
|
|
||||||
to += b.Len()
|
|
||||||
}
|
|
||||||
if to < 0 {
|
|
||||||
to = 0
|
|
||||||
}
|
|
||||||
return b.v[b.start : b.start+to]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check makes sure that 0 <= b.start <= b.end.
|
|
||||||
func (b *Buffer) Check() {
|
|
||||||
if b.start < 0 {
|
|
||||||
b.start = 0
|
|
||||||
}
|
|
||||||
if b.end < 0 {
|
|
||||||
b.end = 0
|
|
||||||
}
|
|
||||||
if b.start > b.end {
|
|
||||||
b.start = b.end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize cuts the buffer at the given position.
|
|
||||||
func (b *Buffer) Resize(from, to int32) {
|
|
||||||
oldEnd := b.end
|
|
||||||
if from < 0 {
|
|
||||||
from += b.Len()
|
|
||||||
}
|
|
||||||
if to < 0 {
|
|
||||||
to += b.Len()
|
|
||||||
}
|
|
||||||
if to < from {
|
|
||||||
panic("Invalid slice")
|
|
||||||
}
|
|
||||||
b.end = b.start + to
|
|
||||||
b.start += from
|
|
||||||
b.Check()
|
|
||||||
if b.end > oldEnd {
|
|
||||||
copy(b.v[oldEnd:b.end], zero[:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance cuts the buffer at the given position.
|
|
||||||
func (b *Buffer) Advance(from int32) {
|
|
||||||
if from < 0 {
|
|
||||||
from += b.Len()
|
|
||||||
}
|
|
||||||
b.start += from
|
|
||||||
b.Check()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the length of the buffer content.
|
|
||||||
func (b *Buffer) Len() int32 {
|
|
||||||
if b == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return b.end - b.start
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap returns the capacity of the buffer content.
|
|
||||||
func (b *Buffer) Cap() int32 {
|
|
||||||
if b == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return int32(len(b.v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEmpty returns true if the buffer is empty.
|
|
||||||
func (b *Buffer) IsEmpty() bool {
|
|
||||||
return b.Len() == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsFull returns true if the buffer has no more room to grow.
|
|
||||||
func (b *Buffer) IsFull() bool {
|
|
||||||
return b != nil && b.end == int32(len(b.v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write implements Write method in io.Writer.
|
|
||||||
func (b *Buffer) Write(data []byte) (int, error) {
|
|
||||||
nBytes := copy(b.v[b.end:], data)
|
|
||||||
b.end += int32(nBytes)
|
|
||||||
return nBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteByte writes a single byte into the buffer.
|
|
||||||
func (b *Buffer) WriteByte(v byte) error {
|
|
||||||
if b.IsFull() {
|
|
||||||
return E.New("buffer full")
|
|
||||||
}
|
|
||||||
b.v[b.end] = v
|
|
||||||
b.end++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteString implements io.StringWriter.
|
|
||||||
func (b *Buffer) WriteString(s string) (int, error) {
|
|
||||||
return b.Write([]byte(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadByte implements io.ByteReader
|
|
||||||
func (b *Buffer) ReadByte() (byte, error) {
|
|
||||||
if b.start == b.end {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
nb := b.v[b.start]
|
|
||||||
b.start++
|
|
||||||
return nb, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBytes implements bufio.Reader.ReadBytes
|
|
||||||
func (b *Buffer) ReadBytes(length int32) ([]byte, error) {
|
|
||||||
if b.end-b.start < length {
|
|
||||||
return nil, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
nb := b.v[b.start : b.start+length]
|
|
||||||
b.start += length
|
|
||||||
return nb, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read implements io.Reader.Read().
|
|
||||||
func (b *Buffer) Read(data []byte) (int, error) {
|
|
||||||
if b.Len() == 0 {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
nBytes := copy(data, b.v[b.start:b.end])
|
|
||||||
if int32(nBytes) == b.Len() {
|
|
||||||
b.Clear()
|
|
||||||
} else {
|
|
||||||
b.start += int32(nBytes)
|
|
||||||
}
|
|
||||||
return nBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFrom implements io.ReaderFrom.
|
|
||||||
func (b *Buffer) ReadFrom(reader io.Reader) (int64, error) {
|
|
||||||
n, err := reader.Read(b.v[b.end:])
|
|
||||||
b.end += int32(n)
|
|
||||||
return int64(n), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFullFrom reads exact size of bytes from given reader, or until error occurs.
|
|
||||||
func (b *Buffer) ReadFullFrom(reader io.Reader, size int32) (int64, error) {
|
|
||||||
end := b.end + size
|
|
||||||
if end > int32(len(b.v)) {
|
|
||||||
v := end
|
|
||||||
return 0, E.New("out of bound: ", v)
|
|
||||||
}
|
|
||||||
n, err := io.ReadFull(reader, b.v[b.end:end])
|
|
||||||
b.end += int32(n)
|
|
||||||
return int64(n), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the string form of this Buffer.
|
|
||||||
func (b *Buffer) String() string {
|
|
||||||
return string(b.Bytes())
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
package buf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray/errors"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/signal"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type dataHandler func(MultiBuffer)
|
|
||||||
|
|
||||||
type copyHandler struct {
|
|
||||||
onData []dataHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
// SizeCounter is for counting bytes copied by Copy().
|
|
||||||
type SizeCounter struct {
|
|
||||||
Size int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyOption is an option for copying data.
|
|
||||||
type CopyOption func(*copyHandler)
|
|
||||||
|
|
||||||
// UpdateActivity is a CopyOption to update activity on each data copy operation.
|
|
||||||
func UpdateActivity(timer signal.ActivityUpdater) CopyOption {
|
|
||||||
return func(handler *copyHandler) {
|
|
||||||
handler.onData = append(handler.onData, func(MultiBuffer) {
|
|
||||||
timer.Update()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountSize is a CopyOption that sums the total size of data copied into the given SizeCounter.
|
|
||||||
func CountSize(sc *SizeCounter) CopyOption {
|
|
||||||
return func(handler *copyHandler) {
|
|
||||||
handler.onData = append(handler.onData, func(b MultiBuffer) {
|
|
||||||
sc.Size += int64(b.Len())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type readError struct {
|
|
||||||
error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e readError) Error() string {
|
|
||||||
return e.error.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e readError) Unwrap() error {
|
|
||||||
return e.error
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsReadError returns true if the error in Copy() comes from reading.
|
|
||||||
func IsReadError(err error) bool {
|
|
||||||
_, ok := err.(readError)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
type writeError struct {
|
|
||||||
error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e writeError) Error() string {
|
|
||||||
return e.error.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e writeError) Unwrap() error {
|
|
||||||
return e.error
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsWriteError returns true if the error in Copy() comes from writing.
|
|
||||||
func IsWriteError(err error) bool {
|
|
||||||
_, ok := err.(writeError)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyInternal(reader Reader, writer Writer, handler *copyHandler) error {
|
|
||||||
for {
|
|
||||||
buffer, err := reader.ReadMultiBuffer()
|
|
||||||
if !buffer.IsEmpty() {
|
|
||||||
for _, handler := range handler.onData {
|
|
||||||
handler(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
if werr := writer.WriteMultiBuffer(buffer); werr != nil {
|
|
||||||
return writeError{werr}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return readError{err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy dumps all payload from reader to writer or stops when an error occurs. It returns nil when EOF.
|
|
||||||
func Copy(reader Reader, writer Writer, options ...CopyOption) error {
|
|
||||||
var handler copyHandler
|
|
||||||
for _, option := range options {
|
|
||||||
option(&handler)
|
|
||||||
}
|
|
||||||
err := copyInternal(reader, writer, &handler)
|
|
||||||
if err != nil && errors.Cause(err) != io.EOF {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrNotTimeoutReader = E.New("not a TimeoutReader")
|
|
||||||
|
|
||||||
func CopyOnceTimeout(reader Reader, writer Writer, timeout time.Duration) error {
|
|
||||||
timeoutReader, ok := reader.(TimeoutReader)
|
|
||||||
if !ok {
|
|
||||||
return ErrNotTimeoutReader
|
|
||||||
}
|
|
||||||
mb, err := timeoutReader.ReadMultiBufferTimeout(timeout)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return writer.WriteMultiBuffer(mb)
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package buf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray/stat"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/stats"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reader extends io.Reader with MultiBuffer.
|
|
||||||
type Reader interface {
|
|
||||||
// ReadMultiBuffer reads content from underlying reader, and put it into a MultiBuffer.
|
|
||||||
ReadMultiBuffer() (MultiBuffer, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrReadTimeout is an error that happens with IO timeout.
|
|
||||||
var ErrReadTimeout = E.New("IO timeout")
|
|
||||||
|
|
||||||
// TimeoutReader is a reader that returns error if Read() operation takes longer than the given timeout.
|
|
||||||
type TimeoutReader interface {
|
|
||||||
ReadMultiBufferTimeout(time.Duration) (MultiBuffer, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writer extends io.Writer with MultiBuffer.
|
|
||||||
type Writer interface {
|
|
||||||
// WriteMultiBuffer writes a MultiBuffer into underlying writer.
|
|
||||||
WriteMultiBuffer(MultiBuffer) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteAllBytes ensures all bytes are written into the given writer.
|
|
||||||
func WriteAllBytes(writer io.Writer, payload []byte, c stats.Counter) error {
|
|
||||||
wc := 0
|
|
||||||
defer func() {
|
|
||||||
if c != nil {
|
|
||||||
c.Add(int64(wc))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for len(payload) > 0 {
|
|
||||||
n, err := writer.Write(payload)
|
|
||||||
wc += n
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
payload = payload[n:]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPacketReader(reader io.Reader) bool {
|
|
||||||
_, ok := reader.(net.PacketConn)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewReader creates a new Reader.
|
|
||||||
// The Reader instance doesn't take the ownership of reader.
|
|
||||||
func NewReader(reader io.Reader) Reader {
|
|
||||||
if mr, ok := reader.(Reader); ok {
|
|
||||||
return mr
|
|
||||||
}
|
|
||||||
|
|
||||||
if isPacketReader(reader) {
|
|
||||||
return &PacketReader{
|
|
||||||
Reader: reader,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &SingleReader{
|
|
||||||
Reader: reader,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPacketReader creates a new PacketReader based on the given reader.
|
|
||||||
func NewPacketReader(reader io.Reader) Reader {
|
|
||||||
if mr, ok := reader.(Reader); ok {
|
|
||||||
return mr
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PacketReader{
|
|
||||||
Reader: reader,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPacketWriter(writer io.Writer) bool {
|
|
||||||
if _, ok := writer.(net.PacketConn); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the writer doesn't implement syscall.Conn, it is probably not a TCP connection.
|
|
||||||
if _, ok := writer.(syscall.Conn); !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWriter creates a new Writer.
|
|
||||||
func NewWriter(writer io.Writer) Writer {
|
|
||||||
if mw, ok := writer.(Writer); ok {
|
|
||||||
return mw
|
|
||||||
}
|
|
||||||
|
|
||||||
iConn := writer
|
|
||||||
if statConn, ok := writer.(*stat.CounterConnection); ok {
|
|
||||||
iConn = statConn.Connection
|
|
||||||
}
|
|
||||||
|
|
||||||
if isPacketWriter(iConn) {
|
|
||||||
return &SequentialWriter{
|
|
||||||
Writer: writer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var counter stats.Counter
|
|
||||||
|
|
||||||
if statConn, ok := writer.(*stat.CounterConnection); ok {
|
|
||||||
counter = statConn.WriteCounter
|
|
||||||
}
|
|
||||||
return &BufferToBytesWriter{
|
|
||||||
Writer: iConn,
|
|
||||||
counter: counter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
package buf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/errors"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/serial"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadAllToBytes reads all content from the reader into a byte array, until EOF.
|
|
||||||
func ReadAllToBytes(reader io.Reader) ([]byte, error) {
|
|
||||||
mb, err := ReadFrom(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if mb.Len() == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
b := make([]byte, mb.Len())
|
|
||||||
mb, _ = SplitBytes(mb, b)
|
|
||||||
ReleaseMulti(mb)
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MultiBuffer is a list of Buffers. The order of Buffer matters.
|
|
||||||
type MultiBuffer []*Buffer
|
|
||||||
|
|
||||||
// MergeMulti merges content from src to dest, and returns the new address of dest and src
|
|
||||||
func MergeMulti(dest MultiBuffer, src MultiBuffer) (MultiBuffer, MultiBuffer) {
|
|
||||||
dest = append(dest, src...)
|
|
||||||
for idx := range src {
|
|
||||||
src[idx] = nil
|
|
||||||
}
|
|
||||||
return dest, src[:0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeBytes merges the given bytes into MultiBuffer and return the new address of the merged MultiBuffer.
|
|
||||||
func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer {
|
|
||||||
n := len(dest)
|
|
||||||
if n > 0 && !(dest)[n-1].IsFull() {
|
|
||||||
nBytes, _ := (dest)[n-1].Write(src)
|
|
||||||
src = src[nBytes:]
|
|
||||||
}
|
|
||||||
|
|
||||||
for len(src) > 0 {
|
|
||||||
b := New()
|
|
||||||
nBytes, _ := b.Write(src)
|
|
||||||
src = src[nBytes:]
|
|
||||||
dest = append(dest, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dest
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReleaseMulti releases all content of the MultiBuffer, and returns an empty MultiBuffer.
|
|
||||||
func ReleaseMulti(mb MultiBuffer) MultiBuffer {
|
|
||||||
for i := range mb {
|
|
||||||
mb[i].Release()
|
|
||||||
mb[i] = nil
|
|
||||||
}
|
|
||||||
return mb[:0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy copied the beginning part of the MultiBuffer into the given byte array.
|
|
||||||
func (mb MultiBuffer) Copy(b []byte) int {
|
|
||||||
total := 0
|
|
||||||
for _, bb := range mb {
|
|
||||||
nBytes := copy(b[total:], bb.Bytes())
|
|
||||||
total += nBytes
|
|
||||||
if int32(nBytes) < bb.Len() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFrom reads all content from reader until EOF.
|
|
||||||
func ReadFrom(reader io.Reader) (MultiBuffer, error) {
|
|
||||||
mb := make(MultiBuffer, 0, 16)
|
|
||||||
for {
|
|
||||||
b := New()
|
|
||||||
_, err := b.ReadFullFrom(reader, Size)
|
|
||||||
if b.IsEmpty() {
|
|
||||||
b.Release()
|
|
||||||
} else {
|
|
||||||
mb = append(mb, b)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if errors.Cause(err) == io.EOF || errors.Cause(err) == io.ErrUnexpectedEOF {
|
|
||||||
return mb, nil
|
|
||||||
}
|
|
||||||
return mb, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SplitBytes splits the given amount of bytes from the beginning of the MultiBuffer.
|
|
||||||
// It returns the new address of MultiBuffer leftover, and number of bytes written into the input byte slice.
|
|
||||||
func SplitBytes(mb MultiBuffer, b []byte) (MultiBuffer, int) {
|
|
||||||
totalBytes := 0
|
|
||||||
endIndex := -1
|
|
||||||
for i := range mb {
|
|
||||||
pBuffer := mb[i]
|
|
||||||
nBytes, _ := pBuffer.Read(b)
|
|
||||||
totalBytes += nBytes
|
|
||||||
b = b[nBytes:]
|
|
||||||
if !pBuffer.IsEmpty() {
|
|
||||||
endIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
pBuffer.Release()
|
|
||||||
mb[i] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if endIndex == -1 {
|
|
||||||
mb = mb[:0]
|
|
||||||
} else {
|
|
||||||
mb = mb[endIndex:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return mb, totalBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
// SplitFirstBytes splits the first buffer from MultiBuffer, and then copy its content into the given slice.
|
|
||||||
func SplitFirstBytes(mb MultiBuffer, p []byte) (MultiBuffer, int) {
|
|
||||||
mb, b := SplitFirst(mb)
|
|
||||||
if b == nil {
|
|
||||||
return mb, 0
|
|
||||||
}
|
|
||||||
n := copy(p, b.Bytes())
|
|
||||||
b.Release()
|
|
||||||
return mb, n
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compact returns another MultiBuffer by merging all content of the given one together.
|
|
||||||
func Compact(mb MultiBuffer) MultiBuffer {
|
|
||||||
if len(mb) == 0 {
|
|
||||||
return mb
|
|
||||||
}
|
|
||||||
|
|
||||||
mb2 := make(MultiBuffer, 0, len(mb))
|
|
||||||
last := mb[0]
|
|
||||||
|
|
||||||
for i := 1; i < len(mb); i++ {
|
|
||||||
curr := mb[i]
|
|
||||||
if last.Len()+curr.Len() > Size {
|
|
||||||
mb2 = append(mb2, last)
|
|
||||||
last = curr
|
|
||||||
} else {
|
|
||||||
common.Must2(last.ReadFrom(curr))
|
|
||||||
curr.Release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mb2 = append(mb2, last)
|
|
||||||
return mb2
|
|
||||||
}
|
|
||||||
|
|
||||||
// SplitFirst splits the first Buffer from the beginning of the MultiBuffer.
|
|
||||||
func SplitFirst(mb MultiBuffer) (MultiBuffer, *Buffer) {
|
|
||||||
if len(mb) == 0 {
|
|
||||||
return mb, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
b := mb[0]
|
|
||||||
mb[0] = nil
|
|
||||||
mb = mb[1:]
|
|
||||||
return mb, b
|
|
||||||
}
|
|
||||||
|
|
||||||
// SplitSize splits the beginning of the MultiBuffer into another one, for at most size bytes.
|
|
||||||
func SplitSize(mb MultiBuffer, size int32) (MultiBuffer, MultiBuffer) {
|
|
||||||
if len(mb) == 0 {
|
|
||||||
return mb, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if mb[0].Len() > size {
|
|
||||||
b := New()
|
|
||||||
copy(b.Extend(size), mb[0].BytesTo(size))
|
|
||||||
mb[0].Advance(size)
|
|
||||||
return mb, MultiBuffer{b}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalBytes := int32(0)
|
|
||||||
var r MultiBuffer
|
|
||||||
endIndex := -1
|
|
||||||
for i := range mb {
|
|
||||||
if totalBytes+mb[i].Len() > size {
|
|
||||||
endIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
totalBytes += mb[i].Len()
|
|
||||||
r = append(r, mb[i])
|
|
||||||
mb[i] = nil
|
|
||||||
}
|
|
||||||
if endIndex == -1 {
|
|
||||||
// To reuse mb array
|
|
||||||
mb = mb[:0]
|
|
||||||
} else {
|
|
||||||
mb = mb[endIndex:]
|
|
||||||
}
|
|
||||||
return mb, r
|
|
||||||
}
|
|
||||||
|
|
||||||
// SplitMulti splits the beginning of the MultiBuffer into first one, the index i and after into second one
|
|
||||||
func SplitMulti(mb MultiBuffer, i int) (MultiBuffer, MultiBuffer) {
|
|
||||||
mb2 := make(MultiBuffer, 0, len(mb))
|
|
||||||
if i < len(mb) && i >= 0 {
|
|
||||||
mb2 = append(mb2, mb[i:]...)
|
|
||||||
for j := i; j < len(mb); j++ {
|
|
||||||
mb[j] = nil
|
|
||||||
}
|
|
||||||
mb = mb[:i]
|
|
||||||
}
|
|
||||||
return mb, mb2
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMultiBuffer writes all buffers from the MultiBuffer to the Writer one by one, and return error if any, with leftover MultiBuffer.
|
|
||||||
func WriteMultiBuffer(writer io.Writer, mb MultiBuffer) (MultiBuffer, error) {
|
|
||||||
for {
|
|
||||||
mb2, b := SplitFirst(mb)
|
|
||||||
mb = mb2
|
|
||||||
if b == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := writer.Write(b.Bytes())
|
|
||||||
b.Release()
|
|
||||||
if err != nil {
|
|
||||||
return mb, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the total number of bytes in the MultiBuffer.
|
|
||||||
func (mb MultiBuffer) Len() int32 {
|
|
||||||
if mb == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
size := int32(0)
|
|
||||||
for _, b := range mb {
|
|
||||||
size += b.Len()
|
|
||||||
}
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEmpty returns true if the MultiBuffer has no content.
|
|
||||||
func (mb MultiBuffer) IsEmpty() bool {
|
|
||||||
for _, b := range mb {
|
|
||||||
if !b.IsEmpty() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the content of the MultiBuffer in string.
|
|
||||||
func (mb MultiBuffer) String() string {
|
|
||||||
v := make([]interface{}, len(mb))
|
|
||||||
for i, b := range mb {
|
|
||||||
v[i] = b
|
|
||||||
}
|
|
||||||
return serial.Concat(v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MultiBufferContainer is a ReadWriteCloser wrapper over MultiBuffer.
|
|
||||||
type MultiBufferContainer struct {
|
|
||||||
MultiBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read implements io.Reader.
|
|
||||||
func (c *MultiBufferContainer) Read(b []byte) (int, error) {
|
|
||||||
if c.MultiBuffer.IsEmpty() {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
mb, nBytes := SplitBytes(c.MultiBuffer, b)
|
|
||||||
c.MultiBuffer = mb
|
|
||||||
return nBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadMultiBuffer implements Reader.
|
|
||||||
func (c *MultiBufferContainer) ReadMultiBuffer() (MultiBuffer, error) {
|
|
||||||
mb := c.MultiBuffer
|
|
||||||
c.MultiBuffer = nil
|
|
||||||
return mb, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write implements io.Writer.
|
|
||||||
func (c *MultiBufferContainer) Write(b []byte) (int, error) {
|
|
||||||
c.MultiBuffer = MergeBytes(c.MultiBuffer, b)
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMultiBuffer implements Writer.
|
|
||||||
func (c *MultiBufferContainer) WriteMultiBuffer(b MultiBuffer) error {
|
|
||||||
mb, _ := MergeMulti(c.MultiBuffer, b)
|
|
||||||
c.MultiBuffer = mb
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements io.Closer.
|
|
||||||
func (c *MultiBufferContainer) Close() error {
|
|
||||||
c.MultiBuffer = ReleaseMulti(c.MultiBuffer)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package buf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/common/xray/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EndpointOverrideReader struct {
|
|
||||||
Reader
|
|
||||||
Dest net.Address
|
|
||||||
OriginalDest net.Address
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *EndpointOverrideReader) ReadMultiBuffer() (MultiBuffer, error) {
|
|
||||||
mb, err := r.Reader.ReadMultiBuffer()
|
|
||||||
if err == nil {
|
|
||||||
for _, b := range mb {
|
|
||||||
if b.UDP != nil && b.UDP.Address == r.OriginalDest {
|
|
||||||
b.UDP.Address = r.Dest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mb, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type EndpointOverrideWriter struct {
|
|
||||||
Writer
|
|
||||||
Dest net.Address
|
|
||||||
OriginalDest net.Address
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *EndpointOverrideWriter) WriteMultiBuffer(mb MultiBuffer) error {
|
|
||||||
for _, b := range mb {
|
|
||||||
if b.UDP != nil && b.UDP.Address == w.Dest {
|
|
||||||
b.UDP.Address = w.OriginalDest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return w.Writer.WriteMultiBuffer(mb)
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
package buf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/errors"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
func readOneUDP(r io.Reader) (*Buffer, error) {
|
|
||||||
b := New()
|
|
||||||
for i := 0; i < 64; i++ {
|
|
||||||
_, err := b.ReadFrom(r)
|
|
||||||
if !b.IsEmpty() {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
b.Release()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Release()
|
|
||||||
return nil, E.New("Reader returns too many empty payloads.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBuffer reads a Buffer from the given reader.
|
|
||||||
func ReadBuffer(r io.Reader) (*Buffer, error) {
|
|
||||||
b := New()
|
|
||||||
n, err := b.ReadFrom(r)
|
|
||||||
if n > 0 {
|
|
||||||
return b, err
|
|
||||||
}
|
|
||||||
b.Release()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// BufferedReader is a Reader that keeps its internal buffer.
|
|
||||||
type BufferedReader struct {
|
|
||||||
// Reader is the underlying reader to be read from
|
|
||||||
Reader Reader
|
|
||||||
// Buffer is the internal buffer to be read from first
|
|
||||||
Buffer MultiBuffer
|
|
||||||
// Splitter is a function to read bytes from MultiBuffer
|
|
||||||
Splitter func(MultiBuffer, []byte) (MultiBuffer, int)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BufferedBytes returns the number of bytes that is cached in this reader.
|
|
||||||
func (r *BufferedReader) BufferedBytes() int32 {
|
|
||||||
return r.Buffer.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadByte implements io.ByteReader.
|
|
||||||
func (r *BufferedReader) ReadByte() (byte, error) {
|
|
||||||
var b [1]byte
|
|
||||||
_, err := r.Read(b[:])
|
|
||||||
return b[0], err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read implements io.Reader. It reads from internal buffer first (if available) and then reads from the underlying reader.
|
|
||||||
func (r *BufferedReader) Read(b []byte) (int, error) {
|
|
||||||
spliter := r.Splitter
|
|
||||||
if spliter == nil {
|
|
||||||
spliter = SplitBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
if !r.Buffer.IsEmpty() {
|
|
||||||
buffer, nBytes := spliter(r.Buffer, b)
|
|
||||||
r.Buffer = buffer
|
|
||||||
if r.Buffer.IsEmpty() {
|
|
||||||
r.Buffer = nil
|
|
||||||
}
|
|
||||||
return nBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mb, err := r.Reader.ReadMultiBuffer()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mb, nBytes := spliter(mb, b)
|
|
||||||
if !mb.IsEmpty() {
|
|
||||||
r.Buffer = mb
|
|
||||||
}
|
|
||||||
return nBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadMultiBuffer implements Reader.
|
|
||||||
func (r *BufferedReader) ReadMultiBuffer() (MultiBuffer, error) {
|
|
||||||
if !r.Buffer.IsEmpty() {
|
|
||||||
mb := r.Buffer
|
|
||||||
r.Buffer = nil
|
|
||||||
return mb, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.Reader.ReadMultiBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadAtMost returns a MultiBuffer with at most size.
|
|
||||||
func (r *BufferedReader) ReadAtMost(size int32) (MultiBuffer, error) {
|
|
||||||
if r.Buffer.IsEmpty() {
|
|
||||||
mb, err := r.Reader.ReadMultiBuffer()
|
|
||||||
if mb.IsEmpty() && err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
r.Buffer = mb
|
|
||||||
}
|
|
||||||
|
|
||||||
rb, mb := SplitSize(r.Buffer, size)
|
|
||||||
r.Buffer = rb
|
|
||||||
if r.Buffer.IsEmpty() {
|
|
||||||
r.Buffer = nil
|
|
||||||
}
|
|
||||||
return mb, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *BufferedReader) writeToInternal(writer io.Writer) (int64, error) {
|
|
||||||
mbWriter := NewWriter(writer)
|
|
||||||
var sc SizeCounter
|
|
||||||
if r.Buffer != nil {
|
|
||||||
sc.Size = int64(r.Buffer.Len())
|
|
||||||
if err := mbWriter.WriteMultiBuffer(r.Buffer); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
r.Buffer = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Copy(r.Reader, mbWriter, CountSize(&sc))
|
|
||||||
return sc.Size, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTo implements io.WriterTo.
|
|
||||||
func (r *BufferedReader) WriteTo(writer io.Writer) (int64, error) {
|
|
||||||
nBytes, err := r.writeToInternal(writer)
|
|
||||||
if errors.Cause(err) == io.EOF {
|
|
||||||
return nBytes, nil
|
|
||||||
}
|
|
||||||
return nBytes, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interrupt implements common.Interruptible.
|
|
||||||
func (r *BufferedReader) Interrupt() {
|
|
||||||
common.Interrupt(r.Reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements io.Closer.
|
|
||||||
func (r *BufferedReader) Close() error {
|
|
||||||
return common.Close(r.Reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SingleReader is a Reader that read one Buffer every time.
|
|
||||||
type SingleReader struct {
|
|
||||||
io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadMultiBuffer implements Reader.
|
|
||||||
func (r *SingleReader) ReadMultiBuffer() (MultiBuffer, error) {
|
|
||||||
b, err := ReadBuffer(r.Reader)
|
|
||||||
return MultiBuffer{b}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PacketReader is a Reader that read one Buffer every time.
|
|
||||||
type PacketReader struct {
|
|
||||||
io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadMultiBuffer implements Reader.
|
|
||||||
func (r *PacketReader) ReadMultiBuffer() (MultiBuffer, error) {
|
|
||||||
b, err := readOneUDP(r.Reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return MultiBuffer{b}, nil
|
|
||||||
}
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
package buf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/errors"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/stats"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BufferToBytesWriter is a Writer that writes alloc.Buffer into underlying writer.
|
|
||||||
type BufferToBytesWriter struct {
|
|
||||||
io.Writer
|
|
||||||
|
|
||||||
counter stats.Counter
|
|
||||||
cache [][]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMultiBuffer implements Writer. This method takes ownership of the given buffer.
|
|
||||||
func (w *BufferToBytesWriter) WriteMultiBuffer(mb MultiBuffer) error {
|
|
||||||
defer ReleaseMulti(mb)
|
|
||||||
|
|
||||||
size := mb.Len()
|
|
||||||
if size == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mb) == 1 {
|
|
||||||
return WriteAllBytes(w.Writer, mb[0].Bytes(), w.counter)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cap(w.cache) < len(mb) {
|
|
||||||
w.cache = make([][]byte, 0, len(mb))
|
|
||||||
}
|
|
||||||
|
|
||||||
bs := w.cache
|
|
||||||
for _, b := range mb {
|
|
||||||
bs = append(bs, b.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
for idx := range bs {
|
|
||||||
bs[idx] = nil
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
nb := net.Buffers(bs)
|
|
||||||
wc := int64(0)
|
|
||||||
defer func() {
|
|
||||||
if w.counter != nil {
|
|
||||||
w.counter.Add(wc)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for size > 0 {
|
|
||||||
n, err := nb.WriteTo(w.Writer)
|
|
||||||
wc += n
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
size -= int32(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFrom implements io.ReaderFrom.
|
|
||||||
func (w *BufferToBytesWriter) ReadFrom(reader io.Reader) (int64, error) {
|
|
||||||
var sc SizeCounter
|
|
||||||
err := Copy(NewReader(reader), w, CountSize(&sc))
|
|
||||||
return sc.Size, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// BufferedWriter is a Writer with internal buffer.
|
|
||||||
type BufferedWriter struct {
|
|
||||||
sync.Mutex
|
|
||||||
writer Writer
|
|
||||||
buffer *Buffer
|
|
||||||
buffered bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBufferedWriter creates a new BufferedWriter.
|
|
||||||
func NewBufferedWriter(writer Writer) *BufferedWriter {
|
|
||||||
return &BufferedWriter{
|
|
||||||
writer: writer,
|
|
||||||
buffer: New(),
|
|
||||||
buffered: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteByte implements io.ByteWriter.
|
|
||||||
func (w *BufferedWriter) WriteByte(c byte) error {
|
|
||||||
return common.Error2(w.Write([]byte{c}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write implements io.Writer.
|
|
||||||
func (w *BufferedWriter) Write(b []byte) (int, error) {
|
|
||||||
if len(b) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Lock()
|
|
||||||
defer w.Unlock()
|
|
||||||
|
|
||||||
if !w.buffered {
|
|
||||||
if writer, ok := w.writer.(io.Writer); ok {
|
|
||||||
return writer.Write(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalBytes := 0
|
|
||||||
for len(b) > 0 {
|
|
||||||
if w.buffer == nil {
|
|
||||||
w.buffer = New()
|
|
||||||
}
|
|
||||||
|
|
||||||
nBytes, err := w.buffer.Write(b)
|
|
||||||
totalBytes += nBytes
|
|
||||||
if err != nil {
|
|
||||||
return totalBytes, err
|
|
||||||
}
|
|
||||||
if !w.buffered || w.buffer.IsFull() {
|
|
||||||
if err := w.flushInternal(); err != nil {
|
|
||||||
return totalBytes, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b = b[nBytes:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMultiBuffer implements Writer. It takes ownership of the given MultiBuffer.
|
|
||||||
func (w *BufferedWriter) WriteMultiBuffer(b MultiBuffer) error {
|
|
||||||
if b.IsEmpty() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Lock()
|
|
||||||
defer w.Unlock()
|
|
||||||
|
|
||||||
if !w.buffered {
|
|
||||||
return w.writer.WriteMultiBuffer(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
reader := MultiBufferContainer{
|
|
||||||
MultiBuffer: b,
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
for !reader.MultiBuffer.IsEmpty() {
|
|
||||||
if w.buffer == nil {
|
|
||||||
w.buffer = New()
|
|
||||||
}
|
|
||||||
common.Must2(w.buffer.ReadFrom(&reader))
|
|
||||||
if w.buffer.IsFull() {
|
|
||||||
if err := w.flushInternal(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush flushes buffered content into underlying writer.
|
|
||||||
func (w *BufferedWriter) Flush() error {
|
|
||||||
w.Lock()
|
|
||||||
defer w.Unlock()
|
|
||||||
|
|
||||||
return w.flushInternal()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *BufferedWriter) flushInternal() error {
|
|
||||||
if w.buffer.IsEmpty() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
b := w.buffer
|
|
||||||
w.buffer = nil
|
|
||||||
|
|
||||||
if writer, ok := w.writer.(io.Writer); ok {
|
|
||||||
err := WriteAllBytes(writer, b.Bytes(), nil)
|
|
||||||
b.Release()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return w.writer.WriteMultiBuffer(MultiBuffer{b})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBuffered sets whether the internal buffer is used. If set to false, Flush() will be called to clear the buffer.
|
|
||||||
func (w *BufferedWriter) SetBuffered(f bool) error {
|
|
||||||
w.Lock()
|
|
||||||
defer w.Unlock()
|
|
||||||
|
|
||||||
w.buffered = f
|
|
||||||
if !f {
|
|
||||||
return w.flushInternal()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFrom implements io.ReaderFrom.
|
|
||||||
func (w *BufferedWriter) ReadFrom(reader io.Reader) (int64, error) {
|
|
||||||
if err := w.SetBuffered(false); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var sc SizeCounter
|
|
||||||
err := Copy(NewReader(reader), w, CountSize(&sc))
|
|
||||||
return sc.Size, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements io.Closable.
|
|
||||||
func (w *BufferedWriter) Close() error {
|
|
||||||
if err := w.Flush(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return common.Close(w.writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SequentialWriter is a Writer that writes MultiBuffer sequentially into the underlying io.Writer.
|
|
||||||
type SequentialWriter struct {
|
|
||||||
io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMultiBuffer implements Writer.
|
|
||||||
func (w *SequentialWriter) WriteMultiBuffer(mb MultiBuffer) error {
|
|
||||||
mb, err := WriteMultiBuffer(w.Writer, mb)
|
|
||||||
ReleaseMulti(mb)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type noOpWriter byte
|
|
||||||
|
|
||||||
func (noOpWriter) WriteMultiBuffer(b MultiBuffer) error {
|
|
||||||
ReleaseMulti(b)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (noOpWriter) Write(b []byte) (int, error) {
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (noOpWriter) ReadFrom(reader io.Reader) (int64, error) {
|
|
||||||
b := New()
|
|
||||||
defer b.Release()
|
|
||||||
|
|
||||||
totalBytes := int64(0)
|
|
||||||
for {
|
|
||||||
b.Clear()
|
|
||||||
_, err := b.ReadFrom(reader)
|
|
||||||
totalBytes += int64(b.Len())
|
|
||||||
if err != nil {
|
|
||||||
if errors.Cause(err) == io.EOF {
|
|
||||||
return totalBytes, nil
|
|
||||||
}
|
|
||||||
return totalBytes, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Discard is a Writer that swallows all contents written in.
|
|
||||||
Discard Writer = noOpWriter(0)
|
|
||||||
|
|
||||||
// DiscardBytes is an io.Writer that swallows all contents written in.
|
|
||||||
DiscardBytes io.Writer = noOpWriter(0)
|
|
||||||
)
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package bytespool
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
func createAllocFunc(size int32) func() interface{} {
|
|
||||||
return func() interface{} {
|
|
||||||
return make([]byte, size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following parameters controls the size of buffer pools.
|
|
||||||
// There are numPools pools. Starting from 2k size, the size of each pool is sizeMulti of the previous one.
|
|
||||||
// Package buf is guaranteed to not use buffers larger than the largest pool.
|
|
||||||
// Other packets may use larger buffers.
|
|
||||||
const (
|
|
||||||
numPools = 4
|
|
||||||
sizeMulti = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
pool [numPools]sync.Pool
|
|
||||||
poolSize [numPools]int32
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
size := int32(2048)
|
|
||||||
for i := 0; i < numPools; i++ {
|
|
||||||
pool[i] = sync.Pool{
|
|
||||||
New: createAllocFunc(size),
|
|
||||||
}
|
|
||||||
poolSize[i] = size
|
|
||||||
size *= sizeMulti
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPool returns a sync.Pool that generates bytes array with at least the given size.
|
|
||||||
// It may return nil if no such pool exists.
|
|
||||||
//
|
|
||||||
// xray:api:stable
|
|
||||||
func GetPool(size int32) *sync.Pool {
|
|
||||||
for idx, ps := range poolSize {
|
|
||||||
if size <= ps {
|
|
||||||
return &pool[idx]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alloc returns a byte slice with at least the given size. Minimum size of returned slice is 2048.
|
|
||||||
//
|
|
||||||
// xray:api:stable
|
|
||||||
func Alloc(size int32) []byte {
|
|
||||||
pool := GetPool(size)
|
|
||||||
if pool != nil {
|
|
||||||
return pool.Get().([]byte)
|
|
||||||
}
|
|
||||||
return make([]byte, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Free puts a byte slice into the internal pool.
|
|
||||||
//
|
|
||||||
// xray:api:stable
|
|
||||||
func Free(b []byte) {
|
|
||||||
size := int32(cap(b))
|
|
||||||
b = b[0:cap(b)]
|
|
||||||
for i := numPools - 1; i >= 0; i-- {
|
|
||||||
if size >= poolSize[i] {
|
|
||||||
pool[i].Put(b)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
// Must panics if err is not nil.
|
|
||||||
func Must(err error) {
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must2 panics if the second parameter is not nil, otherwise returns the first parameter.
|
|
||||||
func Must2(v interface{}, err error) interface{} {
|
|
||||||
Must(err)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error2 returns the err from the 2nd parameter.
|
|
||||||
func Error2(v interface{}, err error) error {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package crypto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"math/big"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RandBetween(from int64, to int64) int64 {
|
|
||||||
if from == to {
|
|
||||||
return from
|
|
||||||
}
|
|
||||||
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
|
|
||||||
return from + bigInt.Int64()
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package errors
|
|
||||||
|
|
||||||
type hasInnerError interface {
|
|
||||||
// Unwrap returns the underlying error of this one.
|
|
||||||
Unwrap() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func Cause(err error) error {
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
L:
|
|
||||||
for {
|
|
||||||
switch inner := err.(type) {
|
|
||||||
case hasInnerError:
|
|
||||||
if inner.Unwrap() == nil {
|
|
||||||
break L
|
|
||||||
}
|
|
||||||
err = inner.Unwrap()
|
|
||||||
default:
|
|
||||||
break L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
// Closable is the interface for objects that can release its resources.
|
|
||||||
//
|
|
||||||
// xray:api:beta
|
|
||||||
type Closable interface {
|
|
||||||
// Close release all resources used by this object, including goroutines.
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interruptible is an interface for objects that can be stopped before its completion.
|
|
||||||
//
|
|
||||||
// xray:api:beta
|
|
||||||
type Interruptible interface {
|
|
||||||
Interrupt()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the obj if it is a Closable.
|
|
||||||
//
|
|
||||||
// xray:api:beta
|
|
||||||
func Close(obj interface{}) error {
|
|
||||||
if c, ok := obj.(Closable); ok {
|
|
||||||
return c.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interrupt calls Interrupt() if object implements Interruptible interface, or Close() if the object implements Closable interface.
|
|
||||||
//
|
|
||||||
// xray:api:beta
|
|
||||||
func Interrupt(obj interface{}) error {
|
|
||||||
if c, ok := obj.(Interruptible); ok {
|
|
||||||
c.Interrupt()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return Close(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runnable is the interface for objects that can start to work and stop on demand.
|
|
||||||
type Runnable interface {
|
|
||||||
// Start starts the runnable object. Upon the method returning nil, the object begins to function properly.
|
|
||||||
Start() error
|
|
||||||
|
|
||||||
Closable
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasType is the interface for objects that knows its type.
|
|
||||||
type HasType interface {
|
|
||||||
// Type returns the type of the object.
|
|
||||||
// Usually it returns (*Type)(nil) of the object.
|
|
||||||
Type() interface{}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package badoption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray/crypto"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Range struct {
|
|
||||||
From int32 `json:"from"`
|
|
||||||
To int32 `json:"to"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Range) Build() *Range {
|
|
||||||
return (*Range)(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Range) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(fmt.Sprintf("%d-%d", c.From, c.To))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Range) UnmarshalJSON(content []byte) error {
|
|
||||||
var stringValue string
|
|
||||||
err := json.Unmarshal(content, &stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
parts := strings.Split(stringValue, "-")
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return E.New("invalid length of range parts")
|
|
||||||
}
|
|
||||||
from, err := strconv.ParseInt(parts[0], 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
to, err := strconv.ParseInt(parts[1], 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if from > to {
|
|
||||||
return E.New("invalid range")
|
|
||||||
}
|
|
||||||
*c = Range{int32(from), int32(to)}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Range) Rand() int32 {
|
|
||||||
return int32(crypto.RandBetween(int64(c.From), int64(c.To)))
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
package net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// LocalHostIP is a constant value for localhost IP in IPv4.
|
|
||||||
LocalHostIP = IPAddress([]byte{127, 0, 0, 1})
|
|
||||||
|
|
||||||
// AnyIP is a constant value for any IP in IPv4.
|
|
||||||
AnyIP = IPAddress([]byte{0, 0, 0, 0})
|
|
||||||
|
|
||||||
// LocalHostDomain is a constant value for localhost domain.
|
|
||||||
LocalHostDomain = DomainAddress("localhost")
|
|
||||||
|
|
||||||
// LocalHostIPv6 is a constant value for localhost IP in IPv6.
|
|
||||||
LocalHostIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
|
||||||
|
|
||||||
// AnyIPv6 is a constant value for any IP in IPv6.
|
|
||||||
AnyIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddressFamily is the type of address.
|
|
||||||
type AddressFamily byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
// AddressFamilyIPv4 represents address as IPv4
|
|
||||||
AddressFamilyIPv4 = AddressFamily(0)
|
|
||||||
|
|
||||||
// AddressFamilyIPv6 represents address as IPv6
|
|
||||||
AddressFamilyIPv6 = AddressFamily(1)
|
|
||||||
|
|
||||||
// AddressFamilyDomain represents address as Domain
|
|
||||||
AddressFamilyDomain = AddressFamily(2)
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsIPv4 returns true if current AddressFamily is IPv4.
|
|
||||||
func (af AddressFamily) IsIPv4() bool {
|
|
||||||
return af == AddressFamilyIPv4
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsIPv6 returns true if current AddressFamily is IPv6.
|
|
||||||
func (af AddressFamily) IsIPv6() bool {
|
|
||||||
return af == AddressFamilyIPv6
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsIP returns true if current AddressFamily is IPv6 or IPv4.
|
|
||||||
func (af AddressFamily) IsIP() bool {
|
|
||||||
return af == AddressFamilyIPv4 || af == AddressFamilyIPv6
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDomain returns true if current AddressFamily is Domain.
|
|
||||||
func (af AddressFamily) IsDomain() bool {
|
|
||||||
return af == AddressFamilyDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address represents a network address to be communicated with. It may be an IP address or domain
|
|
||||||
// address, not both. This interface doesn't resolve IP address for a given domain.
|
|
||||||
type Address interface {
|
|
||||||
IP() net.IP // IP of this Address
|
|
||||||
Domain() string // Domain of this Address
|
|
||||||
Family() AddressFamily
|
|
||||||
|
|
||||||
String() string // String representation of this Address
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAlphaNum(c byte) bool {
|
|
||||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseAddress parses a string into an Address. The return value will be an IPAddress when
|
|
||||||
// the string is in the form of IPv4 or IPv6 address, or a DomainAddress otherwise.
|
|
||||||
func ParseAddress(addr string) Address {
|
|
||||||
// Handle IPv6 address in form as "[2001:4860:0:2001::68]"
|
|
||||||
lenAddr := len(addr)
|
|
||||||
if lenAddr > 0 && addr[0] == '[' && addr[lenAddr-1] == ']' {
|
|
||||||
addr = addr[1 : lenAddr-1]
|
|
||||||
lenAddr -= 2
|
|
||||||
}
|
|
||||||
|
|
||||||
if lenAddr > 0 && (!isAlphaNum(addr[0]) || !isAlphaNum(addr[len(addr)-1])) {
|
|
||||||
addr = strings.TrimSpace(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
ip := net.ParseIP(addr)
|
|
||||||
if ip != nil {
|
|
||||||
return IPAddress(ip)
|
|
||||||
}
|
|
||||||
return DomainAddress(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes0 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
|
||||||
|
|
||||||
// IPAddress creates an Address with given IP.
|
|
||||||
func IPAddress(ip []byte) Address {
|
|
||||||
switch len(ip) {
|
|
||||||
case net.IPv4len:
|
|
||||||
var addr ipv4Address = [4]byte{ip[0], ip[1], ip[2], ip[3]}
|
|
||||||
return addr
|
|
||||||
case net.IPv6len:
|
|
||||||
if bytes.Equal(ip[:10], bytes0) && ip[10] == 0xff && ip[11] == 0xff {
|
|
||||||
return IPAddress(ip[12:16])
|
|
||||||
}
|
|
||||||
var addr ipv6Address = [16]byte{
|
|
||||||
ip[0], ip[1], ip[2], ip[3],
|
|
||||||
ip[4], ip[5], ip[6], ip[7],
|
|
||||||
ip[8], ip[9], ip[10], ip[11],
|
|
||||||
ip[12], ip[13], ip[14], ip[15],
|
|
||||||
}
|
|
||||||
return addr
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DomainAddress creates an Address with given domain.
|
|
||||||
// This is an internal function that forcibly converts a string to domain.
|
|
||||||
// It's mainly used in test files and mux.
|
|
||||||
// Unless you have a specific reason, use net.ParseAddress instead,
|
|
||||||
// as this function does not check whether the input is an IP address.
|
|
||||||
// Otherwise, you will get strange results like domain: 1.1.1.1
|
|
||||||
func DomainAddress(domain string) Address {
|
|
||||||
return domainAddress(domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ipv4Address [4]byte
|
|
||||||
|
|
||||||
func (a ipv4Address) IP() net.IP {
|
|
||||||
return net.IP(a[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ipv4Address) Domain() string {
|
|
||||||
panic("Calling Domain() on an IPv4Address.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ipv4Address) Family() AddressFamily {
|
|
||||||
return AddressFamilyIPv4
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a ipv4Address) String() string {
|
|
||||||
return a.IP().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ipv6Address [16]byte
|
|
||||||
|
|
||||||
func (a ipv6Address) IP() net.IP {
|
|
||||||
return net.IP(a[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ipv6Address) Domain() string {
|
|
||||||
panic("Calling Domain() on an IPv6Address.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ipv6Address) Family() AddressFamily {
|
|
||||||
return AddressFamilyIPv6
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a ipv6Address) String() string {
|
|
||||||
return "[" + a.IP().String() + "]"
|
|
||||||
}
|
|
||||||
|
|
||||||
type domainAddress string
|
|
||||||
|
|
||||||
func (domainAddress) IP() net.IP {
|
|
||||||
panic("Calling IP() on a DomainAddress.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a domainAddress) Domain() string {
|
|
||||||
return string(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (domainAddress) Family() AddressFamily {
|
|
||||||
return AddressFamilyDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a domainAddress) String() string {
|
|
||||||
return a.Domain()
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
package net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Destination represents a network destination including address and protocol (tcp / udp).
|
|
||||||
type Destination struct {
|
|
||||||
Address Address
|
|
||||||
Port Port
|
|
||||||
Network Network
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestinationFromAddr generates a Destination from a net address.
|
|
||||||
func DestinationFromAddr(addr net.Addr) Destination {
|
|
||||||
switch addr := addr.(type) {
|
|
||||||
case *net.TCPAddr:
|
|
||||||
return TCPDestination(IPAddress(addr.IP), Port(addr.Port))
|
|
||||||
case *net.UDPAddr:
|
|
||||||
return UDPDestination(IPAddress(addr.IP), Port(addr.Port))
|
|
||||||
case *net.UnixAddr:
|
|
||||||
return UnixDestination(DomainAddress(addr.Name))
|
|
||||||
default:
|
|
||||||
panic("Net: Unknown address type.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseDestination converts a destination from its string presentation.
|
|
||||||
func ParseDestination(dest string) (Destination, error) {
|
|
||||||
d := Destination{
|
|
||||||
Address: AnyIP,
|
|
||||||
Port: Port(0),
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(dest, "tcp:") {
|
|
||||||
d.Network = Network_TCP
|
|
||||||
dest = dest[4:]
|
|
||||||
} else if strings.HasPrefix(dest, "udp:") {
|
|
||||||
d.Network = Network_UDP
|
|
||||||
dest = dest[4:]
|
|
||||||
} else if strings.HasPrefix(dest, "unix:") {
|
|
||||||
d = UnixDestination(DomainAddress(dest[5:]))
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
hstr, pstr, err := SplitHostPort(dest)
|
|
||||||
if err != nil {
|
|
||||||
return d, err
|
|
||||||
}
|
|
||||||
if len(hstr) > 0 {
|
|
||||||
d.Address = ParseAddress(hstr)
|
|
||||||
}
|
|
||||||
if len(pstr) > 0 {
|
|
||||||
port, err := PortFromString(pstr)
|
|
||||||
if err != nil {
|
|
||||||
return d, err
|
|
||||||
}
|
|
||||||
d.Port = port
|
|
||||||
}
|
|
||||||
return d, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TCPDestination creates a TCP destination with given address
|
|
||||||
func TCPDestination(address Address, port Port) Destination {
|
|
||||||
return Destination{
|
|
||||||
Network: Network_TCP,
|
|
||||||
Address: address,
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UDPDestination creates a UDP destination with given address
|
|
||||||
func UDPDestination(address Address, port Port) Destination {
|
|
||||||
return Destination{
|
|
||||||
Network: Network_UDP,
|
|
||||||
Address: address,
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnixDestination creates a Unix destination with given address
|
|
||||||
func UnixDestination(address Address) Destination {
|
|
||||||
return Destination{
|
|
||||||
Network: Network_UNIX,
|
|
||||||
Address: address,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NetAddr returns the network address in this Destination in string form.
|
|
||||||
func (d Destination) NetAddr() string {
|
|
||||||
addr := ""
|
|
||||||
if d.Network == Network_TCP || d.Network == Network_UDP {
|
|
||||||
addr = d.Address.String() + ":" + d.Port.String()
|
|
||||||
} else if d.Network == Network_UNIX {
|
|
||||||
addr = d.Address.String()
|
|
||||||
}
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawNetAddr converts a net.Addr from its Destination presentation.
|
|
||||||
func (d Destination) RawNetAddr() net.Addr {
|
|
||||||
var addr net.Addr
|
|
||||||
switch d.Network {
|
|
||||||
case Network_TCP:
|
|
||||||
if d.Address.Family().IsIP() {
|
|
||||||
addr = &net.TCPAddr{
|
|
||||||
IP: d.Address.IP(),
|
|
||||||
Port: int(d.Port),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case Network_UDP:
|
|
||||||
if d.Address.Family().IsIP() {
|
|
||||||
addr = &net.UDPAddr{
|
|
||||||
IP: d.Address.IP(),
|
|
||||||
Port: int(d.Port),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case Network_UNIX:
|
|
||||||
if d.Address.Family().IsDomain() {
|
|
||||||
addr = &net.UnixAddr{
|
|
||||||
Name: d.Address.String(),
|
|
||||||
Net: d.Network.SystemString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the strings form of this Destination.
|
|
||||||
func (d Destination) String() string {
|
|
||||||
prefix := "unknown:"
|
|
||||||
switch d.Network {
|
|
||||||
case Network_TCP:
|
|
||||||
prefix = "tcp:"
|
|
||||||
case Network_UDP:
|
|
||||||
prefix = "udp:"
|
|
||||||
case Network_UNIX:
|
|
||||||
prefix = "unix:"
|
|
||||||
}
|
|
||||||
return prefix + d.NetAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValid returns true if this Destination is valid.
|
|
||||||
func (d Destination) IsValid() bool {
|
|
||||||
return d.Network != Network_Unknown
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package net
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// defines the maximum time an idle TCP session can survive in the tunnel, so
|
|
||||||
// it should be consistent across HTTP versions and with other transports.
|
|
||||||
const ConnIdleTimeout = 300 * time.Second
|
|
||||||
|
|
||||||
// consistent with quic-go
|
|
||||||
const QuicgoH3KeepAlivePeriod = 10 * time.Second
|
|
||||||
|
|
||||||
// consistent with chrome
|
|
||||||
const ChromeH2KeepAlivePeriod = 45 * time.Second
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package net
|
|
||||||
|
|
||||||
type Network int32
|
|
||||||
|
|
||||||
const (
|
|
||||||
Network_Unknown Network = 0
|
|
||||||
Network_TCP Network = 2
|
|
||||||
Network_UDP Network = 3
|
|
||||||
Network_UNIX Network = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
func (n Network) SystemString() string {
|
|
||||||
switch n {
|
|
||||||
case Network_TCP:
|
|
||||||
return "tcp"
|
|
||||||
case Network_UDP:
|
|
||||||
return "udp"
|
|
||||||
case Network_UNIX:
|
|
||||||
return "unix"
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasNetwork returns true if the network list has a certain network.
|
|
||||||
func HasNetwork(list []Network, network Network) bool {
|
|
||||||
for _, value := range list {
|
|
||||||
if value == network {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package net
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Port represents a network port in TCP and UDP protocol.
|
|
||||||
type Port uint16
|
|
||||||
|
|
||||||
// PortFromBytes converts a byte array to a Port, assuming bytes are in big endian order.
|
|
||||||
// @unsafe Caller must ensure that the byte array has at least 2 elements.
|
|
||||||
func PortFromBytes(port []byte) Port {
|
|
||||||
return Port(binary.BigEndian.Uint16(port))
|
|
||||||
}
|
|
||||||
|
|
||||||
// PortFromInt converts an integer to a Port.
|
|
||||||
// @error when the integer is not positive or larger then 65535
|
|
||||||
func PortFromInt(val uint32) (Port, error) {
|
|
||||||
if val > 65535 {
|
|
||||||
return Port(0), E.New("invalid port range: ", val)
|
|
||||||
}
|
|
||||||
return Port(val), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PortFromString converts a string to a Port.
|
|
||||||
// @error when the string is not an integer or the integral value is a not a valid Port.
|
|
||||||
func PortFromString(s string) (Port, error) {
|
|
||||||
val, err := strconv.ParseUint(s, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return Port(0), E.New("invalid port range: ", s)
|
|
||||||
}
|
|
||||||
return PortFromInt(uint32(val))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value return the corresponding uint16 value of a Port.
|
|
||||||
func (p Port) Value() uint16 {
|
|
||||||
return uint16(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the string presentation of a Port.
|
|
||||||
func (p Port) String() string {
|
|
||||||
return strconv.Itoa(int(p))
|
|
||||||
}
|
|
||||||
|
|
||||||
type MemoryPortRange struct {
|
|
||||||
From Port
|
|
||||||
To Port
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r MemoryPortRange) Contains(port Port) bool {
|
|
||||||
return r.From <= port && port <= r.To
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package net
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
// DialTCP is an alias of net.DialTCP.
|
|
||||||
var (
|
|
||||||
DialTCP = net.DialTCP
|
|
||||||
DialUDP = net.DialUDP
|
|
||||||
DialUnix = net.DialUnix
|
|
||||||
Dial = net.Dial
|
|
||||||
)
|
|
||||||
|
|
||||||
type ListenConfig = net.ListenConfig
|
|
||||||
|
|
||||||
var (
|
|
||||||
Listen = net.Listen
|
|
||||||
ListenTCP = net.ListenTCP
|
|
||||||
ListenUDP = net.ListenUDP
|
|
||||||
ListenUnix = net.ListenUnix
|
|
||||||
)
|
|
||||||
|
|
||||||
var LookupIP = net.LookupIP
|
|
||||||
|
|
||||||
var FileConn = net.FileConn
|
|
||||||
|
|
||||||
// ParseIP is an alias of net.ParseIP
|
|
||||||
var ParseIP = net.ParseIP
|
|
||||||
|
|
||||||
var SplitHostPort = net.SplitHostPort
|
|
||||||
|
|
||||||
var CIDRMask = net.CIDRMask
|
|
||||||
|
|
||||||
type (
|
|
||||||
Addr = net.Addr
|
|
||||||
Conn = net.Conn
|
|
||||||
PacketConn = net.PacketConn
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
TCPAddr = net.TCPAddr
|
|
||||||
TCPConn = net.TCPConn
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
UDPAddr = net.UDPAddr
|
|
||||||
UDPConn = net.UDPConn
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
UnixAddr = net.UnixAddr
|
|
||||||
UnixConn = net.UnixConn
|
|
||||||
)
|
|
||||||
|
|
||||||
// IP is an alias for net.IP.
|
|
||||||
type (
|
|
||||||
IP = net.IP
|
|
||||||
IPMask = net.IPMask
|
|
||||||
IPNet = net.IPNet
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
IPv4len = net.IPv4len
|
|
||||||
IPv6len = net.IPv6len
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Error = net.Error
|
|
||||||
AddrError = net.AddrError
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Dialer = net.Dialer
|
|
||||||
Listener = net.Listener
|
|
||||||
TCPListener = net.TCPListener
|
|
||||||
UnixListener = net.UnixListener
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ResolveTCPAddr = net.ResolveTCPAddr
|
|
||||||
ResolveUDPAddr = net.ResolveUDPAddr
|
|
||||||
ResolveUnixAddr = net.ResolveUnixAddr
|
|
||||||
)
|
|
||||||
|
|
||||||
type Resolver = net.Resolver
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
package pipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/buf"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/signal"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/signal/done"
|
|
||||||
)
|
|
||||||
|
|
||||||
type state byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
open state = iota
|
|
||||||
closed
|
|
||||||
errord
|
|
||||||
)
|
|
||||||
|
|
||||||
type pipeOption struct {
|
|
||||||
limit int32 // maximum buffer size in bytes
|
|
||||||
discardOverflow bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *pipeOption) isFull(curSize int32) bool {
|
|
||||||
return o.limit >= 0 && curSize > o.limit
|
|
||||||
}
|
|
||||||
|
|
||||||
type pipe struct {
|
|
||||||
sync.Mutex
|
|
||||||
data buf.MultiBuffer
|
|
||||||
readSignal *signal.Notifier
|
|
||||||
writeSignal *signal.Notifier
|
|
||||||
done *done.Instance
|
|
||||||
errChan chan error
|
|
||||||
option pipeOption
|
|
||||||
state state
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
errBufferFull = errors.New("buffer full")
|
|
||||||
errSlowDown = errors.New("slow down")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *pipe) Len() int32 {
|
|
||||||
data := p.data
|
|
||||||
if data == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return data.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pipe) getState(forRead bool) error {
|
|
||||||
switch p.state {
|
|
||||||
case open:
|
|
||||||
if !forRead && p.option.isFull(p.data.Len()) {
|
|
||||||
return errBufferFull
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case closed:
|
|
||||||
if !forRead {
|
|
||||||
return io.ErrClosedPipe
|
|
||||||
}
|
|
||||||
if !p.data.IsEmpty() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return io.EOF
|
|
||||||
case errord:
|
|
||||||
return io.ErrClosedPipe
|
|
||||||
default:
|
|
||||||
panic("impossible case")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pipe) readMultiBufferInternal() (buf.MultiBuffer, error) {
|
|
||||||
p.Lock()
|
|
||||||
defer p.Unlock()
|
|
||||||
|
|
||||||
if err := p.getState(true); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data := p.data
|
|
||||||
p.data = nil
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pipe) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
|
||||||
for {
|
|
||||||
data, err := p.readMultiBufferInternal()
|
|
||||||
if data != nil || err != nil {
|
|
||||||
p.writeSignal.Signal()
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-p.readSignal.Wait():
|
|
||||||
case <-p.done.Wait():
|
|
||||||
case err = <-p.errChan:
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pipe) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) {
|
|
||||||
timer := time.NewTimer(d)
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
data, err := p.readMultiBufferInternal()
|
|
||||||
if data != nil || err != nil {
|
|
||||||
p.writeSignal.Signal()
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-p.readSignal.Wait():
|
|
||||||
case <-p.done.Wait():
|
|
||||||
case <-timer.C:
|
|
||||||
return nil, buf.ErrReadTimeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pipe) writeMultiBufferInternal(mb buf.MultiBuffer) error {
|
|
||||||
p.Lock()
|
|
||||||
defer p.Unlock()
|
|
||||||
|
|
||||||
if err := p.getState(false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.data == nil {
|
|
||||||
p.data = mb
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.data, _ = buf.MergeMulti(p.data, mb)
|
|
||||||
return errSlowDown
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
|
||||||
if mb.IsEmpty() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
err := p.writeMultiBufferInternal(mb)
|
|
||||||
if err == nil {
|
|
||||||
p.readSignal.Signal()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == errSlowDown {
|
|
||||||
p.readSignal.Signal()
|
|
||||||
|
|
||||||
// Yield current goroutine. Hopefully the reading counterpart can pick up the payload.
|
|
||||||
runtime.Gosched()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == errBufferFull && p.option.discardOverflow {
|
|
||||||
buf.ReleaseMulti(mb)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != errBufferFull {
|
|
||||||
buf.ReleaseMulti(mb)
|
|
||||||
p.readSignal.Signal()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-p.writeSignal.Wait():
|
|
||||||
case <-p.done.Wait():
|
|
||||||
return io.ErrClosedPipe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pipe) Close() error {
|
|
||||||
p.Lock()
|
|
||||||
defer p.Unlock()
|
|
||||||
|
|
||||||
if p.state == closed || p.state == errord {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.state = closed
|
|
||||||
common.Must(p.done.Close())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interrupt implements common.Interruptible.
|
|
||||||
func (p *pipe) Interrupt() {
|
|
||||||
p.Lock()
|
|
||||||
defer p.Unlock()
|
|
||||||
|
|
||||||
if p.state == closed || p.state == errord {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p.state = errord
|
|
||||||
|
|
||||||
if !p.data.IsEmpty() {
|
|
||||||
buf.ReleaseMulti(p.data)
|
|
||||||
p.data = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
common.Must(p.done.Close())
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package pipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/common/xray/signal"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/signal/done"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Option for creating new Pipes.
|
|
||||||
type Option func(*pipeOption)
|
|
||||||
|
|
||||||
// WithoutSizeLimit returns an Option for Pipe to have no size limit.
|
|
||||||
func WithoutSizeLimit() Option {
|
|
||||||
return func(opt *pipeOption) {
|
|
||||||
opt.limit = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSizeLimit returns an Option for Pipe to have the given size limit.
|
|
||||||
func WithSizeLimit(limit int32) Option {
|
|
||||||
return func(opt *pipeOption) {
|
|
||||||
opt.limit = limit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiscardOverflow returns an Option for Pipe to discard writes if full.
|
|
||||||
func DiscardOverflow() Option {
|
|
||||||
return func(opt *pipeOption) {
|
|
||||||
opt.discardOverflow = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new Reader and Writer that connects to each other.
|
|
||||||
func New(opts ...Option) (*Reader, *Writer) {
|
|
||||||
p := &pipe{
|
|
||||||
readSignal: signal.NewNotifier(),
|
|
||||||
writeSignal: signal.NewNotifier(),
|
|
||||||
done: done.New(),
|
|
||||||
errChan: make(chan error, 1),
|
|
||||||
option: pipeOption{
|
|
||||||
limit: -1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(&(p.option))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Reader{
|
|
||||||
pipe: p,
|
|
||||||
}, &Writer{
|
|
||||||
pipe: p,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package pipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray/buf"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reader is a buf.Reader that reads content from a pipe.
|
|
||||||
type Reader struct {
|
|
||||||
pipe *pipe
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadMultiBuffer implements buf.Reader.
|
|
||||||
func (r *Reader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
|
||||||
return r.pipe.ReadMultiBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadMultiBufferTimeout reads content from a pipe within the given duration, or returns buf.ErrTimeout otherwise.
|
|
||||||
func (r *Reader) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) {
|
|
||||||
return r.pipe.ReadMultiBufferTimeout(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interrupt implements common.Interruptible.
|
|
||||||
func (r *Reader) Interrupt() {
|
|
||||||
r.pipe.Interrupt()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReturnAnError makes ReadMultiBuffer return an error, only once.
|
|
||||||
func (r *Reader) ReturnAnError(err error) {
|
|
||||||
r.pipe.errChan <- err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recover catches an error set by ReturnAnError, if exists.
|
|
||||||
func (r *Reader) Recover() (err error) {
|
|
||||||
select {
|
|
||||||
case err = <-r.pipe.errChan:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package pipe
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing-box/common/xray/buf"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Writer is a buf.Writer that writes data into a pipe.
|
|
||||||
type Writer struct {
|
|
||||||
pipe *pipe
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMultiBuffer implements buf.Writer.
|
|
||||||
func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
|
||||||
return w.pipe.WriteMultiBuffer(mb)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close implements io.Closer. After the pipe is closed, writing to the pipe will return io.ErrClosedPipe, while reading will return io.EOF.
|
|
||||||
func (w *Writer) Close() error {
|
|
||||||
return w.pipe.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) Len() int32 {
|
|
||||||
return w.pipe.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interrupt implements common.Interruptible.
|
|
||||||
func (w *Writer) Interrupt() {
|
|
||||||
w.pipe.Interrupt()
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package serial
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadUint16 reads first two bytes from the reader, and then converts them to an uint16 value.
|
|
||||||
func ReadUint16(reader io.Reader) (uint16, error) {
|
|
||||||
var b [2]byte
|
|
||||||
if _, err := io.ReadFull(reader, b[:]); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return binary.BigEndian.Uint16(b[:]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteUint16 writes an uint16 value into writer.
|
|
||||||
func WriteUint16(writer io.Writer, value uint16) (int, error) {
|
|
||||||
var b [2]byte
|
|
||||||
binary.BigEndian.PutUint16(b[:], value)
|
|
||||||
return writer.Write(b[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteUint64 writes an uint64 value into writer.
|
|
||||||
func WriteUint64(writer io.Writer, value uint64) (int, error) {
|
|
||||||
var b [8]byte
|
|
||||||
binary.BigEndian.PutUint64(b[:], value)
|
|
||||||
return writer.Write(b[:])
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package serial
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ToString serializes an arbitrary value into string.
|
|
||||||
func ToString(v interface{}) string {
|
|
||||||
if v == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
switch value := v.(type) {
|
|
||||||
case string:
|
|
||||||
return value
|
|
||||||
case *string:
|
|
||||||
return *value
|
|
||||||
case fmt.Stringer:
|
|
||||||
return value.String()
|
|
||||||
case error:
|
|
||||||
return value.Error()
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%+v", value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Concat concatenates all input into a single string.
|
|
||||||
func Concat(v ...interface{}) string {
|
|
||||||
builder := strings.Builder{}
|
|
||||||
for _, value := range v {
|
|
||||||
builder.WriteString(ToString(value))
|
|
||||||
}
|
|
||||||
return builder.String()
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package done
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Instance is a utility for notifications of something being done.
|
|
||||||
type Instance struct {
|
|
||||||
access sync.Mutex
|
|
||||||
c chan struct{}
|
|
||||||
closed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a new Done.
|
|
||||||
func New() *Instance {
|
|
||||||
return &Instance{
|
|
||||||
c: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Done returns true if Close() is called.
|
|
||||||
func (d *Instance) Done() bool {
|
|
||||||
select {
|
|
||||||
case <-d.Wait():
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait returns a channel for waiting for done.
|
|
||||||
func (d *Instance) Wait() <-chan struct{} {
|
|
||||||
return d.c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close marks this Done 'done'. This method may be called multiple times. All calls after first call will have no effect on its status.
|
|
||||||
func (d *Instance) Close() error {
|
|
||||||
d.access.Lock()
|
|
||||||
defer d.access.Unlock()
|
|
||||||
|
|
||||||
if d.closed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
d.closed = true
|
|
||||||
close(d.c)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package signal
|
|
||||||
|
|
||||||
// Notifier is a utility for notifying changes. The change producer may notify changes multiple time, and the consumer may get notified asynchronously.
|
|
||||||
type Notifier struct {
|
|
||||||
c chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewNotifier creates a new Notifier.
|
|
||||||
func NewNotifier() *Notifier {
|
|
||||||
return &Notifier{
|
|
||||||
c: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signal signals a change, usually by producer. This method never blocks.
|
|
||||||
func (n *Notifier) Signal() {
|
|
||||||
select {
|
|
||||||
case n.c <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait returns a channel for waiting for changes. The returned channel never gets closed.
|
|
||||||
func (n *Notifier) Wait() <-chan struct{} {
|
|
||||||
return n.c
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
package pubsub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/signal/done"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/task"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Subscriber struct {
|
|
||||||
buffer chan interface{}
|
|
||||||
done *done.Instance
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subscriber) push(msg interface{}) {
|
|
||||||
select {
|
|
||||||
case s.buffer <- msg:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subscriber) Wait() <-chan interface{} {
|
|
||||||
return s.buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subscriber) Close() error {
|
|
||||||
return s.done.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subscriber) IsClosed() bool {
|
|
||||||
return s.done.Done()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
sync.RWMutex
|
|
||||||
subs map[string][]*Subscriber
|
|
||||||
ctask *task.Periodic
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService() *Service {
|
|
||||||
s := &Service{
|
|
||||||
subs: make(map[string][]*Subscriber),
|
|
||||||
}
|
|
||||||
s.ctask = &task.Periodic{
|
|
||||||
Execute: s.Cleanup,
|
|
||||||
Interval: time.Second * 30,
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup cleans up internal caches of subscribers.
|
|
||||||
// Visible for testing only.
|
|
||||||
func (s *Service) Cleanup() error {
|
|
||||||
s.Lock()
|
|
||||||
defer s.Unlock()
|
|
||||||
|
|
||||||
if len(s.subs) == 0 {
|
|
||||||
return errors.New("nothing to do")
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, subs := range s.subs {
|
|
||||||
newSub := make([]*Subscriber, 0, len(s.subs))
|
|
||||||
for _, sub := range subs {
|
|
||||||
if !sub.IsClosed() {
|
|
||||||
newSub = append(newSub, sub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(newSub) == 0 {
|
|
||||||
delete(s.subs, name)
|
|
||||||
} else {
|
|
||||||
s.subs[name] = newSub
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.subs) == 0 {
|
|
||||||
s.subs = make(map[string][]*Subscriber)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) Subscribe(name string) *Subscriber {
|
|
||||||
sub := &Subscriber{
|
|
||||||
buffer: make(chan interface{}, 16),
|
|
||||||
done: done.New(),
|
|
||||||
}
|
|
||||||
s.Lock()
|
|
||||||
s.subs[name] = append(s.subs[name], sub)
|
|
||||||
s.Unlock()
|
|
||||||
common.Must(s.ctask.Start())
|
|
||||||
return sub
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) Publish(name string, message interface{}) {
|
|
||||||
s.RLock()
|
|
||||||
defer s.RUnlock()
|
|
||||||
|
|
||||||
for _, sub := range s.subs[name] {
|
|
||||||
if !sub.IsClosed() {
|
|
||||||
sub.push(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package semaphore
|
|
||||||
|
|
||||||
// Instance is an implementation of semaphore.
|
|
||||||
type Instance struct {
|
|
||||||
token chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// New create a new Semaphore with n permits.
|
|
||||||
func New(n int) *Instance {
|
|
||||||
s := &Instance{
|
|
||||||
token: make(chan struct{}, n),
|
|
||||||
}
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
s.token <- struct{}{}
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait returns a channel for acquiring a permit.
|
|
||||||
func (s *Instance) Wait() <-chan struct{} {
|
|
||||||
return s.token
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signal releases a permit into the semaphore.
|
|
||||||
func (s *Instance) Signal() {
|
|
||||||
s.token <- struct{}{}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package signal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/task"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ActivityUpdater interface {
|
|
||||||
Update()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActivityTimer struct {
|
|
||||||
sync.RWMutex
|
|
||||||
updated chan struct{}
|
|
||||||
checkTask *task.Periodic
|
|
||||||
onTimeout func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *ActivityTimer) Update() {
|
|
||||||
select {
|
|
||||||
case t.updated <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *ActivityTimer) check() error {
|
|
||||||
select {
|
|
||||||
case <-t.updated:
|
|
||||||
default:
|
|
||||||
t.finish()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *ActivityTimer) finish() {
|
|
||||||
t.Lock()
|
|
||||||
defer t.Unlock()
|
|
||||||
|
|
||||||
if t.onTimeout != nil {
|
|
||||||
t.onTimeout()
|
|
||||||
t.onTimeout = nil
|
|
||||||
}
|
|
||||||
if t.checkTask != nil {
|
|
||||||
t.checkTask.Close()
|
|
||||||
t.checkTask = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
|
|
||||||
if timeout == 0 {
|
|
||||||
t.finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
checkTask := &task.Periodic{
|
|
||||||
Interval: timeout,
|
|
||||||
Execute: t.check,
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Lock()
|
|
||||||
|
|
||||||
if t.checkTask != nil {
|
|
||||||
t.checkTask.Close()
|
|
||||||
}
|
|
||||||
t.checkTask = checkTask
|
|
||||||
t.Unlock()
|
|
||||||
t.Update()
|
|
||||||
common.Must(checkTask.Start())
|
|
||||||
}
|
|
||||||
|
|
||||||
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {
|
|
||||||
timer := &ActivityTimer{
|
|
||||||
updated: make(chan struct{}, 1),
|
|
||||||
onTimeout: cancel,
|
|
||||||
}
|
|
||||||
timer.SetTimeout(timeout)
|
|
||||||
return timer
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package stat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray/stats"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Connection interface {
|
|
||||||
net.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
type CounterConnection struct {
|
|
||||||
Connection
|
|
||||||
ReadCounter stats.Counter
|
|
||||||
WriteCounter stats.Counter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CounterConnection) Read(b []byte) (int, error) {
|
|
||||||
nBytes, err := c.Connection.Read(b)
|
|
||||||
if c.ReadCounter != nil {
|
|
||||||
c.ReadCounter.Add(int64(nBytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nBytes, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CounterConnection) Write(b []byte) (int, error) {
|
|
||||||
nBytes, err := c.Connection.Write(b)
|
|
||||||
if c.WriteCounter != nil {
|
|
||||||
c.WriteCounter.Add(int64(nBytes))
|
|
||||||
}
|
|
||||||
return nBytes, err
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user