mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 20:28:32 +10:00
Compare commits
58 Commits
v1.12.0-be
...
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 |
1
.fpm
1
.fpm
@@ -6,7 +6,6 @@
|
|||||||
--url "https://sing-box.sagernet.org/"
|
--url "https://sing-box.sagernet.org/"
|
||||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||||
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||||
--config-files /etc/sing-box/config.json
|
|
||||||
|
|
||||||
release/config/config.json=/etc/sing-box/config.json
|
release/config/config.json=/etc/sing-box/config.json
|
||||||
|
|
||||||
|
|||||||
47
.github/workflows/build.yml
vendored
47
.github/workflows/build.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24.2
|
go-version: ^1.24
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
- name: Calculate version
|
- name: Calculate version
|
||||||
if: github.event_name != 'workflow_dispatch'
|
if: github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
go run -v ./cmd/internal/read_tag --ci --nightly
|
go run -v ./cmd/internal/read_tag --nightly
|
||||||
- name: Set outputs
|
- name: Set outputs
|
||||||
id: outputs
|
id: outputs
|
||||||
run: |-
|
run: |-
|
||||||
@@ -102,7 +102,7 @@ jobs:
|
|||||||
if: ${{ ! matrix.legacy_go }}
|
if: ${{ ! matrix.legacy_go }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24.2
|
go-version: ^1.24
|
||||||
- name: Cache Legacy Go
|
- name: Cache Legacy Go
|
||||||
if: matrix.require_legacy_go
|
if: matrix.require_legacy_go
|
||||||
id: cache-legacy-go
|
id: cache-legacy-go
|
||||||
@@ -133,7 +133,10 @@ jobs:
|
|||||||
- name: Set build tags
|
- name: Set build tags
|
||||||
run: |
|
run: |
|
||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api,with_tailscale'
|
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api'
|
||||||
|
if [ ! '${{ matrix.legacy_go }}' = 'true' ]; then
|
||||||
|
TAGS="${TAGS},with_ech"
|
||||||
|
fi
|
||||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||||
- name: Build
|
- name: Build
|
||||||
if: matrix.os != 'android'
|
if: matrix.os != 'android'
|
||||||
@@ -173,9 +176,6 @@ jobs:
|
|||||||
PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}"
|
PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}"
|
||||||
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
||||||
echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}"
|
echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}"
|
||||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
|
||||||
PKG_VERSION="${PKG_VERSION//-/\~}"
|
|
||||||
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
|
||||||
- name: Package DEB
|
- name: Package DEB
|
||||||
if: matrix.debian != ''
|
if: matrix.debian != ''
|
||||||
run: |
|
run: |
|
||||||
@@ -183,7 +183,7 @@ jobs:
|
|||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
sudo apt-get install -y debsigs
|
sudo apt-get install -y debsigs
|
||||||
fpm -t deb \
|
fpm -t deb \
|
||||||
-v "$PKG_VERSION" \
|
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||||
-p "dist/${PKG_NAME}.deb" \
|
-p "dist/${PKG_NAME}.deb" \
|
||||||
--architecture ${{ matrix.debian }} \
|
--architecture ${{ matrix.debian }} \
|
||||||
dist/sing-box=/usr/bin/sing-box
|
dist/sing-box=/usr/bin/sing-box
|
||||||
@@ -200,7 +200,7 @@ jobs:
|
|||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
fpm -t rpm \
|
fpm -t rpm \
|
||||||
-v "$PKG_VERSION" \
|
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||||
-p "dist/${PKG_NAME}.rpm" \
|
-p "dist/${PKG_NAME}.rpm" \
|
||||||
--architecture ${{ matrix.rpm }} \
|
--architecture ${{ matrix.rpm }} \
|
||||||
dist/sing-box=/usr/bin/sing-box
|
dist/sing-box=/usr/bin/sing-box
|
||||||
@@ -219,7 +219,7 @@ jobs:
|
|||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
sudo apt-get install -y libarchive-tools
|
sudo apt-get install -y libarchive-tools
|
||||||
fpm -t pacman \
|
fpm -t pacman \
|
||||||
-v "$PKG_VERSION" \
|
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||||
-p "dist/${PKG_NAME}.pkg.tar.zst" \
|
-p "dist/${PKG_NAME}.pkg.tar.zst" \
|
||||||
--architecture ${{ matrix.pacman }} \
|
--architecture ${{ matrix.pacman }} \
|
||||||
dist/sing-box=/usr/bin/sing-box
|
dist/sing-box=/usr/bin/sing-box
|
||||||
@@ -229,8 +229,8 @@ jobs:
|
|||||||
cd dist
|
cd dist
|
||||||
mkdir -p "${DIR_NAME}"
|
mkdir -p "${DIR_NAME}"
|
||||||
cp ../LICENSE "${DIR_NAME}"
|
cp ../LICENSE "${DIR_NAME}"
|
||||||
if [ '${{ matrix.os }}' = 'windows' ]; then
|
if [ '${{ matrix.os }}' = 'windoes' ]; then
|
||||||
cp sing-box "${DIR_NAME}/sing-box.exe"
|
cp sing-box.exe "${DIR_NAME}"
|
||||||
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
|
zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
|
||||||
else
|
else
|
||||||
cp sing-box "${DIR_NAME}"
|
cp sing-box "${DIR_NAME}"
|
||||||
@@ -259,7 +259,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24.2
|
go-version: ^1.24
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -316,9 +316,9 @@ jobs:
|
|||||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||||
- name: Prepare upload
|
- name: Prepare upload
|
||||||
run: |-
|
run: |-
|
||||||
mkdir -p dist
|
mkdir -p dist/release
|
||||||
cp clients/android/app/build/outputs/apk/play/release/*.apk dist
|
cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release
|
||||||
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist
|
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -339,7 +339,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24.2
|
go-version: ^1.24
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -437,7 +437,7 @@ jobs:
|
|||||||
if: matrix.if
|
if: matrix.if
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24.2
|
go-version: ^1.24
|
||||||
- name: Setup Xcode stable
|
- name: Setup Xcode stable
|
||||||
if: matrix.if && github.ref == 'refs/heads/main-next'
|
if: matrix.if && github.ref == 'refs/heads/main-next'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -514,13 +514,10 @@ jobs:
|
|||||||
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
|
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
|
||||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
|
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
|
||||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
|
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
|
||||||
- name: Update version
|
|
||||||
if: matrix.if && matrix.name != 'iOS'
|
|
||||||
run: |-
|
|
||||||
go run -v ./cmd/internal/update_apple_version --ci
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
|
go run -v ./cmd/internal/update_apple_version --ci
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
xcodebuild archive \
|
xcodebuild archive \
|
||||||
-scheme "${{ matrix.scheme }}" \
|
-scheme "${{ matrix.scheme }}" \
|
||||||
@@ -569,9 +566,9 @@ jobs:
|
|||||||
zip -r SFM.dSYMs.zip dSYMs
|
zip -r SFM.dSYMs.zip dSYMs
|
||||||
popd
|
popd
|
||||||
|
|
||||||
mkdir -p dist
|
mkdir -p dist/release
|
||||||
cp clients/apple/SFM.dmg "dist/SFM-${VERSION}-universal.dmg"
|
cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg"
|
||||||
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/SFM-${VERSION}-universal.dSYMs.zip"
|
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip"
|
||||||
- name: Upload image
|
- name: Upload image
|
||||||
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24.2
|
go-version: ^1.24
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
25
.github/workflows/linux.yml
vendored
25
.github/workflows/linux.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24.2
|
go-version: ^1.24
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Calculate version
|
- name: Calculate version
|
||||||
if: github.event_name != 'workflow_dispatch'
|
if: github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
go run -v ./cmd/internal/read_tag --ci --nightly
|
go run -v ./cmd/internal/read_tag --nightly
|
||||||
- name: Set outputs
|
- name: Set outputs
|
||||||
id: outputs
|
id: outputs
|
||||||
run: |-
|
run: |-
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.24.2
|
go-version: ^1.24
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
if: matrix.os == 'android'
|
if: matrix.os == 'android'
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -109,11 +109,6 @@ jobs:
|
|||||||
if: contains(needs.calculate_version.outputs.version, '-')
|
if: contains(needs.calculate_version.outputs.version, '-')
|
||||||
run: |-
|
run: |-
|
||||||
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||||
- name: Set version
|
|
||||||
run: |-
|
|
||||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
|
||||||
PKG_VERSION="${PKG_VERSION//-/\~}"
|
|
||||||
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
|
||||||
- name: Package DEB
|
- name: Package DEB
|
||||||
if: matrix.debian != ''
|
if: matrix.debian != ''
|
||||||
run: |
|
run: |
|
||||||
@@ -121,11 +116,10 @@ jobs:
|
|||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
sudo apt-get install -y debsigs
|
sudo apt-get install -y debsigs
|
||||||
fpm -t deb \
|
fpm -t deb \
|
||||||
--name "${NAME}" \
|
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||||
-v "$PKG_VERSION" \
|
|
||||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
||||||
--architecture ${{ matrix.debian }} \
|
--architecture ${{ matrix.debian }} \
|
||||||
dist/sing-box=/usr/bin/sing-box
|
dist/sing-box=/usr/bin/${NAME}
|
||||||
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
curl -Lo '/tmp/debsigs.diff' 'https://gitlab.com/debsigs/debsigs/-/commit/160138f5de1ec110376d3c807b60a37388bc7c90.diff'
|
||||||
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
sudo patch /usr/bin/debsigs < '/tmp/debsigs.diff'
|
||||||
rm -rf $HOME/.gnupg
|
rm -rf $HOME/.gnupg
|
||||||
@@ -139,11 +133,10 @@ jobs:
|
|||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
fpm -t rpm \
|
fpm -t rpm \
|
||||||
--name "${NAME}" \
|
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||||
-v "$PKG_VERSION" \
|
|
||||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
||||||
--architecture ${{ matrix.rpm }} \
|
--architecture ${{ matrix.rpm }} \
|
||||||
dist/sing-box=/usr/bin/sing-box
|
dist/sing-box=/usr/bin/${NAME}
|
||||||
cat > $HOME/.rpmmacros <<EOF
|
cat > $HOME/.rpmmacros <<EOF
|
||||||
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
%_gpg_name ${{ secrets.GPG_KEY_ID }}
|
||||||
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
%_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase ${{ secrets.GPG_PASSPHRASE }}
|
||||||
@@ -182,4 +175,6 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Publish packages
|
- name: Publish packages
|
||||||
run: |-
|
run: |-
|
||||||
ls dist | xargs -I {} curl -F "package=@dist/{}" https://${{ secrets.FURY_TOKEN }}@push.fury.io/sagernet/
|
wget -O fury-cli.deb https://github.com/gemfury/cli/releases/download/v0.23.0/fury-cli_0.23.0_linux_amd64.deb
|
||||||
|
sudo dpkg -i fury-cli.deb
|
||||||
|
fury migrate dist --as=sagernet --api-token ${{ secrets.FURY_TOKEN }}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
/vendor/
|
/vendor/
|
||||||
/*.json
|
/*.json
|
||||||
|
/*.js
|
||||||
/*.srs
|
/*.srs
|
||||||
/*.db
|
/*.db
|
||||||
/site/
|
/site/
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ run:
|
|||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
|
- with_script
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-dirs:
|
exclude-dirs:
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ nfpms:
|
|||||||
contents:
|
contents:
|
||||||
- src: release/config/config.json
|
- src: release/config/config.json
|
||||||
dst: /etc/sing-box/config.json
|
dst: /etc/sing-box/config.json
|
||||||
type: "config|noreplace"
|
type: config
|
||||||
|
|
||||||
- src: release/config/sing-box.service
|
- src: release/config/sing-box.service
|
||||||
dst: /usr/lib/systemd/system/sing-box.service
|
dst: /usr/lib/systemd/system/sing-box.service
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ builds:
|
|||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
- with_tailscale
|
- with_tailscale
|
||||||
|
- with_script
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GOTOOLCHAIN=local
|
- GOTOOLCHAIN=local
|
||||||
@@ -51,6 +52,7 @@ builds:
|
|||||||
- with_acme
|
- with_acme
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
- with_tailscale
|
- with_tailscale
|
||||||
|
- with_script
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GOROOT={{ .Env.GOPATH }}/go_legacy
|
- GOROOT={{ .Env.GOPATH }}/go_legacy
|
||||||
@@ -132,7 +134,7 @@ nfpms:
|
|||||||
contents:
|
contents:
|
||||||
- src: release/config/config.json
|
- src: release/config/config.json
|
||||||
dst: /etc/sing-box/config.json
|
dst: /etc/sing-box/config.json
|
||||||
type: "config|noreplace"
|
type: config
|
||||||
|
|
||||||
- src: release/config/sing-box.service
|
- src: release/config/sing-box.service
|
||||||
dst: /usr/lib/systemd/system/sing-box.service
|
dst: /usr/lib/systemd/system/sing-box.service
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ RUN set -ex \
|
|||||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||||
&& go build -v -trimpath -tags \
|
&& go build -v -trimpath -tags \
|
||||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api,with_tailscale" \
|
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_acme,with_clash_api" \
|
||||||
-o /go/bin/sing-box \
|
-o /go/bin/sing-box \
|
||||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -1,6 +1,6 @@
|
|||||||
NAME = sing-box
|
NAME = sing-box
|
||||||
COMMIT = $(shell git rev-parse --short HEAD)
|
COMMIT = $(shell git rev-parse --short HEAD)
|
||||||
TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls,with_tailscale
|
TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls,with_tailscale,with_script
|
||||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls,with_reality_server
|
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls,with_reality_server
|
||||||
|
|
||||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||||
@@ -226,8 +226,8 @@ lib:
|
|||||||
go run ./cmd/internal/build_libbox -target ios
|
go run ./cmd/internal/build_libbox -target ios
|
||||||
|
|
||||||
lib_install:
|
lib_install:
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.6
|
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.5
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.6
|
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.5
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
venv/bin/mkdocs serve
|
venv/bin/mkdocs serve
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import (
|
|||||||
type CertificateStore interface {
|
type CertificateStore interface {
|
||||||
LifecycleService
|
LifecycleService
|
||||||
Pool() *x509.CertPool
|
Pool() *x509.CertPool
|
||||||
|
TLSDecryptionEnabled() bool
|
||||||
|
TLSDecryptionCertificate() *x509.Certificate
|
||||||
|
TLSDecryptionPrivateKey() any
|
||||||
}
|
}
|
||||||
|
|
||||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ type CacheFile interface {
|
|||||||
StoreGroupExpand(group string, expand bool) error
|
StoreGroupExpand(group string, expand bool) error
|
||||||
LoadRuleSet(tag string) *SavedBinary
|
LoadRuleSet(tag string) *SavedBinary
|
||||||
SaveRuleSet(tag string, set *SavedBinary) error
|
SaveRuleSet(tag string, set *SavedBinary) error
|
||||||
|
LoadScript(tag string) *SavedBinary
|
||||||
|
SaveScript(tag string, script *SavedBinary) error
|
||||||
|
SurgePersistentStoreRead(key string) string
|
||||||
|
SurgePersistentStoreWrite(key string, value string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedBinary struct {
|
type SavedBinary struct {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -58,6 +60,8 @@ type InboundContext struct {
|
|||||||
Client string
|
Client string
|
||||||
SniffContext any
|
SniffContext any
|
||||||
PacketSniffError error
|
PacketSniffError error
|
||||||
|
HTTPRequest *http.Request
|
||||||
|
ClientHello *tls.ClientHelloInfo
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|
||||||
@@ -74,6 +78,7 @@ type InboundContext struct {
|
|||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
TLSFragment bool
|
TLSFragment bool
|
||||||
TLSFragmentFallbackDelay time.Duration
|
TLSFragmentFallbackDelay time.Duration
|
||||||
|
MITM *option.MITMRouteOptions
|
||||||
|
|
||||||
NetworkStrategy *C.NetworkStrategy
|
NetworkStrategy *C.NetworkStrategy
|
||||||
NetworkType []C.InterfaceType
|
NetworkType []C.InterfaceType
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import E "github.com/sagernet/sing/common/exceptions"
|
import (
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
type StartStage uint8
|
type StartStage uint8
|
||||||
|
|
||||||
@@ -45,6 +47,9 @@ type LifecycleService interface {
|
|||||||
|
|
||||||
func Start(stage StartStage, services ...Lifecycle) error {
|
func Start(stage StartStage, services ...Lifecycle) error {
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
|
if service == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
err := service.Start(stage)
|
err := service.Start(stage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
13
adapter/mitm.go
Normal file
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)
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ type Router interface {
|
|||||||
RuleSet(tag string) (RuleSet, bool)
|
RuleSet(tag string) (RuleSet, bool)
|
||||||
NeedWIFIState() bool
|
NeedWIFIState() bool
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
AppendTracker(tracker ConnectionTracker)
|
SetTracker(tracker ConnectionTracker)
|
||||||
ResetNetwork()
|
ResetNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
54
adapter/script.go
Normal file
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
|
||||||
|
}
|
||||||
55
box.go
55
box.go
@@ -23,9 +23,11 @@ import (
|
|||||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/mitm"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/protocol/direct"
|
"github.com/sagernet/sing-box/protocol/direct"
|
||||||
"github.com/sagernet/sing-box/route"
|
"github.com/sagernet/sing-box/route"
|
||||||
|
"github.com/sagernet/sing-box/script"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
@@ -48,6 +50,8 @@ type Box struct {
|
|||||||
dnsRouter *dns.Router
|
dnsRouter *dns.Router
|
||||||
connection *route.ConnectionManager
|
connection *route.ConnectionManager
|
||||||
router *route.Router
|
router *route.Router
|
||||||
|
script *script.Manager
|
||||||
|
mitm adapter.MITMEngine //*mitm.Engine
|
||||||
services []adapter.LifecycleService
|
services []adapter.LifecycleService
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
@@ -143,18 +147,12 @@ func New(options Options) (*Box, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var services []adapter.LifecycleService
|
var services []adapter.LifecycleService
|
||||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), common.PtrValueOrDefault(options.Certificate))
|
||||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
if err != nil {
|
||||||
len(certificateOptions.Certificate) > 0 ||
|
return nil, err
|
||||||
len(certificateOptions.CertificatePath) > 0 ||
|
|
||||||
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
|
||||||
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
|
||||||
services = append(services, certificateStore)
|
|
||||||
}
|
}
|
||||||
|
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||||
|
services = append(services, certificateStore)
|
||||||
|
|
||||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||||
@@ -173,7 +171,7 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize network manager")
|
return nil, E.Cause(err, "initialize network manager")
|
||||||
}
|
}
|
||||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||||
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
connectionManager := route.NewConnectionManager(ctx, logFactory.NewLogger("connection"))
|
||||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||||
service.MustRegister[adapter.Router](ctx, router)
|
service.MustRegister[adapter.Router](ctx, router)
|
||||||
@@ -181,8 +179,8 @@ func New(options Options) (*Box, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize router")
|
return nil, E.Cause(err, "initialize router")
|
||||||
}
|
}
|
||||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
|
||||||
var timeService *tls.TimeServiceWrapper
|
var timeService *tls.TimeServiceWrapper
|
||||||
|
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||||
if ntpOptions.Enabled {
|
if ntpOptions.Enabled {
|
||||||
timeService = new(tls.TimeServiceWrapper)
|
timeService = new(tls.TimeServiceWrapper)
|
||||||
service.MustRegister[ntp.TimeService](ctx, timeService)
|
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||||
@@ -296,6 +294,11 @@ func New(options Options) (*Box, error) {
|
|||||||
"local",
|
"local",
|
||||||
option.LocalDNSServerOptions{},
|
option.LocalDNSServerOptions{},
|
||||||
)))
|
)))
|
||||||
|
scriptManager, err := script.NewManager(ctx, logFactory, options.Scripts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize script manager")
|
||||||
|
}
|
||||||
|
service.MustRegister[adapter.ScriptManager](ctx, scriptManager)
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
err = platformInterface.Initialize(networkManager)
|
err = platformInterface.Initialize(networkManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -314,7 +317,7 @@ func New(options Options) (*Box, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create clash-server")
|
return nil, E.Cause(err, "create clash-server")
|
||||||
}
|
}
|
||||||
router.AppendTracker(clashServer)
|
router.SetTracker(clashServer)
|
||||||
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||||
services = append(services, clashServer)
|
services = append(services, clashServer)
|
||||||
}
|
}
|
||||||
@@ -324,7 +327,7 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "create v2ray-server")
|
return nil, E.Cause(err, "create v2ray-server")
|
||||||
}
|
}
|
||||||
if v2rayServer.StatsService() != nil {
|
if v2rayServer.StatsService() != nil {
|
||||||
router.AppendTracker(v2rayServer.StatsService())
|
router.SetTracker(v2rayServer.StatsService())
|
||||||
services = append(services, v2rayServer)
|
services = append(services, v2rayServer)
|
||||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||||
}
|
}
|
||||||
@@ -345,6 +348,16 @@ func New(options Options) (*Box, error) {
|
|||||||
timeService.TimeService = ntpService
|
timeService.TimeService = ntpService
|
||||||
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||||
}
|
}
|
||||||
|
mitmOptions := common.PtrValueOrDefault(options.MITM)
|
||||||
|
var mitmEngine adapter.MITMEngine
|
||||||
|
if mitmOptions.Enabled {
|
||||||
|
engine, err := mitm.NewEngine(ctx, logFactory.NewLogger("mitm"), mitmOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create MITM engine")
|
||||||
|
}
|
||||||
|
service.MustRegister[adapter.MITMEngine](ctx, engine)
|
||||||
|
mitmEngine = engine
|
||||||
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
network: networkManager,
|
network: networkManager,
|
||||||
endpoint: endpointManager,
|
endpoint: endpointManager,
|
||||||
@@ -354,6 +367,8 @@ func New(options Options) (*Box, error) {
|
|||||||
dnsRouter: dnsRouter,
|
dnsRouter: dnsRouter,
|
||||||
connection: connectionManager,
|
connection: connectionManager,
|
||||||
router: router,
|
router: router,
|
||||||
|
script: scriptManager,
|
||||||
|
mitm: mitmEngine,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
logFactory: logFactory,
|
logFactory: logFactory,
|
||||||
logger: logFactory.Logger(),
|
logger: logFactory.Logger(),
|
||||||
@@ -412,11 +427,11 @@ func (s *Box) preStart() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router, s.script, s.mitm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -440,7 +455,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -448,7 +463,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -467,7 +482,7 @@ func (s *Box) Close() error {
|
|||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
err := common.Close(
|
err := common.Close(
|
||||||
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
s.inbound, s.outbound, s.endpoint, s.mitm, s.script, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||||
)
|
)
|
||||||
for _, lifecycleService := range s.services {
|
for _, lifecycleService := range s.services {
|
||||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||||
|
|||||||
Submodule clients/android updated: 8354b78e5d...aefe3c0290
@@ -59,7 +59,7 @@ func init() {
|
|||||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||||
|
|
||||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api")
|
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_script")
|
||||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
||||||
memcTags = append(memcTags, "with_tailscale")
|
memcTags = append(memcTags, "with_tailscale")
|
||||||
debugTags = append(debugTags, "debug")
|
debugTags = append(debugTags, "debug")
|
||||||
|
|||||||
@@ -5,49 +5,40 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||||
"github.com/sagernet/sing-box/common/badversion"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var nightly bool
|
||||||
flagRunInCI bool
|
|
||||||
flagRunNightly bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
|
flag.BoolVar(&nightly, "nightly", false, "Print nightly tag")
|
||||||
flag.BoolVar(&flagRunNightly, "nightly", false, "Run nightly")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
var (
|
if nightly {
|
||||||
versionStr string
|
version, err := build_shared.ReadTagVersionRev()
|
||||||
err error
|
|
||||||
)
|
|
||||||
if flagRunNightly {
|
|
||||||
var version badversion.Version
|
|
||||||
version, err = build_shared.ReadTagVersion()
|
|
||||||
if err == nil {
|
|
||||||
versionStr = version.String()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
versionStr, err = build_shared.ReadTag()
|
|
||||||
}
|
|
||||||
if flagRunInCI {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
var versionStr string
|
||||||
|
if version.PreReleaseIdentifier != "" {
|
||||||
|
versionStr = version.VersionString() + "-nightly"
|
||||||
|
} else {
|
||||||
|
version.Patch++
|
||||||
|
versionStr = version.VersionString() + "-nightly"
|
||||||
|
}
|
||||||
err = setGitHubEnv("version", versionStr)
|
err = setGitHubEnv("version", versionStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
tag, err := build_shared.ReadTag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
os.Stdout.WriteString("unknown\n")
|
os.Stdout.WriteString("unknown\n")
|
||||||
} else {
|
} else {
|
||||||
os.Stdout.WriteString(versionStr + "\n")
|
os.Stdout.WriteString(tag + "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,36 +12,5 @@ var commandTools = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
commandTools.PersistentFlags().StringVarP(&commandToolsFlagOutbound, "outbound", "o", "", "Use specified tag instead of default outbound")
|
|
||||||
mainCommand.AddCommand(commandTools)
|
mainCommand.AddCommand(commandTools)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPreStartedClient() (*box.Box, error) {
|
|
||||||
options, err := readConfigAndMerge()
|
|
||||||
if err != nil {
|
|
||||||
if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create service")
|
|
||||||
}
|
|
||||||
err = instance.PreStart()
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "start service")
|
|
||||||
}
|
|
||||||
return instance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) {
|
|
||||||
if outboundTag == "" {
|
|
||||||
return instance.Outbound().Default(), nil
|
|
||||||
} else {
|
|
||||||
outbound, loaded := instance.Outbound().Outbound(outboundTag)
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("outbound not found: ", outboundTag)
|
|
||||||
}
|
|
||||||
return outbound, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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"
|
"github.com/sagernet/sing-box/log"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -39,20 +40,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncTime() error {
|
func syncTime() error {
|
||||||
instance, err := createPreStartedClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer instance.Close()
|
|
||||||
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
|
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
|
||||||
if serverAddress.Port == 0 {
|
if serverAddress.Port == 0 {
|
||||||
serverAddress.Port = 123
|
serverAddress.Port = 123
|
||||||
}
|
}
|
||||||
response, err := ntp.Exchange(context.Background(), dialer, serverAddress)
|
response, err := ntp.Exchange(context.Background(), N.SystemDialer, serverAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package certificate
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -16,6 +17,8 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
|
"software.sslmate.com/src/go-pkcs12"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.CertificateStore = (*Store)(nil)
|
var _ adapter.CertificateStore = (*Store)(nil)
|
||||||
@@ -27,6 +30,9 @@ type Store struct {
|
|||||||
certificatePaths []string
|
certificatePaths []string
|
||||||
certificateDirectoryPaths []string
|
certificateDirectoryPaths []string
|
||||||
watcher *fswatch.Watcher
|
watcher *fswatch.Watcher
|
||||||
|
tlsDecryptionEnabled bool
|
||||||
|
tlsDecryptionPrivateKey any
|
||||||
|
tlsDecryptionCertificate *x509.Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
||||||
@@ -90,6 +96,19 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initializing certificate store")
|
return nil, E.Cause(err, "initializing certificate store")
|
||||||
}
|
}
|
||||||
|
if options.TLSDecryption != nil && options.TLSDecryption.Enabled {
|
||||||
|
pfxBytes, err := base64.StdEncoding.DecodeString(options.TLSDecryption.KeyPair)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "decode key pair base64 bytes")
|
||||||
|
}
|
||||||
|
privateKey, certificate, err := pkcs12.Decode(pfxBytes, options.TLSDecryption.KeyPairPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "decode key pair")
|
||||||
|
}
|
||||||
|
store.tlsDecryptionEnabled = true
|
||||||
|
store.tlsDecryptionPrivateKey = privateKey
|
||||||
|
store.tlsDecryptionCertificate = certificate
|
||||||
|
}
|
||||||
return store, nil
|
return store, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,3 +202,15 @@ func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
|||||||
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||||
return err == nil && !strings.Contains(target, "/")
|
return err == nil && !strings.Contains(target, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) TLSDecryptionEnabled() bool {
|
||||||
|
return s.tlsDecryptionEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) TLSDecryptionCertificate() *x509.Certificate {
|
||||||
|
return s.tlsDecryptionCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) TLSDecryptionPrivateKey() any {
|
||||||
|
return s.tlsDecryptionPrivateKey
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,8 +71,18 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
}
|
}
|
||||||
if options.RoutingMark > 0 {
|
if options.RoutingMark > 0 {
|
||||||
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
|
dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(options.RoutingMark)))
|
||||||
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
|
listener.Control = control.Append(listener.Control, control.RoutingMark(uint32(options.RoutingMark)))
|
||||||
|
}
|
||||||
|
if networkManager != nil {
|
||||||
|
autoRedirectOutputMark := networkManager.AutoRedirectOutputMark()
|
||||||
|
if autoRedirectOutputMark > 0 {
|
||||||
|
if options.RoutingMark > 0 {
|
||||||
|
return nil, E.New("`routing_mark` is conflict with `tun.auto_redirect` with `tun.route_[_exclude]_address_set")
|
||||||
|
}
|
||||||
|
dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark))
|
||||||
|
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
|
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
|
||||||
if disableDefaultBind || options.TCPFastOpen {
|
if disableDefaultBind || options.TCPFastOpen {
|
||||||
@@ -117,8 +127,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
|
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
|
||||||
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
dialer.Control = control.Append(dialer.Control, control.RoutingMark(defaultOptions.RoutingMark))
|
||||||
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
listener.Control = control.Append(listener.Control, control.RoutingMark(defaultOptions.RoutingMark))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.ReuseAddr {
|
if options.ReuseAddr {
|
||||||
@@ -200,22 +210,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefault bool) control.Func {
|
|
||||||
if networkManager == nil {
|
|
||||||
return control.RoutingMark(mark)
|
|
||||||
}
|
|
||||||
return func(network, address string, conn syscall.RawConn) error {
|
|
||||||
if networkManager.AutoRedirectOutputMark() != 0 {
|
|
||||||
if isDefault {
|
|
||||||
return E.New("`route.default_mark` is conflict with `tun.auto_redirect`")
|
|
||||||
} else {
|
|
||||||
return E.New("`routing_mark` is conflict with `tun.auto_redirect`")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return control.RoutingMark(mark)(network, address, conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
||||||
if !address.IsValid() {
|
if !address.IsValid() {
|
||||||
return nil, E.New("invalid address")
|
return nil, E.New("invalid address")
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ type Options struct {
|
|||||||
ResolverOnDetour bool
|
ResolverOnDetour bool
|
||||||
NewDialer bool
|
NewDialer bool
|
||||||
LegacyDNSDialer bool
|
LegacyDNSDialer bool
|
||||||
DirectOutbound bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: merge with NewWithOptions
|
// TODO: merge with NewWithOptions
|
||||||
@@ -103,13 +102,13 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
|||||||
}
|
}
|
||||||
dnsQueryOptions.Transport = transport
|
dnsQueryOptions.Transport = transport
|
||||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||||
|
} else if options.NewDialer {
|
||||||
|
return nil, E.New("missing domain resolver for domain server address")
|
||||||
} else {
|
} else {
|
||||||
transports := dnsTransport.Transports()
|
transports := dnsTransport.Transports()
|
||||||
if len(transports) < 2 {
|
if len(transports) < 2 {
|
||||||
dnsQueryOptions.Transport = dnsTransport.Default()
|
dnsQueryOptions.Transport = dnsTransport.Default()
|
||||||
} else if options.NewDialer {
|
} else {
|
||||||
return nil, E.New("missing domain resolver for domain server address")
|
|
||||||
} else if !options.DirectOutbound {
|
|
||||||
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -24,7 +23,7 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R
|
|||||||
var first byte
|
var first byte
|
||||||
err := binary.Read(reader, binary.BigEndian, &first)
|
err := binary.Read(reader, binary.BigEndian, &first)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if first != 19 {
|
if first != 19 {
|
||||||
@@ -34,7 +33,7 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R
|
|||||||
var protocol [19]byte
|
var protocol [19]byte
|
||||||
_, err = reader.Read(protocol[:])
|
_, err = reader.Read(protocol[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if string(protocol[:]) != "BitTorrent protocol" {
|
if string(protocol[:]) != "BitTorrent protocol" {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
@@ -68,9 +67,7 @@ func UTP(_ context.Context, metadata *adapter.InboundContext, packet []byte) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if extension > 0x04 {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
var length byte
|
var length byte
|
||||||
err = binary.Read(reader, binary.BigEndian, &length)
|
err = binary.Read(reader, binary.BigEndian, &length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -71,19 +71,3 @@ func TestSniffUDPTracker(t *testing.T) {
|
|||||||
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSniffNotUTP(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
packets := []string{
|
|
||||||
"0102736470696e674958d580121500000000000079aaed6717a39c27b07c0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
}
|
|
||||||
for _, pkt := range packets {
|
|
||||||
pkt, err := hex.DecodeString(pkt)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err = sniff.UTP(context.TODO(), &metadata, pkt)
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
"github.com/sagernet/sing/common/task"
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
@@ -18,16 +21,22 @@ func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundCon
|
|||||||
var length uint16
|
var length uint16
|
||||||
err := binary.Read(reader, binary.BigEndian, &length)
|
err := binary.Read(reader, binary.BigEndian, &length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
buffer := buf.NewSize(int(length))
|
buffer := buf.NewSize(int(length))
|
||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
_, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
|
readCtx, cancel := context.WithTimeout(readCtx, time.Millisecond*100)
|
||||||
|
var readTask task.Group
|
||||||
|
readTask.Append0(func(ctx context.Context) error {
|
||||||
|
return common.Error(buffer.ReadFullFrom(reader, buffer.FreeLen()))
|
||||||
|
})
|
||||||
|
err = readTask.Run(readCtx)
|
||||||
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
return DomainNameQuery(readCtx, metadata, buffer.Bytes())
|
return DomainNameQuery(readCtx, metadata, buffer.Bytes())
|
||||||
}
|
}
|
||||||
@@ -38,6 +47,9 @@ func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, pack
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
metadata.Protocol = C.ProtocolDNS
|
metadata.Protocol = C.ProtocolDNS
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package sniff_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/hex"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/sniff"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSniffDNS(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
query, err := hex.DecodeString("740701000001000000000000012a06676f6f676c6503636f6d0000010001")
|
|
||||||
require.NoError(t, err)
|
|
||||||
var metadata adapter.InboundContext
|
|
||||||
err = sniff.DomainNameQuery(context.TODO(), &metadata, query)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
|
|
||||||
}
|
|
||||||
@@ -3,12 +3,10 @@ package sniff
|
|||||||
import (
|
import (
|
||||||
std_bufio "bufio"
|
std_bufio "bufio"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/protocol/http"
|
"github.com/sagernet/sing/protocol/http"
|
||||||
)
|
)
|
||||||
@@ -16,13 +14,10 @@ import (
|
|||||||
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
request, err := http.ReadRequest(std_bufio.NewReader(reader))
|
request, err := http.ReadRequest(std_bufio.NewReader(reader))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
return err
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
metadata.Protocol = C.ProtocolHTTP
|
metadata.Protocol = C.ProtocolHTTP
|
||||||
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||||
|
metadata.HTTPRequest = request
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import (
|
|||||||
"golang.org/x/crypto/hkdf"
|
"golang.org/x/crypto/hkdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrClientHelloFragmented = E.New("need more packet for chromium QUIC connection")
|
||||||
|
|
||||||
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
|
||||||
reader := bytes.NewReader(packet)
|
reader := bytes.NewReader(packet)
|
||||||
typeByte, err := reader.ReadByte()
|
typeByte, err := reader.ReadByte()
|
||||||
@@ -306,7 +308,7 @@ find:
|
|||||||
metadata.Protocol = C.ProtocolQUIC
|
metadata.Protocol = C.ProtocolQUIC
|
||||||
metadata.Client = C.ClientChromium
|
metadata.Client = C.ClientChromium
|
||||||
metadata.SniffContext = fragments
|
metadata.SniffContext = fragments
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return ErrClientHelloFragmented
|
||||||
}
|
}
|
||||||
metadata.Domain = fingerprint.ServerName
|
metadata.Domain = fingerprint.ServerName
|
||||||
for metadata.Client == "" {
|
for metadata.Client == "" {
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ func TestSniffQUICChromeNew(t *testing.T) {
|
|||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
|
||||||
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
|
||||||
pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f")
|
pkt, err = hex.DecodeString("c20000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea4489e2ff30c43a5f63beb2e4501ce7754085bcbe838003a0b4bccb53863c0766df7eac073c2bdc170772b157997945acdc2ab2e84750cc9aa0ffa0fdc023da7fc565a14f87f7c563dbc9183dd226aab79957d263f66e64b85a1b15a24516bd2c7c04eea4fa0a34ef9849c21585db2e4adb7c05e265c4f38d8ffe4cbed0f3b0e68f3693bf1f726c3fb135b8e32a5d22931d7c55fc2ff4b9a354933ab14544df3cdaf3e3217dfb8d7feb3465dc34df6320ea486f12e5b2d609aaa5f4515c20c86fc440f8087be0ee3d339835746ae2573c2afdee6bb6ef7e9eb541feae9209391b2902cfb0bdaccd9da8d290714638b7da588d4a656ca6eabba78b7363922d6037cf060b161a42019d4feb4156459103cffdeefd0e63114af2b0e0c39e70ebc7fecb8dd1ebb8d60b2137f509bb7dcef5f1d3e06ab1d391466652d57440a410fb4f58a6ce1fb62feb453241f64e110709f59a3d9ebdac94f811337d0e4a80fd6b56b2a70cd6eebbf98e1661291da6bf5beb8b8afc376dfd20eb76afe709e8e8f28e0ef82105954e346546ad25973df43f4acddbec0ffd9b215f62abebebf71305b5ea993560316f69430bf5afe50420340622f802b5830f3bcebffff04980c75a59d28902879e5d51a4fb21062a4ae13c42297075b21d54ee04303879c1157e7470c1451673c98a2f3921f2f3e8f6acfe85b01caaca66b59e5ebffbfe68e5e9ab17e9a1b857eb409df91cb76767fc1814fd3c522a9b117edd0b02526e469cb4afb291a4dcc74c79b47ec6e7ce558c597129366f83ec306b11d2598c705fd4ee9ee99df6b7039bef13b08fc6f26853ad213829d24f895747d45a47414f931c583fb6c3e4f6c27d0c2b81a5f3cee390ec6314e1fec637e8d28b675e97caafdfbf8c25d34a635083a7553d219dd80dbb39087d74c6ad6192ca6f48a3ff8d47db41b2a492c63fcd780012780931dae0a325f9dcbd772d09a700f132c4bc1d9809b25b9751b694eb72a8ba4db7208d2b1bab63e1845208e4f841ea30218a559db98751589716b6d059ca673378f5fe7c7d8a1c82e14a561c47313bbcc278412ba86ffb2b87ec308eab9df696f5b4b54f8e361731bf232820a02a35fda7e5d4bf01b8f005ad299a055116e7b23c181f15a66442cf6032ca477bccc55b79d424eb4f245847bd81a581dc369dd20b1a4892733bde3c38e492c0039f69f2b947a4dc251a49ee7ccc0f36b3b75a555fa1d126db75f94dab60f52f6b15a877a0c380b59f82d35c570bc5f8051e9ef87db51f52383d47b50829b7f9e947ccc67aa280566aa48b4a85c1c7eca6f542789d8abcc050f1aa3cc221b6859656a21454aa21c7bfb9d12115f61c3ed46263ade68a8d3679fa62a659a5da7817406bd16618fccf33ed208ada1b03584e8b485d3cb6ed80a0774e60b6cd55aff64169ea998cf8235997049515abac58e0169ca07fb1c8c4c8b2803ba9d27b44c045d0a1cac86e5e188195c68001f53eb44851b6d821fc01ccbb41e27f38e6ddd66540c2d62ed6e0d551e22c0f26b60078c74a6302a1ed3d9e8fc0861257a63f6ac4e759fd54bff088becd28e30944a6c15db4fc8ae6244346869add946d9d92c430d737e042fa18b28a8ed64d1e8987ad9061cdc1335f")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
@@ -40,7 +40,7 @@ func TestSniffQUICChromium(t *testing.T) {
|
|||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
require.ErrorIs(t, err, sniff.ErrClientHelloFragmented)
|
||||||
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var tpktVersion uint8
|
var tpktVersion uint8
|
||||||
err := binary.Read(reader, binary.BigEndian, &tpktVersion)
|
err := binary.Read(reader, binary.BigEndian, &tpktVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if tpktVersion != 0x03 {
|
if tpktVersion != 0x03 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
@@ -25,7 +24,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var tpktReserved uint8
|
var tpktReserved uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &tpktReserved)
|
err = binary.Read(reader, binary.BigEndian, &tpktReserved)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if tpktReserved != 0x00 {
|
if tpktReserved != 0x00 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
@@ -34,7 +33,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var tpktLength uint16
|
var tpktLength uint16
|
||||||
err = binary.Read(reader, binary.BigEndian, &tpktLength)
|
err = binary.Read(reader, binary.BigEndian, &tpktLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if tpktLength != 19 {
|
if tpktLength != 19 {
|
||||||
@@ -44,7 +43,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var cotpLength uint8
|
var cotpLength uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &cotpLength)
|
err = binary.Read(reader, binary.BigEndian, &cotpLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if cotpLength != 14 {
|
if cotpLength != 14 {
|
||||||
@@ -54,7 +53,7 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var cotpTpduType uint8
|
var cotpTpduType uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &cotpTpduType)
|
err = binary.Read(reader, binary.BigEndian, &cotpTpduType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if cotpTpduType != 0xE0 {
|
if cotpTpduType != 0xE0 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
@@ -62,13 +61,13 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
|
|
||||||
err = rw.SkipN(reader, 5)
|
err = rw.SkipN(reader, 5)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var rdpType uint8
|
var rdpType uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &rdpType)
|
err = binary.Read(reader, binary.BigEndian, &rdpType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if rdpType != 0x01 {
|
if rdpType != 0x01 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
@@ -76,12 +75,12 @@ func RDP(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
|
|||||||
var rdpFlags uint8
|
var rdpFlags uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &rdpFlags)
|
err = binary.Read(reader, binary.BigEndian, &rdpFlags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
var rdpLength uint8
|
var rdpLength uint8
|
||||||
err = binary.Read(reader, binary.BigEndian, &rdpLength)
|
err = binary.Read(reader, binary.BigEndian, &rdpLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return err
|
||||||
}
|
}
|
||||||
if rdpLength != 8 {
|
if rdpLength != 8 {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package sniff
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
@@ -20,8 +19,6 @@ type (
|
|||||||
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
|
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrNeedMoreData = E.New("need more data")
|
|
||||||
|
|
||||||
func Skip(metadata *adapter.InboundContext) bool {
|
func Skip(metadata *adapter.InboundContext) bool {
|
||||||
// skip server first protocols
|
// skip server first protocols
|
||||||
switch metadata.Destination.Port {
|
switch metadata.Destination.Port {
|
||||||
@@ -43,7 +40,7 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
|
|||||||
timeout = C.ReadPayloadTimeout
|
timeout = C.ReadPayloadTimeout
|
||||||
}
|
}
|
||||||
deadline := time.Now().Add(timeout)
|
deadline := time.Now().Add(timeout)
|
||||||
var sniffError error
|
var errors []error
|
||||||
for i := 0; ; i++ {
|
for i := 0; ; i++ {
|
||||||
err := conn.SetReadDeadline(deadline)
|
err := conn.SetReadDeadline(deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -57,7 +54,7 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
|
|||||||
}
|
}
|
||||||
return E.Cause(err, "read payload")
|
return E.Cause(err, "read payload")
|
||||||
}
|
}
|
||||||
sniffError = nil
|
errors = nil
|
||||||
for _, sniffer := range sniffers {
|
for _, sniffer := range sniffers {
|
||||||
reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader {
|
reader := io.MultiReader(common.Map(append(buffers, buffer), func(it *buf.Buffer) io.Reader {
|
||||||
return bytes.NewReader(it.Bytes())
|
return bytes.NewReader(it.Bytes())
|
||||||
@@ -66,23 +63,20 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
sniffError = E.Errors(sniffError, err)
|
errors = append(errors, err)
|
||||||
}
|
|
||||||
if !errors.Is(err, ErrNeedMoreData) {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sniffError
|
return E.Errors(errors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
|
func PeekPacket(ctx context.Context, metadata *adapter.InboundContext, packet []byte, sniffers ...PacketSniffer) error {
|
||||||
var sniffError []error
|
var errors []error
|
||||||
for _, sniffer := range sniffers {
|
for _, sniffer := range sniffers {
|
||||||
err := sniffer(ctx, metadata, packet)
|
err := sniffer(ctx, metadata, packet)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
sniffError = append(sniffError, err)
|
errors = append(errors, err)
|
||||||
}
|
}
|
||||||
return E.Errors(sniffError...)
|
return E.Errors(errors...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,26 +5,22 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
const sshPrefix = "SSH-2.0-"
|
scanner := bufio.NewScanner(reader)
|
||||||
bReader := bufio.NewReader(reader)
|
if !scanner.Scan() {
|
||||||
prefix, err := bReader.Peek(len(sshPrefix))
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
|
||||||
} else if string(prefix) != sshPrefix {
|
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
fistLine, _, err := bReader.ReadLine()
|
fistLine := scanner.Text()
|
||||||
if err != nil {
|
if !strings.HasPrefix(fistLine, "SSH-2.0-") {
|
||||||
return err
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
metadata.Protocol = C.ProtocolSSH
|
metadata.Protocol = C.ProtocolSSH
|
||||||
metadata.Client = string(fistLine)[8:]
|
metadata.Client = fistLine[8:]
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ package sniff
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reader io.Reader) error {
|
||||||
@@ -23,11 +21,8 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
|
|||||||
if clientHello != nil {
|
if clientHello != nil {
|
||||||
metadata.Protocol = C.ProtocolTLS
|
metadata.Protocol = C.ProtocolTLS
|
||||||
metadata.Domain = clientHello.ServerName
|
metadata.Domain = clientHello.ServerName
|
||||||
|
metadata.ClientHello = clientHello
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
return err
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,6 @@ func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
|
|||||||
if response.Rcode != mDNS.RcodeSuccess {
|
if response.Rcode != mDNS.RcodeSuccess {
|
||||||
return nil, E.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list")
|
return nil, E.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list")
|
||||||
}
|
}
|
||||||
match:
|
|
||||||
for _, rr := range response.Answer {
|
for _, rr := range response.Answer {
|
||||||
switch resource := rr.(type) {
|
switch resource := rr.(type) {
|
||||||
case *mDNS.HTTPS:
|
case *mDNS.HTTPS:
|
||||||
@@ -134,14 +133,11 @@ func (s *STDECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn)
|
|||||||
return nil, E.Cause(err, "decode ECH config")
|
return nil, E.Cause(err, "decode ECH config")
|
||||||
}
|
}
|
||||||
s.config.EncryptedClientHelloConfigList = echConfigList
|
s.config.EncryptedClientHelloConfigList = echConfigList
|
||||||
break match
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(s.config.EncryptedClientHelloConfigList) == 0 {
|
return nil, E.New("no ECH config found in DNS records")
|
||||||
return nil, E.New("no ECH config found in DNS records")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tlsConn, err := s.Client(conn)
|
tlsConn, err := s.Client(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import (
|
|||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
||||||
@@ -35,17 +38,30 @@ func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
template := &x509.Certificate{
|
var template *x509.Certificate
|
||||||
SerialNumber: serialNumber,
|
if serverAddress := M.ParseAddr(serverName); serverAddress.IsValid() {
|
||||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
template = &x509.Certificate{
|
||||||
NotAfter: expire,
|
SerialNumber: serialNumber,
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
IPAddresses: []net.IP{serverAddress.AsSlice()},
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||||
BasicConstraintsValid: true,
|
NotAfter: expire,
|
||||||
Subject: pkix.Name{
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
CommonName: serverName,
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
},
|
BasicConstraintsValid: true,
|
||||||
DNSNames: []string{serverName},
|
}
|
||||||
|
} else {
|
||||||
|
template = &x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||||
|
NotAfter: expire,
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: serverName,
|
||||||
|
},
|
||||||
|
DNSNames: []string{serverName},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if parent == nil {
|
if parent == nil {
|
||||||
parent = template
|
parent = template
|
||||||
|
|||||||
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"
|
||||||
|
)
|
||||||
@@ -15,8 +15,6 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
|
|||||||
}
|
}
|
||||||
responseLen := response.Len()
|
responseLen := response.Len()
|
||||||
if responseLen > maxLen {
|
if responseLen > maxLen {
|
||||||
copyResponse := *response
|
|
||||||
response = ©Response
|
|
||||||
response.Truncate(maxLen)
|
response.Truncate(maxLen)
|
||||||
}
|
}
|
||||||
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
||||||
|
|||||||
@@ -140,12 +140,12 @@ func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn quic.C
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer stream.Close()
|
||||||
|
defer stream.CancelRead(0)
|
||||||
err = transport.WriteMessage(stream, 0, message)
|
err = transport.WriteMessage(stream, 0, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stream.Close()
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
stream.Close()
|
|
||||||
return transport.ReadMessage(stream)
|
return transport.ReadMessage(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -212,8 +212,8 @@ type dnsConnection struct {
|
|||||||
|
|
||||||
func (c *dnsConnection) Close(err error) {
|
func (c *dnsConnection) Close(err error) {
|
||||||
c.closeOnce.Do(func() {
|
c.closeOnce.Do(func() {
|
||||||
c.err = err
|
|
||||||
close(c.done)
|
close(c.done)
|
||||||
|
c.err = err
|
||||||
})
|
})
|
||||||
c.Conn.Close()
|
c.Conn.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,31 +2,6 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
#### 1.12.0-beta.3
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
### 1.11.7
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._
|
|
||||||
|
|
||||||
#### 1.12.0-beta.1
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks,
|
|
||||||
see [Tun](/configuration/inbound/tun/#auto_redirect).
|
|
||||||
|
|
||||||
### 1.11.6
|
|
||||||
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._
|
|
||||||
|
|
||||||
#### 1.12.0-alpha.19
|
#### 1.12.0-alpha.19
|
||||||
|
|
||||||
* Update gVisor to 20250319.0
|
* Update gVisor to 20250319.0
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ Default padding scheme:
|
|||||||
|
|
||||||
```
|
```
|
||||||
stop=8
|
stop=8
|
||||||
0=30-30
|
0=34-120
|
||||||
1=100-400
|
1=100-400
|
||||||
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
|
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
|
||||||
3=9-9,500-1000
|
3=500-1000
|
||||||
4=500-1000
|
4=500-1000
|
||||||
5=500-1000
|
5=500-1000
|
||||||
6=500-1000
|
6=500-1000
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ AnyTLS 填充方案行数组。
|
|||||||
|
|
||||||
```
|
```
|
||||||
stop=8
|
stop=8
|
||||||
0=30-30
|
0=34-120
|
||||||
1=100-400
|
1=100-400
|
||||||
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
|
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
|
||||||
3=9-9,500-1000
|
3=500-1000
|
||||||
4=500-1000
|
4=500-1000
|
||||||
5=500-1000
|
5=500-1000
|
||||||
6=500-1000
|
6=500-1000
|
||||||
|
|||||||
@@ -211,10 +211,6 @@ Set the default route to the Tun.
|
|||||||
|
|
||||||
By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`.
|
By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`.
|
||||||
|
|
||||||
!!! note "Also enable `auto_redirect`"
|
|
||||||
|
|
||||||
`auto_redirect` is always recommended on Linux, it provides better routing, higher performance (better than tproxy), and avoids conflicts with Docker bridge networks.
|
|
||||||
|
|
||||||
#### iproute2_table_index
|
#### iproute2_table_index
|
||||||
|
|
||||||
!!! question "Since sing-box 1.10.0"
|
!!! question "Since sing-box 1.10.0"
|
||||||
@@ -241,10 +237,6 @@ Linux iproute2 rule start index generated by `auto_route`.
|
|||||||
|
|
||||||
Automatically configure iptables/nftables to redirect connections.
|
Automatically configure iptables/nftables to redirect connections.
|
||||||
|
|
||||||
Auto redirect is always recommended on Linux, it provides better routing,
|
|
||||||
higher performance (better than tproxy),
|
|
||||||
and avoids conflicts with Docker bridge networks.
|
|
||||||
|
|
||||||
*In Android*:
|
*In Android*:
|
||||||
|
|
||||||
Only local IPv4 connections are forwarded. To share your VPN connection over hotspot or repeater,
|
Only local IPv4 connections are forwarded. To share your VPN connection over hotspot or repeater,
|
||||||
@@ -254,13 +246,11 @@ use [VPNHotspot](https://github.com/Mygod/VPNHotspot).
|
|||||||
|
|
||||||
`auto_route` with `auto_redirect` works as expected on routers **without intervention**.
|
`auto_route` with `auto_redirect` works as expected on routers **without intervention**.
|
||||||
|
|
||||||
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
|
|
||||||
|
|
||||||
#### auto_redirect_input_mark
|
#### auto_redirect_input_mark
|
||||||
|
|
||||||
!!! question "Since sing-box 1.10.0"
|
!!! question "Since sing-box 1.10.0"
|
||||||
|
|
||||||
Connection input mark used by `auto_redirect`.
|
Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`.
|
||||||
|
|
||||||
`0x2023` is used by default.
|
`0x2023` is used by default.
|
||||||
|
|
||||||
@@ -268,7 +258,7 @@ Connection input mark used by `auto_redirect`.
|
|||||||
|
|
||||||
!!! question "Since sing-box 1.10.0"
|
!!! question "Since sing-box 1.10.0"
|
||||||
|
|
||||||
Connection output mark used by `auto_redirect`.
|
Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`.
|
||||||
|
|
||||||
`0x2024` is used by default.
|
`0x2024` is used by default.
|
||||||
|
|
||||||
@@ -378,6 +368,8 @@ Exclude custom routes when `auto_route` is enabled.
|
|||||||
Add the destination IP CIDR rules in the specified rule-sets to the firewall.
|
Add the destination IP CIDR rules in the specified rule-sets to the firewall.
|
||||||
Matched traffic will bypass the sing-box routes.
|
Matched traffic will bypass the sing-box routes.
|
||||||
|
|
||||||
|
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
|
||||||
|
|
||||||
=== "Without `auto_redirect` enabled"
|
=== "Without `auto_redirect` enabled"
|
||||||
|
|
||||||
!!! question "Since sing-box 1.11.0"
|
!!! question "Since sing-box 1.11.0"
|
||||||
|
|||||||
@@ -215,10 +215,6 @@ tun 接口的 IPv6 前缀。
|
|||||||
|
|
||||||
VPN 默认优先于 tun。要使 tun 经过 VPN,启用 `route.override_android_vpn`。
|
VPN 默认优先于 tun。要使 tun 经过 VPN,启用 `route.override_android_vpn`。
|
||||||
|
|
||||||
!!! note "也启用 `auto_redirect`"
|
|
||||||
|
|
||||||
在 Linux 上始终推荐使用 `auto_redirect`,它提供更好的路由, 更高的性能(优于 tproxy), 并避免与 Docker 桥接网络冲突。
|
|
||||||
|
|
||||||
#### iproute2_table_index
|
#### iproute2_table_index
|
||||||
|
|
||||||
!!! question "自 sing-box 1.10.0 起"
|
!!! question "自 sing-box 1.10.0 起"
|
||||||
@@ -245,23 +241,19 @@ tun 接口的 IPv6 前缀。
|
|||||||
|
|
||||||
自动配置 iptables/nftables 以重定向连接。
|
自动配置 iptables/nftables 以重定向连接。
|
||||||
|
|
||||||
在 Linux 上始终推荐使用 auto redirect,它提供更好的路由, 更高的性能(优于 tproxy), 并避免与 Docker 桥接网络冲突。
|
|
||||||
|
|
||||||
*在 Android 中*:
|
*在 Android 中*:
|
||||||
|
|
||||||
仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
|
仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
|
||||||
|
|
||||||
*在 Linux 中*:
|
*在 Linux 中*:
|
||||||
|
|
||||||
带有 `auto_redirect` 的 `auto_route` 在路由器上**无需干预**即可按预期工作。
|
带有 `auto_redirect `的 `auto_route` 可以在路由器上按预期工作,**无需干预**。
|
||||||
|
|
||||||
与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。
|
|
||||||
|
|
||||||
#### auto_redirect_input_mark
|
#### auto_redirect_input_mark
|
||||||
|
|
||||||
!!! question "自 sing-box 1.10.0 起"
|
!!! question "自 sing-box 1.10.0 起"
|
||||||
|
|
||||||
`auto_redriect` 使用的连接输入标记。
|
`route_address_set` 和 `route_exclude_address_set` 使用的连接输入标记。
|
||||||
|
|
||||||
默认使用 `0x2023`。
|
默认使用 `0x2023`。
|
||||||
|
|
||||||
@@ -269,7 +261,7 @@ tun 接口的 IPv6 前缀。
|
|||||||
|
|
||||||
!!! question "自 sing-box 1.10.0 起"
|
!!! question "自 sing-box 1.10.0 起"
|
||||||
|
|
||||||
`auto_redriect` 使用的连接输出标记。
|
`route_address_set` 和 `route_exclude_address_set` 使用的连接输出标记。
|
||||||
|
|
||||||
默认使用 `0x2024`。
|
默认使用 `0x2024`。
|
||||||
|
|
||||||
@@ -350,6 +342,8 @@ tun 接口的 IPv6 前缀。
|
|||||||
将指定规则集中的目标 IP CIDR 规则添加到防火墙。
|
将指定规则集中的目标 IP CIDR 规则添加到防火墙。
|
||||||
不匹配的流量将绕过 sing-box 路由。
|
不匹配的流量将绕过 sing-box 路由。
|
||||||
|
|
||||||
|
与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。
|
||||||
|
|
||||||
=== "`auto_redirect` 未启用"
|
=== "`auto_redirect` 未启用"
|
||||||
|
|
||||||
!!! question "自 sing-box 1.11.0 起"
|
!!! question "自 sing-box 1.11.0 起"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ If enabled in the inbound, the protocol and domain name (if present) of by the c
|
|||||||
|
|
||||||
| QUIC Client | Type |
|
| QUIC Client | Type |
|
||||||
|:------------------------:|:----------:|
|
|:------------------------:|:----------:|
|
||||||
| Chromium/Cronet | `chromium` |
|
| Chromium/Cronet | `chrimium` |
|
||||||
| Safari/Apple Network API | `safari` |
|
| Safari/Apple Network API | `safari` |
|
||||||
| Firefox / uquic firefox | `firefox` |
|
| Firefox / uquic firefox | `firefox` |
|
||||||
| quic-go / uquic chrome | `quic-go` |
|
| quic-go / uquic chrome | `quic-go` |
|
||||||
@@ -19,10 +19,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
bucketSelected = []byte("selected")
|
bucketSelected = []byte("selected")
|
||||||
bucketExpand = []byte("group_expand")
|
bucketExpand = []byte("group_expand")
|
||||||
bucketMode = []byte("clash_mode")
|
bucketMode = []byte("clash_mode")
|
||||||
bucketRuleSet = []byte("rule_set")
|
bucketRuleSet = []byte("rule_set")
|
||||||
|
bucketScript = []byte("script")
|
||||||
|
bucketSgPersistentStore = []byte("sg_persistent_store")
|
||||||
|
|
||||||
bucketNameList = []string{
|
bucketNameList = []string{
|
||||||
string(bucketSelected),
|
string(bucketSelected),
|
||||||
@@ -316,3 +318,70 @@ func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error {
|
|||||||
return bucket.Put([]byte(tag), setBinary)
|
return bucket.Put([]byte(tag), setBinary)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) LoadScript(tag string) *adapter.SavedBinary {
|
||||||
|
var savedSet adapter.SavedBinary
|
||||||
|
err := c.DB.View(func(t *bbolt.Tx) error {
|
||||||
|
bucket := c.bucket(t, bucketScript)
|
||||||
|
if bucket == nil {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
scriptBinary := bucket.Get([]byte(tag))
|
||||||
|
if len(scriptBinary) == 0 {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
return savedSet.UnmarshalBinary(scriptBinary)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &savedSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) SaveScript(tag string, set *adapter.SavedBinary) error {
|
||||||
|
return c.DB.Batch(func(t *bbolt.Tx) error {
|
||||||
|
bucket, err := c.createBucket(t, bucketScript)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scriptBinary, err := set.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(tag), scriptBinary)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) SurgePersistentStoreRead(key string) string {
|
||||||
|
var value string
|
||||||
|
_ = c.DB.View(func(t *bbolt.Tx) error {
|
||||||
|
bucket := c.bucket(t, bucketSgPersistentStore)
|
||||||
|
if bucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
valueBinary := bucket.Get([]byte(key))
|
||||||
|
if len(valueBinary) > 0 {
|
||||||
|
value = string(valueBinary)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) SurgePersistentStoreWrite(key string, value string) error {
|
||||||
|
return c.DB.Batch(func(t *bbolt.Tx) error {
|
||||||
|
if value != "" {
|
||||||
|
bucket, err := c.createBucket(t, bucketSgPersistentStore)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(key), []byte(value))
|
||||||
|
} else {
|
||||||
|
bucket := c.bucket(t, bucketSgPersistentStore)
|
||||||
|
if bucket == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bucket.Delete([]byte(key))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
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("/profile", profileRouter())
|
||||||
r.Mount("/cache", cacheRouter(ctx))
|
r.Mount("/cache", cacheRouter(ctx))
|
||||||
r.Mount("/dns", dnsRouter(s.dnsRouter))
|
r.Mount("/dns", dnsRouter(s.dnsRouter))
|
||||||
|
r.Mount("/mitm", mitmRouter(ctx))
|
||||||
|
|
||||||
s.setupMetaAPI(r)
|
s.setupMetaAPI(r)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,4 +32,9 @@ type Notification struct {
|
|||||||
Subtitle string
|
Subtitle string
|
||||||
Body string
|
Body string
|
||||||
OpenURL string
|
OpenURL string
|
||||||
|
Clipboard string
|
||||||
|
MediaURL string
|
||||||
|
MediaData []byte
|
||||||
|
MediaType string
|
||||||
|
Timeout int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ type BoxService struct {
|
|||||||
clashServer adapter.ClashServer
|
clashServer adapter.ClashServer
|
||||||
pauseManager pause.Manager
|
pauseManager pause.Manager
|
||||||
|
|
||||||
iOSPauseFields
|
servicePauseFields
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
|
func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
package libbox
|
package libbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type iOSPauseFields struct {
|
type servicePauseFields struct {
|
||||||
endPauseTimer *time.Timer
|
pauseAccess sync.Mutex
|
||||||
|
pauseTimer *time.Timer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BoxService) Pause() {
|
func (s *BoxService) Pause() {
|
||||||
s.pauseManager.DevicePause()
|
s.pauseAccess.Lock()
|
||||||
if !C.IsIos {
|
defer s.pauseAccess.Unlock()
|
||||||
s.instance.Router().ResetNetwork()
|
if s.pauseTimer != nil {
|
||||||
} else {
|
s.pauseTimer.Stop()
|
||||||
if s.endPauseTimer == nil {
|
|
||||||
s.endPauseTimer = time.AfterFunc(time.Minute, s.pauseManager.DeviceWake)
|
|
||||||
} else {
|
|
||||||
s.endPauseTimer.Reset(time.Minute)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
s.pauseTimer = time.AfterFunc(3*time.Second, s.ResetNetwork)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BoxService) Wake() {
|
func (s *BoxService) Wake() {
|
||||||
if !C.IsIos {
|
s.pauseAccess.Lock()
|
||||||
s.pauseManager.DeviceWake()
|
defer s.pauseAccess.Unlock()
|
||||||
s.instance.Router().ResetNetwork()
|
if s.pauseTimer != nil {
|
||||||
|
s.pauseTimer.Stop()
|
||||||
}
|
}
|
||||||
|
s.pauseTimer = time.AfterFunc(3*time.Minute, s.ResetNetwork)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BoxService) ResetNetwork() {
|
func (s *BoxService) ResetNetwork() {
|
||||||
|
|||||||
27
go.mod
27
go.mod
@@ -3,13 +3,15 @@ module github.com/sagernet/sing-box
|
|||||||
go 1.23.1
|
go 1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anytls/sing-anytls v0.0.8
|
github.com/adhocore/gronx v1.19.5
|
||||||
|
github.com/anytls/sing-anytls v0.0.6
|
||||||
github.com/caddyserver/certmagic v0.21.7
|
github.com/caddyserver/certmagic v0.21.7
|
||||||
github.com/cloudflare/circl v1.6.0
|
github.com/cloudflare/circl v1.6.0
|
||||||
github.com/cretz/bine v0.2.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/chi/v5 v5.2.1
|
||||||
github.com/go-chi/render v1.0.3
|
github.com/go-chi/render v1.0.3
|
||||||
github.com/gofrs/uuid/v5 v5.3.2
|
github.com/gofrs/uuid/v5 v5.3.1
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
|
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
|
||||||
github.com/libdns/alidns v1.0.3
|
github.com/libdns/alidns v1.0.3
|
||||||
github.com/libdns/cloudflare v0.1.1
|
github.com/libdns/cloudflare v0.1.1
|
||||||
@@ -23,19 +25,19 @@ require (
|
|||||||
github.com/sagernet/cors v1.2.1
|
github.com/sagernet/cors v1.2.1
|
||||||
github.com/sagernet/fswatch v0.1.1
|
github.com/sagernet/fswatch v0.1.1
|
||||||
github.com/sagernet/gomobile v0.1.4
|
github.com/sagernet/gomobile v0.1.4
|
||||||
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
|
github.com/sagernet/gvisor v0.0.0-20250324121324-d3f3d7570296
|
||||||
github.com/sagernet/quic-go v0.49.0-beta.1
|
github.com/sagernet/quic-go v0.49.0-beta.1
|
||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
||||||
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c
|
github.com/sagernet/sing v0.6.5-0.20250324102321-1ddf4ccbfab8
|
||||||
github.com/sagernet/sing-mux v0.3.1
|
github.com/sagernet/sing-mux v0.3.1
|
||||||
github.com/sagernet/sing-quic v0.4.1
|
github.com/sagernet/sing-quic v0.4.1-beta.1
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||||
github.com/sagernet/sing-shadowsocks2 v0.2.0
|
github.com/sagernet/sing-shadowsocks2 v0.2.0
|
||||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056
|
||||||
github.com/sagernet/sing-tun v0.6.3-0.20250409030157-219c612399be
|
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec
|
||||||
github.com/sagernet/sing-vmess v0.2.0
|
github.com/sagernet/sing-vmess v0.2.0
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
|
||||||
github.com/sagernet/tailscale v1.80.3-mod.2
|
github.com/sagernet/tailscale v1.80.3-mod.0
|
||||||
github.com/sagernet/utls v1.6.7
|
github.com/sagernet/utls v1.6.7
|
||||||
github.com/sagernet/wireguard-go v0.0.1-beta.5
|
github.com/sagernet/wireguard-go v0.0.1-beta.5
|
||||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||||
@@ -53,6 +55,7 @@ require (
|
|||||||
google.golang.org/grpc v1.70.0
|
google.golang.org/grpc v1.70.0
|
||||||
google.golang.org/protobuf v1.36.5
|
google.golang.org/protobuf v1.36.5
|
||||||
howett.net/plist v1.0.1
|
howett.net/plist v1.0.1
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
//replace github.com/sagernet/sing => ../sing
|
//replace github.com/sagernet/sing => ../sing
|
||||||
@@ -72,12 +75,14 @@ require (
|
|||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // 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/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/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
github.com/gaissmai/bart v0.11.1 // indirect
|
github.com/gaissmai/bart v0.11.1 // indirect
|
||||||
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // 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-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/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
||||||
@@ -86,7 +91,7 @@ require (
|
|||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/go-querystring v1.1.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/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/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect
|
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
@@ -104,7 +109,7 @@ require (
|
|||||||
github.com/mdlayher/sdnotify v1.0.0 // indirect
|
github.com/mdlayher/sdnotify v1.0.0 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // 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/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
||||||
@@ -139,5 +144,3 @@ require (
|
|||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
//replace github.com/sagernet/sing => ../sing
|
|
||||||
|
|||||||
58
go.sum
58
go.sum
@@ -1,5 +1,9 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
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 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||||
@@ -8,8 +12,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
|
|||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/anytls/sing-anytls v0.0.8 h1:1u/fnH1HoeeMV5mX7/eUOjLBvPdkd1UJRmXiRi6Vymc=
|
github.com/anytls/sing-anytls v0.0.6 h1:UatIjl/OvzWQGXQ1I2bAIkabL9WtihW0fA7G+DXGBUg=
|
||||||
github.com/anytls/sing-anytls v0.0.8/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
github.com/anytls/sing-anytls v0.0.6/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=
|
github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=
|
||||||
@@ -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 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
||||||
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
||||||
@@ -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/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 h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
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 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
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=
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
@@ -58,16 +67,18 @@ 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-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 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
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-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
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 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
||||||
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
|
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
|
||||||
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
@@ -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/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 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
||||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
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-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
|
||||||
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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=
|
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/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 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
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.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss=
|
||||||
github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
|
github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0=
|
||||||
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
|
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
|
||||||
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
|
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 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
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=
|
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/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 h1:WzX9ka+iHdupMgy2Vdich+OAt7TM8C2cZbIbzNjBrJY=
|
||||||
github.com/sagernet/gomobile v0.1.4/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E=
|
github.com/sagernet/gomobile v0.1.4/go.mod h1:Pqq2+ZVvs10U7xK+UwJgwYWUykewi8H6vlslAO73n9E=
|
||||||
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb h1:pprQtDqNgqXkRsXn+0E8ikKOemzmum8bODjSfDene38=
|
github.com/sagernet/gvisor v0.0.0-20250324121324-d3f3d7570296 h1:zovOW85AOnNgGS5dzR/M5Ly1qTRt3a8Ymiu0TIonvAE=
|
||||||
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4=
|
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 h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||||
@@ -178,26 +189,26 @@ 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 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
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.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||||
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c h1:Zi+WR7f9SQ96yNHmyxj42BtaVb3kTouQ8bQLBHReTSI=
|
github.com/sagernet/sing v0.6.5-0.20250324102321-1ddf4ccbfab8 h1:Kg/OPLceU3Ty46ceEwLfL9NLbKBCLj5dNQb1Ia+Q0VI=
|
||||||
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
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 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI=
|
||||||
github.com/sagernet/sing-mux v0.3.1/go.mod h1:Mkdz8LnDstthz0HWuA/5foncnDIdcNN5KZ6AdJX+x78=
|
github.com/sagernet/sing-mux v0.3.1/go.mod h1:Mkdz8LnDstthz0HWuA/5foncnDIdcNN5KZ6AdJX+x78=
|
||||||
github.com/sagernet/sing-quic v0.4.1 h1:pxlMa4efZu/M07RgGagNNDDyl6ZUwpmNUjRTpgHOWK4=
|
github.com/sagernet/sing-quic v0.4.1-beta.1 h1:V2VfMckT3EQR3ZdfSzJgZZDsvfZZH42QAZpnOnHKa0s=
|
||||||
github.com/sagernet/sing-quic v0.4.1/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY=
|
github.com/sagernet/sing-quic v0.4.1-beta.1/go.mod h1:c+CytOEyeN20KCTFIP8YQUkNDVFLSzjrEPqP7Hlnxys=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
||||||
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
|
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
|
||||||
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
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 h1:GFNJQAHhSXqAfxAw1wDG/QWbdpGH5Na3k8qUynqWnEA=
|
||||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056/go.mod h1:HyacBPIFiKihJQR8LQp56FM4hBtd/7MZXnRxxQIOPsc=
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056/go.mod h1:HyacBPIFiKihJQR8LQp56FM4hBtd/7MZXnRxxQIOPsc=
|
||||||
github.com/sagernet/sing-tun v0.6.3-0.20250409030157-219c612399be h1:E/JnQ1DEz5XIH6VVk3dIMD7aIX5o79jPkbyw57tWlJs=
|
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec h1:9/OYGb9qDmUFIhqd3S+3eni62EKRQR1rSmRH18baA/M=
|
||||||
github.com/sagernet/sing-tun v0.6.3-0.20250409030157-219c612399be/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
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 h1:pCMGUXN2k7RpikQV65/rtXtDHzb190foTfF9IGTMZrI=
|
||||||
github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
|
github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||||
github.com/sagernet/tailscale v1.80.3-mod.2 h1:hT0CI74q727EuCcgQ+T4pvon8V0aoi4vTAxah7GsNMQ=
|
github.com/sagernet/tailscale v1.80.3-mod.0 h1:oHIdivbR/yxoiA9d3a2rRlhYn2shY9XVF35Rr8jW508=
|
||||||
github.com/sagernet/tailscale v1.80.3-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
github.com/sagernet/tailscale v1.80.3-mod.0/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
||||||
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
|
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
|
||||||
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
|
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
|
||||||
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
|
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
|
||||||
@@ -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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
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/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.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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
@@ -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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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.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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultTimeFormat = "-0700 2006-01-02 15:04:05"
|
||||||
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
Options option.LogOptions
|
Options option.LogOptions
|
||||||
@@ -47,7 +51,7 @@ func New(options Options) (Factory, error) {
|
|||||||
DisableColors: logOptions.DisableColor || logFilePath != "",
|
DisableColors: logOptions.DisableColor || logFilePath != "",
|
||||||
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
|
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
|
||||||
FullTimestamp: logOptions.Timestamp,
|
FullTimestamp: logOptions.Timestamp,
|
||||||
TimestampFormat: "-0700 2006-01-02 15:04:05",
|
TimestampFormat: DefaultTimeFormat,
|
||||||
}
|
}
|
||||||
factory := NewDefaultFactory(
|
factory := NewDefaultFactory(
|
||||||
options.Context,
|
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"`
|
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
|
||||||
CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"`
|
CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"`
|
||||||
CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_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
|
type CertificateOptions _CertificateOptions
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,13 +12,15 @@ type _Options struct {
|
|||||||
Schema string `json:"$schema,omitempty"`
|
Schema string `json:"$schema,omitempty"`
|
||||||
Log *LogOptions `json:"log,omitempty"`
|
Log *LogOptions `json:"log,omitempty"`
|
||||||
DNS *DNSOptions `json:"dns,omitempty"`
|
DNS *DNSOptions `json:"dns,omitempty"`
|
||||||
NTP *NTPOptions `json:"ntp,omitempty"`
|
|
||||||
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
|
||||||
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
||||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||||
Route *RouteOptions `json:"route,omitempty"`
|
Route *RouteOptions `json:"route,omitempty"`
|
||||||
Experimental *ExperimentalOptions `json:"experimental,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
|
type Options _Options
|
||||||
@@ -32,7 +33,7 @@ func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
o.RawMessage = content
|
o.RawMessage = content
|
||||||
return checkOptions(o)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogOptions struct {
|
type LogOptions struct {
|
||||||
@@ -44,52 +45,3 @@ type LogOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StubOptions struct{}
|
type StubOptions struct{}
|
||||||
|
|
||||||
func checkOptions(options *Options) error {
|
|
||||||
err := checkInbounds(options.Inbounds)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = checkOutbounds(options.Outbounds, options.Endpoints)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkInbounds(inbounds []Inbound) error {
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
for _, inbound := range inbounds {
|
|
||||||
if inbound.Tag == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if seen[inbound.Tag] {
|
|
||||||
return E.New("duplicate inbound tag: ", inbound.Tag)
|
|
||||||
}
|
|
||||||
seen[inbound.Tag] = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error {
|
|
||||||
seen := make(map[string]bool)
|
|
||||||
for _, outbound := range outbounds {
|
|
||||||
if outbound.Tag == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if seen[outbound.Tag] {
|
|
||||||
return E.New("duplicate outbound/endpoint tag: ", outbound.Tag)
|
|
||||||
}
|
|
||||||
seen[outbound.Tag] = true
|
|
||||||
}
|
|
||||||
for _, endpoint := range endpoints {
|
|
||||||
if endpoint.Tag == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if seen[endpoint.Tag] {
|
|
||||||
return E.New("duplicate outbound/endpoint tag: ", endpoint.Tag)
|
|
||||||
}
|
|
||||||
seen[endpoint.Tag] = true
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ type RawRouteOptionsActionOptions struct {
|
|||||||
|
|
||||||
TLSFragment bool `json:"tls_fragment,omitempty"`
|
TLSFragment bool `json:"tls_fragment,omitempty"`
|
||||||
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
|
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
|
||||||
|
|
||||||
|
MITM *MITMRouteOptions `json:"mitm,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouteOptionsActionOptions RawRouteOptionsActionOptions
|
type RouteOptionsActionOptions RawRouteOptionsActionOptions
|
||||||
|
|||||||
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"`
|
||||||
|
}
|
||||||
@@ -48,12 +48,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
if options.Detour != "" {
|
if options.Detour != "" {
|
||||||
return nil, E.New("`detour` is not supported in direct context")
|
return nil, E.New("`detour` is not supported in direct context")
|
||||||
}
|
}
|
||||||
outboundDialer, err := dialer.NewWithOptions(dialer.Options{
|
outboundDialer, err := dialer.New(ctx, options.DialerOptions, true)
|
||||||
Context: ctx,
|
|
||||||
Options: options.DialerOptions,
|
|
||||||
RemoteIsDomain: true,
|
|
||||||
DirectOutbound: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
"github.com/sagernet/sing/service/pause"
|
"github.com/sagernet/sing/service/pause"
|
||||||
)
|
)
|
||||||
@@ -28,7 +27,10 @@ func RegisterURLTest(registry *outbound.Registry) {
|
|||||||
outbound.Register[option.URLTestOutboundOptions](registry, C.TypeURLTest, NewURLTest)
|
outbound.Register[option.URLTestOutboundOptions](registry, C.TypeURLTest, NewURLTest)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ adapter.OutboundGroup = (*URLTest)(nil)
|
var (
|
||||||
|
_ adapter.OutboundGroup = (*URLTest)(nil)
|
||||||
|
_ adapter.InterfaceUpdateListener = (*URLTest)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
type URLTest struct {
|
type URLTest struct {
|
||||||
outbound.Adapter
|
outbound.Adapter
|
||||||
@@ -170,12 +172,15 @@ func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
|||||||
s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose)
|
s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *URLTest) InterfaceUpdated() {
|
||||||
|
go s.group.CheckOutbounds(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type URLTestGroup struct {
|
type URLTestGroup struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
router adapter.Router
|
router adapter.Router
|
||||||
outbound adapter.OutboundManager
|
outboundManager adapter.OutboundManager
|
||||||
pause pause.Manager
|
|
||||||
pauseCallback *list.Element[pause.Callback]
|
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
outbounds []adapter.Outbound
|
outbounds []adapter.Outbound
|
||||||
link string
|
link string
|
||||||
@@ -184,15 +189,17 @@ type URLTestGroup struct {
|
|||||||
idleTimeout time.Duration
|
idleTimeout time.Duration
|
||||||
history adapter.URLTestHistoryStorage
|
history adapter.URLTestHistoryStorage
|
||||||
checking atomic.Bool
|
checking atomic.Bool
|
||||||
|
pauseManager pause.Manager
|
||||||
selectedOutboundTCP adapter.Outbound
|
selectedOutboundTCP adapter.Outbound
|
||||||
selectedOutboundUDP adapter.Outbound
|
selectedOutboundUDP adapter.Outbound
|
||||||
interruptGroup *interrupt.Group
|
interruptGroup *interrupt.Group
|
||||||
interruptExternalConnections bool
|
interruptExternalConnections bool
|
||||||
access sync.Mutex
|
|
||||||
ticker *time.Ticker
|
access sync.Mutex
|
||||||
close chan struct{}
|
ticker *time.Ticker
|
||||||
started bool
|
close chan struct{}
|
||||||
lastActive atomic.TypedValue[time.Time]
|
started bool
|
||||||
|
lastActive atomic.TypedValue[time.Time]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManager, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16, idleTimeout time.Duration, interruptExternalConnections bool) (*URLTestGroup, error) {
|
func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManager, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16, idleTimeout time.Duration, interruptExternalConnections bool) (*URLTestGroup, error) {
|
||||||
@@ -218,7 +225,7 @@ func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManage
|
|||||||
}
|
}
|
||||||
return &URLTestGroup{
|
return &URLTestGroup{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
outbound: outboundManager,
|
outboundManager: outboundManager,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
outbounds: outbounds,
|
outbounds: outbounds,
|
||||||
link: link,
|
link: link,
|
||||||
@@ -227,15 +234,13 @@ func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManage
|
|||||||
idleTimeout: idleTimeout,
|
idleTimeout: idleTimeout,
|
||||||
history: history,
|
history: history,
|
||||||
close: make(chan struct{}),
|
close: make(chan struct{}),
|
||||||
pause: service.FromContext[pause.Manager](ctx),
|
pauseManager: service.FromContext[pause.Manager](ctx),
|
||||||
interruptGroup: interrupt.NewGroup(),
|
interruptGroup: interrupt.NewGroup(),
|
||||||
interruptExternalConnections: interruptExternalConnections,
|
interruptExternalConnections: interruptExternalConnections,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *URLTestGroup) PostStart() {
|
func (g *URLTestGroup) PostStart() {
|
||||||
g.access.Lock()
|
|
||||||
defer g.access.Unlock()
|
|
||||||
g.started = true
|
g.started = true
|
||||||
g.lastActive.Store(time.Now())
|
g.lastActive.Store(time.Now())
|
||||||
go g.CheckOutbounds(false)
|
go g.CheckOutbounds(false)
|
||||||
@@ -245,25 +250,24 @@ func (g *URLTestGroup) Touch() {
|
|||||||
if !g.started {
|
if !g.started {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g.access.Lock()
|
|
||||||
defer g.access.Unlock()
|
|
||||||
if g.ticker != nil {
|
if g.ticker != nil {
|
||||||
g.lastActive.Store(time.Now())
|
g.lastActive.Store(time.Now())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
g.access.Lock()
|
||||||
|
defer g.access.Unlock()
|
||||||
|
if g.ticker != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
g.ticker = time.NewTicker(g.interval)
|
g.ticker = time.NewTicker(g.interval)
|
||||||
go g.loopCheck()
|
go g.loopCheck()
|
||||||
g.pauseCallback = pause.RegisterTicker(g.pause, g.ticker, g.interval, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *URLTestGroup) Close() error {
|
func (g *URLTestGroup) Close() error {
|
||||||
g.access.Lock()
|
|
||||||
defer g.access.Unlock()
|
|
||||||
if g.ticker == nil {
|
if g.ticker == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
g.ticker.Stop()
|
g.ticker.Stop()
|
||||||
g.pause.UnregisterCallback(g.pauseCallback)
|
|
||||||
close(g.close)
|
close(g.close)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -327,11 +331,10 @@ func (g *URLTestGroup) loopCheck() {
|
|||||||
g.access.Lock()
|
g.access.Lock()
|
||||||
g.ticker.Stop()
|
g.ticker.Stop()
|
||||||
g.ticker = nil
|
g.ticker = nil
|
||||||
g.pause.UnregisterCallback(g.pauseCallback)
|
|
||||||
g.pauseCallback = nil
|
|
||||||
g.access.Unlock()
|
g.access.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
g.pauseManager.WaitActive()
|
||||||
g.CheckOutbounds(false)
|
g.CheckOutbounds(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,7 +367,7 @@ func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
checked[realTag] = true
|
checked[realTag] = true
|
||||||
p, loaded := g.outbound.Outbound(realTag)
|
p, loaded := g.outboundManager.Outbound(realTag)
|
||||||
if !loaded {
|
if !loaded {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,14 @@ func (t *TProxy) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
|
|||||||
t.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, M.SocksaddrFromNetIP(destination), nil)
|
t.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, M.SocksaddrFromNetIP(destination), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tproxyPacketWriter struct {
|
||||||
|
ctx context.Context
|
||||||
|
listener *listener.Listener
|
||||||
|
source netip.AddrPort
|
||||||
|
destination M.Socksaddr
|
||||||
|
conn *net.UDPConn
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TProxy) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) {
|
func (t *TProxy) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) {
|
||||||
ctx := log.ContextWithNewID(t.ctx)
|
ctx := log.ContextWithNewID(t.ctx)
|
||||||
writer := &tproxyPacketWriter{
|
writer := &tproxyPacketWriter{
|
||||||
@@ -134,25 +142,15 @@ func (t *TProxy) preparePacketConnection(source M.Socksaddr, destination M.Socks
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type tproxyPacketWriter struct {
|
|
||||||
ctx context.Context
|
|
||||||
listener *listener.Listener
|
|
||||||
source netip.AddrPort
|
|
||||||
destination M.Socksaddr
|
|
||||||
conn *net.UDPConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
if w.listener.ListenOptions().NetNs == "" {
|
conn := w.conn
|
||||||
conn := w.conn
|
if w.destination == destination && conn != nil {
|
||||||
if w.destination == destination && conn != nil {
|
_, err := conn.WriteToUDPAddrPort(buffer.Bytes(), w.source)
|
||||||
_, err := conn.WriteToUDPAddrPort(buffer.Bytes(), w.source)
|
if err != nil {
|
||||||
if err != nil {
|
w.conn = nil
|
||||||
w.conn = nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
var listenConfig net.ListenConfig
|
var listenConfig net.ListenConfig
|
||||||
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
||||||
@@ -162,7 +160,7 @@ func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socks
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
udpConn := packetConn.(*net.UDPConn)
|
udpConn := packetConn.(*net.UDPConn)
|
||||||
if w.listener.ListenOptions().NetNs == "" && w.destination == destination {
|
if w.destination == destination {
|
||||||
w.conn = udpConn
|
w.conn = udpConn
|
||||||
} else {
|
} else {
|
||||||
defer udpConn.Close()
|
defer udpConn.Close()
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package tailscale
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -149,17 +147,6 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions())
|
return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions())
|
||||||
},
|
},
|
||||||
DNS: &dnsConfigurtor{},
|
DNS: &dnsConfigurtor{},
|
||||||
HTTPClient: &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
ForceAttemptHTTP2: true,
|
|
||||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(address))
|
|
||||||
},
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
RootCAs: adapter.RootPoolFromContext(ctx),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
return &Endpoint{
|
return &Endpoint{
|
||||||
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
|
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
|
||||||
@@ -459,10 +446,6 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
|||||||
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Endpoint) Server() *tsnet.Server {
|
|
||||||
return t.server
|
|
||||||
}
|
|
||||||
|
|
||||||
func addressFromAddr(destination netip.Addr) tcpip.Address {
|
func addressFromAddr(destination netip.Addr) tcpip.Address {
|
||||||
if destination.Is6() {
|
if destination.Is6() {
|
||||||
return tcpip.AddrFrom16(destination.As16())
|
return tcpip.AddrFrom16(destination.As16())
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize auto-redirect")
|
return nil, E.Cause(err, "initialize auto-redirect")
|
||||||
}
|
}
|
||||||
if !C.IsAndroid {
|
if !C.IsAndroid && (len(inbound.routeRuleSet) > 0 || len(inbound.routeExcludeRuleSet) > 0) {
|
||||||
inbound.tunOptions.AutoRedirectMarkMode = true
|
inbound.tunOptions.AutoRedirectMarkMode = true
|
||||||
err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
|
err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ func (w *Endpoint) Close() error {
|
|||||||
|
|
||||||
func (w *Endpoint) InterfaceUpdated() {
|
func (w *Endpoint) InterfaceUpdated() {
|
||||||
w.endpoint.BindUpdate()
|
w.endpoint.BindUpdate()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error {
|
func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error {
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ func (o *Outbound) Close() error {
|
|||||||
|
|
||||||
func (o *Outbound) InterfaceUpdated() {
|
func (o *Outbound) InterfaceUpdated() {
|
||||||
o.endpoint.BindUpdate()
|
o.endpoint.BindUpdate()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
|||||||
@@ -24,23 +24,31 @@ import (
|
|||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.ConnectionManager = (*ConnectionManager)(nil)
|
var _ adapter.ConnectionManager = (*ConnectionManager)(nil)
|
||||||
|
|
||||||
type ConnectionManager struct {
|
type ConnectionManager struct {
|
||||||
|
ctx context.Context
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
|
mitm adapter.MITMEngine
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
connections list.List[io.Closer]
|
connections list.List[io.Closer]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConnectionManager(logger logger.ContextLogger) *ConnectionManager {
|
func NewConnectionManager(ctx context.Context, logger logger.ContextLogger) *ConnectionManager {
|
||||||
return &ConnectionManager{
|
return &ConnectionManager{
|
||||||
|
ctx: ctx,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ConnectionManager) Start(stage adapter.StartStage) error {
|
func (m *ConnectionManager) Start(stage adapter.StartStage) error {
|
||||||
|
switch stage {
|
||||||
|
case adapter.StartStateInitialize:
|
||||||
|
m.mitm = service.FromContext[adapter.MITMEngine](m.ctx)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,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) {
|
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)
|
ctx = adapter.WithContext(ctx, &metadata)
|
||||||
var (
|
var (
|
||||||
remoteConn net.Conn
|
remoteConn net.Conn
|
||||||
@@ -262,7 +278,7 @@ func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err := bufio.CopyWithCounters(destinationWriter, sourceReader, source, readCounters, writeCounters)
|
_, err := bufio.CopyWithCounters(destination, sourceReader, source, readCounters, writeCounters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Close(source, destination)
|
common.Close(source, destination)
|
||||||
} else if duplexDst, isDuplex := destination.(N.WriteCloser); isDuplex {
|
} else if duplexDst, isDuplex := destination.(N.WriteCloser); isDuplex {
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ func (r *NetworkManager) AutoDetectInterfaceFunc() control.Func {
|
|||||||
if r.interfaceMonitor == nil {
|
if r.interfaceMonitor == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
bindFunc := control.BindToInterfaceFunc(r.interfaceFinder, func(network string, address string) (interfaceName string, interfaceIndex int, err error) {
|
return control.BindToInterfaceFunc(r.interfaceFinder, func(network string, address string) (interfaceName string, interfaceIndex int, err error) {
|
||||||
remoteAddr := M.ParseSocksaddr(address).Addr
|
remoteAddr := M.ParseSocksaddr(address).Addr
|
||||||
if remoteAddr.IsValid() {
|
if remoteAddr.IsValid() {
|
||||||
iif, err := r.interfaceFinder.ByAddr(remoteAddr)
|
iif, err := r.interfaceFinder.ByAddr(remoteAddr)
|
||||||
@@ -317,16 +317,6 @@ func (r *NetworkManager) AutoDetectInterfaceFunc() control.Func {
|
|||||||
}
|
}
|
||||||
return defaultInterface.Name, defaultInterface.Index, nil
|
return defaultInterface.Name, defaultInterface.Index, nil
|
||||||
})
|
})
|
||||||
return func(network, address string, conn syscall.RawConn) error {
|
|
||||||
err := bindFunc(network, address, conn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if r.autoRedirectOutputMark > 0 {
|
|
||||||
return control.RoutingMark(r.autoRedirectOutputMark)(network, address, conn)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
||||||
|
if r.pauseManager.IsDevicePaused() {
|
||||||
|
return E.New("reject connection to ", metadata.Destination, " while device paused")
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:staticcheck
|
//nolint:staticcheck
|
||||||
if metadata.InboundDetour != "" {
|
if metadata.InboundDetour != "" {
|
||||||
if metadata.LastInbound == metadata.InboundDetour {
|
if metadata.LastInbound == metadata.InboundDetour {
|
||||||
@@ -135,8 +139,8 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
|
|||||||
for _, buffer := range buffers {
|
for _, buffer := range buffers {
|
||||||
conn = bufio.NewCachedConn(conn, buffer)
|
conn = bufio.NewCachedConn(conn, buffer)
|
||||||
}
|
}
|
||||||
for _, tracker := range r.trackers {
|
if r.tracker != nil {
|
||||||
conn = tracker.RoutedConnection(ctx, conn, metadata, selectedRule, selectedOutbound)
|
conn = r.tracker.RoutedConnection(ctx, conn, metadata, selectedRule, selectedOutbound)
|
||||||
}
|
}
|
||||||
if outboundHandler, isHandler := selectedOutbound.(adapter.ConnectionHandlerEx); isHandler {
|
if outboundHandler, isHandler := selectedOutbound.(adapter.ConnectionHandlerEx); isHandler {
|
||||||
outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose)
|
outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose)
|
||||||
@@ -181,6 +185,9 @@ func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
||||||
|
if r.pauseManager.IsDevicePaused() {
|
||||||
|
return E.New("reject packet connection to ", metadata.Destination, " while device paused")
|
||||||
|
}
|
||||||
//nolint:staticcheck
|
//nolint:staticcheck
|
||||||
if metadata.InboundDetour != "" {
|
if metadata.InboundDetour != "" {
|
||||||
if metadata.LastInbound == metadata.InboundDetour {
|
if metadata.LastInbound == metadata.InboundDetour {
|
||||||
@@ -250,8 +257,8 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
|
|||||||
conn = bufio.NewCachedPacketConn(conn, buffer.Buffer, buffer.Destination)
|
conn = bufio.NewCachedPacketConn(conn, buffer.Buffer, buffer.Destination)
|
||||||
N.PutPacketBuffer(buffer)
|
N.PutPacketBuffer(buffer)
|
||||||
}
|
}
|
||||||
for _, tracker := range r.trackers {
|
if r.tracker != nil {
|
||||||
conn = tracker.RoutedPacketConnection(ctx, conn, metadata, selectedRule, selectedOutbound)
|
conn = r.tracker.RoutedPacketConnection(ctx, conn, metadata, selectedRule, selectedOutbound)
|
||||||
}
|
}
|
||||||
if metadata.FakeIP {
|
if metadata.FakeIP {
|
||||||
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
|
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
|
||||||
@@ -451,6 +458,9 @@ match:
|
|||||||
metadata.TLSFragment = true
|
metadata.TLSFragment = true
|
||||||
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
|
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
|
||||||
}
|
}
|
||||||
|
if routeOptions.MITM != nil && routeOptions.MITM.Enabled {
|
||||||
|
metadata.MITM = routeOptions.MITM
|
||||||
|
}
|
||||||
}
|
}
|
||||||
switch action := currentRule.Action().(type) {
|
switch action := currentRule.Action().(type) {
|
||||||
case *rule.RuleActionSniff:
|
case *rule.RuleActionSniff:
|
||||||
@@ -546,7 +556,7 @@ func (r *Router) actionSniff(
|
|||||||
sniffBuffer.Release()
|
sniffBuffer.Release()
|
||||||
}
|
}
|
||||||
} else if inputPacketConn != nil {
|
} else if inputPacketConn != nil {
|
||||||
if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) {
|
if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrClientHelloFragmented) {
|
||||||
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError)
|
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -616,8 +626,7 @@ func (r *Router) actionSniff(
|
|||||||
}
|
}
|
||||||
packetBuffers = append(packetBuffers, packetBuffer)
|
packetBuffers = append(packetBuffers, packetBuffer)
|
||||||
metadata.PacketSniffError = err
|
metadata.PacketSniffError = err
|
||||||
if errors.Is(err, sniff.ErrNeedMoreData) {
|
if errors.Is(err, sniff.ErrClientHelloFragmented) {
|
||||||
// TODO: replace with generic message when there are more multi-packet protocols
|
|
||||||
r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello")
|
r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type Router struct {
|
|||||||
ruleSetMap map[string]adapter.RuleSet
|
ruleSetMap map[string]adapter.RuleSet
|
||||||
processSearcher process.Searcher
|
processSearcher process.Searcher
|
||||||
pauseManager pause.Manager
|
pauseManager pause.Manager
|
||||||
trackers []adapter.ConnectionTracker
|
tracker adapter.ConnectionTracker
|
||||||
platformInterface platform.Interface
|
platformInterface platform.Interface
|
||||||
needWIFIState bool
|
needWIFIState bool
|
||||||
started bool
|
started bool
|
||||||
@@ -203,8 +203,8 @@ func (r *Router) Rules() []adapter.Rule {
|
|||||||
return r.rules
|
return r.rules
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) AppendTracker(tracker adapter.ConnectionTracker) {
|
func (r *Router) SetTracker(tracker adapter.ConnectionTracker) {
|
||||||
r.trackers = append(r.trackers, tracker)
|
r.tracker = tracker
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) ResetNetwork() {
|
func (r *Router) ResetNetwork() {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
|||||||
UDPConnect: action.RouteOptions.UDPConnect,
|
UDPConnect: action.RouteOptions.UDPConnect,
|
||||||
TLSFragment: action.RouteOptions.TLSFragment,
|
TLSFragment: action.RouteOptions.TLSFragment,
|
||||||
TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
|
TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
|
||||||
|
MITM: action.RouteOptions.MITM,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
case C.RuleActionTypeRouteOptions:
|
case C.RuleActionTypeRouteOptions:
|
||||||
@@ -53,6 +54,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
|||||||
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
|
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
|
||||||
TLSFragment: action.RouteOptionsOptions.TLSFragment,
|
TLSFragment: action.RouteOptionsOptions.TLSFragment,
|
||||||
TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
|
TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
|
||||||
|
MITM: action.RouteOptionsOptions.MITM,
|
||||||
}, nil
|
}, nil
|
||||||
case C.RuleActionTypeDirect:
|
case C.RuleActionTypeDirect:
|
||||||
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
|
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
|
||||||
@@ -152,15 +154,7 @@ func (r *RuleActionRoute) Type() string {
|
|||||||
func (r *RuleActionRoute) String() string {
|
func (r *RuleActionRoute) String() string {
|
||||||
var descriptions []string
|
var descriptions []string
|
||||||
descriptions = append(descriptions, r.Outbound)
|
descriptions = append(descriptions, r.Outbound)
|
||||||
if r.UDPDisableDomainUnmapping {
|
descriptions = append(descriptions, r.Descriptions()...)
|
||||||
descriptions = append(descriptions, "udp-disable-domain-unmapping")
|
|
||||||
}
|
|
||||||
if r.UDPConnect {
|
|
||||||
descriptions = append(descriptions, "udp-connect")
|
|
||||||
}
|
|
||||||
if r.TLSFragment {
|
|
||||||
descriptions = append(descriptions, "tls-fragment")
|
|
||||||
}
|
|
||||||
return F.ToString("route(", strings.Join(descriptions, ","), ")")
|
return F.ToString("route(", strings.Join(descriptions, ","), ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,13 +170,14 @@ type RuleActionRouteOptions struct {
|
|||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
TLSFragment bool
|
TLSFragment bool
|
||||||
TLSFragmentFallbackDelay time.Duration
|
TLSFragmentFallbackDelay time.Duration
|
||||||
|
MITM *option.MITMRouteOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuleActionRouteOptions) Type() string {
|
func (r *RuleActionRouteOptions) Type() string {
|
||||||
return C.RuleActionTypeRouteOptions
|
return C.RuleActionTypeRouteOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuleActionRouteOptions) String() string {
|
func (r *RuleActionRouteOptions) Descriptions() []string {
|
||||||
var descriptions []string
|
var descriptions []string
|
||||||
if r.OverrideAddress.IsValid() {
|
if r.OverrideAddress.IsValid() {
|
||||||
descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString()))
|
descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString()))
|
||||||
@@ -209,9 +204,22 @@ func (r *RuleActionRouteOptions) String() string {
|
|||||||
descriptions = append(descriptions, "udp-connect")
|
descriptions = append(descriptions, "udp-connect")
|
||||||
}
|
}
|
||||||
if r.UDPTimeout > 0 {
|
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 {
|
type RuleActionDNSRoute struct {
|
||||||
@@ -305,9 +313,6 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
|
|||||||
default:
|
default:
|
||||||
panic(F.ToString("unknown reject method: ", r.Method))
|
panic(F.ToString("unknown reject method: ", r.Method))
|
||||||
}
|
}
|
||||||
if r.NoDrop {
|
|
||||||
return returnErr
|
|
||||||
}
|
|
||||||
r.dropAccess.Lock()
|
r.dropAccess.Lock()
|
||||||
defer r.dropAccess.Unlock()
|
defer r.dropAccess.Unlock()
|
||||||
timeNow := time.Now()
|
timeNow := time.Now()
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.lastUpdated.IsZero() {
|
if s.lastUpdated.IsZero() {
|
||||||
err := s.fetch(ctx, startContext)
|
err := s.fetchOnce(ctx, startContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "initial rule-set: ", s.options.Tag)
|
return E.Cause(err, "initial rule-set: ", s.options.Tag)
|
||||||
}
|
}
|
||||||
@@ -200,7 +200,7 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
|
|||||||
|
|
||||||
func (s *RemoteRuleSet) loopUpdate() {
|
func (s *RemoteRuleSet) loopUpdate() {
|
||||||
if time.Since(s.lastUpdated) > s.updateInterval {
|
if time.Since(s.lastUpdated) > s.updateInterval {
|
||||||
err := s.fetch(s.ctx, nil)
|
err := s.fetchOnce(s.ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
||||||
} else if s.refs.Load() == 0 {
|
} else if s.refs.Load() == 0 {
|
||||||
@@ -213,21 +213,18 @@ func (s *RemoteRuleSet) loopUpdate() {
|
|||||||
case <-s.ctx.Done():
|
case <-s.ctx.Done():
|
||||||
return
|
return
|
||||||
case <-s.updateTicker.C:
|
case <-s.updateTicker.C:
|
||||||
s.updateOnce()
|
s.pauseManager.WaitActive()
|
||||||
|
err := s.fetchOnce(s.ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
||||||
|
} else if s.refs.Load() == 0 {
|
||||||
|
s.rules = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RemoteRuleSet) updateOnce() {
|
func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext *adapter.HTTPStartContext) error {
|
||||||
err := s.fetch(s.ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
|
||||||
} else if s.refs.Load() == 0 {
|
|
||||||
s.rules = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RemoteRuleSet) fetch(ctx context.Context, startContext *adapter.HTTPStartContext) error {
|
|
||||||
s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL)
|
s.logger.Debug("updating rule-set ", s.options.Tag, " from URL: ", s.options.RemoteOptions.URL)
|
||||||
var httpClient *http.Client
|
var httpClient *http.Client
|
||||||
if startContext != nil {
|
if startContext != nil {
|
||||||
|
|||||||
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]"
|
||||||
|
}
|
||||||
35
script/modules/boxctx/module.go
Normal file
35
script/modules/boxctx/module.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package boxctx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/script/jsc"
|
||||||
|
"github.com/sagernet/sing-box/script/modules/require"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ModuleName = "context"
|
||||||
|
|
||||||
|
type Module struct {
|
||||||
|
runtime *goja.Runtime
|
||||||
|
classContext jsc.Class[*Module, *Context]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Require(runtime *goja.Runtime, module *goja.Object) {
|
||||||
|
m := &Module{
|
||||||
|
runtime: runtime,
|
||||||
|
}
|
||||||
|
m.classContext = createContext(m)
|
||||||
|
exports := module.Get("exports").(*goja.Object)
|
||||||
|
exports.Set("Context", m.classContext.ToValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Enable(runtime *goja.Runtime, context *Context) {
|
||||||
|
exports := require.Require(runtime, ModuleName).ToObject(runtime)
|
||||||
|
classContext := jsc.GetClass[*Module, *Context](runtime, exports, "Context")
|
||||||
|
context.class = classContext
|
||||||
|
runtime.Set("context", classContext.New(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Runtime() *goja.Runtime {
|
||||||
|
return m.runtime
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user