mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-12 01:57:18 +10:00
Compare commits
64 Commits
v1.12.0-al
...
dev-mitm-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cca8893c9 | ||
|
|
58fdae33bb | ||
|
|
29ad1e3888 | ||
|
|
799a005ead | ||
|
|
0d5737dded | ||
|
|
6b6f02ea0f | ||
|
|
6c0785eb50 | ||
|
|
74f9bec2ba | ||
|
|
405560622a | ||
|
|
b5740dae0e | ||
|
|
4c8b868553 | ||
|
|
967940cfcc | ||
|
|
236e0765a5 | ||
|
|
fcc5ec8a5d | ||
|
|
3012c47695 | ||
|
|
2bc58c5f11 | ||
|
|
d5e174c2a8 | ||
|
|
5f5d5f8b3d | ||
|
|
87f122b2ab | ||
|
|
c7cc28d61f | ||
|
|
ba8b85f0d7 | ||
|
|
3419df90e2 | ||
|
|
3348c65c9b | ||
|
|
c9d6848eae | ||
|
|
9615f48327 | ||
|
|
1823bbde91 | ||
|
|
3834c1f5cd | ||
|
|
fc3424af15 | ||
|
|
f031d759ae | ||
|
|
e1fc66f072 | ||
|
|
00a0911252 | ||
|
|
e16c021308 | ||
|
|
9b5d9588ab | ||
|
|
7a1fb6b825 | ||
|
|
fa64180e49 | ||
|
|
9319e58a61 | ||
|
|
f2f50e7f79 | ||
|
|
3ea305b882 | ||
|
|
1c4bcefa8f | ||
|
|
e5d12c9b72 | ||
|
|
6a75487b34 | ||
|
|
fa4ed37d23 | ||
|
|
219cc6a944 | ||
|
|
0ff9e3833e | ||
|
|
a71b583a1a | ||
|
|
1462208d68 | ||
|
|
16babfc8ac | ||
|
|
a35c5d0fae | ||
|
|
259dae4399 | ||
|
|
96dabc048f | ||
|
|
833dbe7cb8 | ||
|
|
b810f34c30 | ||
|
|
e0a2e9412b | ||
|
|
f43974dd8c | ||
|
|
6bfad0200e | ||
|
|
045fb72420 | ||
|
|
ab4ea6ed68 | ||
|
|
cf72196a1a | ||
|
|
f56131f38e | ||
|
|
273a11d550 | ||
|
|
ae8ce75e41 | ||
|
|
d6d94b689f | ||
|
|
30d785f1ee | ||
|
|
db5ec3cdfc |
19
.fpm
Normal file
19
.fpm
Normal file
@@ -0,0 +1,19 @@
|
||||
-s dir
|
||||
--name sing-box
|
||||
--category net
|
||||
--license GPLv3-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"
|
||||
|
||||
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
|
||||
7
.github/setup_legacy_go.sh
vendored
7
.github/setup_legacy_go.sh
vendored
@@ -1,10 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION="1.23.6"
|
||||
|
||||
mkdir -p $HOME/go
|
||||
cd $HOME/go
|
||||
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
|
||||
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
|
||||
mv go $HOME/go/go_legacy
|
||||
cd $HOME/go/go_legacy
|
||||
mv go go_legacy
|
||||
cd go_legacy
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# this patch file only works on golang1.23.x
|
||||
|
||||
267
.github/workflows/build.yml
vendored
267
.github/workflows/build.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
echo "version=${{ inputs.version }}"
|
||||
echo "version=${{ inputs.version }}"
|
||||
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Calculate version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
@@ -68,73 +68,42 @@ jobs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ linux, windows, darwin, android ]
|
||||
arch: [ "386", amd64, arm64 ]
|
||||
legacy_go: [ false ]
|
||||
include:
|
||||
- name: linux_386
|
||||
goos: linux
|
||||
goarch: 386
|
||||
- name: linux_amd64
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- name: linux_arm64
|
||||
goos: linux
|
||||
goarch: arm64
|
||||
- name: linux_arm
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
- name: linux_arm_v7
|
||||
goos: linux
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
- name: linux_s390x
|
||||
goos: linux
|
||||
goarch: s390x
|
||||
- name: linux_riscv64
|
||||
goos: linux
|
||||
goarch: riscv64
|
||||
- name: linux_mips64le
|
||||
goos: linux
|
||||
goarch: mips64le
|
||||
- name: windows_amd64
|
||||
goos: windows
|
||||
goarch: amd64
|
||||
require_legacy_go: true
|
||||
- name: windows_386
|
||||
goos: windows
|
||||
goarch: 386
|
||||
require_legacy_go: true
|
||||
- name: windows_arm64
|
||||
goos: windows
|
||||
goarch: arm64
|
||||
- name: darwin_arm64
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
- name: darwin_amd64
|
||||
goos: darwin
|
||||
goarch: amd64
|
||||
- name: android_arm64
|
||||
goos: android
|
||||
goarch: arm64
|
||||
- name: android_arm
|
||||
goos: android
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
- name: android_amd64
|
||||
goos: android
|
||||
goarch: amd64
|
||||
- name: android_386
|
||||
goos: android
|
||||
goarch: 386
|
||||
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
|
||||
- { 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 }
|
||||
|
||||
- { os: windows, arch: "386", legacy_go: true }
|
||||
- { os: windows, arch: amd64, legacy_go: true }
|
||||
|
||||
- { os: android, arch: "386", ndk: "i686-linux-android21" }
|
||||
- { os: android, arch: amd64, ndk: "x86_64-linux-android21" }
|
||||
- { os: android, arch: arm64, ndk: "aarch64-linux-android21" }
|
||||
- { os: android, arch: arm, ndk: "armv7a-linux-androideabi21" }
|
||||
exclude:
|
||||
- { os: darwin, arch: "386" }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
if: ${{ ! matrix.legacy_go }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24
|
||||
- name: Cache legacy Go
|
||||
- name: Cache Legacy Go
|
||||
if: matrix.require_legacy_go
|
||||
id: cache-legacy-go
|
||||
uses: actions/cache@v4
|
||||
@@ -142,64 +111,139 @@ jobs:
|
||||
path: |
|
||||
~/go/go_legacy
|
||||
key: go_legacy_1236
|
||||
- name: Setup legacy Go
|
||||
if: matrix.require_legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
||||
run: bash .github/setup_legacy_go.sh
|
||||
- name: Setup Legacy Go
|
||||
if: matrix.legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
||||
run: |-
|
||||
.github/setup_legacy_go.sh
|
||||
- name: Setup Legacy Go 2
|
||||
if: matrix.legacy_go
|
||||
run: |-
|
||||
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
|
||||
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
|
||||
- name: Setup Android NDK
|
||||
if: matrix.goos == 'android'
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28
|
||||
local-cache: true
|
||||
- name: Setup Goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: '~> v2'
|
||||
install-only: true
|
||||
- name: Extract signing key
|
||||
run: |-
|
||||
mkdir -p $HOME/.gnupg
|
||||
cat > $HOME/.gnupg/sagernet.key <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api'
|
||||
if [ ! '${{ matrix.legacy_go }}' = 'true' ]; then
|
||||
TAGS="${TAGS},with_ech"
|
||||
fi
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
if: matrix.goos != 'android'
|
||||
run: |-
|
||||
goreleaser release --clean --split
|
||||
if: matrix.os != 'android'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOPATH: ${{ env.HOME }}/go
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
- name: Build Android
|
||||
if: matrix.goos == 'android'
|
||||
run: |-
|
||||
if: matrix.os == 'android'
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
go install -v ./cmd/internal/build
|
||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build goreleaser release --clean --split
|
||||
export CC='${{ matrix.ndk }}-clang'
|
||||
export CXX="${CC}++"
|
||||
mkdir -p dist
|
||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
BUILD_GOOS: ${{ matrix.goos }}
|
||||
BUILD_GOARCH: ${{ matrix.goarch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
CGO_ENABLED: "1"
|
||||
BUILD_GOOS: ${{ matrix.os }}
|
||||
BUILD_GOARCH: ${{ matrix.arch }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
- name: Set name
|
||||
run: |-
|
||||
ARM_VERSION=$([ -n '${{ matrix.goarm}}' ] && echo 'v${{ matrix.goarm}}' || true)
|
||||
LEGACY=$([ '${{ matrix.legacy_go }}' = 'true' ] && echo "-legacy" || true)
|
||||
DIR_NAME="sing-box-${{ needs.calculate_version.outputs.version }}-${{ matrix.os }}-${{ matrix.arch }}${ARM_VERSION}${LEGACY}"
|
||||
PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}"
|
||||
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
||||
echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}"
|
||||
- name: Package DEB
|
||||
if: matrix.debian != ''
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get install -y debsigs
|
||||
fpm -t deb \
|
||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||
-p "dist/${PKG_NAME}.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
|
||||
fpm -t rpm \
|
||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||
-p "dist/${PKG_NAME}.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 install -y libarchive-tools
|
||||
fpm -t pacman \
|
||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||
-p "dist/${PKG_NAME}.pkg.tar.zst" \
|
||||
--architecture ${{ matrix.pacman }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
- name: Archive
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
cd dist
|
||||
mkdir -p "${DIR_NAME}"
|
||||
cp ../LICENSE "${DIR_NAME}"
|
||||
if [ '${{ matrix.os }}' = 'windoes' ]; then
|
||||
cp sing-box.exe "${DIR_NAME}"
|
||||
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
|
||||
else
|
||||
cp sing-box "${DIR_NAME}"
|
||||
tar -czvf "${DIR_NAME}.tar.gz" "${DIR_NAME}"
|
||||
fi
|
||||
rm -r "${DIR_NAME}"
|
||||
- name: Cleanup
|
||||
run: rm dist/sing-box
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-${{ matrix.name }}
|
||||
path: 'dist'
|
||||
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||
path: "dist"
|
||||
build_android:
|
||||
name: Build Android
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
||||
@@ -271,13 +315,11 @@ jobs:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||
- name: Prepare upload
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
mkdir -p dist/release
|
||||
cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release
|
||||
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-android-apks
|
||||
@@ -435,19 +477,19 @@ jobs:
|
||||
|
||||
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
|
||||
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
|
||||
|
||||
|
||||
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
|
||||
mkdir -p "$PROFILES_PATH"
|
||||
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
|
||||
|
||||
|
||||
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
|
||||
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
|
||||
|
||||
|
||||
xcrun notarytool store-credentials "notarytool-password" \
|
||||
--key $ASC_KEY_PATH \
|
||||
--key-id $ASC_KEY_ID \
|
||||
--issuer $ASC_KEY_ISSUER_ID
|
||||
|
||||
|
||||
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
|
||||
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
|
||||
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
|
||||
@@ -523,7 +565,7 @@ jobs:
|
||||
cd "${{ matrix.archive }}"
|
||||
zip -r SFM.dSYMs.zip dSYMs
|
||||
popd
|
||||
|
||||
|
||||
mkdir -p dist/release
|
||||
cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg"
|
||||
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip"
|
||||
@@ -547,12 +589,6 @@ jobs:
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: '~> v2'
|
||||
install-only: true
|
||||
- name: Cache ghr
|
||||
uses: actions/cache@v4
|
||||
id: cache-ghr
|
||||
@@ -577,26 +613,17 @@ jobs:
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Merge builds
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
||||
run: |-
|
||||
goreleaser continue --merge --skip publish
|
||||
mkdir -p dist/release
|
||||
mv dist/*/sing-box*{tar.gz,zip,deb,rpm,_amd64.pkg.tar.zst,_arm64.pkg.tar.zst} dist/release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
- name: Upload builds
|
||||
if: ${{ env.PUBLISHED == 'false' }}
|
||||
run: |-
|
||||
export PATH="$PATH:$HOME/go/bin"
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Replace builds
|
||||
if: ${{ env.PUBLISHED != 'false' }}
|
||||
run: |-
|
||||
export PATH="$PATH:$HOME/go/bin"
|
||||
ghr --replace -p 5 "v${VERSION}" dist/release
|
||||
ghr --replace -p 5 "v${VERSION}" dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
3
.github/workflows/lint.yml
vendored
3
.github/workflows/lint.yml
vendored
@@ -34,4 +34,5 @@ jobs:
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=30m
|
||||
install-mode: binary
|
||||
install-mode: binary
|
||||
verify: false
|
||||
|
||||
176
.github/workflows/linux.yml
vendored
176
.github/workflows/linux.yml
vendored
@@ -1,13 +1,22 @@
|
||||
name: Release to Linux repository
|
||||
name: Build Linux Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version name"
|
||||
required: true
|
||||
type: string
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
build:
|
||||
calculate_version:
|
||||
name: Calculate version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.outputs.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
@@ -17,22 +26,155 @@ jobs:
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24
|
||||
- name: Extract signing key
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
mkdir -p $HOME/.gnupg
|
||||
cat > $HOME/.gnupg/sagernet.key <<EOF
|
||||
echo "version=${{ inputs.version }}"
|
||||
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Calculate version
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/read_tag --nightly
|
||||
- name: Set outputs
|
||||
id: outputs
|
||||
run: |-
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
build:
|
||||
name: Build binary
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- { os: linux, arch: amd64, debian: amd64, rpm: x86_64, pacman: x86_64 }
|
||||
- { os: linux, arch: "386", debian: i386, rpm: i386 }
|
||||
- { os: linux, arch: arm, goarm: "6", debian: armel, rpm: armv6hl }
|
||||
- { os: linux, arch: arm, goarm: "7", debian: armhf, rpm: armv7hl, pacman: armv7hl }
|
||||
- { os: linux, arch: arm64, debian: arm64, rpm: aarch64, pacman: aarch64 }
|
||||
- { os: linux, arch: mips64le, debian: mips64el, rpm: mips64el }
|
||||
- { os: linux, arch: mipsle, debian: mipsel, rpm: mipsel }
|
||||
- { os: linux, arch: s390x, debian: s390x, rpm: s390x }
|
||||
- { os: linux, arch: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
- { os: linux, arch: riscv64, debian: riscv64, rpm: riscv64 }
|
||||
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64 }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.24
|
||||
- name: Setup Android NDK
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
with:
|
||||
ndk-version: r28
|
||||
local-cache: true
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api'
|
||||
if [ ! '${{ matrix.legacy_go }}' = 'true' ]; then
|
||||
TAGS="${TAGS},with_ech"
|
||||
fi
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
GOARM: ${{ matrix.goarm }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set mtime
|
||||
run: |-
|
||||
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||
- name: Set name
|
||||
if: ${{ ! contains(needs.calculate_version.outputs.version, '-') }}
|
||||
run: |-
|
||||
echo "NAME=sing-box" >> "$GITHUB_ENV"
|
||||
- name: Set beta name
|
||||
if: contains(needs.calculate_version.outputs.version, '-')
|
||||
run: |-
|
||||
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||
- name: Package DEB
|
||||
if: matrix.debian != ''
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get install -y debsigs
|
||||
fpm -t deb \
|
||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
||||
--architecture ${{ matrix.debian }} \
|
||||
dist/sing-box=/usr/bin/${NAME}
|
||||
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||
rm -rf $HOME/.gnupg
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
||||
- name: Publish release
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
debsigs --sign=origin -k ${{ secrets.GPG_KEY_ID }} --gpgopts '--pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}"' dist/*.deb
|
||||
- name: Package RPM
|
||||
if: matrix.rpm != ''
|
||||
run: |-
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
fpm -t rpm \
|
||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
||||
--architecture ${{ matrix.rpm }} \
|
||||
dist/sing-box=/usr/bin/${NAME}
|
||||
cat > $HOME/.rpmmacros <<EOF
|
||||
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||
EOF
|
||||
gpg --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --import <<EOF
|
||||
${{ secrets.GPG_KEY }}
|
||||
EOF
|
||||
rpmsign --addsign dist/*.rpm
|
||||
- name: Cleanup
|
||||
run: rm dist/sing-box
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: '~> v2'
|
||||
args: release -f .goreleaser.fury.yaml --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
|
||||
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
|
||||
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
name: binary-${{ matrix.os }}_${{ matrix.arch }}${{ matrix.goarm && format('v{0}', matrix.goarm) }}${{ matrix.legacy_go && '-legacy' || '' }}
|
||||
path: "dist"
|
||||
upload:
|
||||
name: Upload builds
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
- build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set tag
|
||||
run: |-
|
||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Download builds
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- name: Publish packages
|
||||
run: |-
|
||||
wget -O fury-cli.deb https://github.com/gemfury/cli/releases/download/v0.23.0/fury-cli_0.23.0_linux_amd64.deb
|
||||
sudo dpkg -i fury-cli.deb
|
||||
fury migrate dist --as=sagernet --api-token ${{ secrets.FURY_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
/.idea/
|
||||
/vendor/
|
||||
/*.json
|
||||
/*.js
|
||||
/*.srs
|
||||
/*.db
|
||||
/site/
|
||||
|
||||
@@ -31,6 +31,7 @@ run:
|
||||
- with_reality_server
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_script
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
|
||||
@@ -21,6 +21,7 @@ builds:
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_script
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOTOOLCHAIN=local
|
||||
@@ -51,6 +52,7 @@ builds:
|
||||
- with_acme
|
||||
- with_clash_api
|
||||
- with_tailscale
|
||||
- with_script
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOROOT={{ .Env.GOPATH }}/go_legacy
|
||||
@@ -126,8 +128,8 @@ nfpms:
|
||||
- deb
|
||||
- rpm
|
||||
- archlinux
|
||||
# - apk
|
||||
# - ipk
|
||||
# - apk
|
||||
# - ipk
|
||||
priority: extra
|
||||
contents:
|
||||
- src: release/config/config.json
|
||||
|
||||
13
Makefile
13
Makefile
@@ -1,8 +1,6 @@
|
||||
NAME = sing-box
|
||||
COMMIT = $(shell git rev-parse --short HEAD)
|
||||
TAGS_GO120 = with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls
|
||||
TAGS_GO123 = with_tailscale
|
||||
TAGS ?= $(TAGS_GO120),$(TAGS_GO123)
|
||||
TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls,with_tailscale,with_script
|
||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls,with_reality_server
|
||||
|
||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||
@@ -20,11 +18,6 @@ build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
ci_build_go120:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(PARAMS) $(MAIN) && \
|
||||
go build $(PARAMS) -tags "$(TAGS_GO120)" $(MAIN)
|
||||
|
||||
ci_build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(PARAMS) $(MAIN) && \
|
||||
@@ -233,8 +226,8 @@ lib:
|
||||
go run ./cmd/internal/build_libbox -target ios
|
||||
|
||||
lib_install:
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.4
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.4
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.5
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.5
|
||||
|
||||
docs:
|
||||
venv/bin/mkdocs serve
|
||||
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
type CertificateStore interface {
|
||||
LifecycleService
|
||||
Pool() *x509.CertPool
|
||||
TLSDecryptionEnabled() bool
|
||||
TLSDecryptionCertificate() *x509.Certificate
|
||||
TLSDecryptionPrivateKey() any
|
||||
}
|
||||
|
||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||
|
||||
@@ -45,10 +45,10 @@ type RDRCStore interface {
|
||||
}
|
||||
|
||||
type DNSTransport interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
Dependencies() []string
|
||||
Reset()
|
||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ type CacheFile interface {
|
||||
StoreGroupExpand(group string, expand bool) error
|
||||
LoadRuleSet(tag string) *SavedBinary
|
||||
SaveRuleSet(tag string, set *SavedBinary) error
|
||||
LoadScript(tag string) *SavedBinary
|
||||
SaveScript(tag string, script *SavedBinary) error
|
||||
SurgePersistentStoreRead(key string) string
|
||||
SurgePersistentStoreWrite(key string, value string) error
|
||||
}
|
||||
|
||||
type SavedBinary struct {
|
||||
|
||||
@@ -2,6 +2,8 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
@@ -58,6 +60,8 @@ type InboundContext struct {
|
||||
Client string
|
||||
SniffContext any
|
||||
PacketSniffError error
|
||||
HTTPRequest *http.Request
|
||||
ClientHello *tls.ClientHelloInfo
|
||||
|
||||
// cache
|
||||
|
||||
@@ -74,6 +78,7 @@ type InboundContext struct {
|
||||
UDPTimeout time.Duration
|
||||
TLSFragment bool
|
||||
TLSFragmentFallbackDelay time.Duration
|
||||
MITM *option.MITMRouteOptions
|
||||
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package adapter
|
||||
|
||||
import E "github.com/sagernet/sing/common/exceptions"
|
||||
import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type StartStage uint8
|
||||
|
||||
@@ -45,6 +47,9 @@ type LifecycleService interface {
|
||||
|
||||
func Start(stage StartStage, services ...Lifecycle) error {
|
||||
for _, service := range services {
|
||||
if service == nil {
|
||||
continue
|
||||
}
|
||||
err := service.Start(stage)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
13
adapter/mitm.go
Normal file
13
adapter/mitm.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type MITMEngine interface {
|
||||
Lifecycle
|
||||
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
||||
}
|
||||
54
adapter/script.go
Normal file
54
adapter/script.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ScriptManager interface {
|
||||
Lifecycle
|
||||
Scripts() []Script
|
||||
Script(name string) (Script, bool)
|
||||
SurgeCache() *SurgeInMemoryCache
|
||||
}
|
||||
|
||||
type SurgeInMemoryCache struct {
|
||||
sync.RWMutex
|
||||
Data map[string]string
|
||||
}
|
||||
|
||||
type Script interface {
|
||||
Type() string
|
||||
Tag() string
|
||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||
PostStart() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type SurgeScript interface {
|
||||
Script
|
||||
ExecuteGeneric(ctx context.Context, scriptType string, timeout time.Duration, arguments []string) error
|
||||
ExecuteHTTPRequest(ctx context.Context, timeout time.Duration, request *http.Request, body []byte, binaryBody bool, arguments []string) (*HTTPRequestScriptResult, error)
|
||||
ExecuteHTTPResponse(ctx context.Context, timeout time.Duration, request *http.Request, response *http.Response, body []byte, binaryBody bool, arguments []string) (*HTTPResponseScriptResult, error)
|
||||
}
|
||||
|
||||
type HTTPRequestScriptResult struct {
|
||||
URL string
|
||||
Headers http.Header
|
||||
Body []byte
|
||||
Response *HTTPRequestScriptResponse
|
||||
}
|
||||
|
||||
type HTTPRequestScriptResponse struct {
|
||||
Status int
|
||||
Headers http.Header
|
||||
Body []byte
|
||||
}
|
||||
|
||||
type HTTPResponseScriptResult struct {
|
||||
Status int
|
||||
Headers http.Header
|
||||
Body []byte
|
||||
}
|
||||
51
box.go
51
box.go
@@ -23,9 +23,11 @@ import (
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/mitm"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
"github.com/sagernet/sing-box/script"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
@@ -48,6 +50,8 @@ type Box struct {
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
script *script.Manager
|
||||
mitm adapter.MITMEngine //*mitm.Engine
|
||||
services []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
@@ -143,18 +147,12 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
|
||||
var services []adapter.LifecycleService
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
len(certificateOptions.CertificatePath) > 0 ||
|
||||
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
||||
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
services = append(services, certificateStore)
|
||||
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), common.PtrValueOrDefault(options.Certificate))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||
services = append(services, certificateStore)
|
||||
|
||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
@@ -173,7 +171,7 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
}
|
||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||
connectionManager := route.NewConnectionManager(ctx, logFactory.NewLogger("connection"))
|
||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||
service.MustRegister[adapter.Router](ctx, router)
|
||||
@@ -181,8 +179,8 @@ func New(options Options) (*Box, error) {
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize router")
|
||||
}
|
||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||
if ntpOptions.Enabled {
|
||||
timeService = new(tls.TimeServiceWrapper)
|
||||
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||
@@ -296,6 +294,11 @@ func New(options Options) (*Box, error) {
|
||||
"local",
|
||||
option.LocalDNSServerOptions{},
|
||||
)))
|
||||
scriptManager, err := script.NewManager(ctx, logFactory, options.Scripts)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize script manager")
|
||||
}
|
||||
service.MustRegister[adapter.ScriptManager](ctx, scriptManager)
|
||||
if platformInterface != nil {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
if err != nil {
|
||||
@@ -345,6 +348,16 @@ func New(options Options) (*Box, error) {
|
||||
timeService.TimeService = ntpService
|
||||
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||
}
|
||||
mitmOptions := common.PtrValueOrDefault(options.MITM)
|
||||
var mitmEngine adapter.MITMEngine
|
||||
if mitmOptions.Enabled {
|
||||
engine, err := mitm.NewEngine(ctx, logFactory.NewLogger("mitm"), mitmOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create MITM engine")
|
||||
}
|
||||
service.MustRegister[adapter.MITMEngine](ctx, engine)
|
||||
mitmEngine = engine
|
||||
}
|
||||
return &Box{
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
@@ -354,6 +367,8 @@ func New(options Options) (*Box, error) {
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
script: scriptManager,
|
||||
mitm: mitmEngine,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
@@ -412,11 +427,11 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router, s.script, s.mitm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -440,7 +455,7 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint)
|
||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.inbound, s.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -448,7 +463,7 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -467,7 +482,7 @@ func (s *Box) Close() error {
|
||||
close(s.done)
|
||||
}
|
||||
err := common.Close(
|
||||
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||
s.inbound, s.outbound, s.endpoint, s.mitm, s.script, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||
)
|
||||
for _, lifecycleService := range s.services {
|
||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||
|
||||
@@ -45,6 +45,7 @@ var (
|
||||
debugFlags []string
|
||||
sharedTags []string
|
||||
iosTags []string
|
||||
memcTags []string
|
||||
debugTags []string
|
||||
)
|
||||
|
||||
@@ -58,8 +59,9 @@ func init() {
|
||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_tailscale")
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_script")
|
||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
||||
memcTags = append(memcTags, "with_tailscale")
|
||||
debugTags = append(debugTags, "debug")
|
||||
}
|
||||
|
||||
@@ -99,18 +101,19 @@ func buildAndroid() {
|
||||
"-javapkg=io.nekohasekai",
|
||||
"-libname=box",
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
args = append(args, sharedFlags...)
|
||||
} else {
|
||||
args = append(args, debugFlags...)
|
||||
}
|
||||
|
||||
args = append(args, "-tags")
|
||||
if !debugEnabled {
|
||||
args = append(args, strings.Join(sharedTags, ","))
|
||||
} else {
|
||||
args = append(args, strings.Join(append(sharedTags, debugTags...), ","))
|
||||
tags := append(sharedTags, memcTags...)
|
||||
if debugEnabled {
|
||||
tags = append(tags, debugTags...)
|
||||
}
|
||||
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
args = append(args, "./experimental/libbox")
|
||||
|
||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||
@@ -148,7 +151,9 @@ func buildApple() {
|
||||
"-v",
|
||||
"-target", bindTarget,
|
||||
"-libname=box",
|
||||
"-tags-macos=" + strings.Join(memcTags, ","),
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
args = append(args, sharedFlags...)
|
||||
} else {
|
||||
@@ -156,12 +161,11 @@ func buildApple() {
|
||||
}
|
||||
|
||||
tags := append(sharedTags, iosTags...)
|
||||
args = append(args, "-tags")
|
||||
if !debugEnabled {
|
||||
args = append(args, strings.Join(tags, ","))
|
||||
} else {
|
||||
args = append(args, strings.Join(append(tags, debugTags...), ","))
|
||||
if debugEnabled {
|
||||
tags = append(tags, debugTags...)
|
||||
}
|
||||
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
args = append(args, "./experimental/libbox")
|
||||
|
||||
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
|
||||
|
||||
121
cmd/sing-box/cmd_generate_ca.go
Normal file
121
cmd/sing-box/cmd_generate_ca.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
)
|
||||
|
||||
var (
|
||||
flagGenerateCAName string
|
||||
flagGenerateCAPKCS12Password string
|
||||
flagGenerateOutput string
|
||||
)
|
||||
|
||||
var commandGenerateCAKeyPair = &cobra.Command{
|
||||
Use: "ca-keypair",
|
||||
Short: "Generate CA key pair",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := generateCAKeyPair()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAName, "name", "n", "", "Set custom CA name")
|
||||
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAPKCS12Password, "p12-password", "p", "", "Set custom PKCS12 password")
|
||||
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateOutput, "output", "o", ".", "Set output directory")
|
||||
commandGenerate.AddCommand(commandGenerateCAKeyPair)
|
||||
}
|
||||
|
||||
func generateCAKeyPair() error {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
spkiASN1, err := x509.MarshalPKIXPublicKey(privateKey.Public())
|
||||
var spki struct {
|
||||
Algorithm pkix.AlgorithmIdentifier
|
||||
SubjectPublicKey asn1.BitString
|
||||
}
|
||||
_, err = asn1.Unmarshal(spkiASN1, &spki)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
|
||||
var caName string
|
||||
if flagGenerateCAName != "" {
|
||||
caName = flagGenerateCAName
|
||||
} else {
|
||||
caName = "sing-box Generated CA " + strings.ToUpper(hex.EncodeToString(skid[:4]))
|
||||
}
|
||||
caTpl := &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{caName},
|
||||
CommonName: caName,
|
||||
},
|
||||
SubjectKeyId: skid[:],
|
||||
NotAfter: time.Now().AddDate(10, 0, 0),
|
||||
NotBefore: time.Now(),
|
||||
KeyUsage: x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
MaxPathLenZero: true,
|
||||
}
|
||||
publicDer, err := x509.CreateCertificate(rand.Reader, caTpl, caTpl, privateKey.Public(), privateKey)
|
||||
var caPassword string
|
||||
if flagGenerateCAPKCS12Password != "" {
|
||||
caPassword = flagGenerateCAPKCS12Password
|
||||
} else {
|
||||
caPassword = strings.ToUpper(hex.EncodeToString(skid[:4]))
|
||||
}
|
||||
caTpl.Raw = publicDer
|
||||
p12Bytes, err := pkcs12.Modern.Encode(privateKey, caTpl, nil, caPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateDer, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".pem"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
|
||||
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".private.pem"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer}), 0o644)
|
||||
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".crt"), publicDer, 0o644)
|
||||
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".p12"), p12Bytes, 0o644)
|
||||
var tlsDecryptionOptions option.TLSDecryptionOptions
|
||||
tlsDecryptionOptions.Enabled = true
|
||||
tlsDecryptionOptions.KeyPair = base64.StdEncoding.EncodeToString(p12Bytes)
|
||||
tlsDecryptionOptions.KeyPairPassword = caPassword
|
||||
var certificateOptions option.CertificateOptions
|
||||
certificateOptions.TLSDecryption = &tlsDecryptionOptions
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(certificateOptions)
|
||||
}
|
||||
@@ -1,13 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -19,36 +12,5 @@ var commandTools = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandTools.PersistentFlags().StringVarP(&commandToolsFlagOutbound, "outbound", "o", "", "Use specified tag instead of default outbound")
|
||||
mainCommand.AddCommand(commandTools)
|
||||
}
|
||||
|
||||
func createPreStartedClient() (*box.Box, error) {
|
||||
options, err := readConfigAndMerge()
|
||||
if err != nil {
|
||||
if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create service")
|
||||
}
|
||||
err = instance.PreStart()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "start service")
|
||||
}
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) {
|
||||
if outboundTag == "" {
|
||||
return instance.Outbound().Default(), nil
|
||||
} else {
|
||||
outbound, loaded := instance.Outbound().Outbound(outboundTag)
|
||||
if !loaded {
|
||||
return nil, E.New("outbound not found: ", outboundTag)
|
||||
}
|
||||
return outbound, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/task"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandConnectFlagNetwork string
|
||||
|
||||
var commandConnect = &cobra.Command{
|
||||
Use: "connect <address>",
|
||||
Short: "Connect to an address",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := connect(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandConnect.Flags().StringVarP(&commandConnectFlagNetwork, "network", "n", "tcp", "network type")
|
||||
commandTools.AddCommand(commandConnect)
|
||||
}
|
||||
|
||||
func connect(address string) error {
|
||||
switch N.NetworkName(commandConnectFlagNetwork) {
|
||||
case N.NetworkTCP, N.NetworkUDP:
|
||||
default:
|
||||
return E.Cause(N.ErrUnknownNetwork, commandConnectFlagNetwork)
|
||||
}
|
||||
instance, err := createPreStartedClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer instance.Close()
|
||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := dialer.DialContext(context.Background(), commandConnectFlagNetwork, M.ParseSocksaddr(address))
|
||||
if err != nil {
|
||||
return E.Cause(err, "connect to server")
|
||||
}
|
||||
var group task.Group
|
||||
group.Append("upload", func(ctx context.Context) error {
|
||||
return common.Error(bufio.Copy(conn, os.Stdin))
|
||||
})
|
||||
group.Append("download", func(ctx context.Context) error {
|
||||
return common.Error(bufio.Copy(os.Stdout, conn))
|
||||
})
|
||||
group.Cleanup(func() {
|
||||
conn.Close()
|
||||
})
|
||||
err = group.Run(context.Background())
|
||||
if E.IsClosed(err) {
|
||||
log.Info(err)
|
||||
} else {
|
||||
log.Error(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandFetch = &cobra.Command{
|
||||
Use: "fetch",
|
||||
Short: "Fetch an URL",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := fetch(args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandTools.AddCommand(commandFetch)
|
||||
}
|
||||
|
||||
var (
|
||||
httpClient *http.Client
|
||||
http3Client *http.Client
|
||||
)
|
||||
|
||||
func fetch(args []string) error {
|
||||
instance, err := createPreStartedClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer instance.Close()
|
||||
httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
},
|
||||
ForceAttemptHTTP2: true,
|
||||
},
|
||||
}
|
||||
defer httpClient.CloseIdleConnections()
|
||||
if C.WithQUIC {
|
||||
err = initializeHTTP3Client(instance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer http3Client.CloseIdleConnections()
|
||||
}
|
||||
for _, urlString := range args {
|
||||
var parsedURL *url.URL
|
||||
parsedURL, err = url.Parse(urlString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch parsedURL.Scheme {
|
||||
case "":
|
||||
parsedURL.Scheme = "http"
|
||||
fallthrough
|
||||
case "http", "https":
|
||||
err = fetchHTTP(httpClient, parsedURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "http3":
|
||||
if !C.WithQUIC {
|
||||
return C.ErrQUICNotIncluded
|
||||
}
|
||||
parsedURL.Scheme = "https"
|
||||
err = fetchHTTP(http3Client, parsedURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return E.New("unsupported scheme: ", parsedURL.Scheme)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
|
||||
request, err := http.NewRequest("GET", parsedURL.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Add("User-Agent", "curl/7.88.0")
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
_, err = bufio.Copy(os.Stdout, response.Body)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
//go:build with_quic
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
box "github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func initializeHTTP3Client(instance *box.Box) error {
|
||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
http3Client = &http.Client{
|
||||
Transport: &http3.Transport{
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
destination := M.ParseSocksaddr(addr)
|
||||
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||
if dErr != nil {
|
||||
return nil, dErr
|
||||
}
|
||||
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
|
||||
},
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//go:build !with_quic
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
box "github.com/sagernet/sing-box"
|
||||
)
|
||||
|
||||
func initializeHTTP3Client(instance *box.Box) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func fetchHTTP3(parsedURL *url.URL) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
108
cmd/sing-box/cmd_tools_install_ca.go
Normal file
108
cmd/sing-box/cmd_tools_install_ca.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/shell"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandInstallCACertificate = &cobra.Command{
|
||||
Use: "install-ca <path to certificate>",
|
||||
Short: "Install CA certificate to system",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := installCACertificate(args[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandTools.AddCommand(commandInstallCACertificate)
|
||||
}
|
||||
|
||||
func installCACertificate(path string) error {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return shell.Exec("powershell", "-Command", "Import-Certificate -FilePath \""+path+"\" -CertStoreLocation Cert:\\LocalMachine\\Root").Attach().Run()
|
||||
case "darwin":
|
||||
return shell.Exec("sudo", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", path).Attach().Run()
|
||||
case "linux":
|
||||
updateCertPath, updateCertPathNotFoundErr := exec.LookPath("update-ca-certificates")
|
||||
if updateCertPathNotFoundErr == nil {
|
||||
publicDer, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll("/usr/local/share/ca-certificates", 0o755)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
log.Info("Try running with sudo")
|
||||
return shell.Exec("sudo", os.Args...).Attach().Run()
|
||||
}
|
||||
return err
|
||||
}
|
||||
fileName := filepath.Base(updateCertPath)
|
||||
if !strings.HasSuffix(fileName, ".crt") {
|
||||
fileName = fileName + ".crt"
|
||||
}
|
||||
filePath, _ := filepath.Abs(filepath.Join("/usr/local/share/ca-certificates", fileName))
|
||||
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
log.Info("Try running with sudo")
|
||||
return shell.Exec("sudo", os.Args...).Attach().Run()
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Info("certificate written to " + filePath + "\n")
|
||||
err = shell.Exec(updateCertPath).Attach().Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("certificate installed")
|
||||
return nil
|
||||
}
|
||||
updateTrustPath, updateTrustPathNotFoundErr := exec.LookPath("update-ca-trust")
|
||||
if updateTrustPathNotFoundErr == nil {
|
||||
publicDer, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileName := filepath.Base(updateTrustPath)
|
||||
fileExt := filepath.Ext(path)
|
||||
if fileExt != "" {
|
||||
fileName = fileName[:len(fileName)-len(fileExt)]
|
||||
}
|
||||
filePath, _ := filepath.Abs(filepath.Join("/etc/pki/ca-trust/source/anchors/", fileName+".pem"))
|
||||
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
log.Info("Try running with sudo")
|
||||
return shell.Exec("sudo", os.Args...).Attach().Run()
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Info("certificate written to " + filePath + "\n")
|
||||
err = shell.Exec(updateTrustPath, "extract").Attach().Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("certificate installed")
|
||||
}
|
||||
return E.New("update-ca-certificates or update-ca-trust not found")
|
||||
default:
|
||||
return E.New("unsupported operating system: ", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -39,20 +40,11 @@ func init() {
|
||||
}
|
||||
|
||||
func syncTime() error {
|
||||
instance, err := createPreStartedClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer instance.Close()
|
||||
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
|
||||
if serverAddress.Port == 0 {
|
||||
serverAddress.Port = 123
|
||||
}
|
||||
response, err := ntp.Exchange(context.Background(), dialer, serverAddress)
|
||||
response, err := ntp.Exchange(context.Background(), N.SystemDialer, serverAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package certificate
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -16,6 +17,8 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
)
|
||||
|
||||
var _ adapter.CertificateStore = (*Store)(nil)
|
||||
@@ -27,6 +30,9 @@ type Store struct {
|
||||
certificatePaths []string
|
||||
certificateDirectoryPaths []string
|
||||
watcher *fswatch.Watcher
|
||||
tlsDecryptionEnabled bool
|
||||
tlsDecryptionPrivateKey any
|
||||
tlsDecryptionCertificate *x509.Certificate
|
||||
}
|
||||
|
||||
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
||||
@@ -90,6 +96,19 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initializing certificate store")
|
||||
}
|
||||
if options.TLSDecryption != nil && options.TLSDecryption.Enabled {
|
||||
pfxBytes, err := base64.StdEncoding.DecodeString(options.TLSDecryption.KeyPair)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode key pair base64 bytes")
|
||||
}
|
||||
privateKey, certificate, err := pkcs12.Decode(pfxBytes, options.TLSDecryption.KeyPairPassword)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode key pair")
|
||||
}
|
||||
store.tlsDecryptionEnabled = true
|
||||
store.tlsDecryptionPrivateKey = privateKey
|
||||
store.tlsDecryptionCertificate = certificate
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
@@ -183,3 +202,15 @@ func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
||||
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||
return err == nil && !strings.Contains(target, "/")
|
||||
}
|
||||
|
||||
func (s *Store) TLSDecryptionEnabled() bool {
|
||||
return s.tlsDecryptionEnabled
|
||||
}
|
||||
|
||||
func (s *Store) TLSDecryptionCertificate() *x509.Certificate {
|
||||
return s.tlsDecryptionCertificate
|
||||
}
|
||||
|
||||
func (s *Store) TLSDecryptionPrivateKey() any {
|
||||
return s.tlsDecryptionPrivateKey
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/conntrack"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -35,6 +36,7 @@ type DefaultDialer struct {
|
||||
udpListener net.ListenConfig
|
||||
udpAddr4 string
|
||||
udpAddr6 string
|
||||
netns string
|
||||
networkManager adapter.NetworkManager
|
||||
networkStrategy *C.NetworkStrategy
|
||||
defaultNetworkStrategy bool
|
||||
@@ -198,6 +200,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
udpListener: listener,
|
||||
udpAddr4: udpAddr4,
|
||||
udpAddr6: udpAddr6,
|
||||
netns: options.NetNs,
|
||||
networkManager: networkManager,
|
||||
networkStrategy: networkStrategy,
|
||||
defaultNetworkStrategy: defaultNetworkStrategy,
|
||||
@@ -214,19 +217,21 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
|
||||
return nil, E.New("domain not resolved")
|
||||
}
|
||||
if d.networkStrategy == nil {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkUDP:
|
||||
if !address.IsIPv6() {
|
||||
return trackConn(d.udpDialer4.DialContext(ctx, network, address.String()))
|
||||
} else {
|
||||
return trackConn(d.udpDialer6.DialContext(ctx, network, address.String()))
|
||||
return trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkUDP:
|
||||
if !address.IsIPv6() {
|
||||
return d.udpDialer4.DialContext(ctx, network, address.String())
|
||||
} else {
|
||||
return d.udpDialer6.DialContext(ctx, network, address.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
if !address.IsIPv6() {
|
||||
return trackConn(DialSlowContext(&d.dialer4, ctx, network, address))
|
||||
} else {
|
||||
return trackConn(DialSlowContext(&d.dialer6, ctx, network, address))
|
||||
}
|
||||
if !address.IsIPv6() {
|
||||
return DialSlowContext(&d.dialer4, ctx, network, address)
|
||||
} else {
|
||||
return DialSlowContext(&d.dialer6, ctx, network, address)
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
||||
}
|
||||
@@ -282,13 +287,15 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
||||
|
||||
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
if d.networkStrategy == nil {
|
||||
if destination.IsIPv6() {
|
||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
|
||||
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4))
|
||||
} else {
|
||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4))
|
||||
}
|
||||
return trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) {
|
||||
if destination.IsIPv6() {
|
||||
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
|
||||
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||
return d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4)
|
||||
} else {
|
||||
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)
|
||||
}
|
||||
|
||||
@@ -6,26 +6,39 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type DirectDialer interface {
|
||||
IsEmpty() bool
|
||||
}
|
||||
|
||||
type DetourDialer struct {
|
||||
outboundManager adapter.OutboundManager
|
||||
detour string
|
||||
legacyDNSDialer bool
|
||||
dialer N.Dialer
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
}
|
||||
|
||||
func NewDetour(outboundManager adapter.OutboundManager, detour string) N.Dialer {
|
||||
return &DetourDialer{outboundManager: outboundManager, detour: detour}
|
||||
func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNSDialer bool) N.Dialer {
|
||||
return &DetourDialer{
|
||||
outboundManager: outboundManager,
|
||||
detour: detour,
|
||||
legacyDNSDialer: legacyDNSDialer,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DetourDialer) Start() error {
|
||||
_, err := d.Dialer()
|
||||
return err
|
||||
func InitializeDetour(dialer N.Dialer) error {
|
||||
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
||||
if !isDetour {
|
||||
return nil
|
||||
}
|
||||
return common.Error(detourDialer.Dialer())
|
||||
}
|
||||
|
||||
func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
||||
@@ -34,11 +47,20 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
||||
}
|
||||
|
||||
func (d *DetourDialer) init() {
|
||||
var loaded bool
|
||||
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
|
||||
dialer, loaded := d.outboundManager.Outbound(d.detour)
|
||||
if !loaded {
|
||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||
return
|
||||
}
|
||||
if !d.legacyDNSDialer {
|
||||
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
||||
if directDialer.IsEmpty() {
|
||||
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
d.dialer = dialer
|
||||
}
|
||||
|
||||
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
|
||||
@@ -23,6 +23,7 @@ type Options struct {
|
||||
DirectResolver bool
|
||||
ResolverOnDetour bool
|
||||
NewDialer bool
|
||||
LegacyDNSDialer bool
|
||||
}
|
||||
|
||||
// TODO: merge with NewWithOptions
|
||||
@@ -45,7 +46,7 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
if outboundManager == nil {
|
||||
return nil, E.New("missing outbound manager")
|
||||
}
|
||||
dialer = NewDetour(outboundManager, dialOptions.Detour)
|
||||
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
||||
} else {
|
||||
dialer, err = NewDefault(options.Context, dialOptions)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -14,6 +16,8 @@ import (
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/vishvananda/netns"
|
||||
)
|
||||
|
||||
type Listener struct {
|
||||
@@ -135,3 +139,30 @@ func (l *Listener) UDPConn() *net.UDPConn {
|
||||
func (l *Listener) ListenOptions() option.ListenOptions {
|
||||
return l.listenOptions
|
||||
}
|
||||
|
||||
func ListenNetworkNamespace[T any](nameOrPath string, block func() (T, error)) (T, error) {
|
||||
if nameOrPath != "" {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
currentNs, err := netns.Get()
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "get current netns")
|
||||
}
|
||||
defer netns.Set(currentNs)
|
||||
var targetNs netns.NsHandle
|
||||
if strings.HasPrefix(nameOrPath, "/") {
|
||||
targetNs, err = netns.GetFromPath(nameOrPath)
|
||||
} else {
|
||||
targetNs, err = netns.GetFromName(nameOrPath)
|
||||
}
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "get netns ", nameOrPath)
|
||||
}
|
||||
defer targetNs.Close()
|
||||
err = netns.Set(targetNs)
|
||||
if err != nil {
|
||||
return common.DefaultValue[T](), E.Cause(err, "set netns to ", nameOrPath)
|
||||
}
|
||||
}
|
||||
return block()
|
||||
}
|
||||
|
||||
@@ -16,9 +16,12 @@ import (
|
||||
)
|
||||
|
||||
func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
//nolint:staticcheck
|
||||
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
|
||||
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
|
||||
}
|
||||
var err error
|
||||
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort)
|
||||
var tcpListener net.Listener
|
||||
var listenConfig net.ListenConfig
|
||||
if l.listenOptions.TCPKeepAlive >= 0 {
|
||||
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
|
||||
@@ -37,20 +40,19 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
}
|
||||
setMultiPathTCP(&listenConfig)
|
||||
}
|
||||
if l.listenOptions.TCPFastOpen {
|
||||
var tfoConfig tfo.ListenConfig
|
||||
tfoConfig.ListenConfig = listenConfig
|
||||
tcpListener, err = tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
|
||||
} else {
|
||||
tcpListener, err = listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
|
||||
}
|
||||
if err == nil {
|
||||
l.logger.Info("tcp server started at ", tcpListener.Addr())
|
||||
}
|
||||
//nolint:staticcheck
|
||||
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
|
||||
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
|
||||
tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) {
|
||||
if l.listenOptions.TCPFastOpen {
|
||||
var tfoConfig tfo.ListenConfig
|
||||
tfoConfig.ListenConfig = listenConfig
|
||||
return tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
|
||||
} else {
|
||||
return listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.logger.Info("tcp server started at ", tcpListener.Addr())
|
||||
l.tcpListener = tcpListener
|
||||
return tcpListener, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package listener
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -24,7 +25,9 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
||||
if !udpFragment {
|
||||
lc.Control = control.Append(lc.Control, control.DisableUDPFragment())
|
||||
}
|
||||
udpConn, err := lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
|
||||
udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||
return lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -34,6 +37,12 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
|
||||
return udpConn, err
|
||||
}
|
||||
|
||||
func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) {
|
||||
return ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||
return listenConfig.ListenPacket(ctx, network, address)
|
||||
})
|
||||
}
|
||||
|
||||
func (l *Listener) UDPAddr() M.Socksaddr {
|
||||
return l.udpAddr
|
||||
}
|
||||
|
||||
@@ -18,5 +18,6 @@ func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Rea
|
||||
}
|
||||
metadata.Protocol = C.ProtocolHTTP
|
||||
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||
metadata.HTTPRequest = request
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
|
||||
if clientHello != nil {
|
||||
metadata.Protocol = C.ProtocolTLS
|
||||
metadata.Domain = clientHello.ServerName
|
||||
metadata.ClientHello = clientHello
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
|
||||
@@ -8,7 +8,10 @@ import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
||||
@@ -35,17 +38,30 @@ func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func(
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||
NotAfter: expire,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
Subject: pkix.Name{
|
||||
CommonName: serverName,
|
||||
},
|
||||
DNSNames: []string{serverName},
|
||||
var template *x509.Certificate
|
||||
if serverAddress := M.ParseAddr(serverName); serverAddress.IsValid() {
|
||||
template = &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
IPAddresses: []net.IP{serverAddress.AsSlice()},
|
||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||
NotAfter: expire,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
} else {
|
||||
template = &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||
NotAfter: expire,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
Subject: pkix.Name{
|
||||
CommonName: serverName,
|
||||
},
|
||||
DNSNames: []string{serverName},
|
||||
}
|
||||
}
|
||||
if parent == nil {
|
||||
parent = template
|
||||
|
||||
7
constant/script.go
Normal file
7
constant/script.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
ScriptTypeSurge = "surge"
|
||||
ScriptSourceTypeLocal = "local"
|
||||
ScriptSourceTypeRemote = "remote"
|
||||
)
|
||||
@@ -263,20 +263,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
return nil, tun.ErrDrop
|
||||
}
|
||||
case *R.RuleActionPredefined:
|
||||
return &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionDesired: true,
|
||||
RecursionAvailable: true,
|
||||
Rcode: action.Rcode,
|
||||
},
|
||||
Question: message.Question,
|
||||
Answer: action.Answer,
|
||||
Ns: action.Ns,
|
||||
Extra: action.Extra,
|
||||
}, nil
|
||||
return action.Response(message), nil
|
||||
}
|
||||
}
|
||||
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||
@@ -462,6 +449,6 @@ func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) {
|
||||
func (r *Router) ResetNetwork() {
|
||||
r.ClearCache()
|
||||
for _, transport := range r.transport.Transports() {
|
||||
transport.Reset()
|
||||
transport.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
for _, transport := range t.transports {
|
||||
transport.Reset()
|
||||
transport.Close()
|
||||
}
|
||||
if t.interfaceCallback != nil {
|
||||
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
|
||||
@@ -89,12 +89,6 @@ func (t *Transport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
for _, transport := range t.transports {
|
||||
transport.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
err := t.fetchServers()
|
||||
if err != nil {
|
||||
@@ -252,7 +246,7 @@ func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []M.So
|
||||
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, serverAddr))
|
||||
}
|
||||
for _, transport := range t.transports {
|
||||
transport.Reset()
|
||||
transport.Close()
|
||||
}
|
||||
t.transports = transports
|
||||
return nil
|
||||
|
||||
@@ -51,7 +51,12 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
@@ -149,9 +150,17 @@ func NewHTTPSRaw(
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Reset() {
|
||||
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return dialer.InitializeDetour(t.dialer)
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Close() error {
|
||||
t.transport.CloseIdleConnections()
|
||||
t.transport = t.transport.Clone()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
|
||||
@@ -40,7 +40,12 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
|
||||
@@ -111,8 +111,12 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *HTTP3Transport) Reset() {
|
||||
t.transport.Close()
|
||||
func (t *HTTP3Transport) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *HTTP3Transport) Close() error {
|
||||
return t.transport.Close()
|
||||
}
|
||||
|
||||
func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
|
||||
@@ -68,13 +68,18 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Transport) Reset() {
|
||||
func (t *Transport) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Close() error {
|
||||
t.access.Lock()
|
||||
defer t.access.Unlock()
|
||||
connection := t.connection
|
||||
if connection != nil {
|
||||
connection.CloseWithError(0, "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -46,7 +47,15 @@ func NewTCP(ctx context.Context, logger log.ContextLogger, tag string, options o
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *TCPTransport) Reset() {
|
||||
func (t *TCPTransport) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return dialer.InitializeDetour(t.dialer)
|
||||
}
|
||||
|
||||
func (t *TCPTransport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
@@ -65,13 +66,21 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *TLSTransport) Reset() {
|
||||
func (t *TLSTransport) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return dialer.InitializeDetour(t.dialer)
|
||||
}
|
||||
|
||||
func (t *TLSTransport) Close() error {
|
||||
t.access.Lock()
|
||||
defer t.access.Unlock()
|
||||
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
|
||||
connection.Value.Close()
|
||||
}
|
||||
t.connections.Init()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -64,11 +65,19 @@ func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer
|
||||
}
|
||||
}
|
||||
|
||||
func (t *UDPTransport) Reset() {
|
||||
func (t *UDPTransport) Start(stage adapter.StartStage) error {
|
||||
if stage != adapter.StartStateStart {
|
||||
return nil
|
||||
}
|
||||
return dialer.InitializeDetour(t.dialer)
|
||||
}
|
||||
|
||||
func (t *UDPTransport) Close() error {
|
||||
t.access.Lock()
|
||||
defer t.access.Unlock()
|
||||
close(t.done)
|
||||
t.done = make(chan struct{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
|
||||
@@ -20,9 +20,10 @@ func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (
|
||||
return dialer.NewDefaultOutbound(ctx), nil
|
||||
} else {
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
DirectResolver: true,
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
DirectResolver: true,
|
||||
LegacyDNSDialer: options.Legacy,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -43,10 +44,11 @@ func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions)
|
||||
return transportDialer, nil
|
||||
} else {
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
RemoteIsDomain: options.ServerIsDomain(),
|
||||
DirectResolver: true,
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
RemoteIsDomain: options.ServerIsDomain(),
|
||||
DirectResolver: true,
|
||||
LegacyDNSDialer: options.Legacy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@ func (m *TransportManager) Start(stage adapter.StartStage) error {
|
||||
transports := m.transports
|
||||
m.access.Unlock()
|
||||
if stage == adapter.StartStateStart {
|
||||
if m.defaultTag != "" && m.defaultTransport == nil {
|
||||
return E.New("default DNS server not found: ", m.defaultTag)
|
||||
}
|
||||
return m.startTransports(m.transports)
|
||||
} else {
|
||||
for _, outbound := range transports {
|
||||
@@ -225,7 +228,7 @@ func (m *TransportManager) Remove(tag string) error {
|
||||
}
|
||||
}
|
||||
if started {
|
||||
transport.Reset()
|
||||
transport.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.12.0-alpha.19
|
||||
|
||||
* Update gVisor to 20250319.0
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.0-alpha.18
|
||||
|
||||
* Add wildcard SNI support for ShadowTLS inbound **1**
|
||||
|
||||
@@ -5,7 +5,8 @@ icon: material/new-box
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [domain_resolver](#domain_resolver)
|
||||
:material-delete-clock: [domain_strategy](#domain_strategy)
|
||||
:material-delete-clock: [domain_strategy](#domain_strategy)
|
||||
:material-plus: [netns](#netns)
|
||||
|
||||
!!! quote "Changes in sing-box 1.11.0"
|
||||
|
||||
@@ -18,24 +19,25 @@ icon: material/new-box
|
||||
|
||||
```json
|
||||
{
|
||||
"detour": "upstream-out",
|
||||
"bind_interface": "en0",
|
||||
"inet4_bind_address": "0.0.0.0",
|
||||
"inet6_bind_address": "::",
|
||||
"routing_mark": 1234,
|
||||
"detour": "",
|
||||
"bind_interface": "",
|
||||
"inet4_bind_address": "",
|
||||
"inet6_bind_address": "",
|
||||
"routing_mark": 0,
|
||||
"reuse_addr": false,
|
||||
"connect_timeout": "5s",
|
||||
"connect_timeout": "",
|
||||
"tcp_fast_open": false,
|
||||
"tcp_multi_path": false,
|
||||
"udp_fragment": false,
|
||||
"netns": "",
|
||||
"domain_resolver": "", // or {}
|
||||
"network_strategy": "default",
|
||||
"network_strategy": "",
|
||||
"network_type": [],
|
||||
"fallback_network_type": [],
|
||||
"fallback_delay": "300ms",
|
||||
"fallback_delay": "",
|
||||
|
||||
// Deprecated
|
||||
"domain_strategy": "prefer_ipv6"
|
||||
"domain_strategy": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,6 +77,15 @@ Set netfilter routing mark.
|
||||
|
||||
Reuse listener address.
|
||||
|
||||
#### connect_timeout
|
||||
|
||||
Connect timeout, in golang's Duration format.
|
||||
|
||||
A duration string is a possibly signed sequence of
|
||||
decimal numbers, each with optional fraction and a unit suffix,
|
||||
such as "300ms", "-1.5h" or "2h45m".
|
||||
Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
|
||||
#### tcp_fast_open
|
||||
|
||||
Enable TCP Fast Open.
|
||||
@@ -91,14 +102,15 @@ Enable TCP Multi Path.
|
||||
|
||||
Enable UDP fragmentation.
|
||||
|
||||
#### connect_timeout
|
||||
#### netns
|
||||
|
||||
Connect timeout, in golang's Duration format.
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
A duration string is a possibly signed sequence of
|
||||
decimal numbers, each with optional fraction and a unit suffix,
|
||||
such as "300ms", "-1.5h" or "2h45m".
|
||||
Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux.
|
||||
|
||||
Set network namespace, name or path.
|
||||
|
||||
#### domain_resolver
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ icon: material/new-box
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [domain_resolver](#domain_resolver)
|
||||
:material-delete-clock: [domain_strategy](#domain_strategy)
|
||||
:material-delete-clock: [domain_strategy](#domain_strategy)
|
||||
:material-plus: [netns](#netns)
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
@@ -18,25 +19,26 @@ icon: material/new-box
|
||||
|
||||
```json
|
||||
{
|
||||
"detour": "upstream-out",
|
||||
"bind_interface": "en0",
|
||||
"inet4_bind_address": "0.0.0.0",
|
||||
"inet6_bind_address": "::",
|
||||
"routing_mark": 1234,
|
||||
"detour": "",
|
||||
"bind_interface": "",
|
||||
"inet4_bind_address": "",
|
||||
"inet6_bind_address": "",
|
||||
"routing_mark": 0,
|
||||
"reuse_addr": false,
|
||||
"connect_timeout": "5s",
|
||||
"connect_timeout": "",
|
||||
"tcp_fast_open": false,
|
||||
"tcp_multi_path": false,
|
||||
"udp_fragment": false,
|
||||
"netns": "",
|
||||
"domain_resolver": "", // 或 {}
|
||||
"network_strategy": "",
|
||||
"network_type": [],
|
||||
"fallback_network_type": [],
|
||||
"fallback_delay": "300ms",
|
||||
"fallback_delay": "",
|
||||
|
||||
// 废弃的
|
||||
|
||||
"domain_strategy": "prefer_ipv6"
|
||||
"domain_strategy": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,6 +78,13 @@ icon: material/new-box
|
||||
|
||||
重用监听地址。
|
||||
|
||||
#### connect_timeout
|
||||
|
||||
连接超时,采用 golang 的 Duration 格式。
|
||||
|
||||
持续时间字符串是一个可能有符号的序列十进制数,每个都有可选的分数和单位后缀, 例如 "300ms"、"-1.5h" 或 "2h45m"。
|
||||
有效时间单位为 "ns"、"us"(或 "µs")、"ms"、"s"、"m"、"h"。
|
||||
|
||||
#### tcp_fast_open
|
||||
|
||||
启用 TCP Fast Open。
|
||||
@@ -92,12 +101,15 @@ icon: material/new-box
|
||||
|
||||
启用 UDP 分段。
|
||||
|
||||
#### connect_timeout
|
||||
#### netns
|
||||
|
||||
连接超时,采用 golang 的 Duration 格式。
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
持续时间字符串是一个可能有符号的序列十进制数,每个都有可选的分数和单位后缀, 例如 "300ms"、"-1.5h" 或 "2h45m"。
|
||||
有效时间单位为 "ns"、"us"(或 "µs")、"ms"、"s"、"m"、"h"。
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux。
|
||||
|
||||
设置网络命名空间,名称或路径。
|
||||
|
||||
#### domain_resolver
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
icon: material/delete-clock
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [netns](#netns)
|
||||
|
||||
!!! quote "Changes in sing-box 1.11.0"
|
||||
|
||||
:material-delete-clock: [sniff](#sniff)
|
||||
@@ -14,17 +18,18 @@ icon: material/delete-clock
|
||||
|
||||
```json
|
||||
{
|
||||
"listen": "::",
|
||||
"listen_port": 5353,
|
||||
"listen": "",
|
||||
"listen_port": 0,
|
||||
"tcp_fast_open": false,
|
||||
"tcp_multi_path": false,
|
||||
"udp_fragment": false,
|
||||
"udp_timeout": "5m",
|
||||
"detour": "another-in",
|
||||
"udp_timeout": "",
|
||||
"netns": "",
|
||||
"detour": "",
|
||||
"sniff": false,
|
||||
"sniff_override_destination": false,
|
||||
"sniff_timeout": "300ms",
|
||||
"domain_strategy": "prefer_ipv6",
|
||||
"sniff_timeout": "",
|
||||
"domain_strategy": "",
|
||||
"udp_disable_domain_unmapping": false
|
||||
}
|
||||
```
|
||||
@@ -72,6 +77,16 @@ UDP NAT expiration time.
|
||||
|
||||
`5m` will be used by default.
|
||||
|
||||
#### netns
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux.
|
||||
|
||||
Set network namespace, name or path.
|
||||
|
||||
#### detour
|
||||
|
||||
If set, connections will be forwarded to the specified inbound.
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
icon: material/delete-clock
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [netns](#netns)
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-delete-clock: [sniff](#sniff)
|
||||
@@ -14,17 +18,18 @@ icon: material/delete-clock
|
||||
|
||||
```json
|
||||
{
|
||||
"listen": "::",
|
||||
"listen_port": 5353,
|
||||
"listen": "",
|
||||
"listen_port": 0,
|
||||
"tcp_fast_open": false,
|
||||
"tcp_multi_path": false,
|
||||
"udp_fragment": false,
|
||||
"udp_timeout": "5m",
|
||||
"detour": "another-in",
|
||||
"udp_timeout": "",
|
||||
"netns": "",
|
||||
"detour": "",
|
||||
"sniff": false,
|
||||
"sniff_override_destination": false,
|
||||
"sniff_timeout": "300ms",
|
||||
"domain_strategy": "prefer_ipv6",
|
||||
"sniff_timeout": "",
|
||||
"domain_strategy": "",
|
||||
"udp_disable_domain_unmapping": false
|
||||
}
|
||||
```
|
||||
@@ -73,6 +78,16 @@ UDP NAT 过期时间。
|
||||
|
||||
默认使用 `5m`。
|
||||
|
||||
#### netns
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux。
|
||||
|
||||
设置网络命名空间,名称或路径。
|
||||
|
||||
#### detour
|
||||
|
||||
如果设置,连接将被转发到指定的入站。
|
||||
|
||||
@@ -19,10 +19,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
bucketSelected = []byte("selected")
|
||||
bucketExpand = []byte("group_expand")
|
||||
bucketMode = []byte("clash_mode")
|
||||
bucketRuleSet = []byte("rule_set")
|
||||
bucketSelected = []byte("selected")
|
||||
bucketExpand = []byte("group_expand")
|
||||
bucketMode = []byte("clash_mode")
|
||||
bucketRuleSet = []byte("rule_set")
|
||||
bucketScript = []byte("script")
|
||||
bucketSgPersistentStore = []byte("sg_persistent_store")
|
||||
|
||||
bucketNameList = []string{
|
||||
string(bucketSelected),
|
||||
@@ -316,3 +318,70 @@ func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error {
|
||||
return bucket.Put([]byte(tag), setBinary)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CacheFile) LoadScript(tag string) *adapter.SavedBinary {
|
||||
var savedSet adapter.SavedBinary
|
||||
err := c.DB.View(func(t *bbolt.Tx) error {
|
||||
bucket := c.bucket(t, bucketScript)
|
||||
if bucket == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
scriptBinary := bucket.Get([]byte(tag))
|
||||
if len(scriptBinary) == 0 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
return savedSet.UnmarshalBinary(scriptBinary)
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &savedSet
|
||||
}
|
||||
|
||||
func (c *CacheFile) SaveScript(tag string, set *adapter.SavedBinary) error {
|
||||
return c.DB.Batch(func(t *bbolt.Tx) error {
|
||||
bucket, err := c.createBucket(t, bucketScript)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scriptBinary, err := set.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(tag), scriptBinary)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CacheFile) SurgePersistentStoreRead(key string) string {
|
||||
var value string
|
||||
_ = c.DB.View(func(t *bbolt.Tx) error {
|
||||
bucket := c.bucket(t, bucketSgPersistentStore)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
valueBinary := bucket.Get([]byte(key))
|
||||
if len(valueBinary) > 0 {
|
||||
value = string(valueBinary)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
func (c *CacheFile) SurgePersistentStoreWrite(key string, value string) error {
|
||||
return c.DB.Batch(func(t *bbolt.Tx) error {
|
||||
if value != "" {
|
||||
bucket, err := c.createBucket(t, bucketSgPersistentStore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(key), []byte(value))
|
||||
} else {
|
||||
bucket := c.bucket(t, bucketSgPersistentStore)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
return bucket.Delete([]byte(key))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
186
experimental/clashapi/mitm.go
Normal file
186
experimental/clashapi/mitm.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package clashapi
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"howett.net/plist"
|
||||
)
|
||||
|
||||
func mitmRouter(ctx context.Context) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/mobileconfig", getMobileConfig(ctx))
|
||||
r.Get("/crt", getCertificate(ctx))
|
||||
r.Get("/pem", getCertificatePEM(ctx))
|
||||
r.Get("/magisk", getMagiskModule(ctx))
|
||||
return r
|
||||
}
|
||||
|
||||
func getMobileConfig(ctx context.Context) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
store := service.FromContext[adapter.CertificateStore](ctx)
|
||||
if !store.TLSDecryptionEnabled() {
|
||||
http.NotFound(writer, request)
|
||||
render.PlainText(writer, request, "TLS decryption not enabled")
|
||||
return
|
||||
}
|
||||
certificate := store.TLSDecryptionCertificate()
|
||||
writer.Header().Set("Content-Type", "application/x-apple-aspen-config")
|
||||
uuidGen := common.Must1(uuid.NewV4()).String()
|
||||
mobileConfig := map[string]interface{}{
|
||||
"PayloadContent": []interface{}{
|
||||
map[string]interface{}{
|
||||
"PayloadCertificateFileName": "Certificates.cer",
|
||||
"PayloadContent": certificate.Raw,
|
||||
"PayloadDescription": "Adds a root certificate",
|
||||
"PayloadDisplayName": certificate.Subject.CommonName,
|
||||
"PayloadIdentifier": "com.apple.security.root." + uuidGen,
|
||||
"PayloadType": "com.apple.security.root",
|
||||
"PayloadUUID": uuidGen,
|
||||
"PayloadVersion": 1,
|
||||
},
|
||||
},
|
||||
"PayloadDisplayName": certificate.Subject.CommonName,
|
||||
"PayloadIdentifier": "io.nekohasekai.sfa.ca.profile." + uuidGen,
|
||||
"PayloadRemovalDisallowed": false,
|
||||
"PayloadType": "Configuration",
|
||||
"PayloadUUID": uuidGen,
|
||||
"PayloadVersion": 1,
|
||||
}
|
||||
encoder := plist.NewEncoder(writer)
|
||||
encoder.Indent("\t")
|
||||
encoder.Encode(mobileConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func getCertificate(ctx context.Context) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
store := service.FromContext[adapter.CertificateStore](ctx)
|
||||
if !store.TLSDecryptionEnabled() {
|
||||
http.NotFound(writer, request)
|
||||
render.PlainText(writer, request, "TLS decryption not enabled")
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/x-x509-ca-cert")
|
||||
writer.Header().Set("Content-Disposition", "attachment; filename=Certificate.crt")
|
||||
writer.Write(store.TLSDecryptionCertificate().Raw)
|
||||
}
|
||||
}
|
||||
|
||||
func getCertificatePEM(ctx context.Context) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
store := service.FromContext[adapter.CertificateStore](ctx)
|
||||
if !store.TLSDecryptionEnabled() {
|
||||
http.NotFound(writer, request)
|
||||
render.PlainText(writer, request, "TLS decryption not enabled")
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/x-pem-file")
|
||||
writer.Header().Set("Content-Disposition", "attachment; filename=Certificate.pem")
|
||||
writer.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: store.TLSDecryptionCertificate().Raw}))
|
||||
}
|
||||
}
|
||||
|
||||
func getMagiskModule(ctx context.Context) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
store := service.FromContext[adapter.CertificateStore](ctx)
|
||||
if !store.TLSDecryptionEnabled() {
|
||||
http.NotFound(writer, request)
|
||||
render.PlainText(writer, request, "TLS decryption not enabled")
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/zip")
|
||||
writer.Header().Set("Content-Disposition", "attachment; filename="+store.TLSDecryptionCertificate().Subject.CommonName+".zip")
|
||||
createMagiskModule(writer, store.TLSDecryptionCertificate())
|
||||
}
|
||||
}
|
||||
|
||||
func createMagiskModule(writer io.Writer, certificate *x509.Certificate) error {
|
||||
zipWriter := zip.NewWriter(writer)
|
||||
defer zipWriter.Close()
|
||||
moduleProp, err := zipWriter.Create("module.prop")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = moduleProp.Write([]byte(`
|
||||
id=sing-box-certificate
|
||||
name=` + certificate.Subject.CommonName + `
|
||||
version=v0.0.1
|
||||
versionCode=1
|
||||
author=sing-box
|
||||
description=This module adds ` + certificate.Subject.CommonName + ` to the system trust store.
|
||||
`))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certificateFile, err := zipWriter.Create("system/etc/security/cacerts/" + certificate.Subject.CommonName + ".pem")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = pem.Encode(certificateFile, &pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateBinary, err := zipWriter.Create("META-INF/com/google/android/update-binary")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = updateBinary.Write([]byte(`
|
||||
#!/sbin/sh
|
||||
|
||||
#################
|
||||
# Initialization
|
||||
#################
|
||||
|
||||
umask 022
|
||||
|
||||
# echo before loading util_functions
|
||||
ui_print() { echo "$1"; }
|
||||
|
||||
require_new_magisk() {
|
||||
ui_print "*******************************"
|
||||
ui_print " Please install Magisk v20.4+! "
|
||||
ui_print "*******************************"
|
||||
exit 1
|
||||
}
|
||||
|
||||
#########################
|
||||
# Load util_functions.sh
|
||||
#########################
|
||||
|
||||
OUTFD=$2
|
||||
ZIPFILE=$3
|
||||
|
||||
mount /data 2>/dev/null
|
||||
|
||||
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
|
||||
. /data/adb/magisk/util_functions.sh
|
||||
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
|
||||
|
||||
install_module
|
||||
exit 0
|
||||
`))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updaterScript, err := zipWriter.Create("META-INF/com/google/android/updater-script")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = updaterScript.Write([]byte("#MAGISK"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -124,6 +124,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
||||
r.Mount("/profile", profileRouter())
|
||||
r.Mount("/cache", cacheRouter(ctx))
|
||||
r.Mount("/dns", dnsRouter(s.dnsRouter))
|
||||
r.Mount("/mitm", mitmRouter(ctx))
|
||||
|
||||
s.setupMetaAPI(r)
|
||||
})
|
||||
|
||||
@@ -38,7 +38,12 @@ func newPlatformTransport(iif LocalDNSTransport, tag string, options option.Loca
|
||||
}
|
||||
}
|
||||
|
||||
func (p *platformTransport) Reset() {
|
||||
func (p *platformTransport) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *platformTransport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *platformTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
|
||||
@@ -32,4 +32,9 @@ type Notification struct {
|
||||
Subtitle string
|
||||
Body string
|
||||
OpenURL string
|
||||
Clipboard string
|
||||
MediaURL string
|
||||
MediaData []byte
|
||||
MediaType string
|
||||
Timeout int
|
||||
}
|
||||
|
||||
21
go.mod
21
go.mod
@@ -3,10 +3,12 @@ module github.com/sagernet/sing-box
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/adhocore/gronx v1.19.5
|
||||
github.com/anytls/sing-anytls v0.0.6
|
||||
github.com/caddyserver/certmagic v0.21.7
|
||||
github.com/cloudflare/circl v1.6.0
|
||||
github.com/cretz/bine v0.2.0
|
||||
github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/gofrs/uuid/v5 v5.3.1
|
||||
@@ -23,16 +25,16 @@ require (
|
||||
github.com/sagernet/cors v1.2.1
|
||||
github.com/sagernet/fswatch v0.1.1
|
||||
github.com/sagernet/gomobile v0.1.4
|
||||
github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff
|
||||
github.com/sagernet/gvisor v0.0.0-20250324121324-d3f3d7570296
|
||||
github.com/sagernet/quic-go v0.49.0-beta.1
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
||||
github.com/sagernet/sing v0.6.4-0.20250316065121-38f666955109
|
||||
github.com/sagernet/sing v0.6.5-0.20250324102321-1ddf4ccbfab8
|
||||
github.com/sagernet/sing-mux v0.3.1
|
||||
github.com/sagernet/sing-quic v0.4.1-beta.1
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.0
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056
|
||||
github.com/sagernet/sing-tun v0.6.1
|
||||
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec
|
||||
github.com/sagernet/sing-vmess v0.2.0
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
|
||||
github.com/sagernet/tailscale v1.80.3-mod.0
|
||||
@@ -41,6 +43,7 @@ require (
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vishvananda/netns v0.0.4
|
||||
go.uber.org/zap v1.27.0
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.33.0
|
||||
@@ -52,6 +55,7 @@ require (
|
||||
google.golang.org/grpc v1.70.0
|
||||
google.golang.org/protobuf v1.36.5
|
||||
howett.net/plist v1.0.1
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0
|
||||
)
|
||||
|
||||
//replace github.com/sagernet/sing => ../sing
|
||||
@@ -71,12 +75,14 @@ require (
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/gaissmai/bart v0.11.1 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
||||
@@ -85,7 +91,7 @@ require (
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
|
||||
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
@@ -103,7 +109,7 @@ require (
|
||||
github.com/mdlayher/sdnotify v1.0.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.17.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.7 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
||||
@@ -122,7 +128,6 @@ require (
|
||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
@@ -139,5 +144,3 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
)
|
||||
|
||||
//replace github.com/sagernet/sing => ../sing
|
||||
|
||||
42
go.sum
42
go.sum
@@ -1,5 +1,9 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/adhocore/gronx v1.19.5 h1:cwIG4nT1v9DvadxtHBe6MzE+FZ1JDvAUC45U2fl4eSQ=
|
||||
github.com/adhocore/gronx v1.19.5/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||
@@ -30,6 +34,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
||||
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
||||
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
||||
@@ -38,6 +43,10 @@ github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbY
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17 h1:spJaibPy2sZNwo6Q0HjBVufq7hBUj5jNFOKRoogCBow=
|
||||
github.com/dop251/goja v0.0.0-20250125213203-5ef83b82af17/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
@@ -58,8 +67,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
@@ -83,8 +94,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
||||
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
|
||||
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M=
|
||||
@@ -137,10 +148,10 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g=
|
||||
github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
|
||||
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
|
||||
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
|
||||
github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss=
|
||||
github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0=
|
||||
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
|
||||
github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
@@ -167,8 +178,8 @@ github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQ
|
||||
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
|
||||
github.com/sagernet/gomobile v0.1.4 h1:WzX9ka+iHdupMgy2Vdich+OAt7TM8C2cZbIbzNjBrJY=
|
||||
github.com/sagernet/gomobile v0.1.4/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E=
|
||||
github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff h1:mlohw3360Wg1BNGook/UHnISXhUx4Gd/3tVLs5T0nSs=
|
||||
github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff/go.mod h1:ehZwnT2UpmOWAHFL48XdBhnd4Qu4hN2O3Ji0us3ZHMw=
|
||||
github.com/sagernet/gvisor v0.0.0-20250324121324-d3f3d7570296 h1:zovOW85AOnNgGS5dzR/M5Ly1qTRt3a8Ymiu0TIonvAE=
|
||||
github.com/sagernet/gvisor v0.0.0-20250324121324-d3f3d7570296/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||
@@ -178,8 +189,8 @@ github.com/sagernet/quic-go v0.49.0-beta.1/go.mod h1:uesWD1Ihrldq1M3XtjuEvIUqi8W
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.6.4-0.20250316065121-38f666955109 h1:clwEzQu0oiapGllEDtbGQjcmQaIAt8DH3EeOHAWyiKs=
|
||||
github.com/sagernet/sing v0.6.4-0.20250316065121-38f666955109/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.6.5-0.20250324102321-1ddf4ccbfab8 h1:Kg/OPLceU3Ty46ceEwLfL9NLbKBCLj5dNQb1Ia+Q0VI=
|
||||
github.com/sagernet/sing v0.6.5-0.20250324102321-1ddf4ccbfab8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.3.1 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI=
|
||||
github.com/sagernet/sing-mux v0.3.1/go.mod h1:Mkdz8LnDstthz0HWuA/5foncnDIdcNN5KZ6AdJX+x78=
|
||||
github.com/sagernet/sing-quic v0.4.1-beta.1 h1:V2VfMckT3EQR3ZdfSzJgZZDsvfZZH42QAZpnOnHKa0s=
|
||||
@@ -190,8 +201,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wK
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056 h1:GFNJQAHhSXqAfxAw1wDG/QWbdpGH5Na3k8qUynqWnEA=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056/go.mod h1:HyacBPIFiKihJQR8LQp56FM4hBtd/7MZXnRxxQIOPsc=
|
||||
github.com/sagernet/sing-tun v0.6.1 h1:4l0+gnEKcGjlWfUVTD+W0BRApqIny/lU2ZliurE+VMo=
|
||||
github.com/sagernet/sing-tun v0.6.1/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec h1:9/OYGb9qDmUFIhqd3S+3eni62EKRQR1rSmRH18baA/M=
|
||||
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-vmess v0.2.0 h1:pCMGUXN2k7RpikQV65/rtXtDHzb190foTfF9IGTMZrI=
|
||||
github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||
@@ -209,6 +220,7 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
@@ -321,6 +333,8 @@ google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -10,6 +10,10 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultTimeFormat = "-0700 2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Context context.Context
|
||||
Options option.LogOptions
|
||||
@@ -47,7 +51,7 @@ func New(options Options) (Factory, error) {
|
||||
DisableColors: logOptions.DisableColor || logFilePath != "",
|
||||
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
|
||||
FullTimestamp: logOptions.Timestamp,
|
||||
TimestampFormat: "-0700 2006-01-02 15:04:05",
|
||||
TimestampFormat: DefaultTimeFormat,
|
||||
}
|
||||
factory := NewDefaultFactory(
|
||||
options.Context,
|
||||
|
||||
11
mitm/constants.go
Normal file
11
mitm/constants.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
var surgeTinyGif = common.OnceValue(func() []byte {
|
||||
return common.Must1(base64.StdEncoding.DecodeString("R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA="))
|
||||
})
|
||||
1099
mitm/engine.go
Normal file
1099
mitm/engine.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,13 @@ type _CertificateOptions struct {
|
||||
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
|
||||
CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"`
|
||||
CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_path,omitempty"`
|
||||
TLSDecryption *TLSDecryptionOptions `json:"tls_decryption,omitempty"`
|
||||
}
|
||||
|
||||
type TLSDecryptionOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
KeyPair string `json:"key_pair_p12,omitempty"`
|
||||
KeyPairPassword string `json:"key_pair_p12_password,omitempty"`
|
||||
}
|
||||
|
||||
type CertificateOptions _CertificateOptions
|
||||
|
||||
@@ -191,34 +191,24 @@ func (o *DNSServerOptions) Upgrade(ctx context.Context) error {
|
||||
serverType = C.DNSTypeUDP
|
||||
}
|
||||
}
|
||||
var remoteOptions RemoteDNSServerOptions
|
||||
if options.Detour == "" {
|
||||
remoteOptions = RemoteDNSServerOptions{
|
||||
LocalDNSServerOptions: LocalDNSServerOptions{
|
||||
LegacyStrategy: options.Strategy,
|
||||
LegacyDefaultDialer: options.Detour == "",
|
||||
LegacyClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
},
|
||||
LegacyAddressResolver: options.AddressResolver,
|
||||
LegacyAddressStrategy: options.AddressStrategy,
|
||||
LegacyAddressFallbackDelay: options.AddressFallbackDelay,
|
||||
}
|
||||
} else {
|
||||
remoteOptions = RemoteDNSServerOptions{
|
||||
LocalDNSServerOptions: LocalDNSServerOptions{
|
||||
DialerOptions: DialerOptions{
|
||||
Detour: options.Detour,
|
||||
DomainResolver: &DomainResolveOptions{
|
||||
Server: options.AddressResolver,
|
||||
Strategy: options.AddressStrategy,
|
||||
},
|
||||
FallbackDelay: options.AddressFallbackDelay,
|
||||
remoteOptions := RemoteDNSServerOptions{
|
||||
LocalDNSServerOptions: LocalDNSServerOptions{
|
||||
DialerOptions: DialerOptions{
|
||||
Detour: options.Detour,
|
||||
DomainResolver: &DomainResolveOptions{
|
||||
Server: options.AddressResolver,
|
||||
Strategy: options.AddressStrategy,
|
||||
},
|
||||
LegacyStrategy: options.Strategy,
|
||||
LegacyDefaultDialer: options.Detour == "",
|
||||
LegacyClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
FallbackDelay: options.AddressFallbackDelay,
|
||||
},
|
||||
}
|
||||
Legacy: true,
|
||||
LegacyStrategy: options.Strategy,
|
||||
LegacyDefaultDialer: options.Detour == "",
|
||||
LegacyClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
},
|
||||
LegacyAddressResolver: options.AddressResolver,
|
||||
LegacyAddressStrategy: options.AddressStrategy,
|
||||
LegacyAddressFallbackDelay: options.AddressFallbackDelay,
|
||||
}
|
||||
switch serverType {
|
||||
case C.DNSTypeLocal:
|
||||
@@ -362,6 +352,7 @@ type HostsDNSServerOptions struct {
|
||||
|
||||
type LocalDNSServerOptions struct {
|
||||
DialerOptions
|
||||
Legacy bool `json:"-"`
|
||||
LegacyStrategy DomainStrategy `json:"-"`
|
||||
LegacyDefaultDialer bool `json:"-"`
|
||||
LegacyClientSubnet netip.Prefix `json:"-"`
|
||||
|
||||
@@ -68,6 +68,7 @@ type ListenOptions struct {
|
||||
UDPFragment *bool `json:"udp_fragment,omitempty"`
|
||||
UDPFragmentDefault bool `json:"-"`
|
||||
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
||||
NetNs string `json:"netns,omitempty"`
|
||||
|
||||
// Deprecated: removed
|
||||
ProxyProtocol bool `json:"proxy_protocol,omitempty"`
|
||||
|
||||
31
option/mitm.go
Normal file
31
option/mitm.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
type MITMOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
HTTP2Enabled bool `json:"http2_enabled,omitempty"`
|
||||
}
|
||||
|
||||
type MITMRouteOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Print bool `json:"print,omitempty"`
|
||||
Script badoption.Listable[MITMRouteSurgeScriptOptions] `json:"surge_script,omitempty"`
|
||||
SurgeURLRewrite badoption.Listable[SurgeURLRewriteLine] `json:"surge_url_rewrite,omitempty"`
|
||||
SurgeHeaderRewrite badoption.Listable[SurgeHeaderRewriteLine] `json:"surge_header_rewrite,omitempty"`
|
||||
SurgeBodyRewrite badoption.Listable[SurgeBodyRewriteLine] `json:"surge_body_rewrite,omitempty"`
|
||||
SurgeMapLocal badoption.Listable[SurgeMapLocalLine] `json:"surge_map_local,omitempty"`
|
||||
}
|
||||
|
||||
type MITMRouteSurgeScriptOptions struct {
|
||||
Tag string `json:"tag"`
|
||||
Type badoption.Listable[string] `json:"type"`
|
||||
Pattern badoption.Listable[*badoption.Regexp] `json:"pattern"`
|
||||
Timeout badoption.Duration `json:"timeout,omitempty"`
|
||||
RequiresBody bool `json:"requires_body,omitempty"`
|
||||
MaxSize int64 `json:"max_size,omitempty"`
|
||||
BinaryBodyMode bool `json:"binary_body_mode,omitempty"`
|
||||
Arguments badoption.Listable[string] `json:"arguments,omitempty"`
|
||||
}
|
||||
449
option/mitm_surge_urlrewrite.go
Normal file
449
option/mitm_surge_urlrewrite.go
Normal file
@@ -0,0 +1,449 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
)
|
||||
|
||||
type SurgeURLRewriteLine struct {
|
||||
Pattern *regexp.Regexp
|
||||
Destination *url.URL
|
||||
Redirect bool
|
||||
Reject bool
|
||||
}
|
||||
|
||||
func (l SurgeURLRewriteLine) String() string {
|
||||
var fields []string
|
||||
fields = append(fields, l.Pattern.String())
|
||||
if l.Reject {
|
||||
fields = append(fields, "_")
|
||||
} else {
|
||||
fields = append(fields, l.Destination.String())
|
||||
}
|
||||
switch {
|
||||
case l.Redirect:
|
||||
fields = append(fields, "302")
|
||||
case l.Reject:
|
||||
fields = append(fields, "reject")
|
||||
default:
|
||||
fields = append(fields, "header")
|
||||
}
|
||||
return encodeSurgeKeys(fields)
|
||||
}
|
||||
|
||||
func (l SurgeURLRewriteLine) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(l.String())
|
||||
}
|
||||
|
||||
func (l *SurgeURLRewriteLine) UnmarshalJSON(bytes []byte) error {
|
||||
var stringValue string
|
||||
err := json.Unmarshal(bytes, &stringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fields, err := surgeFields(stringValue)
|
||||
if err != nil {
|
||||
return E.Cause(err, "invalid surge_url_rewrite line: ", stringValue)
|
||||
} else if len(fields) < 2 || len(fields) > 3 {
|
||||
return E.New("invalid surge_url_rewrite line: ", stringValue)
|
||||
}
|
||||
pattern, err := regexp.Compile(fields[0].Key)
|
||||
if err != nil {
|
||||
return E.Cause(err, "invalid surge_url_rewrite line: invalid pattern: ", stringValue)
|
||||
}
|
||||
l.Pattern = pattern
|
||||
l.Destination, err = url.Parse(fields[1].Key)
|
||||
if err != nil {
|
||||
return E.Cause(err, "invalid surge_url_rewrite line: invalid destination: ", stringValue)
|
||||
}
|
||||
if len(fields) == 3 {
|
||||
switch fields[2].Key {
|
||||
case "header":
|
||||
case "302":
|
||||
l.Redirect = true
|
||||
case "reject":
|
||||
l.Reject = true
|
||||
default:
|
||||
return E.New("invalid surge_url_rewrite line: invalid action: ", stringValue)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SurgeHeaderRewriteLine struct {
|
||||
Response bool
|
||||
Pattern *regexp.Regexp
|
||||
Add bool
|
||||
Delete bool
|
||||
Replace bool
|
||||
ReplaceRegex bool
|
||||
Key string
|
||||
Match *regexp.Regexp
|
||||
Value string
|
||||
}
|
||||
|
||||
func (l SurgeHeaderRewriteLine) String() string {
|
||||
var fields []string
|
||||
if !l.Response {
|
||||
fields = append(fields, "http-request")
|
||||
} else {
|
||||
fields = append(fields, "http-response")
|
||||
}
|
||||
fields = append(fields, l.Pattern.String())
|
||||
if l.Add {
|
||||
fields = append(fields, "header-add")
|
||||
} else if l.Delete {
|
||||
fields = append(fields, "header-del")
|
||||
} else if l.Replace {
|
||||
fields = append(fields, "header-replace")
|
||||
} else if l.ReplaceRegex {
|
||||
fields = append(fields, "header-replace-regex")
|
||||
}
|
||||
fields = append(fields, l.Key)
|
||||
if l.Add || l.Replace {
|
||||
fields = append(fields, l.Value)
|
||||
} else if l.ReplaceRegex {
|
||||
fields = append(fields, l.Match.String(), l.Value)
|
||||
}
|
||||
return encodeSurgeKeys(fields)
|
||||
}
|
||||
|
||||
func (l SurgeHeaderRewriteLine) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(l.String())
|
||||
}
|
||||
|
||||
func (l *SurgeHeaderRewriteLine) UnmarshalJSON(bytes []byte) error {
|
||||
var stringValue string
|
||||
err := json.Unmarshal(bytes, &stringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fields, err := surgeFields(stringValue)
|
||||
if err != nil {
|
||||
return E.Cause(err, "invalid surge_header_rewrite line: ", stringValue)
|
||||
} else if len(fields) < 4 {
|
||||
return E.New("invalid surge_header_rewrite line: ", stringValue)
|
||||
}
|
||||
switch fields[0].Key {
|
||||
case "http-request":
|
||||
case "http-response":
|
||||
l.Response = true
|
||||
default:
|
||||
return E.New("invalid surge_header_rewrite line: invalid type: ", stringValue)
|
||||
}
|
||||
l.Pattern, err = regexp.Compile(fields[1].Key)
|
||||
if err != nil {
|
||||
return E.Cause(err, "invalid surge_header_rewrite line: invalid pattern: ", stringValue)
|
||||
}
|
||||
switch fields[2].Key {
|
||||
case "header-add":
|
||||
l.Add = true
|
||||
if len(fields) != 5 {
|
||||
return E.New("invalid surge_header_rewrite line: " + stringValue)
|
||||
}
|
||||
l.Key = fields[3].Key
|
||||
l.Value = fields[4].Key
|
||||
case "header-del":
|
||||
l.Delete = true
|
||||
l.Key = fields[3].Key
|
||||
case "header-replace":
|
||||
l.Replace = true
|
||||
if len(fields) != 5 {
|
||||
return E.New("invalid surge_header_rewrite line: " + stringValue)
|
||||
}
|
||||
l.Key = fields[3].Key
|
||||
l.Value = fields[4].Key
|
||||
case "header-replace-regex":
|
||||
l.ReplaceRegex = true
|
||||
if len(fields) != 6 {
|
||||
return E.New("invalid surge_header_rewrite line: " + stringValue)
|
||||
}
|
||||
l.Key = fields[3].Key
|
||||
l.Match, err = regexp.Compile(fields[4].Key)
|
||||
if err != nil {
|
||||
return E.Cause(err, "invalid surge_header_rewrite line: invalid match: ", stringValue)
|
||||
}
|
||||
l.Value = fields[5].Key
|
||||
default:
|
||||
return E.New("invalid surge_header_rewrite line: invalid action: ", stringValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SurgeBodyRewriteLine struct {
|
||||
Response bool
|
||||
Pattern *regexp.Regexp
|
||||
Match []*regexp.Regexp
|
||||
Replace []string
|
||||
}
|
||||
|
||||
func (l SurgeBodyRewriteLine) String() string {
|
||||
var fields []string
|
||||
if !l.Response {
|
||||
fields = append(fields, "http-request")
|
||||
} else {
|
||||
fields = append(fields, "http-response")
|
||||
}
|
||||
for i := 0; i < len(l.Match); i += 2 {
|
||||
fields = append(fields, l.Match[i].String(), l.Replace[i])
|
||||
}
|
||||
return strings.Join(fields, " ")
|
||||
}
|
||||
|
||||
func (l SurgeBodyRewriteLine) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(l.String())
|
||||
}
|
||||
|
||||
func (l *SurgeBodyRewriteLine) UnmarshalJSON(bytes []byte) error {
|
||||
var stringValue string
|
||||
err := json.Unmarshal(bytes, &stringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fields, err := surgeFields(stringValue)
|
||||
if err != nil {
|
||||
return E.Cause(err, "invalid surge_body_rewrite line: ", stringValue)
|
||||
} else if len(fields) < 4 {
|
||||
return E.New("invalid surge_body_rewrite line: ", stringValue)
|
||||
} else if len(fields)%2 != 0 {
|
||||
return E.New("invalid surge_body_rewrite line: ", stringValue)
|
||||
}
|
||||
switch fields[0].Key {
|
||||
case "http-request":
|
||||
case "http-response":
|
||||
l.Response = true
|
||||
default:
|
||||
return E.New("invalid surge_body_rewrite line: invalid type: ", stringValue)
|
||||
}
|
||||
l.Pattern, err = regexp.Compile(fields[1].Key)
|
||||
for i := 2; i < len(fields); i += 2 {
|
||||
var match *regexp.Regexp
|
||||
match, err = regexp.Compile(fields[i].Key)
|
||||
if err != nil {
|
||||
return E.Cause(err, "invalid surge_body_rewrite line: invalid match: ", stringValue)
|
||||
}
|
||||
l.Match = append(l.Match, match)
|
||||
l.Replace = append(l.Replace, fields[i+1].Key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SurgeMapLocalLine struct {
|
||||
Pattern *regexp.Regexp
|
||||
StatusCode int
|
||||
File bool
|
||||
Text bool
|
||||
TinyGif bool
|
||||
Base64 bool
|
||||
Data string
|
||||
Base64Data []byte
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
func (l SurgeMapLocalLine) String() string {
|
||||
var fields []surgeField
|
||||
fields = append(fields, surgeField{Key: l.Pattern.String()})
|
||||
if l.File {
|
||||
fields = append(fields, surgeField{Key: "data-type", Value: "file"})
|
||||
fields = append(fields, surgeField{Key: "data", Value: l.Data})
|
||||
} else if l.Text {
|
||||
fields = append(fields, surgeField{Key: "data-type", Value: "text"})
|
||||
fields = append(fields, surgeField{Key: "data", Value: l.Data})
|
||||
} else if l.TinyGif {
|
||||
fields = append(fields, surgeField{Key: "data-type", Value: "tiny-gif"})
|
||||
} else if l.Base64 {
|
||||
fields = append(fields, surgeField{Key: "data-type", Value: "base64"})
|
||||
fields = append(fields, surgeField{Key: "data-type", Value: base64.StdEncoding.EncodeToString(l.Base64Data)})
|
||||
}
|
||||
if l.StatusCode != 0 {
|
||||
fields = append(fields, surgeField{Key: "status-code", Value: F.ToString(l.StatusCode), ValueSet: true})
|
||||
}
|
||||
if len(l.Headers) > 0 {
|
||||
var headers []string
|
||||
for key, values := range l.Headers {
|
||||
for _, value := range values {
|
||||
headers = append(headers, key+":"+value)
|
||||
}
|
||||
}
|
||||
fields = append(fields, surgeField{Key: "headers", Value: strings.Join(headers, "|")})
|
||||
}
|
||||
return encodeSurgeFields(fields)
|
||||
}
|
||||
|
||||
func (l SurgeMapLocalLine) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(l.String())
|
||||
}
|
||||
|
||||
func (l *SurgeMapLocalLine) UnmarshalJSON(bytes []byte) error {
|
||||
var stringValue string
|
||||
err := json.Unmarshal(bytes, &stringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fields, err := surgeFields(stringValue)
|
||||
if err != nil {
|
||||
return E.Cause(err, "invalid surge_map_local line: ", stringValue)
|
||||
} else if len(fields) < 1 {
|
||||
return E.New("invalid surge_map_local line: ", stringValue)
|
||||
}
|
||||
l.Pattern, err = regexp.Compile(fields[0].Key)
|
||||
if err != nil {
|
||||
return E.Cause(err, "invalid surge_map_local line: invalid pattern: ", stringValue)
|
||||
}
|
||||
dataTypeField := common.Find(fields, func(it surgeField) bool {
|
||||
return it.Key == "data-type"
|
||||
})
|
||||
if !dataTypeField.ValueSet {
|
||||
return E.New("invalid surge_map_local line: missing data-type: ", stringValue)
|
||||
}
|
||||
switch dataTypeField.Value {
|
||||
case "file":
|
||||
l.File = true
|
||||
case "text":
|
||||
l.Text = true
|
||||
case "tiny-gif":
|
||||
l.TinyGif = true
|
||||
case "base64":
|
||||
l.Base64 = true
|
||||
default:
|
||||
return E.New("unsupported data-type ", dataTypeField.Value)
|
||||
}
|
||||
for i := 1; i < len(fields); i++ {
|
||||
switch fields[i].Key {
|
||||
case "data-type":
|
||||
continue
|
||||
case "data":
|
||||
if l.File {
|
||||
l.Data = fields[i].Value
|
||||
} else if l.Text {
|
||||
l.Data = fields[i].Value
|
||||
} else if l.Base64 {
|
||||
l.Base64Data, err = base64.StdEncoding.DecodeString(fields[i].Value)
|
||||
if err != nil {
|
||||
return E.New("invalid surge_map_local line: invalid base64 data: ", stringValue)
|
||||
}
|
||||
}
|
||||
case "status-code":
|
||||
statusCode, err := strconv.ParseInt(fields[i].Value, 10, 16)
|
||||
if err != nil {
|
||||
return E.New("invalid surge_map_local line: invalid status code: ", stringValue)
|
||||
}
|
||||
l.StatusCode = int(statusCode)
|
||||
case "header":
|
||||
headers := make(http.Header)
|
||||
for _, headerLine := range strings.Split(fields[i].Value, "|") {
|
||||
if !strings.Contains(headerLine, ":") {
|
||||
return E.New("invalid surge_map_local line: headers: missing `:` in item: ", stringValue, ": ", headerLine)
|
||||
}
|
||||
headers.Add(common.SubstringBefore(headerLine, ":"), common.SubstringAfter(headerLine, ":"))
|
||||
}
|
||||
l.Headers = headers
|
||||
default:
|
||||
return E.New("invalid surge_map_local line: unknown options: ", fields[i].Key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type surgeField struct {
|
||||
Key string
|
||||
Value string
|
||||
ValueSet bool
|
||||
}
|
||||
|
||||
func encodeSurgeKeys(keys []string) string {
|
||||
keys = common.Map(keys, func(it string) string {
|
||||
if strings.ContainsFunc(it, unicode.IsSpace) {
|
||||
return "\"" + it + "\""
|
||||
} else {
|
||||
return it
|
||||
}
|
||||
})
|
||||
return strings.Join(keys, " ")
|
||||
}
|
||||
|
||||
func encodeSurgeFields(fields []surgeField) string {
|
||||
return strings.Join(common.Map(fields, func(it surgeField) string {
|
||||
if !it.ValueSet {
|
||||
if strings.ContainsFunc(it.Key, unicode.IsSpace) {
|
||||
return "\"" + it.Key + "\""
|
||||
} else {
|
||||
return it.Key
|
||||
}
|
||||
} else {
|
||||
if strings.ContainsFunc(it.Value, unicode.IsSpace) {
|
||||
return it.Key + "=\"" + it.Value + "\""
|
||||
} else {
|
||||
return it.Key + "=" + it.Value
|
||||
}
|
||||
}
|
||||
}), " ")
|
||||
}
|
||||
|
||||
func surgeFields(s string) ([]surgeField, error) {
|
||||
var (
|
||||
fields []surgeField
|
||||
currentField *surgeField
|
||||
)
|
||||
for _, field := range strings.Fields(s) {
|
||||
if currentField != nil {
|
||||
field = " " + field
|
||||
if strings.HasSuffix(field, "\"") {
|
||||
field = field[:len(field)-1]
|
||||
if !currentField.ValueSet {
|
||||
currentField.Key += field
|
||||
} else {
|
||||
currentField.Value += field
|
||||
}
|
||||
fields = append(fields, *currentField)
|
||||
currentField = nil
|
||||
} else {
|
||||
if !currentField.ValueSet {
|
||||
currentField.Key += field
|
||||
} else {
|
||||
currentField.Value += field
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(field, "=") {
|
||||
if strings.HasPrefix(field, "\"") {
|
||||
field = field[1:]
|
||||
if strings.HasSuffix(field, "\"") {
|
||||
field = field[:len(field)-1]
|
||||
} else {
|
||||
currentField = &surgeField{Key: field}
|
||||
continue
|
||||
}
|
||||
}
|
||||
fields = append(fields, surgeField{Key: field})
|
||||
} else {
|
||||
key := common.SubstringBefore(field, "=")
|
||||
value := common.SubstringAfter(field, "=")
|
||||
if strings.HasPrefix(value, "\"") {
|
||||
value = value[1:]
|
||||
if strings.HasSuffix(field, "\"") {
|
||||
value = value[:len(value)-1]
|
||||
} else {
|
||||
currentField = &surgeField{Key: key, Value: value, ValueSet: true}
|
||||
continue
|
||||
}
|
||||
}
|
||||
fields = append(fields, surgeField{Key: key, Value: value, ValueSet: true})
|
||||
}
|
||||
}
|
||||
if currentField != nil {
|
||||
return nil, E.New("invalid surge fields line: ", s)
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
@@ -12,13 +12,15 @@ type _Options struct {
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Log *LogOptions `json:"log,omitempty"`
|
||||
DNS *DNSOptions `json:"dns,omitempty"`
|
||||
NTP *NTPOptions `json:"ntp,omitempty"`
|
||||
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
||||
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
Route *RouteOptions `json:"route,omitempty"`
|
||||
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
|
||||
NTP *NTPOptions `json:"ntp,omitempty"`
|
||||
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
||||
MITM *MITMOptions `json:"mitm,omitempty"`
|
||||
Scripts []Script `json:"scripts,omitempty"`
|
||||
}
|
||||
|
||||
type Options _Options
|
||||
|
||||
@@ -77,6 +77,7 @@ type DialerOptions struct {
|
||||
TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
|
||||
UDPFragment *bool `json:"udp_fragment,omitempty"`
|
||||
UDPFragmentDefault bool `json:"-"`
|
||||
NetNs string `json:"netns,omitempty"`
|
||||
DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"`
|
||||
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
|
||||
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
||||
|
||||
@@ -125,10 +125,9 @@ func (r *DefaultRule) UnmarshalJSON(data []byte) error {
|
||||
return badjson.UnmarshallExcluded(data, &r.RawDefaultRule, &r.RuleAction)
|
||||
}
|
||||
|
||||
func (r *DefaultRule) IsValid() bool {
|
||||
func (r DefaultRule) IsValid() bool {
|
||||
var defaultValue DefaultRule
|
||||
defaultValue.Invert = r.Invert
|
||||
defaultValue.Action = r.Action
|
||||
return !reflect.DeepEqual(r, defaultValue)
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,8 @@ type RawRouteOptionsActionOptions struct {
|
||||
|
||||
TLSFragment bool `json:"tls_fragment,omitempty"`
|
||||
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
|
||||
|
||||
MITM *MITMRouteOptions `json:"mitm,omitempty"`
|
||||
}
|
||||
|
||||
type RouteOptionsActionOptions RawRouteOptionsActionOptions
|
||||
|
||||
@@ -132,7 +132,6 @@ func (r *DefaultDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte)
|
||||
func (r DefaultDNSRule) IsValid() bool {
|
||||
var defaultValue DefaultDNSRule
|
||||
defaultValue.Invert = r.Invert
|
||||
defaultValue.DNSRuleAction = r.DNSRuleAction
|
||||
return !reflect.DeepEqual(r, defaultValue)
|
||||
}
|
||||
|
||||
|
||||
128
option/script.go
Normal file
128
option/script.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
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/json/badoption"
|
||||
)
|
||||
|
||||
type _ScriptSourceOptions struct {
|
||||
Source string `json:"source"`
|
||||
LocalOptions LocalScriptSource `json:"-"`
|
||||
RemoteOptions RemoteScriptSource `json:"-"`
|
||||
}
|
||||
|
||||
type LocalScriptSource struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type RemoteScriptSource struct {
|
||||
URL string `json:"url"`
|
||||
DownloadDetour string `json:"download_detour,omitempty"`
|
||||
UpdateInterval badoption.Duration `json:"update_interval,omitempty"`
|
||||
}
|
||||
|
||||
type ScriptSourceOptions _ScriptSourceOptions
|
||||
|
||||
func (o ScriptSourceOptions) MarshalJSON() ([]byte, error) {
|
||||
var source any
|
||||
switch o.Source {
|
||||
case C.ScriptSourceTypeLocal:
|
||||
source = o.LocalOptions
|
||||
case C.ScriptSourceTypeRemote:
|
||||
source = o.RemoteOptions
|
||||
default:
|
||||
return nil, E.New("unknown script source: ", o.Source)
|
||||
}
|
||||
return badjson.MarshallObjects((_ScriptSourceOptions)(o), source)
|
||||
}
|
||||
|
||||
func (o *ScriptSourceOptions) UnmarshalJSON(bytes []byte) error {
|
||||
err := json.Unmarshal(bytes, (*_ScriptSourceOptions)(o))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var source any
|
||||
switch o.Source {
|
||||
case C.ScriptSourceTypeLocal:
|
||||
source = &o.LocalOptions
|
||||
case C.ScriptSourceTypeRemote:
|
||||
source = &o.RemoteOptions
|
||||
default:
|
||||
return E.New("unknown script source: ", o.Source)
|
||||
}
|
||||
return json.Unmarshal(bytes, source)
|
||||
}
|
||||
|
||||
// TODO: make struct in order
|
||||
type Script struct {
|
||||
ScriptSourceOptions
|
||||
ScriptOptions
|
||||
}
|
||||
|
||||
func (s Script) MarshalJSON() ([]byte, error) {
|
||||
return badjson.MarshallObjects(s.ScriptSourceOptions, s.ScriptOptions)
|
||||
}
|
||||
|
||||
func (s *Script) UnmarshalJSON(bytes []byte) error {
|
||||
err := json.Unmarshal(bytes, &s.ScriptSourceOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return badjson.UnmarshallExcluded(bytes, &s.ScriptSourceOptions, &s.ScriptOptions)
|
||||
}
|
||||
|
||||
type _ScriptOptions struct {
|
||||
Type string `json:"type"`
|
||||
Tag string `json:"tag"`
|
||||
SurgeOptions SurgeScriptOptions `json:"-"`
|
||||
}
|
||||
|
||||
type ScriptOptions _ScriptOptions
|
||||
|
||||
func (o ScriptOptions) MarshalJSON() ([]byte, error) {
|
||||
var v any
|
||||
switch o.Type {
|
||||
case C.ScriptTypeSurge:
|
||||
v = &o.SurgeOptions
|
||||
default:
|
||||
return nil, E.New("unknown script type: ", o.Type)
|
||||
}
|
||||
if v == nil {
|
||||
return badjson.MarshallObjects((_ScriptOptions)(o))
|
||||
}
|
||||
return badjson.MarshallObjects((_ScriptOptions)(o), v)
|
||||
}
|
||||
|
||||
func (o *ScriptOptions) UnmarshalJSON(bytes []byte) error {
|
||||
err := json.Unmarshal(bytes, (*_ScriptOptions)(o))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var v any
|
||||
switch o.Type {
|
||||
case C.ScriptTypeSurge:
|
||||
v = &o.SurgeOptions
|
||||
case "":
|
||||
return E.New("missing script type")
|
||||
default:
|
||||
return E.New("unknown script type: ", o.Type)
|
||||
}
|
||||
if v == nil {
|
||||
// check unknown fields
|
||||
return json.UnmarshalDisallowUnknownFields(bytes, &_ScriptOptions{})
|
||||
}
|
||||
return badjson.UnmarshallExcluded(bytes, (*_ScriptOptions)(o), v)
|
||||
}
|
||||
|
||||
type SurgeScriptOptions struct {
|
||||
CronOptions *CronScriptOptions `json:"cron,omitempty"`
|
||||
}
|
||||
|
||||
type CronScriptOptions struct {
|
||||
Expression string `json:"expression"`
|
||||
Arguments []string `json:"arguments,omitempty"`
|
||||
Timeout badoption.Duration `json:"timeout,omitempty"`
|
||||
}
|
||||
@@ -4,17 +4,18 @@ import (
|
||||
"encoding/json"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
)
|
||||
|
||||
type ShadowTLSInboundOptions struct {
|
||||
ListenOptions
|
||||
Version int `json:"version,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Users []ShadowTLSUser `json:"users,omitempty"`
|
||||
Handshake ShadowTLSHandshakeOptions `json:"handshake,omitempty"`
|
||||
HandshakeForServerName map[string]ShadowTLSHandshakeOptions `json:"handshake_for_server_name,omitempty"`
|
||||
StrictMode bool `json:"strict_mode,omitempty"`
|
||||
WildcardSNI WildcardSNI `json:"wildcard_sni,omitempty"`
|
||||
Version int `json:"version,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Users []ShadowTLSUser `json:"users,omitempty"`
|
||||
Handshake ShadowTLSHandshakeOptions `json:"handshake,omitempty"`
|
||||
HandshakeForServerName *badjson.TypedMap[string, ShadowTLSHandshakeOptions] `json:"handshake_for_server_name,omitempty"`
|
||||
StrictMode bool `json:"strict_mode,omitempty"`
|
||||
WildcardSNI WildcardSNI `json:"wildcard_sni,omitempty"`
|
||||
}
|
||||
|
||||
type WildcardSNI int
|
||||
|
||||
@@ -7,13 +7,15 @@ import (
|
||||
|
||||
type SocksInboundOptions struct {
|
||||
ListenOptions
|
||||
Users []auth.User `json:"users,omitempty"`
|
||||
Users []auth.User `json:"users,omitempty"`
|
||||
DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"`
|
||||
}
|
||||
|
||||
type HTTPMixedInboundOptions struct {
|
||||
ListenOptions
|
||||
Users []auth.User `json:"users,omitempty"`
|
||||
SetSystemProxy bool `json:"set_system_proxy,omitempty"`
|
||||
Users []auth.User `json:"users,omitempty"`
|
||||
DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"`
|
||||
SetSystemProxy bool `json:"set_system_proxy,omitempty"`
|
||||
InboundTLSOptionsContainer
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -27,6 +28,7 @@ func RegisterOutbound(registry *outbound.Registry) {
|
||||
var (
|
||||
_ N.ParallelDialer = (*Outbound)(nil)
|
||||
_ dialer.ParallelNetworkDialer = (*Outbound)(nil)
|
||||
_ dialer.DirectDialer = (*Outbound)(nil)
|
||||
)
|
||||
|
||||
type Outbound struct {
|
||||
@@ -37,6 +39,7 @@ type Outbound struct {
|
||||
fallbackDelay time.Duration
|
||||
overrideOption int
|
||||
overrideDestination M.Socksaddr
|
||||
isEmpty bool
|
||||
// loopBack *loopBackDetector
|
||||
}
|
||||
|
||||
@@ -56,6 +59,8 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
domainStrategy: C.DomainStrategy(options.DomainStrategy),
|
||||
fallbackDelay: time.Duration(options.FallbackDelay),
|
||||
dialer: outboundDialer.(dialer.ParallelInterfaceDialer),
|
||||
//nolint:staticcheck
|
||||
isEmpty: reflect.DeepEqual(options.DialerOptions, option.DialerOptions{UDPFragmentDefault: true}) && options.OverrideAddress == "" && options.OverridePort == 0,
|
||||
// loopBack: newLoopBackDetector(router),
|
||||
}
|
||||
//nolint:staticcheck
|
||||
@@ -242,6 +247,10 @@ func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.
|
||||
return conn, newDestination, nil
|
||||
}
|
||||
|
||||
func (h *Outbound) IsEmpty() bool {
|
||||
return h.isEmpty
|
||||
}
|
||||
|
||||
/*func (h *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||
if h.loopBack.CheckConn(metadata.Source.AddrPort(), M.AddrPortFromNet(conn.LocalAddr())) {
|
||||
return E.New("reject loopback connection to ", metadata.Destination)
|
||||
|
||||
@@ -85,7 +85,7 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada
|
||||
}
|
||||
switch headerBytes[0] {
|
||||
case socks4.Version, socks5.Version:
|
||||
return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose)
|
||||
return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, metadata.Source, onClose)
|
||||
default:
|
||||
return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose)
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ func (t *TProxy) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
|
||||
|
||||
type tproxyPacketWriter struct {
|
||||
ctx context.Context
|
||||
listener *listener.Listener
|
||||
source netip.AddrPort
|
||||
destination M.Socksaddr
|
||||
conn *net.UDPConn
|
||||
@@ -130,7 +131,12 @@ type tproxyPacketWriter struct {
|
||||
|
||||
func (t *TProxy) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) {
|
||||
ctx := log.ContextWithNewID(t.ctx)
|
||||
writer := &tproxyPacketWriter{ctx: ctx, source: source.AddrPort(), destination: destination}
|
||||
writer := &tproxyPacketWriter{
|
||||
ctx: ctx,
|
||||
listener: t.listener,
|
||||
source: source.AddrPort(),
|
||||
destination: destination,
|
||||
}
|
||||
return true, ctx, writer, func(it error) {
|
||||
common.Close(common.PtrOrNil(writer.conn))
|
||||
}
|
||||
@@ -146,10 +152,10 @@ func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socks
|
||||
}
|
||||
return err
|
||||
}
|
||||
var listener net.ListenConfig
|
||||
listener.Control = control.Append(listener.Control, control.ReuseAddr())
|
||||
listener.Control = control.Append(listener.Control, redir.TProxyWriteBack())
|
||||
packetConn, err := listener.ListenPacket(w.ctx, "udp", destination.String())
|
||||
var listenConfig net.ListenConfig
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
||||
listenConfig.Control = control.Append(listenConfig.Control, redir.TProxyWriteBack())
|
||||
packetConn, err := w.listener.ListenPacket(listenConfig, w.ctx, "udp", destination.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -46,14 +46,16 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
var handshakeForServerName map[string]shadowtls.HandshakeConfig
|
||||
if options.Version > 1 {
|
||||
handshakeForServerName = make(map[string]shadowtls.HandshakeConfig)
|
||||
for serverName, serverOptions := range options.HandshakeForServerName {
|
||||
handshakeDialer, err := dialer.New(ctx, serverOptions.DialerOptions, serverOptions.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handshakeForServerName[serverName] = shadowtls.HandshakeConfig{
|
||||
Server: serverOptions.ServerOptions.Build(),
|
||||
Dialer: handshakeDialer,
|
||||
if options.HandshakeForServerName != nil {
|
||||
for _, entry := range options.HandshakeForServerName.Entries() {
|
||||
handshakeDialer, err := dialer.New(ctx, entry.Value.DialerOptions, entry.Value.ServerIsDomain())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handshakeForServerName[entry.Key] = shadowtls.HandshakeConfig{
|
||||
Server: entry.Value.ServerOptions.Build(),
|
||||
Dialer: handshakeDialer,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (h *Inbound) Close() error {
|
||||
}
|
||||
|
||||
func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose)
|
||||
err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), h.listener, metadata.Source, onClose)
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
if err != nil {
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
|
||||
@@ -99,7 +99,7 @@ func (l *ProxyListener) acceptLoop() {
|
||||
}
|
||||
|
||||
func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error {
|
||||
return socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), l.authenticator, l, M.SocksaddrFromNet(conn.RemoteAddr()), nil)
|
||||
return socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), l.authenticator, l, nil, M.SocksaddrFromNet(conn.RemoteAddr()), nil)
|
||||
}
|
||||
|
||||
func (l *ProxyListener) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
@@ -23,23 +24,31 @@ import (
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ adapter.ConnectionManager = (*ConnectionManager)(nil)
|
||||
|
||||
type ConnectionManager struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
mitm adapter.MITMEngine
|
||||
access sync.Mutex
|
||||
connections list.List[io.Closer]
|
||||
}
|
||||
|
||||
func NewConnectionManager(logger logger.ContextLogger) *ConnectionManager {
|
||||
func NewConnectionManager(ctx context.Context, logger logger.ContextLogger) *ConnectionManager {
|
||||
return &ConnectionManager{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ConnectionManager) Start(stage adapter.StartStage) error {
|
||||
switch stage {
|
||||
case adapter.StartStateInitialize:
|
||||
m.mitm = service.FromContext[adapter.MITMEngine](m.ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -54,6 +63,14 @@ func (m *ConnectionManager) Close() error {
|
||||
}
|
||||
|
||||
func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
if metadata.MITM != nil && metadata.MITM.Enabled {
|
||||
if m.mitm == nil {
|
||||
m.logger.WarnContext(ctx, "MITM disabled")
|
||||
} else {
|
||||
m.mitm.NewConnection(ctx, this, conn, metadata, onClose)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx = adapter.WithContext(ctx, &metadata)
|
||||
var (
|
||||
remoteConn net.Conn
|
||||
@@ -306,7 +323,7 @@ func (m *ConnectionManager) connectionCopyEarly(source net.Conn, destination io.
|
||||
return err
|
||||
}
|
||||
_, err = payload.ReadOnceFrom(source)
|
||||
if err != nil && !E.IsTimeout(err) {
|
||||
if err != nil && !(E.IsTimeout(err) || errors.Is(err, io.EOF)) {
|
||||
return E.Cause(err, "read payload")
|
||||
}
|
||||
_ = source.SetReadDeadline(time.Time{})
|
||||
|
||||
@@ -458,6 +458,9 @@ match:
|
||||
metadata.TLSFragment = true
|
||||
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
|
||||
}
|
||||
if routeOptions.MITM != nil && routeOptions.MITM.Enabled {
|
||||
metadata.MITM = routeOptions.MITM
|
||||
}
|
||||
}
|
||||
switch action := currentRule.Action().(type) {
|
||||
case *rule.RuleActionSniff:
|
||||
|
||||
@@ -40,6 +40,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
||||
UDPConnect: action.RouteOptions.UDPConnect,
|
||||
TLSFragment: action.RouteOptions.TLSFragment,
|
||||
TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
|
||||
MITM: action.RouteOptions.MITM,
|
||||
},
|
||||
}, nil
|
||||
case C.RuleActionTypeRouteOptions:
|
||||
@@ -53,6 +54,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
||||
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
|
||||
TLSFragment: action.RouteOptionsOptions.TLSFragment,
|
||||
TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
|
||||
MITM: action.RouteOptionsOptions.MITM,
|
||||
}, nil
|
||||
case C.RuleActionTypeDirect:
|
||||
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
|
||||
@@ -152,15 +154,7 @@ func (r *RuleActionRoute) Type() string {
|
||||
func (r *RuleActionRoute) String() string {
|
||||
var descriptions []string
|
||||
descriptions = append(descriptions, r.Outbound)
|
||||
if r.UDPDisableDomainUnmapping {
|
||||
descriptions = append(descriptions, "udp-disable-domain-unmapping")
|
||||
}
|
||||
if r.UDPConnect {
|
||||
descriptions = append(descriptions, "udp-connect")
|
||||
}
|
||||
if r.TLSFragment {
|
||||
descriptions = append(descriptions, "tls-fragment")
|
||||
}
|
||||
descriptions = append(descriptions, r.Descriptions()...)
|
||||
return F.ToString("route(", strings.Join(descriptions, ","), ")")
|
||||
}
|
||||
|
||||
@@ -176,13 +170,14 @@ type RuleActionRouteOptions struct {
|
||||
UDPTimeout time.Duration
|
||||
TLSFragment bool
|
||||
TLSFragmentFallbackDelay time.Duration
|
||||
MITM *option.MITMRouteOptions
|
||||
}
|
||||
|
||||
func (r *RuleActionRouteOptions) Type() string {
|
||||
return C.RuleActionTypeRouteOptions
|
||||
}
|
||||
|
||||
func (r *RuleActionRouteOptions) String() string {
|
||||
func (r *RuleActionRouteOptions) Descriptions() []string {
|
||||
var descriptions []string
|
||||
if r.OverrideAddress.IsValid() {
|
||||
descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString()))
|
||||
@@ -209,9 +204,22 @@ func (r *RuleActionRouteOptions) String() string {
|
||||
descriptions = append(descriptions, "udp-connect")
|
||||
}
|
||||
if r.UDPTimeout > 0 {
|
||||
descriptions = append(descriptions, "udp-timeout")
|
||||
descriptions = append(descriptions, F.ToString("udp-timeout=", r.UDPTimeout))
|
||||
}
|
||||
return F.ToString("route-options(", strings.Join(descriptions, ","), ")")
|
||||
if r.TLSFragment {
|
||||
descriptions = append(descriptions, "tls-fragment")
|
||||
if r.TLSFragmentFallbackDelay > 0 {
|
||||
descriptions = append(descriptions, F.ToString("tls-fragment-fallbac-delay=", r.TLSFragmentFallbackDelay.String()))
|
||||
}
|
||||
}
|
||||
if r.MITM != nil && r.MITM.Enabled {
|
||||
descriptions = append(descriptions, "mitm")
|
||||
}
|
||||
return descriptions
|
||||
}
|
||||
|
||||
func (r *RuleActionRouteOptions) String() string {
|
||||
return F.ToString("route-options(", strings.Join(r.Descriptions(), ","), ")")
|
||||
}
|
||||
|
||||
type RuleActionDNSRoute struct {
|
||||
@@ -444,3 +452,32 @@ func (r *RuleActionPredefined) String() string {
|
||||
options = append(options, common.Map(r.Extra, dns.RR.String)...)
|
||||
return F.ToString("predefined(", strings.Join(options, ","), ")")
|
||||
}
|
||||
|
||||
func (r *RuleActionPredefined) Response(request *dns.Msg) *dns.Msg {
|
||||
return &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: request.Id,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionDesired: true,
|
||||
RecursionAvailable: true,
|
||||
Rcode: r.Rcode,
|
||||
},
|
||||
Question: request.Question,
|
||||
Answer: rewriteRecords(r.Answer, request.Question[0]),
|
||||
Ns: rewriteRecords(r.Ns, request.Question[0]),
|
||||
Extra: rewriteRecords(r.Extra, request.Question[0]),
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteRecords(records []dns.RR, question dns.Question) []dns.RR {
|
||||
return common.Map(records, func(it dns.RR) dns.RR {
|
||||
if strings.HasPrefix(it.Header().Name, "*") {
|
||||
if strings.HasSuffix(question.Name, it.Header().Name[1:]) {
|
||||
it = dns.Copy(it)
|
||||
it.Header().Name = question.Name
|
||||
}
|
||||
}
|
||||
return it
|
||||
})
|
||||
}
|
||||
|
||||
23
script/jsc/array.go
Normal file
23
script/jsc/array.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package jsc
|
||||
|
||||
import (
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func NewUint8Array(runtime *goja.Runtime, data []byte) goja.Value {
|
||||
buffer := runtime.NewArrayBuffer(data)
|
||||
ctor, loaded := goja.AssertConstructor(runtimeGetUint8Array(runtime))
|
||||
if !loaded {
|
||||
panic(runtime.NewTypeError("missing UInt8Array constructor"))
|
||||
}
|
||||
array, err := ctor(nil, runtime.ToValue(buffer))
|
||||
if err != nil {
|
||||
panic(runtime.NewGoError(err))
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
//go:linkname runtimeGetUint8Array github.com/dop251/goja.(*Runtime).getUint8Array
|
||||
func runtimeGetUint8Array(r *goja.Runtime) *goja.Object
|
||||
18
script/jsc/array_test.go
Normal file
18
script/jsc/array_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package jsc_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/script/jsc"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewUInt8Array(t *testing.T) {
|
||||
runtime := goja.New()
|
||||
runtime.Set("hello", jsc.NewUint8Array(runtime, []byte("world")))
|
||||
result, err := runtime.RunString("hello instanceof Uint8Array")
|
||||
require.NoError(t, err)
|
||||
require.True(t, result.ToBoolean())
|
||||
}
|
||||
124
script/jsc/assert.go
Normal file
124
script/jsc/assert.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package jsc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func IsNil(value goja.Value) bool {
|
||||
return value == nil || goja.IsUndefined(value) || goja.IsNull(value)
|
||||
}
|
||||
|
||||
func AssertObject(vm *goja.Runtime, value goja.Value, name string, nilable bool) *goja.Object {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return nil
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
objectValue, isObject := value.(*goja.Object)
|
||||
if !isObject {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected object, but got ", value)))
|
||||
}
|
||||
return objectValue
|
||||
}
|
||||
|
||||
func AssertString(vm *goja.Runtime, value goja.Value, name string, nilable bool) string {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return ""
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
stringValue, isString := value.Export().(string)
|
||||
if !isString {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected string, but got ", value)))
|
||||
}
|
||||
return stringValue
|
||||
}
|
||||
|
||||
func AssertInt(vm *goja.Runtime, value goja.Value, name string, nilable bool) int64 {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return 0
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
integerValue, isNumber := value.Export().(int64)
|
||||
if !isNumber {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected integer, but got ", value)))
|
||||
}
|
||||
return integerValue
|
||||
}
|
||||
|
||||
func AssertBool(vm *goja.Runtime, value goja.Value, name string, nilable bool) bool {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return false
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
boolValue, isBool := value.Export().(bool)
|
||||
if !isBool {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected boolean, but got ", value)))
|
||||
}
|
||||
return boolValue
|
||||
}
|
||||
|
||||
func AssertBinary(vm *goja.Runtime, value goja.Value, name string, nilable bool) []byte {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return nil
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
switch exportedValue := value.Export().(type) {
|
||||
case []byte:
|
||||
return exportedValue
|
||||
case goja.ArrayBuffer:
|
||||
return exportedValue.Bytes()
|
||||
default:
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected Uint8Array or ArrayBuffer, but got ", value)))
|
||||
}
|
||||
}
|
||||
|
||||
func AssertStringBinary(vm *goja.Runtime, value goja.Value, name string, nilable bool) []byte {
|
||||
if IsNil(value) {
|
||||
if nilable {
|
||||
return nil
|
||||
}
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
switch exportedValue := value.Export().(type) {
|
||||
case string:
|
||||
return []byte(exportedValue)
|
||||
case []byte:
|
||||
return exportedValue
|
||||
case goja.ArrayBuffer:
|
||||
return exportedValue.Bytes()
|
||||
default:
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected string, Uint8Array or ArrayBuffer, but got ", value)))
|
||||
}
|
||||
}
|
||||
|
||||
func AssertFunction(vm *goja.Runtime, value goja.Value, name string) goja.Callable {
|
||||
if IsNil(value) {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: missing ", name)))
|
||||
}
|
||||
functionValue, isFunction := goja.AssertFunction(value)
|
||||
if !isFunction {
|
||||
panic(vm.NewTypeError(F.ToString("invalid argument: ", name, ": expected function, but got ", value)))
|
||||
}
|
||||
return functionValue
|
||||
}
|
||||
|
||||
func AssertHTTPHeader(vm *goja.Runtime, value goja.Value, name string) http.Header {
|
||||
headersObject := AssertObject(vm, value, name, true)
|
||||
if headersObject == nil {
|
||||
return nil
|
||||
}
|
||||
return ObjectToHeaders(vm, headersObject, name)
|
||||
}
|
||||
192
script/jsc/class.go
Normal file
192
script/jsc/class.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package jsc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
Runtime() *goja.Runtime
|
||||
}
|
||||
|
||||
type Class[M Module, C any] interface {
|
||||
Module() M
|
||||
Runtime() *goja.Runtime
|
||||
DefineField(name string, getter func(this C) any, setter func(this C, value goja.Value))
|
||||
DefineMethod(name string, method func(this C, call goja.FunctionCall) any)
|
||||
DefineStaticMethod(name string, method func(c Class[M, C], call goja.FunctionCall) any)
|
||||
DefineConstructor(constructor func(c Class[M, C], call goja.ConstructorCall) C)
|
||||
ToValue() goja.Value
|
||||
New(instance C) *goja.Object
|
||||
Prototype() *goja.Object
|
||||
Is(value goja.Value) bool
|
||||
As(value goja.Value) C
|
||||
}
|
||||
|
||||
func GetClass[M Module, C any](runtime *goja.Runtime, exports *goja.Object, className string) Class[M, C] {
|
||||
objectValue := exports.Get(className)
|
||||
if objectValue == nil {
|
||||
panic(runtime.NewTypeError("Missing class: " + className))
|
||||
}
|
||||
object, isObject := objectValue.(*goja.Object)
|
||||
if !isObject {
|
||||
panic(runtime.NewTypeError("Invalid class: " + className))
|
||||
}
|
||||
classObject, isClass := object.Get("_class").(*goja.Object)
|
||||
if !isClass {
|
||||
panic(runtime.NewTypeError("Invalid class: " + className))
|
||||
}
|
||||
class, isClass := classObject.Export().(Class[M, C])
|
||||
if !isClass {
|
||||
panic(runtime.NewTypeError("Invalid class: " + className))
|
||||
}
|
||||
return class
|
||||
}
|
||||
|
||||
type goClass[M Module, C any] struct {
|
||||
m M
|
||||
prototype *goja.Object
|
||||
constructor goja.Value
|
||||
}
|
||||
|
||||
func NewClass[M Module, C any](module M) Class[M, C] {
|
||||
class := &goClass[M, C]{
|
||||
m: module,
|
||||
prototype: module.Runtime().NewObject(),
|
||||
}
|
||||
clazz := module.Runtime().ToValue(class).(*goja.Object)
|
||||
clazz.Set("toString", module.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
return module.Runtime().ToValue("[sing-box Class]")
|
||||
}))
|
||||
class.prototype.DefineAccessorProperty("_class", class.Runtime().ToValue(func(call goja.FunctionCall) goja.Value { return clazz }), nil, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||
return class
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) Module() M {
|
||||
return c.m
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) Runtime() *goja.Runtime {
|
||||
return c.m.Runtime()
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) DefineField(name string, getter func(this C) any, setter func(this C, value goja.Value)) {
|
||||
var (
|
||||
getterValue goja.Value
|
||||
setterValue goja.Value
|
||||
)
|
||||
if getter != nil {
|
||||
getterValue = c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
this, isThis := call.This.Export().(C)
|
||||
if !isThis {
|
||||
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.ExportType().String()))
|
||||
}
|
||||
return c.toValue(getter(this), goja.Null())
|
||||
})
|
||||
}
|
||||
if setter != nil {
|
||||
setterValue = c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
this, isThis := call.This.Export().(C)
|
||||
if !isThis {
|
||||
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.String()))
|
||||
}
|
||||
setter(this, call.Argument(0))
|
||||
return goja.Undefined()
|
||||
})
|
||||
}
|
||||
c.prototype.DefineAccessorProperty(name, getterValue, setterValue, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) DefineMethod(name string, method func(this C, call goja.FunctionCall) any) {
|
||||
methodValue := c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
this, isThis := call.This.Export().(C)
|
||||
if !isThis {
|
||||
panic(c.Runtime().NewTypeError("Illegal this value: " + call.This.String()))
|
||||
}
|
||||
return c.toValue(method(this, call), goja.Undefined())
|
||||
})
|
||||
c.prototype.Set(name, methodValue)
|
||||
if name == "entries" {
|
||||
c.prototype.DefineDataPropertySymbol(goja.SymIterator, methodValue, goja.FLAG_TRUE, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) DefineStaticMethod(name string, method func(c Class[M, C], call goja.FunctionCall) any) {
|
||||
c.prototype.Set(name, c.Runtime().ToValue(func(call goja.FunctionCall) goja.Value {
|
||||
return c.toValue(method(c, call), goja.Undefined())
|
||||
}))
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) DefineConstructor(constructor func(c Class[M, C], call goja.ConstructorCall) C) {
|
||||
constructorObject := c.Runtime().ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||
value := constructor(c, call)
|
||||
object := c.toValue(value, goja.Undefined()).(*goja.Object)
|
||||
object.SetPrototype(call.This.Prototype())
|
||||
return object
|
||||
}).(*goja.Object)
|
||||
constructorObject.SetPrototype(c.prototype)
|
||||
c.prototype.DefineDataProperty("constructor", constructorObject, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE)
|
||||
c.constructor = constructorObject
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) toValue(rawValue any, defaultValue goja.Value) goja.Value {
|
||||
switch value := rawValue.(type) {
|
||||
case nil:
|
||||
return defaultValue
|
||||
case time.Time:
|
||||
return TimeToValue(c.Runtime(), value)
|
||||
default:
|
||||
return c.Runtime().ToValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) ToValue() goja.Value {
|
||||
if c.constructor == nil {
|
||||
constructorObject := c.Runtime().ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||
panic(c.Runtime().NewTypeError("Illegal constructor call"))
|
||||
}).(*goja.Object)
|
||||
constructorObject.SetPrototype(c.prototype)
|
||||
c.prototype.DefineDataProperty("constructor", constructorObject, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE)
|
||||
c.constructor = constructorObject
|
||||
}
|
||||
return c.constructor
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) New(instance C) *goja.Object {
|
||||
object := c.Runtime().ToValue(instance).(*goja.Object)
|
||||
object.SetPrototype(c.prototype)
|
||||
return object
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) Prototype() *goja.Object {
|
||||
return c.prototype
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) Is(value goja.Value) bool {
|
||||
object, isObject := value.(*goja.Object)
|
||||
if !isObject {
|
||||
return false
|
||||
}
|
||||
prototype := object.Prototype()
|
||||
for prototype != nil {
|
||||
if prototype == c.prototype {
|
||||
return true
|
||||
}
|
||||
prototype = prototype.Prototype()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *goClass[M, C]) As(value goja.Value) C {
|
||||
object, isObject := value.(*goja.Object)
|
||||
if !isObject {
|
||||
return common.DefaultValue[C]()
|
||||
}
|
||||
if !c.Is(object) {
|
||||
return common.DefaultValue[C]()
|
||||
}
|
||||
return object.Export().(C)
|
||||
}
|
||||
56
script/jsc/headers.go
Normal file
56
script/jsc/headers.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package jsc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func HeadersToValue(runtime *goja.Runtime, headers http.Header) goja.Value {
|
||||
object := runtime.NewObject()
|
||||
for key, value := range headers {
|
||||
if len(value) == 1 {
|
||||
object.Set(key, value[0])
|
||||
} else {
|
||||
object.Set(key, ArrayToValue(runtime, value))
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
func ArrayToValue[T any](runtime *goja.Runtime, values []T) goja.Value {
|
||||
return runtime.NewArray(common.Map(values, func(it T) any { return it })...)
|
||||
}
|
||||
|
||||
func ObjectToHeaders(vm *goja.Runtime, object *goja.Object, name string) http.Header {
|
||||
headers := make(http.Header)
|
||||
for _, key := range object.Keys() {
|
||||
valueObject := object.Get(key)
|
||||
switch headerValue := valueObject.(type) {
|
||||
case goja.String:
|
||||
headers.Set(key, headerValue.String())
|
||||
case *goja.Object:
|
||||
values := headerValue.Export()
|
||||
valueArray, isArray := values.([]any)
|
||||
if !isArray {
|
||||
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, "expected string or string array, got ", valueObject.String())))
|
||||
}
|
||||
newValues := make([]string, 0, len(valueArray))
|
||||
for _, value := range valueArray {
|
||||
stringValue, isString := value.(string)
|
||||
if !isString {
|
||||
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, " expected string or string array, got array item type: ", reflect.TypeOf(value))))
|
||||
}
|
||||
newValues = append(newValues, stringValue)
|
||||
}
|
||||
headers[key] = newValues
|
||||
default:
|
||||
panic(vm.NewTypeError(F.ToString("invalid value: ", name, ".", key, " expected string or string array, got ", valueObject.String())))
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
31
script/jsc/headers_test.go
Normal file
31
script/jsc/headers_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package jsc_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/script/jsc"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHeaders(t *testing.T) {
|
||||
runtime := goja.New()
|
||||
runtime.Set("headers", jsc.HeadersToValue(runtime, http.Header{
|
||||
"My-Header": []string{"My-Value1", "My-Value2"},
|
||||
}))
|
||||
headers := runtime.Get("headers").(*goja.Object).Get("My-Header").(*goja.Object)
|
||||
fmt.Println(reflect.ValueOf(headers.Export()).Type().String())
|
||||
}
|
||||
|
||||
func TestBody(t *testing.T) {
|
||||
runtime := goja.New()
|
||||
_, err := runtime.RunString(`
|
||||
var responseBody = new Uint8Array([1, 2, 3, 4, 5])
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
fmt.Println(reflect.TypeOf(runtime.Get("responseBody").Export()))
|
||||
}
|
||||
36
script/jsc/iterator.go
Normal file
36
script/jsc/iterator.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package jsc
|
||||
|
||||
import "github.com/dop251/goja"
|
||||
|
||||
type Iterator[M Module, T any] struct {
|
||||
c Class[M, *Iterator[M, T]]
|
||||
values []T
|
||||
block func(this T) any
|
||||
}
|
||||
|
||||
func NewIterator[M Module, T any](class Class[M, *Iterator[M, T]], values []T, block func(this T) any) goja.Value {
|
||||
return class.New(&Iterator[M, T]{class, values, block})
|
||||
}
|
||||
|
||||
func CreateIterator[M Module, T any](module M) Class[M, *Iterator[M, T]] {
|
||||
class := NewClass[M, *Iterator[M, T]](module)
|
||||
class.DefineMethod("next", (*Iterator[M, T]).next)
|
||||
class.DefineMethod("toString", (*Iterator[M, T]).toString)
|
||||
return class
|
||||
}
|
||||
|
||||
func (i *Iterator[M, T]) next(call goja.FunctionCall) any {
|
||||
result := i.c.Runtime().NewObject()
|
||||
if len(i.values) == 0 {
|
||||
result.Set("done", true)
|
||||
} else {
|
||||
result.Set("done", false)
|
||||
result.Set("value", i.block(i.values[0]))
|
||||
i.values = i.values[1:]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (i *Iterator[M, T]) toString(call goja.FunctionCall) any {
|
||||
return "[sing-box Iterator]"
|
||||
}
|
||||
18
script/jsc/time.go
Normal file
18
script/jsc/time.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package jsc
|
||||
|
||||
import (
|
||||
"time"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func TimeToValue(runtime *goja.Runtime, time time.Time) goja.Value {
|
||||
return runtimeNewDateObject(runtime, time, true, runtimeGetDatePrototype(runtime))
|
||||
}
|
||||
|
||||
//go:linkname runtimeNewDateObject github.com/dop251/goja.(*Runtime).newDateObject
|
||||
func runtimeNewDateObject(r *goja.Runtime, t time.Time, isSet bool, proto *goja.Object) *goja.Object
|
||||
|
||||
//go:linkname runtimeGetDatePrototype github.com/dop251/goja.(*Runtime).getDatePrototype
|
||||
func runtimeGetDatePrototype(r *goja.Runtime) *goja.Object
|
||||
20
script/jsc/time_test.go
Normal file
20
script/jsc/time_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package jsc_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/script/jsc"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTimeToValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
runtime := goja.New()
|
||||
now := time.Now()
|
||||
err := runtime.Set("now", jsc.TimeToValue(runtime, now))
|
||||
require.NoError(t, err)
|
||||
println(runtime.Get("now").String())
|
||||
}
|
||||
83
script/jstest/assert.js
Normal file
83
script/jstest/assert.js
Normal file
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
|
||||
const assert = {
|
||||
_isSameValue(a, b) {
|
||||
if (a === b) {
|
||||
// Handle +/-0 vs. -/+0
|
||||
return a !== 0 || 1 / a === 1 / b;
|
||||
}
|
||||
|
||||
// Handle NaN vs. NaN
|
||||
return a !== a && b !== b;
|
||||
},
|
||||
|
||||
_toString(value) {
|
||||
try {
|
||||
if (value === 0 && 1 / value === -Infinity) {
|
||||
return '-0';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
} catch (err) {
|
||||
if (err.name === 'TypeError') {
|
||||
return Object.prototype.toString.call(value);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
sameValue(actual, expected, message) {
|
||||
if (assert._isSameValue(actual, expected)) {
|
||||
return;
|
||||
}
|
||||
if (message === undefined) {
|
||||
message = '';
|
||||
} else {
|
||||
message += ' ';
|
||||
}
|
||||
|
||||
message += 'Expected SameValue(«' + assert._toString(actual) + '», «' + assert._toString(expected) + '») to be true';
|
||||
|
||||
throw new Error(message);
|
||||
},
|
||||
|
||||
throws(f, ctor, message) {
|
||||
if (message === undefined) {
|
||||
message = '';
|
||||
} else {
|
||||
message += ' ';
|
||||
}
|
||||
try {
|
||||
f();
|
||||
} catch (e) {
|
||||
if (e.constructor !== ctor) {
|
||||
throw new Error(message + "Wrong exception type was thrown: " + e.constructor.name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new Error(message + "No exception was thrown");
|
||||
},
|
||||
|
||||
throwsNodeError(f, ctor, code, message) {
|
||||
if (message === undefined) {
|
||||
message = '';
|
||||
} else {
|
||||
message += ' ';
|
||||
}
|
||||
try {
|
||||
f();
|
||||
} catch (e) {
|
||||
if (e.constructor !== ctor) {
|
||||
throw new Error(message + "Wrong exception type was thrown: " + e.constructor.name);
|
||||
}
|
||||
if (e.code !== code) {
|
||||
throw new Error(message + "Wrong exception code was thrown: " + e.code);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new Error(message + "No exception was thrown");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = assert;
|
||||
21
script/jstest/test.go
Normal file
21
script/jstest/test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package jstest
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/sagernet/sing-box/script/modules/require"
|
||||
)
|
||||
|
||||
//go:embed assert.js
|
||||
var assertJS []byte
|
||||
|
||||
func NewRegistry() *require.Registry {
|
||||
return require.NewRegistry(require.WithFsEnable(true), require.WithLoader(func(path string) ([]byte, error) {
|
||||
switch path {
|
||||
case "assert.js":
|
||||
return assertJS, nil
|
||||
default:
|
||||
return require.DefaultSourceLoader(path)
|
||||
}
|
||||
}))
|
||||
}
|
||||
118
script/manager.go
Normal file
118
script/manager.go
Normal file
@@ -0,0 +1,118 @@
|
||||
//go:build with_script
|
||||
|
||||
package script
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/common/task"
|
||||
)
|
||||
|
||||
var _ adapter.ScriptManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
scripts []adapter.Script
|
||||
scriptByName map[string]adapter.Script
|
||||
surgeCache *adapter.SurgeInMemoryCache
|
||||
}
|
||||
|
||||
func NewManager(ctx context.Context, logFactory log.Factory, scripts []option.Script) (*Manager, error) {
|
||||
manager := &Manager{
|
||||
ctx: ctx,
|
||||
logger: logFactory.NewLogger("script"),
|
||||
scriptByName: make(map[string]adapter.Script),
|
||||
}
|
||||
for _, scriptOptions := range scripts {
|
||||
script, err := NewScript(ctx, logFactory.NewLogger(F.ToString("script/", scriptOptions.Type, "[", scriptOptions.Tag, "]")), scriptOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize script: ", scriptOptions.Tag)
|
||||
}
|
||||
manager.scripts = append(manager.scripts, script)
|
||||
manager.scriptByName[scriptOptions.Tag] = script
|
||||
}
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
monitor := taskmonitor.New(m.logger, C.StartTimeout)
|
||||
switch stage {
|
||||
case adapter.StartStateStart:
|
||||
var cacheContext *adapter.HTTPStartContext
|
||||
if len(m.scripts) > 0 {
|
||||
monitor.Start("initialize rule-set")
|
||||
cacheContext = adapter.NewHTTPStartContext(m.ctx)
|
||||
var scriptStartGroup task.Group
|
||||
for _, script := range m.scripts {
|
||||
scriptInPlace := script
|
||||
scriptStartGroup.Append0(func(ctx context.Context) error {
|
||||
err := scriptInPlace.StartContext(ctx, cacheContext)
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize script/", scriptInPlace.Type(), "[", scriptInPlace.Tag(), "]")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
scriptStartGroup.Concurrency(5)
|
||||
scriptStartGroup.FastFail()
|
||||
err := scriptStartGroup.Run(m.ctx)
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if cacheContext != nil {
|
||||
cacheContext.Close()
|
||||
}
|
||||
case adapter.StartStatePostStart:
|
||||
for _, script := range m.scripts {
|
||||
monitor.Start(F.ToString("post start script/", script.Type(), "[", script.Tag(), "]"))
|
||||
err := script.PostStart()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "post start script/", script.Type(), "[", script.Tag(), "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, script := range m.scripts {
|
||||
monitor.Start(F.ToString("close start script/", script.Type(), "[", script.Tag(), "]"))
|
||||
err = E.Append(err, script.Close(), func(err error) error {
|
||||
return E.Cause(err, "close script/", script.Type(), "[", script.Tag(), "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) Scripts() []adapter.Script {
|
||||
return m.scripts
|
||||
}
|
||||
|
||||
func (m *Manager) Script(name string) (adapter.Script, bool) {
|
||||
script, loaded := m.scriptByName[name]
|
||||
return script, loaded
|
||||
}
|
||||
|
||||
func (m *Manager) SurgeCache() *adapter.SurgeInMemoryCache {
|
||||
if m.surgeCache == nil {
|
||||
m.surgeCache = &adapter.SurgeInMemoryCache{
|
||||
Data: make(map[string]string),
|
||||
}
|
||||
}
|
||||
return m.surgeCache
|
||||
}
|
||||
43
script/manager_stub.go
Normal file
43
script/manager_stub.go
Normal file
@@ -0,0 +1,43 @@
|
||||
//go:build !with_script
|
||||
|
||||
package script
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ adapter.ScriptManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct{}
|
||||
|
||||
func NewManager(ctx context.Context, logFactory log.Factory, scripts []option.Script) (*Manager, error) {
|
||||
if len(scripts) > 0 {
|
||||
return nil, E.New(`script is not included in this build, rebuild with -tags with_script`)
|
||||
}
|
||||
return (*Manager)(nil), nil
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Scripts() []adapter.Script {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Script(name string) (adapter.Script, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (m *Manager) SurgeCache() *adapter.SurgeInMemoryCache {
|
||||
return nil
|
||||
}
|
||||
50
script/modules/boxctx/context.go
Normal file
50
script/modules/boxctx/context.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package boxctx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/script/jsc"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
class jsc.Class[*Module, *Context]
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
Tag string
|
||||
StartedAt time.Time
|
||||
ErrorHandler func(error)
|
||||
}
|
||||
|
||||
func FromRuntime(runtime *goja.Runtime) *Context {
|
||||
contextValue := runtime.Get("context")
|
||||
if contextValue == nil {
|
||||
return nil
|
||||
}
|
||||
context, isContext := contextValue.Export().(*Context)
|
||||
if !isContext {
|
||||
return nil
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
func MustFromRuntime(runtime *goja.Runtime) *Context {
|
||||
context := FromRuntime(runtime)
|
||||
if context == nil {
|
||||
panic(runtime.NewTypeError("Missing sing-box context"))
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
func createContext(module *Module) jsc.Class[*Module, *Context] {
|
||||
class := jsc.NewClass[*Module, *Context](module)
|
||||
class.DefineMethod("toString", (*Context).toString)
|
||||
return class
|
||||
}
|
||||
|
||||
func (c *Context) toString(call goja.FunctionCall) any {
|
||||
return "[sing-box Context]"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user