Compare commits

..

66 Commits

Author SHA1 Message Date
世界
658d4a180b Use stdlib ech 2024-12-11 19:33:53 +08:00
世界
93966bdd36 documentation: Bump version 2024-12-10 21:38:13 +08:00
世界
14a4b0f922 Fix socks5 UDP implementation 2024-12-10 21:38:04 +08:00
世界
919b08e64c clash-api: Fix missing endpoints 2024-12-10 21:37:36 +08:00
世界
bdd2472065 hysteria2: Add more masquerade options 2024-12-10 21:37:36 +08:00
世界
705c23866a Improve timeouts 2024-12-10 21:37:36 +08:00
世界
ec310170cc Add UDP timeout route option 2024-12-10 21:37:35 +08:00
世界
7405b22dc8 Make GSO adaptive 2024-12-10 21:37:35 +08:00
世界
cd2cf2450e Fix tests 2024-12-10 21:37:35 +08:00
世界
a525f139dc Fix lint 2024-12-10 21:37:34 +08:00
世界
cc8ba050dd refactor: WireGuard endpoint 2024-12-10 21:37:34 +08:00
世界
987556fd3d refactor: connection manager 2024-12-10 21:37:33 +08:00
世界
e6dd7d279d documentation: Fix typo 2024-12-10 21:37:32 +08:00
世界
b1cbf141c7 Add override destination to route options 2024-12-10 21:37:32 +08:00
世界
ab616d8510 Add dns.cache_capacity 2024-12-10 21:37:32 +08:00
世界
097818dfef Refactor multi networks strategy 2024-12-10 21:37:31 +08:00
世界
47af425e45 documentation: Remove unused titles 2024-12-10 21:37:31 +08:00
世界
5d49685683 Add multi network dialing 2024-12-10 21:37:30 +08:00
世界
15624a648c documentation: Merge route options to route actions 2024-12-10 21:37:30 +08:00
世界
1bfedf5332 Add network_[type/is_expensive/is_constrained] rule items 2024-12-10 21:37:30 +08:00
世界
10d15259b6 Merge route options to route actions 2024-12-10 21:37:29 +08:00
世界
ee11ca4935 refactor: Platform Interfaces 2024-12-10 21:37:28 +08:00
世界
e83331c2d9 refactor: Extract services form router 2024-12-10 21:37:28 +08:00
世界
b74df53a9c refactor: Modular network manager 2024-12-10 21:37:27 +08:00
世界
f64107f040 refactor: Modular inbound/outbound manager 2024-12-10 21:37:26 +08:00
世界
7db0e712b6 documentation: Add rule action 2024-12-10 21:37:26 +08:00
世界
edfdf1d4f3 documentation: Update the scheduled removal time of deprecated features 2024-12-10 21:37:25 +08:00
世界
eb3023d66c documentation: Remove outdated icons 2024-12-10 21:37:25 +08:00
世界
2c39c4d19c Migrate bad options to library 2024-12-10 21:37:25 +08:00
世界
aa3fcefb72 Implement udp connect 2024-12-10 21:37:25 +08:00
世界
ce503fd682 Implement new deprecated warnings 2024-12-10 21:37:24 +08:00
世界
017fa5e298 Improve rule actions 2024-12-10 21:37:23 +08:00
世界
94d187397d Remove unused reject methods 2024-12-10 21:37:23 +08:00
世界
1d497b08d7 refactor: Modular inbounds/outbounds 2024-12-10 21:37:23 +08:00
世界
e52ab781bb Implement dns-hijack 2024-12-10 21:37:23 +08:00
世界
2c1e398c78 Implement resolve(server) 2024-12-10 21:37:22 +08:00
世界
d4ad7ff638 Implement TCP and ICMP rejects 2024-12-10 21:37:22 +08:00
世界
ee0f3ba739 Crazy sekai overturns the small pond 2024-12-10 21:37:01 +08:00
世界
b365b1369d Fix play release 2024-12-10 20:44:08 +08:00
世界
69f8d7fa72 Add workaround for bulkBarrierPreWrite: unaligned arguments panic 2024-12-10 20:36:32 +08:00
世界
746f63a9ac Update debug iOS library build 2024-12-10 20:36:32 +08:00
世界
f0b6818b4c Add workaround for golang/go#68760 2024-12-10 10:30:42 +08:00
世界
3032317918 release: Add workflow build 2024-12-10 09:25:11 +08:00
世界
db22f61846 Update NDK to r28-rc1 2024-12-09 15:11:38 +08:00
世界
8c3a98faa2 wireguard: Fix set reserved 2024-12-05 17:58:09 +08:00
世界
1e787cb607 Fix initial traffic value 2024-12-03 21:43:56 +08:00
世界
558585b01d release: Set upload threads to 5 2024-12-03 17:36:19 +08:00
世界
6e7ecbd4f5 Fix wireguard listen 2024-11-30 12:20:48 +08:00
世界
5a661cde67 clashapi: Remove traffic loop 2024-11-28 12:57:56 +08:00
世界
3cc0e87cfb Bump version 2024-11-27 10:45:24 +08:00
世界
effea5a2b3 Update quic-go to v0.48.2 2024-11-27 10:37:00 +08:00
世界
7f168c5ec6 release: Clean before iOS build 2024-11-27 10:35:20 +08:00
Zephyruso
0e9129ee3f clashapi: Add mode list 2024-11-27 10:34:47 +08:00
世界
1086d5e665 Fix test tags 2024-11-23 12:20:12 +08:00
世界
d9102ba599 Fix build on bsd systems 2024-11-23 12:16:46 +08:00
世界
17019f1729 Bump version 2024-11-20 12:03:42 +08:00
世界
6be07ed51f Fix start watcher 2024-11-20 11:32:53 +08:00
世界
af58e3bec0 Fix debug listener 2024-11-20 11:32:53 +08:00
世界
e58b549d0f Fix "Fix reloading of tls.certificate_path, tls.key_path and tls.ech.key_path" 2024-11-18 19:03:24 +08:00
zeetex
1d81996ceb Fix reloading of tls.certificate_path, tls.key_path and tls.ech.key_path 2024-11-18 14:09:54 +08:00
世界
97c47e72c4 Update dependencies 2024-11-18 14:07:00 +08:00
世界
122be275b0 release: Notarize macos standalone manually with --no-s3-acceleration 2024-11-18 14:07:00 +08:00
世界
0bb1132034 selector: Fix crash before start 2024-11-18 14:07:00 +08:00
世界
de14337b4b Fix deprecated check 2024-11-18 14:07:00 +08:00
世界
1e07633914 Downgrade NDK to 26.2.11394342 2024-11-18 13:10:06 +08:00
世界
e3e203844e Fix decompile rule-set 2024-11-18 13:10:06 +08:00
194 changed files with 4962 additions and 2287 deletions

615
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,615 @@
name: Build
on:
workflow_dispatch:
inputs:
version:
description: "Version name"
required: true
type: string
prerelease:
description: "Is prerelease"
required: true
type: boolean
default: true
build:
description: "Build type"
required: true
type: choice
default: "All"
options:
- All
- Binary
- Android
- Apple
- app-store
- iOS
- macOS
- tvOS
- macOS-standalone
- publish-android
macos_project_version:
description: "macOS project version"
required: false
type: string
push:
branches:
- main-next
- dev-next
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
cancel-in-progress: true
jobs:
calculate_version:
name: Calculate version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.outputs.outputs.version }}
prerelease: ${{ steps.outputs.outputs.prerelease }}
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
echo "version=${{ inputs.version }}"
echo "prerelease=${{ inputs.prerelease }}"
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
echo "prerelease=${{ inputs.prerelease }}" >> "$GITHUB_ENV"
- name: Calculate version
if: github.event_name != 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/read_tag --nightly
- name: Set outputs
id: outputs
run: |-
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
build:
name: Build binary
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
runs-on: ubuntu-latest
needs:
- calculate_version
strategy:
matrix:
include:
- name: linux_386
goos: linux
goarch: 386
- name: linux_amd64
goos: linux
goarch: amd64
- name: linux_arm64
goos: linux
goarch: arm64
- name: linux_arm
goos: linux
goarch: arm
goarm: 6
- name: linux_arm_v7
goos: linux
goarch: arm
goarm: 7
- name: linux_s390x
goos: linux
goarch: s390x
- name: linux_riscv64
goos: linux
goarch: riscv64
- name: linux_mips64le
goos: linux
goarch: mips64le
- name: windows_amd64
goos: windows
goarch: amd64
require_legacy_go: true
- name: windows_386
goos: windows
goarch: 386
require_legacy_go: true
- name: windows_arm64
goos: windows
goarch: arm64
- name: darwin_arm64
goos: darwin
goarch: arm64
- name: darwin_amd64
goos: darwin
goarch: amd64
require_legacy_go: true
- name: android_arm64
goos: android
goarch: arm64
- name: android_arm
goos: android
goarch: arm
goarm: 7
- name: android_amd64
goos: android
goarch: amd64
- name: android_386
goos: android
goarch: 386
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
- name: Cache legacy Go
if: matrix.require_legacy_go
id: cache-legacy-go
uses: actions/cache@v4
with:
path: |
~/go/go1.20.14
key: go120
- name: Setup legacy Go
if: matrix.require_legacy_go == 'true' && steps.cache-legacy-go.outputs.cache-hit != 'true'
run: |-
wget https://dl.google.com/go/go1.20.14.linux-amd64.tar.gz
tar -xzf go1.20.14.linux-amd64.tar.gz
mv go $HOME/go/go1.20.14
- name: Setup Android NDK
if: matrix.goos == 'android'
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
local-cache: true
- name: Setup Goreleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
install-only: true
- name: Extract signing key
run: |-
mkdir -p $HOME/.gnupg
cat > $HOME/.gnupg/sagernet.key <<EOF
${{ secrets.GPG_KEY }}
EOF
echo "HOME=$HOME" >> "$GITHUB_ENV"
- name: Set tag
run: |-
git tag v${{ needs.calculate_version.outputs.version }}
- name: Build
if: matrix.goos != 'android'
run: |-
goreleaser release --clean --split
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOPATH: ${{ env.HOME }}/go
GOARM: ${{ matrix.goarm }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- name: Build Android
if: matrix.goos == 'android'
run: |-
go install -v ./cmd/internal/build
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build goreleaser release --clean --split
env:
BUILD_GOOS: ${{ matrix.goos }}
BUILD_GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
NFPM_KEY_PATH: ${{ env.HOME }}/.gnupg/sagernet.key
NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- name: Upload artifact
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.name }}
path: 'dist'
build_android:
name: Build Android
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
runs-on: ubuntu-latest
needs:
- calculate_version
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
submodules: 'recursive'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
- name: Setup OpenJDK
run: |-
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
- name: Set tag
run: |-
git tag v${{ needs.calculate_version.outputs.version }}
- name: Build library
run: |-
make lib_install
export PATH="$PATH:$(go env GOPATH)/bin"
make lib_android
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: Checkout main branch
if: needs.calculate_version.outputs.prerelease == 'false'
run: |-
cd clients/android
git checkout main
- name: Checkout dev branch
if: needs.calculate_version.outputs.prerelease == 'true'
run: |-
cd clients/android
git checkout dev
- name: Gradle cache
uses: actions/cache@v4
with:
path: ~/.gradle
key: gradle-${{ hashFiles('**/*.gradle') }}
- name: Build
run: |-
go run -v ./cmd/internal/update_android_version --ci
mkdir clients/android/app/libs
cp libbox.aar clients/android/app/libs
cd clients/android
./gradlew :app:assemblePlayRelease :app:assembleOtherRelease
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
- name: Prepare upload
if: github.event_name == 'workflow_dispatch'
run: |-
mkdir -p dist/release
cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release
cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release
- name: Upload artifact
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: binary-android-apks
path: 'dist'
publish_android:
name: Publish Android
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android'
runs-on: ubuntu-latest
needs:
- calculate_version
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
submodules: 'recursive'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r28-beta2
- name: Setup OpenJDK
run: |-
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
- name: Set tag
run: |-
git tag v${{ needs.calculate_version.outputs.version }}
- name: Build library
run: |-
make lib_install
export PATH="$PATH:$(go env GOPATH)/bin"
make lib_android
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
- name: Checkout main branch
if: needs.calculate_version.outputs.prerelease == 'false'
run: |-
cd clients/android
git checkout main
- name: Checkout dev branch
if: needs.calculate_version.outputs.prerelease == 'true'
run: |-
cd clients/android
git checkout dev
- name: Gradle cache
uses: actions/cache@v4
with:
path: ~/.gradle
key: gradle-${{ hashFiles('**/*.gradle') }}
- name: Build
run: |-
go run -v ./cmd/internal/update_android_version --ci
mkdir clients/android/app/libs
cp libbox.aar clients/android/app/libs
cd clients/android
echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json
./gradlew :app:publishPlayReleaseBundle
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
build_apple_library:
name: Build Apple library
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone'
runs-on: macos-15
needs:
- calculate_version
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
submodules: 'recursive'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
- name: Setup Xcode
run: |-
sudo xcode-select -s /Applications/Xcode_16.2_beta_3.app
- name: Set tag
run: |-
git tag v${{ needs.calculate_version.outputs.version }}
- name: Build library
run: |-
make lib_install
export PATH="$PATH:$(go env GOPATH)/bin"
make lib_ios
- name: Upload library
uses: actions/upload-artifact@v4
with:
name: library-apple
path: 'Libbox.xcframework'
build_apple:
name: Build Apple clients
runs-on: macos-15
needs:
- calculate_version
- build_apple_library
strategy:
matrix:
include:
- name: iOS
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'iOS' }}
scheme: SFI
destination: 'generic/platform=iOS'
archive: build/SFI.xcarchive
upload: SFI/Upload.plist
- name: macOS
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'macOS' }}
scheme: SFM
destination: 'generic/platform=macOS'
archive: build/SFM.xcarchive
upload: SFI/Upload.plist
- name: tvOS
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'tvOS' }}
scheme: SFT
destination: 'generic/platform=tvOS'
archive: build/SFT.xcarchive
upload: SFI/Upload.plist
- name: macOS-standalone
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone' }}
scheme: SFM.System
destination: 'generic/platform=macOS'
archive: build/SFM.System.xcarchive
export: SFM.System/Export.plist
export_path: build/SFM.System
steps:
- name: Checkout
if: matrix.if
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
submodules: 'recursive'
- name: Setup Go
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ^1.23
- name: Setup Xcode
if: matrix.if
run: |-
sudo xcode-select -s /Applications/Xcode_16.2_beta_3.app
- name: Set tag
if: matrix.if
run: |-
git tag v${{ needs.calculate_version.outputs.version }}
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Checkout main branch
if: matrix.if && needs.calculate_version.outputs.prerelease == 'false'
run: |-
cd clients/apple
git checkout main
- name: Checkout dev branch
if: matrix.if && needs.calculate_version.outputs.prerelease == 'true'
run: |-
cd clients/apple
git checkout dev
- name: Setup certificates
if: matrix.if
run: |-
CERTIFICATE_PATH=$RUNNER_TEMP/Certificates.p12
KEYCHAIN_PATH=$RUNNER_TEMP/certificates.keychain-db
echo -n "$CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
PROFILES_ZIP_PATH=$RUNNER_TEMP/Profiles.zip
echo -n "$PROVISIONING_PROFILES" | base64 --decode -o $PROFILES_ZIP_PATH
PROFILES_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILES_PATH"
unzip $PROFILES_ZIP_PATH -d "$PROFILES_PATH"
ASC_KEY_PATH=$RUNNER_TEMP/Key.p12
echo -n "$ASC_KEY" | base64 --decode -o $ASC_KEY_PATH
xcrun notarytool store-credentials "notarytool-password" \
--key $ASC_KEY_PATH \
--key-id $ASC_KEY_ID \
--issuer $ASC_KEY_ISSUER_ID
env:
CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.P12_PASSWORD }}
PROVISIONING_PROFILES: ${{ secrets.PROVISIONING_PROFILES }}
ASC_KEY: ${{ secrets.ASC_KEY }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
- name: Download library
if: matrix.if
uses: actions/download-artifact@v4
with:
name: library-apple
path: clients/apple/Libbox.xcframework
- name: Build
if: matrix.if
run: |-
go run -v ./cmd/internal/update_apple_version --ci
cd clients/apple
xcodebuild archive \
-scheme "${{ matrix.scheme }}" \
-configuration Release \
-destination "${{ matrix.destination }}" \
-archivePath "${{ matrix.archive }}" \
-allowProvisioningUpdates \
-authenticationKeyPath $RUNNER_TEMP/Key.p12 \
-authenticationKeyID $ASC_KEY_ID \
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
env:
MACOS_PROJECT_VERSION: ${{ inputs.macos_project_version }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
- name: Upload to App Store Connect
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch'
run: |-
cd clients/apple
xcodebuild -exportArchive \
-archivePath "${{ matrix.archive }}" \
-exportOptionsPlist ${{ matrix.upload }} \
-allowProvisioningUpdates \
-authenticationKeyPath $RUNNER_TEMP/Key.p12 \
-authenticationKeyID $ASC_KEY_ID \
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
- name: Build image
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
run: |-
pushd clients/apple
xcodebuild -exportArchive \
-archivePath "${{ matrix.archive }}" \
-exportOptionsPlist ${{ matrix.export }} \
-exportPath "${{ matrix.export_path }}"
brew install create-dmg
create-dmg \
--volname "sing-box" \
--volicon "${{ matrix.export_path }}/SFM.app/Contents/Resources/AppIcon.icns" \
--icon "SFM.app" 0 0 \
--hide-extension "SFM.app" \
--app-drop-link 0 0 \
--skip-jenkins \
SFM.dmg "${{ matrix.export_path }}/SFM.app"
xcrun notarytool submit "SFM.dmg" --wait --keychain-profile "notarytool-password"
cd "${{ matrix.archive }}"
zip -r SFM.dSYMs.zip dSYMs
popd
mkdir -p dist/release
cp clients/apple/SFM.dmg "dist/release/SFM-${VERSION}-universal.dmg"
cp "clients/apple/${{ matrix.archive }}/SFM.dSYMs.zip" "dist/release/SFM-${VERSION}-universal.dSYMs.zip"
- name: Upload image
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: binary-macos-dmg
path: 'dist'
upload:
name: Upload builds
if: always() && github.event_name == 'workflow_dispatch' && inputs.build != 'publish-android'
runs-on: ubuntu-latest
needs:
- calculate_version
- build
- build_android
- build_apple
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Goreleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
install-only: true
- name: Cache ghr
uses: actions/cache@v4
id: cache-ghr
with:
path: |
~/go/bin/ghr
key: ghr
- name: Setup ghr
if: steps.cache-ghr.outputs.cache-hit != 'true'
run: |-
cd $HOME
git clone https://github.com/nekohasekai/ghr ghr
cd ghr
go install -v .
- name: Set tag
run: |-
git tag v${{ needs.calculate_version.outputs.version }}
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Download builds
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Merge builds
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
run: |-
goreleaser continue --merge --skip publish
mkdir -p dist/release
mv dist/*/sing-box*{tar.gz,zip,deb,rpm,_amd64.pkg.tar.zst,_arm64.pkg.tar.zst} dist/release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- name: Upload builds
run: |-
export PATH="$PATH:$HOME/go/bin"
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,219 +0,0 @@
name: Debug build
on:
push:
branches:
- stable-next
- main-next
- dev-next
paths-ignore:
- '**.md'
- '.github/**'
- '!.github/workflows/debug.yml'
pull_request:
branches:
- stable-next
- main-next
- dev-next
jobs:
build:
name: Debug build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.23
- name: Run Test
run: |
go test -v ./...
build_go120:
name: Debug build (Go 1.20)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.20
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go120-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build_go120
build_go121:
name: Debug build (Go 1.21)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.21
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go121-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build
build_go122:
name: Debug build (Go 1.22)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.22
- name: Cache go module
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
key: go122-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build
cross:
strategy:
matrix:
include:
# windows
- name: windows-amd64
goos: windows
goarch: amd64
goamd64: v1
- name: windows-amd64-v3
goos: windows
goarch: amd64
goamd64: v3
- name: windows-386
goos: windows
goarch: 386
- name: windows-arm64
goos: windows
goarch: arm64
- name: windows-arm32v7
goos: windows
goarch: arm
goarm: 7
# linux
- name: linux-amd64
goos: linux
goarch: amd64
goamd64: v1
- name: linux-amd64-v3
goos: linux
goarch: amd64
goamd64: v3
- name: linux-386
goos: linux
goarch: 386
- name: linux-arm64
goos: linux
goarch: arm64
- name: linux-armv5
goos: linux
goarch: arm
goarm: 5
- name: linux-armv6
goos: linux
goarch: arm
goarm: 6
- name: linux-armv7
goos: linux
goarch: arm
goarm: 7
- name: linux-mips-softfloat
goos: linux
goarch: mips
gomips: softfloat
- name: linux-mips-hardfloat
goos: linux
goarch: mips
gomips: hardfloat
- name: linux-mipsel-softfloat
goos: linux
goarch: mipsle
gomips: softfloat
- name: linux-mipsel-hardfloat
goos: linux
goarch: mipsle
gomips: hardfloat
- name: linux-mips64
goos: linux
goarch: mips64
- name: linux-mips64el
goos: linux
goarch: mips64le
- name: linux-s390x
goos: linux
goarch: s390x
# darwin
- name: darwin-amd64
goos: darwin
goarch: amd64
goamd64: v1
- name: darwin-amd64-v3
goos: darwin
goarch: amd64
goamd64: v3
- name: darwin-arm64
goos: darwin
goarch: arm64
# freebsd
- name: freebsd-amd64
goos: freebsd
goarch: amd64
goamd64: v1
- name: freebsd-amd64-v3
goos: freebsd
goarch: amd64
goamd64: v3
- name: freebsd-386
goos: freebsd
goarch: 386
- name: freebsd-arm64
goos: freebsd
goarch: arm64
fail-fast: true
runs-on: ubuntu-latest
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOAMD64: ${{ matrix.goamd64 }}
GOARM: ${{ matrix.goarm }}
GOMIPS: ${{ matrix.gomips }}
CGO_ENABLED: 0
TAGS: with_clash_api,with_quic
steps:
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.21
- name: Build
id: build
run: make

View File

@@ -22,7 +22,6 @@ jobs:
mkdir -p $HOME/.gnupg mkdir -p $HOME/.gnupg
cat > $HOME/.gnupg/sagernet.key <<EOF cat > $HOME/.gnupg/sagernet.key <<EOF
${{ secrets.GPG_KEY }} ${{ secrets.GPG_KEY }}
echo "HOME=$HOME" >> "$GITHUB_ENV"
EOF EOF
echo "HOME=$HOME" >> "$GITHUB_ENV" echo "HOME=$HOME" >> "$GITHUB_ENV"
- name: Publish release - name: Publish release

View File

@@ -200,4 +200,6 @@ release:
ids: ids:
- archive - archive
- package - package
skip_upload: true skip_upload: true
partial:
by: target

View File

@@ -71,7 +71,7 @@ release:
dist/*_amd64.pkg.tar.zst \ dist/*_amd64.pkg.tar.zst \
dist/*_arm64.pkg.tar.zst \ dist/*_arm64.pkg.tar.zst \
dist/release dist/release
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
rm -r dist/release rm -r dist/release
release_repo: release_repo:
@@ -90,7 +90,7 @@ upload_android:
mkdir -p dist/release_android mkdir -p dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android cp ../sing-box-for-android/app/build/outputs/apk/play/release/*.apk dist/release_android
cp ../sing-box-for-android/app/build/outputs/apk/other/release/*-universal.apk dist/release_android cp ../sing-box-for-android/app/build/outputs/apk/other/release/*-universal.apk dist/release_android
ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release_android ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release_android
rm -rf dist/release_android rm -rf dist/release_android
release_android: lib_android update_android_version build_android upload_android release_android: lib_android update_android_version build_android upload_android
@@ -99,9 +99,11 @@ publish_android:
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop
# TODO: find why and remove `-destination 'generic/platform=iOS'` # TODO: find why and remove `-destination 'generic/platform=iOS'`
# TODO: remove xcode clean when fix control widget fixed
build_ios: build_ios:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
rm -rf build/SFI.xcarchive && \ rm -rf build/SFI.xcarchive && \
xcodebuild clean -scheme SFI && \
xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates xcodebuild archive -scheme SFI -configuration Release -destination 'generic/platform=iOS' -archivePath build/SFI.xcarchive -allowProvisioningUpdates
upload_ios_app_store: upload_ios_app_store:
@@ -199,9 +201,15 @@ test_stdio:
lib_android: lib_android:
go run ./cmd/internal/build_libbox -target android go run ./cmd/internal/build_libbox -target android
lib_android_debug:
go run ./cmd/internal/build_libbox -target android -debug
lib_ios: lib_ios:
go run ./cmd/internal/build_libbox -target ios go run ./cmd/internal/build_libbox -target ios
lib_ios_debug:
go run ./cmd/internal/build_libbox -target ios -debug
lib: lib:
go run ./cmd/internal/build_libbox -target android go run ./cmd/internal/build_libbox -target android
go run ./cmd/internal/build_libbox -target ios go run ./cmd/internal/build_libbox -target ios

14
adapter/connections.go Normal file
View File

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

28
adapter/endpoint.go Normal file
View File

@@ -0,0 +1,28 @@
package adapter
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type Endpoint interface {
Lifecycle
Type() string
Tag() string
Outbound
}
type EndpointRegistry interface {
option.EndpointOptionsRegistry
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) (Endpoint, error)
}
type EndpointManager interface {
Lifecycle
Endpoints() []Endpoint
Get(tag string) (Endpoint, bool)
Remove(tag string) error
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) error
}

View File

@@ -0,0 +1,43 @@
package endpoint
import "github.com/sagernet/sing-box/option"
type Adapter struct {
endpointType string
endpointTag string
network []string
dependencies []string
}
func NewAdapter(endpointType string, endpointTag string, network []string, dependencies []string) Adapter {
return Adapter{
endpointType: endpointType,
endpointTag: endpointTag,
network: network,
dependencies: dependencies,
}
}
func NewAdapterWithDialerOptions(endpointType string, endpointTag string, network []string, dialOptions option.DialerOptions) Adapter {
var dependencies []string
if dialOptions.Detour != "" {
dependencies = []string{dialOptions.Detour}
}
return NewAdapter(endpointType, endpointTag, network, dependencies)
}
func (a *Adapter) Type() string {
return a.endpointType
}
func (a *Adapter) Tag() string {
return a.endpointTag
}
func (a *Adapter) Network() []string {
return a.network
}
func (a *Adapter) Dependencies() []string {
return a.dependencies
}

147
adapter/endpoint/manager.go Normal file
View File

@@ -0,0 +1,147 @@
package endpoint
import (
"context"
"os"
"sync"
"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/common"
E "github.com/sagernet/sing/common/exceptions"
)
var _ adapter.EndpointManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.EndpointRegistry
access sync.Mutex
started bool
stage adapter.StartStage
endpoints []adapter.Endpoint
endpointByTag map[string]adapter.Endpoint
}
func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
endpointByTag: make(map[string]adapter.Endpoint),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
defer m.access.Unlock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
if stage == adapter.StartStateStart {
// started with outbound manager
return nil
}
for _, endpoint := range m.endpoints {
err := adapter.LegacyStart(endpoint, stage)
if err != nil {
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
endpoints := m.endpoints
m.endpoints = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, endpoint := range endpoints {
monitor.Start("close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
err = E.Append(err, endpoint.Close(), func(err error) error {
return E.Cause(err, "close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
})
monitor.Finish()
}
return nil
}
func (m *Manager) Endpoints() []adapter.Endpoint {
m.access.Lock()
defer m.access.Unlock()
return m.endpoints
}
func (m *Manager) Get(tag string) (adapter.Endpoint, bool) {
m.access.Lock()
defer m.access.Unlock()
endpoint, found := m.endpointByTag[tag]
return endpoint, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
endpoint, found := m.endpointByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.endpointByTag, tag)
index := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
return it == endpoint
})
if index == -1 {
panic("invalid endpoint index")
}
m.endpoints = append(m.endpoints[:index], m.endpoints[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return endpoint.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
endpoint, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(endpoint, stage)
if err != nil {
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
}
}
}
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {
if m.started {
err = existsEndpoint.Close()
if err != nil {
return E.Cause(err, "close endpoint/", existsEndpoint.Type(), "[", existsEndpoint.Tag(), "]")
}
}
existsIndex := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
return it == existsEndpoint
})
if existsIndex == -1 {
panic("invalid endpoint index")
}
m.endpoints = append(m.endpoints[:existsIndex], m.endpoints[existsIndex+1:]...)
}
m.endpoints = append(m.endpoints, endpoint)
m.endpointByTag[tag] = endpoint
return nil
}

View File

@@ -0,0 +1,72 @@
package endpoint
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Endpoint, error)
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any {
return new(Options)
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Endpoint, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.EndpointRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Endpoint, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructor map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructor: make(map[string]constructorFunc),
}
}
func (m *Registry) CreateOptions(outboundType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
optionsConstructor, loaded := m.optionsType[outboundType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Endpoint, error) {
m.access.Lock()
defer m.access.Unlock()
constructor, loaded := m.constructor[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
}
return constructor(ctx, router, logger, tag, options)
}
func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
m.access.Lock()
defer m.access.Unlock()
m.optionsType[outboundType] = optionsConstructor
m.constructor[outboundType] = constructor
}

View File

@@ -46,6 +46,9 @@ type PacketConnectionHandlerEx interface {
NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
} }
// Deprecated: use TCPConnectionHandlerEx instead
//
//nolint:staticcheck
type UpstreamHandlerAdapter interface { type UpstreamHandlerAdapter interface {
N.TCPConnectionHandler N.TCPConnectionHandler
N.UDPConnectionHandler N.UDPConnectionHandler

View File

@@ -13,7 +13,7 @@ import (
) )
type Inbound interface { type Inbound interface {
Service Lifecycle
Type() string Type() string
Tag() string Tag() string
} }
@@ -65,14 +65,17 @@ type InboundContext struct {
LastInbound string LastInbound string
OriginDestination M.Socksaddr OriginDestination M.Socksaddr
RouteOriginalDestination M.Socksaddr RouteOriginalDestination M.Socksaddr
// Deprecated // Deprecated: to be removed
//nolint:staticcheck
InboundOptions option.InboundOptions InboundOptions option.InboundOptions
UDPDisableDomainUnmapping bool UDPDisableDomainUnmapping bool
UDPConnect bool UDPConnect bool
NetworkStrategy C.NetworkStrategy UDPTimeout time.Duration
NetworkType []C.InterfaceType
FallbackNetworkType []C.InterfaceType NetworkStrategy C.NetworkStrategy
FallbackDelay time.Duration NetworkType []C.InterfaceType
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
DNSServer string DNSServer string

View File

@@ -18,6 +18,7 @@ var _ adapter.InboundManager = (*Manager)(nil)
type Manager struct { type Manager struct {
logger log.ContextLogger logger log.ContextLogger
registry adapter.InboundRegistry registry adapter.InboundRegistry
endpoint adapter.EndpointManager
access sync.Mutex access sync.Mutex
started bool started bool
stage adapter.StartStage stage adapter.StartStage
@@ -25,10 +26,11 @@ type Manager struct {
inboundByTag map[string]adapter.Inbound inboundByTag map[string]adapter.Inbound
} }
func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry) *Manager { func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
return &Manager{ return &Manager{
logger: logger, logger: logger,
registry: registry, registry: registry,
endpoint: endpoint,
inboundByTag: make(map[string]adapter.Inbound), inboundByTag: make(map[string]adapter.Inbound),
} }
} }
@@ -79,9 +81,12 @@ func (m *Manager) Inbounds() []adapter.Inbound {
func (m *Manager) Get(tag string) (adapter.Inbound, bool) { func (m *Manager) Get(tag string) (adapter.Inbound, bool) {
m.access.Lock() m.access.Lock()
defer m.access.Unlock()
inbound, found := m.inboundByTag[tag] inbound, found := m.inboundByTag[tag]
return inbound, found m.access.Unlock()
if found {
return inbound, true
}
return m.endpoint.Get(tag)
} }
func (m *Manager) Remove(tag string) error { func (m *Manager) Remove(tag string) error {

View File

@@ -15,8 +15,12 @@ type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, log
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any { registry.register(outboundType, func() any {
return new(Options) return new(Options)
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error) { }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Inbound, error) {
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options))) var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
}) })
} }

View File

@@ -1,6 +1,9 @@
package adapter package adapter
func LegacyStart(starter any, stage StartStage) error { func LegacyStart(starter any, stage StartStage) error {
if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle {
return lifecycle.Start(stage)
}
switch stage { switch stage {
case StartStateInitialize: case StartStateInitialize:
if preStarter, isPreStarter := starter.(interface { if preStarter, isPreStarter := starter.(interface {

View File

@@ -5,35 +5,35 @@ import (
) )
type Adapter struct { type Adapter struct {
protocol string outboundType string
outboundTag string
network []string network []string
tag string
dependencies []string dependencies []string
} }
func NewAdapter(protocol string, network []string, tag string, dependencies []string) Adapter { func NewAdapter(outboundType string, outboundTag string, network []string, dependencies []string) Adapter {
return Adapter{ return Adapter{
protocol: protocol, outboundType: outboundType,
outboundTag: outboundTag,
network: network, network: network,
tag: tag,
dependencies: dependencies, dependencies: dependencies,
} }
} }
func NewAdapterWithDialerOptions(protocol string, network []string, tag string, dialOptions option.DialerOptions) Adapter { func NewAdapterWithDialerOptions(outboundType string, outboundTag string, network []string, dialOptions option.DialerOptions) Adapter {
var dependencies []string var dependencies []string
if dialOptions.Detour != "" { if dialOptions.Detour != "" {
dependencies = []string{dialOptions.Detour} dependencies = []string{dialOptions.Detour}
} }
return NewAdapter(protocol, network, tag, dependencies) return NewAdapter(outboundType, outboundTag, network, dependencies)
} }
func (a *Adapter) Type() string { func (a *Adapter) Type() string {
return a.protocol return a.outboundType
} }
func (a *Adapter) Tag() string { func (a *Adapter) Tag() string {
return a.tag return a.outboundTag
} }
func (a *Adapter) Network() []string { func (a *Adapter) Network() []string {

View File

@@ -1,157 +0,0 @@
package outbound
import (
"context"
"net"
"net/netip"
"os"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/canceler"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext) error {
defer conn.Close()
ctx = adapter.WithContext(ctx, &metadata)
var outConn net.Conn
var err error
if len(metadata.DestinationAddresses) > 0 {
outConn, err = dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
} else {
outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
}
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
err = N.ReportConnHandshakeSuccess(conn, outConn)
if err != nil {
outConn.Close()
return err
}
return CopyEarlyConn(ctx, conn, outConn)
}
func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext) error {
defer conn.Close()
ctx = adapter.WithContext(ctx, &metadata)
var (
outPacketConn net.PacketConn
outConn net.Conn
destinationAddress netip.Addr
err error
)
if metadata.UDPConnect {
if len(metadata.DestinationAddresses) > 0 {
if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer {
outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
} else {
outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses)
}
} else {
outConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination)
}
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
outPacketConn = bufio.NewUnbindPacketConn(outConn)
connRemoteAddr := M.AddrFromNet(outConn.RemoteAddr())
if connRemoteAddr != metadata.Destination.Addr {
destinationAddress = connRemoteAddr
}
} else {
if len(metadata.DestinationAddresses) > 0 {
outPacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, this, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
} else {
outPacketConn, err = this.ListenPacket(ctx, metadata.Destination)
}
if err != nil {
return N.ReportHandshakeFailure(conn, err)
}
}
err = N.ReportPacketConnHandshakeSuccess(conn, outPacketConn)
if err != nil {
outPacketConn.Close()
return err
}
if destinationAddress.IsValid() {
var originDestination M.Socksaddr
if metadata.RouteOriginalDestination.IsValid() {
originDestination = metadata.RouteOriginalDestination
} else {
originDestination = metadata.Destination
}
if metadata.Destination != M.SocksaddrFrom(destinationAddress, metadata.Destination.Port) {
if metadata.UDPDisableDomainUnmapping {
outPacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), originDestination)
} else {
outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), originDestination)
}
}
if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded {
natConn.UpdateDestination(destinationAddress)
}
}
switch metadata.Protocol {
case C.ProtocolSTUN:
ctx, conn = canceler.NewPacketConn(ctx, conn, C.STUNTimeout)
case C.ProtocolQUIC:
ctx, conn = canceler.NewPacketConn(ctx, conn, C.QUICTimeout)
case C.ProtocolDNS:
ctx, conn = canceler.NewPacketConn(ctx, conn, C.DNSTimeout)
}
return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outPacketConn))
}
func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) error {
if cachedReader, isCached := conn.(N.CachedReader); isCached {
payload := cachedReader.ReadCached()
if payload != nil && !payload.IsEmpty() {
_, err := serverConn.Write(payload.Bytes())
payload.Release()
if err != nil {
serverConn.Close()
return err
}
return bufio.CopyConn(ctx, conn, serverConn)
}
}
if earlyConn, isEarlyConn := common.Cast[N.EarlyConn](serverConn); isEarlyConn && earlyConn.NeedHandshake() {
payload := buf.NewPacket()
err := conn.SetReadDeadline(time.Now().Add(C.ReadPayloadTimeout))
if err != os.ErrInvalid {
if err != nil {
payload.Release()
serverConn.Close()
return err
}
_, err = payload.ReadOnceFrom(conn)
if err != nil && !E.IsTimeout(err) {
payload.Release()
serverConn.Close()
return E.Cause(err, "read payload")
}
err = conn.SetReadDeadline(time.Time{})
if err != nil {
payload.Release()
serverConn.Close()
return err
}
}
_, err = serverConn.Write(payload.Bytes())
payload.Release()
if err != nil {
serverConn.Close()
return N.ReportHandshakeFailure(conn, err)
}
}
return bufio.CopyConn(ctx, conn, serverConn)
}

View File

@@ -21,6 +21,7 @@ var _ adapter.OutboundManager = (*Manager)(nil)
type Manager struct { type Manager struct {
logger log.ContextLogger logger log.ContextLogger
registry adapter.OutboundRegistry registry adapter.OutboundRegistry
endpoint adapter.EndpointManager
defaultTag string defaultTag string
access sync.Mutex access sync.Mutex
started bool started bool
@@ -32,10 +33,11 @@ type Manager struct {
defaultOutboundFallback adapter.Outbound defaultOutboundFallback adapter.Outbound
} }
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, defaultTag string) *Manager { func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
return &Manager{ return &Manager{
logger: logger, logger: logger,
registry: registry, registry: registry,
endpoint: endpoint,
defaultTag: defaultTag, defaultTag: defaultTag,
outboundByTag: make(map[string]adapter.Outbound), outboundByTag: make(map[string]adapter.Outbound),
dependByTag: make(map[string][]string), dependByTag: make(map[string][]string),
@@ -56,7 +58,14 @@ func (m *Manager) Start(stage adapter.StartStage) error {
outbounds := m.outbounds outbounds := m.outbounds
m.access.Unlock() m.access.Unlock()
if stage == adapter.StartStateStart { if stage == adapter.StartStateStart {
return m.startOutbounds(outbounds) if m.defaultTag != "" && m.defaultOutbound == nil {
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
if !loaded {
return E.New("default outbound not found: ", m.defaultTag)
}
m.defaultOutbound = defaultEndpoint
}
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
} else { } else {
for _, outbound := range outbounds { for _, outbound := range outbounds {
err := adapter.LegacyStart(outbound, stage) err := adapter.LegacyStart(outbound, stage)
@@ -87,7 +96,14 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
} }
started[outboundTag] = true started[outboundTag] = true
canContinue = true canContinue = true
if starter, isStarter := outboundToStart.(interface { if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
err := starter.Start(adapter.StartStateStart)
monitor.Finish()
if err != nil {
return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
}
} else if starter, isStarter := outboundToStart.(interface {
Start() error Start() error
}); isStarter { }); isStarter {
monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]") monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]")
@@ -160,9 +176,12 @@ func (m *Manager) Outbounds() []adapter.Outbound {
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) { func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
m.access.Lock() m.access.Lock()
defer m.access.Unlock()
outbound, found := m.outboundByTag[tag] outbound, found := m.outboundByTag[tag]
return outbound, found m.access.Unlock()
if found {
return outbound, true
}
return m.endpoint.Get(tag)
} }
func (m *Manager) Default() adapter.Outbound { func (m *Manager) Default() adapter.Outbound {

View File

@@ -15,8 +15,12 @@ type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, log
func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
registry.register(outboundType, func() any { registry.register(outboundType, func() any {
return new(Options) return new(Options)
}, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error) { }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Outbound, error) {
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options))) var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options))
}) })
} }

View File

@@ -18,6 +18,8 @@ type (
) )
// Deprecated // Deprecated
//
//nolint:staticcheck
func NewUpstreamHandler( func NewUpstreamHandler(
metadata InboundContext, metadata InboundContext,
connectionHandler ConnectionHandlerFunc, connectionHandler ConnectionHandlerFunc,
@@ -34,7 +36,9 @@ func NewUpstreamHandler(
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil) var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
// Deprecated // Deprecated: use myUpstreamHandlerWrapperEx instead.
//
//nolint:staticcheck
type myUpstreamHandlerWrapper struct { type myUpstreamHandlerWrapper struct {
metadata InboundContext metadata InboundContext
connectionHandler ConnectionHandlerFunc connectionHandler ConnectionHandlerFunc
@@ -42,6 +46,7 @@ type myUpstreamHandlerWrapper struct {
errorHandler E.Handler errorHandler E.Handler
} }
// Deprecated: use myUpstreamHandlerWrapperEx instead.
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata myMetadata := w.metadata
if metadata.Source.IsValid() { if metadata.Source.IsValid() {
@@ -53,6 +58,7 @@ func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.C
return w.connectionHandler(ctx, conn, myMetadata) return w.connectionHandler(ctx, conn, myMetadata)
} }
// Deprecated: use myUpstreamHandlerWrapperEx instead.
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata myMetadata := w.metadata
if metadata.Source.IsValid() { if metadata.Source.IsValid() {
@@ -64,11 +70,12 @@ func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn
return w.packetHandler(ctx, conn, myMetadata) return w.packetHandler(ctx, conn, myMetadata)
} }
// Deprecated: use myUpstreamHandlerWrapperEx instead.
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) { func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
w.errorHandler.NewError(ctx, err) w.errorHandler.NewError(ctx, err)
} }
// Deprecated // Deprecated: removed
func UpstreamMetadata(metadata InboundContext) M.Metadata { func UpstreamMetadata(metadata InboundContext) M.Metadata {
return M.Metadata{ return M.Metadata{
Source: metadata.Source, Source: metadata.Source,
@@ -76,14 +83,14 @@ func UpstreamMetadata(metadata InboundContext) M.Metadata {
} }
} }
// Deprecated // Deprecated: Use NewUpstreamContextHandlerEx instead.
type myUpstreamContextHandlerWrapper struct { type myUpstreamContextHandlerWrapper struct {
connectionHandler ConnectionHandlerFunc connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler errorHandler E.Handler
} }
// Deprecated // Deprecated: Use NewUpstreamContextHandlerEx instead.
func NewUpstreamContextHandler( func NewUpstreamContextHandler(
connectionHandler ConnectionHandlerFunc, connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc, packetHandler PacketConnectionHandlerFunc,
@@ -96,6 +103,7 @@ func NewUpstreamContextHandler(
} }
} }
// Deprecated: Use NewUpstreamContextHandlerEx instead.
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx) myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() { if metadata.Source.IsValid() {
@@ -107,6 +115,7 @@ func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, con
return w.connectionHandler(ctx, conn, *myMetadata) return w.connectionHandler(ctx, conn, *myMetadata)
} }
// Deprecated: Use NewUpstreamContextHandlerEx instead.
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx) myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() { if metadata.Source.IsValid() {
@@ -118,6 +127,7 @@ func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Contex
return w.packetHandler(ctx, conn, *myMetadata) return w.packetHandler(ctx, conn, *myMetadata)
} }
// Deprecated: Use NewUpstreamContextHandlerEx instead.
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) { func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.errorHandler.NewError(ctx, err) w.errorHandler.NewError(ctx, err)
} }
@@ -149,12 +159,15 @@ func NewRouteContextHandler(
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil) var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
// Deprecated: Use ConnectionRouterEx instead. // Deprecated: Use ConnectionRouterEx instead.
//
//nolint:staticcheck
type routeHandlerWrapper struct { type routeHandlerWrapper struct {
metadata InboundContext metadata InboundContext
router ConnectionRouter router ConnectionRouter
logger logger.ContextLogger logger logger.ContextLogger
} }
// Deprecated: Use ConnectionRouterEx instead.
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata myMetadata := w.metadata
if metadata.Source.IsValid() { if metadata.Source.IsValid() {
@@ -166,6 +179,7 @@ func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn,
return w.router.RouteConnection(ctx, conn, myMetadata) return w.router.RouteConnection(ctx, conn, myMetadata)
} }
// Deprecated: Use ConnectionRouterEx instead.
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata myMetadata := w.metadata
if metadata.Source.IsValid() { if metadata.Source.IsValid() {
@@ -177,6 +191,7 @@ func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.Pa
return w.router.RoutePacketConnection(ctx, conn, myMetadata) return w.router.RoutePacketConnection(ctx, conn, myMetadata)
} }
// Deprecated: Use ConnectionRouterEx instead.
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) { func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err) w.logger.ErrorContext(ctx, err)
} }
@@ -189,6 +204,7 @@ type routeContextHandlerWrapper struct {
logger logger.ContextLogger logger logger.ContextLogger
} }
// Deprecated: Use ConnectionRouterEx instead.
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx) myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() { if metadata.Source.IsValid() {
@@ -200,6 +216,7 @@ func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net
return w.router.RouteConnection(ctx, conn, *myMetadata) return w.router.RouteConnection(ctx, conn, *myMetadata)
} }
// Deprecated: Use ConnectionRouterEx instead.
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx) myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() { if metadata.Source.IsValid() {
@@ -211,6 +228,7 @@ func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, co
return w.router.RoutePacketConnection(ctx, conn, *myMetadata) return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
} }
// Deprecated: Use ConnectionRouterEx instead.
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) { func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err) w.logger.ErrorContext(ctx, err)
} }

77
box.go
View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
@@ -36,9 +37,11 @@ type Box struct {
logFactory log.Factory logFactory log.Factory
logger log.ContextLogger logger log.ContextLogger
network *route.NetworkManager network *route.NetworkManager
router *route.Router endpoint *endpoint.Manager
inbound *inbound.Manager inbound *inbound.Manager
outbound *outbound.Manager outbound *outbound.Manager
connection *route.ConnectionManager
router *route.Router
services []adapter.LifecycleService services []adapter.LifecycleService
done chan struct{} done chan struct{}
} }
@@ -53,6 +56,7 @@ func Context(
ctx context.Context, ctx context.Context,
inboundRegistry adapter.InboundRegistry, inboundRegistry adapter.InboundRegistry,
outboundRegistry adapter.OutboundRegistry, outboundRegistry adapter.OutboundRegistry,
endpointRegistry adapter.EndpointRegistry,
) context.Context { ) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil { service.FromContext[adapter.InboundRegistry](ctx) == nil {
@@ -64,6 +68,11 @@ func Context(
ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry) ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry)
ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry) ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
} }
if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.EndpointRegistry](ctx) == nil {
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
}
return ctx return ctx
} }
@@ -75,12 +84,16 @@ func New(options Options) (*Box, error) {
} }
ctx = service.ContextWithDefaultRegistry(ctx) ctx = service.ContextWithDefaultRegistry(ctx)
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
if endpointRegistry == nil {
return nil, E.New("missing endpoint registry in context")
}
if inboundRegistry == nil { if inboundRegistry == nil {
return nil, E.New("missing inbound registry in context") return nil, E.New("missing inbound registry in context")
} }
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
if outboundRegistry == nil { if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context") return nil, E.New("missing outbound registry in context")
} }
@@ -118,8 +131,10 @@ func New(options Options) (*Box, error) {
} }
routeOptions := common.PtrValueOrDefault(options.Route) routeOptions := common.PtrValueOrDefault(options.Route)
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry) endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, routeOptions.Final) inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager) service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager) service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
@@ -128,28 +143,28 @@ 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"))
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS)) router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS))
if err != nil { if err != nil {
return nil, E.Cause(err, "initialize router") return nil, E.Cause(err, "initialize router")
} }
//nolint:staticcheck for i, endpointOptions := range options.Endpoints {
if len(options.LegacyInbounds) > 0 { var tag string
for _, legacyInbound := range options.LegacyInbounds { if endpointOptions.Tag != "" {
options.Inbounds = append(options.Inbounds, option.Inbound{ tag = endpointOptions.Tag
Type: legacyInbound.Type, } else {
Tag: legacyInbound.Tag, tag = F.ToString(i)
Options: common.Must1(legacyInbound.RawOptions()),
})
} }
} err = endpointManager.Create(ctx,
//nolint:staticcheck router,
if len(options.LegacyOutbounds) > 0 { logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
for _, legacyOutbound := range options.LegacyOutbounds { tag,
options.Outbounds = append(options.Outbounds, option.Outbound{ endpointOptions.Type,
Type: legacyOutbound.Type, endpointOptions.Options,
Tag: legacyOutbound.Tag, )
Options: common.Must1(legacyOutbound.RawOptions()), if err != nil {
}) return nil, E.Cause(err, "initialize inbound[", i, "]")
} }
} }
for i, inboundOptions := range options.Inbounds { for i, inboundOptions := range options.Inbounds {
@@ -258,9 +273,11 @@ func New(options Options) (*Box, error) {
} }
return &Box{ return &Box{
network: networkManager, network: networkManager,
router: router, endpoint: endpointManager,
inbound: inboundManager, inbound: inboundManager,
outbound: outboundManager, outbound: outboundManager,
connection: connectionManager,
router: router,
createdAt: createdAt, createdAt: createdAt,
logFactory: logFactory, logFactory: logFactory,
logger: logFactory.Logger(), logger: logFactory.Logger(),
@@ -319,11 +336,11 @@ func (s *Box) preStart() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateInitialize, s.network, s.router, s.outbound, s.inbound) err = adapter.Start(adapter.StartStateInitialize, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.router) err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.connection, s.router)
if err != nil { if err != nil {
return err return err
} }
@@ -343,7 +360,11 @@ 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.router, s.inbound) err = adapter.Start(adapter.StartStateStart, s.endpoint)
if err != nil {
return err
}
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.connection, s.router, s.inbound, s.endpoint)
if err != nil { if err != nil {
return err return err
} }
@@ -351,7 +372,7 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStarted, s.network, s.router, s.outbound, s.inbound) err = adapter.Start(adapter.StartStateStarted, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
if err != nil { if err != nil {
return err return err
} }
@@ -370,7 +391,7 @@ func (s *Box) Close() error {
close(s.done) close(s.done)
} }
err := common.Close( err := common.Close(
s.inbound, s.outbound, s.router, s.network, s.inbound, s.outbound, s.router, s.connection, s.network,
) )
for _, lifecycleService := range s.services { for _, lifecycleService := range s.services {
err = E.Append(err, lifecycleService.Close(), func(err error) error { err = E.Append(err, lifecycleService.Close(), func(err error) error {

View File

@@ -10,7 +10,9 @@ import (
_ "github.com/sagernet/gomobile" _ "github.com/sagernet/gomobile"
"github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/shell"
) )
var ( var (
@@ -62,9 +64,33 @@ func init() {
func buildAndroid() { func buildAndroid() {
build_shared.FindSDK() build_shared.FindSDK()
var javaPath string
javaHome := os.Getenv("JAVA_HOME")
if javaHome == "" {
javaPath = "java"
} else {
javaPath = filepath.Join(javaHome, "bin", "java")
}
javaVersion, err := shell.Exec(javaPath, "--version").ReadOutput()
if err != nil {
log.Fatal(E.Cause(err, "check java version"))
}
if !strings.Contains(javaVersion, "openjdk 17") {
log.Fatal("java version should be openjdk 17")
}
var bindTarget string
if debugEnabled {
bindTarget = "android/arm64"
} else {
bindTarget = "android"
}
args := []string{ args := []string{
"bind", "bind",
"-v", "-v",
"-target", bindTarget,
"-androidapi", "21", "-androidapi", "21",
"-javapkg=io.nekohasekai", "-javapkg=io.nekohasekai",
"-libname=box", "-libname=box",
@@ -86,7 +112,7 @@ func buildAndroid() {
command := exec.Command(build_shared.GoBinPath+"/gomobile", args...) command := exec.Command(build_shared.GoBinPath+"/gomobile", args...)
command.Stdout = os.Stdout command.Stdout = os.Stdout
command.Stderr = os.Stderr command.Stderr = os.Stderr
err := command.Run() err = command.Run()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -104,10 +130,17 @@ func buildAndroid() {
} }
func buildiOS() { func buildiOS() {
var bindTarget string
if debugEnabled {
bindTarget = "ios"
} else {
bindTarget = "ios,iossimulator,tvos,tvossimulator,macos"
}
args := []string{ args := []string{
"bind", "bind",
"-v", "-v",
"-target", "ios,iossimulator,tvos,tvossimulator,macos", "-target", bindTarget,
"-libname=box", "-libname=box",
} }
if !debugEnabled { if !debugEnabled {

View File

@@ -11,9 +11,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/shell"
) )
var ( var (
@@ -42,14 +40,6 @@ func FindSDK() {
log.Fatal("android NDK not found") log.Fatal("android NDK not found")
} }
javaVersion, err := shell.Exec("java", "--version").ReadOutput()
if err != nil {
log.Fatal(E.Cause(err, "check java version"))
}
if !strings.Contains(javaVersion, "openjdk 17") {
log.Fatal("java version should be openjdk 17")
}
os.Setenv("ANDROID_HOME", androidSDKPath) os.Setenv("ANDROID_HOME", androidSDKPath)
os.Setenv("ANDROID_SDK_HOME", androidSDKPath) os.Setenv("ANDROID_SDK_HOME", androidSDKPath)
os.Setenv("ANDROID_NDK_HOME", androidNDKPath) os.Setenv("ANDROID_NDK_HOME", androidNDKPath)
@@ -58,12 +48,16 @@ func FindSDK() {
} }
func findNDK() bool { func findNDK() bool {
const fixedVersion = "26.2.11394342" const fixedVersion = "28.0.12674087"
const versionFile = "source.properties" const versionFile = "source.properties"
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) { if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
androidNDKPath = fixedPath androidNDKPath = fixedPath
return true return true
} }
if ndkHomeEnv := os.Getenv("ANDROID_NDK_HOME"); rw.IsFile(filepath.Join(ndkHomeEnv, versionFile)) {
androidNDKPath = ndkHomeEnv
return true
}
ndkVersions, err := os.ReadDir(filepath.Join(androidSDKPath, "ndk")) ndkVersions, err := os.ReadDir(filepath.Join(androidSDKPath, "ndk"))
if err != nil { if err != nil {
return false return false

View File

@@ -20,6 +20,11 @@ func ReadTag() (string, error) {
return version.String() + "-" + shortCommit, nil return version.String() + "-" + shortCommit, nil
} }
func ReadTagVersionRev() (badversion.Version, error) {
currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput())
return badversion.Parse(currentTagRev[1:]), nil
}
func ReadTagVersion() (badversion.Version, error) { func ReadTagVersion() (badversion.Version, error) {
currentTag := common.Must1(shell.Exec("git", "describe", "--tags").ReadOutput()) currentTag := common.Must1(shell.Exec("git", "describe", "--tags").ReadOutput())
currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput()) currentTagRev := common.Must1(shell.Exec("git", "describe", "--tags", "--abbrev=0").ReadOutput())
@@ -31,3 +36,11 @@ func ReadTagVersion() (badversion.Version, error) {
} }
return version, nil return version, nil
} }
func IsDevBranch() bool {
branch, err := shell.Exec("git", "branch", "--show-current").ReadOutput()
if err != nil {
return false
}
return branch == "dev-next"
}

View File

@@ -1,21 +1,74 @@
package main package main
import ( import (
"flag"
"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/log" "github.com/sagernet/sing-box/log"
F "github.com/sagernet/sing/common/format"
) )
var nightly bool
func init() {
flag.BoolVar(&nightly, "nightly", false, "Print nightly tag")
}
func main() { func main() {
currentTag, err := build_shared.ReadTag() flag.Parse()
if err != nil { if nightly {
log.Error(err) version, err := build_shared.ReadTagVersionRev()
_, err = os.Stdout.WriteString("unknown\n") if err != nil {
log.Fatal(err)
}
var (
versionStr string
isPrerelease bool
)
if version.PreReleaseIdentifier != "" {
isPrerelease = true
versionStr = version.VersionString() + "-nightly"
} else {
version.Patch++
versionStr = version.VersionString() + "-nightly"
}
if build_shared.IsDevBranch() {
isPrerelease = true
}
err = setGitHubOutput("version", versionStr)
if err != nil {
log.Fatal(err)
}
err = setGitHubOutput("prerelease", F.ToString(isPrerelease))
if err != nil {
log.Fatal(err)
}
} else { } else {
_, err = os.Stdout.WriteString(currentTag + "\n") tag, err := build_shared.ReadTag()
} if err != nil {
if err != nil { log.Error(err)
log.Error(err) os.Stdout.WriteString("unknown\n")
} else {
os.Stdout.WriteString(tag + "\n")
}
} }
} }
func setGitHubOutput(name string, value string) error {
outputFile, err := os.OpenFile(os.Getenv("GITHUB_ENV"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return err
}
_, err = outputFile.WriteString(name + "=" + value + "\n")
if err != nil {
outputFile.Close()
return err
}
err = outputFile.Close()
if err != nil {
return err
}
os.Stderr.WriteString(name + "=" + value + "\n")
return nil
}

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"flag"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@@ -12,9 +13,22 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
) )
var flagRunInCI bool
func init() {
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
}
func main() { func main() {
newVersion := common.Must1(build_shared.ReadTagVersion()) flag.Parse()
androidPath, err := filepath.Abs("../sing-box-for-android") newVersion := common.Must1(build_shared.ReadTag())
var androidPath string
if flagRunInCI {
androidPath = "clients/android"
} else {
androidPath = "../sing-box-for-android"
}
androidPath, err := filepath.Abs(androidPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -31,10 +45,10 @@ func main() {
for _, propPair := range propsList { for _, propPair := range propsList {
switch propPair[0] { switch propPair[0] {
case "VERSION_NAME": case "VERSION_NAME":
if propPair[1] != newVersion.String() { if propPair[1] != newVersion {
versionUpdated = true versionUpdated = true
propPair[1] = newVersion.String() propPair[1] = newVersion
log.Info("updated version to ", newVersion.String()) log.Info("updated version to ", newVersion)
} }
case "GO_VERSION": case "GO_VERSION":
if propPair[1] != runtime.Version() { if propPair[1] != runtime.Version() {

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"flag"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -13,9 +14,22 @@ import (
"howett.net/plist" "howett.net/plist"
) )
var flagRunInCI bool
func init() {
flag.BoolVar(&flagRunInCI, "ci", false, "Run in CI")
}
func main() { func main() {
flag.Parse()
newVersion := common.Must1(build_shared.ReadTagVersion()) newVersion := common.Must1(build_shared.ReadTagVersion())
applePath, err := filepath.Abs("../sing-box-for-apple") var applePath string
if flagRunInCI {
applePath = "clients/apple"
} else {
applePath = "../sing-box-for-apple"
}
applePath, err := filepath.Abs(applePath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) {
configPaths = append(configPaths, "config.json") configPaths = append(configPaths, "config.json")
} }
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())) globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry()) globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry())
} }

View File

@@ -30,7 +30,7 @@ func createPreStartedClient() (*box.Box, error) {
return nil, err return nil, err
} }
} }
instance, err := box.New(box.Options{Options: options}) instance, err := box.New(box.Options{Context: globalCtx, Options: options})
if err != nil { if err != nil {
return nil, E.Cause(err, "create service") return nil, E.Cause(err, "create service")
} }

View File

@@ -88,25 +88,31 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti
} }
if networkManager != nil && options.BindInterface == "" && options.Inet4BindAddress == nil && options.Inet6BindAddress == nil { if networkManager != nil && options.BindInterface == "" && options.Inet4BindAddress == nil && options.Inet6BindAddress == nil {
defaultOptions := networkManager.DefaultOptions() defaultOptions := networkManager.DefaultOptions()
if defaultOptions.BindInterface != "" { if options.BindInterface == "" {
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1) if defaultOptions.BindInterface != "" {
dialer.Control = control.Append(dialer.Control, bindFunc) bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
listener.Control = control.Append(listener.Control, bindFunc)
} else if networkManager.AutoDetectInterface() {
if defaultOptions.NetworkStrategy != C.NetworkStrategyDefault && C.NetworkStrategy(options.NetworkStrategy) == C.NetworkStrategyDefault {
networkStrategy = defaultOptions.NetworkStrategy
networkType = defaultOptions.NetworkType
fallbackNetworkType = defaultOptions.FallbackNetworkType
networkFallbackDelay = defaultOptions.FallbackDelay
bindFunc := networkManager.ProtectFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else {
bindFunc := networkManager.AutoDetectInterfaceFunc()
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} else if networkManager.AutoDetectInterface() {
if defaultOptions.NetworkStrategy != C.NetworkStrategyDefault && C.NetworkStrategy(options.NetworkStrategy) == C.NetworkStrategyDefault {
networkStrategy = defaultOptions.NetworkStrategy
networkType = defaultOptions.NetworkType
fallbackNetworkType = defaultOptions.FallbackNetworkType
networkFallbackDelay = defaultOptions.FallbackDelay
bindFunc := networkManager.ProtectFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else {
bindFunc := networkManager.AutoDetectInterfaceFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
}
} }
} }
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
dialer.Control = control.Append(dialer.Control, control.RoutingMark(defaultOptions.RoutingMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(defaultOptions.RoutingMark))
}
} }
if options.ReuseAddr { if options.ReuseAddr {
listener.Control = control.Append(listener.Control, control.ReuseAddr()) listener.Control = control.Append(listener.Control, control.ReuseAddr())
@@ -279,7 +285,7 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
} }
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) { func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
return trackPacketConn(d.listenSerialInterfacePacket(context.Background(), d.udpListener, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay)) return d.udpListener.ListenPacket(context.Background(), network, address)
} }
func trackConn(conn net.Conn, err error) (net.Conn, error) { func trackConn(conn net.Conn, err error) (net.Conn, error) {

View File

@@ -149,9 +149,6 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { if len(primaryInterfaces)+len(fallbackInterfaces) == 0 {
return nil, E.New("no available network interface") return nil, E.New("no available network interface")
} }
if fallbackDelay == 0 {
fallbackDelay = N.DefaultFallbackDelay
}
var errors []error var errors []error
for _, primaryInterface := range primaryInterfaces { for _, primaryInterface := range primaryInterfaces {
perNetListener := listener perNetListener := listener

View File

@@ -4,7 +4,6 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"time"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
@@ -124,7 +123,7 @@ func (l *Listener) loopUDPOut() {
case packet := <-l.packetOutbound: case packet := <-l.packetOutbound:
packet.Buffer.Release() packet.Buffer.Release()
N.PutPacketBuffer(packet) N.PutPacketBuffer(packet)
case <-time.After(time.Second): default:
return return
} }
} }

View File

@@ -41,10 +41,10 @@ func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.Conte
NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context { NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context {
return log.ContextWithNewID(ctx) return log.ContextWithNewID(ctx)
}, },
Logger: logger, Logger: logger,
Handler: adapter.NewRouteContextHandler(router, logger), HandlerEx: adapter.NewRouteContextHandlerEx(router),
Padding: options.Padding, Padding: options.Padding,
Brutal: brutalOptions, Brutal: brutalOptions,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -52,6 +52,7 @@ func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.Conte
return &Router{router, service}, nil return &Router{router, service}, nil
} }
// Deprecated: Use RouteConnectionEx instead.
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
if metadata.Destination == mux.Destination { if metadata.Destination == mux.Destination {
// TODO: check if WithContext is necessary // TODO: check if WithContext is necessary
@@ -61,6 +62,7 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
} }
} }
// Deprecated: Use RoutePacketConnectionEx instead.
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
return r.router.RoutePacketConnection(ctx, conn, metadata) return r.router.RoutePacketConnection(ctx, conn, metadata)
} }

View File

@@ -30,14 +30,15 @@ func NewClient(ctx context.Context, serverAddress string, options option.Outboun
return nil, nil return nil, nil
} }
if options.ECH != nil && options.ECH.Enabled { if options.ECH != nil && options.ECH.Enabled {
return NewECHClient(ctx, serverAddress, options) if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
return NewECHClient(ctx, serverAddress, options)
}
} else if options.Reality != nil && options.Reality.Enabled { } else if options.Reality != nil && options.Reality.Enabled {
return NewRealityClient(ctx, serverAddress, options) return NewRealityClient(ctx, serverAddress, options)
} else if options.UTLS != nil && options.UTLS.Enabled { } else if options.UTLS != nil && options.UTLS.Enabled {
return NewUTLSClient(ctx, serverAddress, options) return NewUTLSClient(ctx, serverAddress, options)
} else {
return NewSTDClient(ctx, serverAddress, options)
} }
return NewSTDClient(ctx, serverAddress, options)
} }
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) { func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {

View File

@@ -7,7 +7,6 @@ import (
"encoding/binary" "encoding/binary"
"encoding/pem" "encoding/pem"
cftls "github.com/sagernet/cloudflare-tls"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/cloudflare/circl/hpke" "github.com/cloudflare/circl/hpke"
@@ -59,7 +58,6 @@ func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (config
type echKeyConfigPair struct { type echKeyConfigPair struct {
id uint8 id uint8
key cftls.EXP_ECHKey
rawKey []byte rawKey []byte
conf myECHKeyConfig conf myECHKeyConfig
rawConf []byte rawConf []byte
@@ -153,14 +151,13 @@ func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite [
sk = be.AppendUint16(sk, uint16(len(b))) sk = be.AppendUint16(sk, uint16(len(b)))
sk = append(sk, b...) sk = append(sk, b...)
cfECHKeys, err := cftls.EXP_UnmarshalECHKeys(sk) cfECHKeys, err := UnmarshalECHKeys(sk)
if err != nil { if err != nil {
return nil, E.Cause(err, "bug: can't parse generated ECH server key") return nil, E.Cause(err, "bug: can't parse generated ECH server key")
} }
if len(cfECHKeys) != 1 { if len(cfECHKeys) != 1 {
return nil, E.New("bug: unexpected server key count") return nil, E.New("bug: unexpected server key count")
} }
pair.key = cfECHKeys[0]
pair.rawKey = sk pair.rawKey = sk
pairs = append(pairs, pair) pairs = append(pairs, pair)

View File

@@ -97,6 +97,10 @@ func (c *echServerConfig) startWatcher() error {
if err != nil { if err != nil {
return err return err
} }
err = watcher.Start()
if err != nil {
return err
}
c.watcher = watcher c.watcher = watcher
return nil return nil
} }
@@ -232,7 +236,7 @@ func NewECHServer(ctx context.Context, logger log.Logger, options option.Inbound
var echKey []byte var echKey []byte
if len(options.ECH.Key) > 0 { if len(options.ECH.Key) > 0 {
echKey = []byte(strings.Join(options.ECH.Key, "\n")) echKey = []byte(strings.Join(options.ECH.Key, "\n"))
} else if options.KeyPath != "" { } else if options.ECH.KeyPath != "" {
content, err := os.ReadFile(options.ECH.KeyPath) content, err := os.ReadFile(options.ECH.KeyPath)
if err != nil { if err != nil {
return nil, E.Cause(err, "read ECH key") return nil, E.Cause(err, "read ECH key")

View File

@@ -17,12 +17,13 @@ func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLS
return nil, nil return nil, nil
} }
if options.ECH != nil && options.ECH.Enabled { if options.ECH != nil && options.ECH.Enabled {
return NewECHServer(ctx, logger, options) if options.ECH.PQSignatureSchemesEnabled || options.ECH.DynamicRecordSizingDisabled {
return NewECHServer(ctx, logger, options)
}
} else if options.Reality != nil && options.Reality.Enabled { } else if options.Reality != nil && options.Reality.Enabled {
return NewRealityServer(ctx, logger, options) return NewRealityServer(ctx, logger, options)
} else {
return NewSTDServer(ctx, logger, options)
} }
return NewSTDServer(ctx, logger, options)
} }
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) { func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {

View File

@@ -4,16 +4,25 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/base64"
"net" "net"
"net/netip" "net/netip"
"os" "os"
"strings" "strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-dns"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
aTLS "github.com/sagernet/sing/common/tls"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
) )
var _ ConfigCompat = (*STDClientConfig)(nil)
type STDClientConfig struct { type STDClientConfig struct {
config *tls.Config config *tls.Config
} }
@@ -46,6 +55,63 @@ func (s *STDClientConfig) Clone() Config {
return &STDClientConfig{s.config.Clone()} return &STDClientConfig{s.config.Clone()}
} }
type STDECHClientConfig struct {
STDClientConfig
}
func (s *STDClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
if len(s.config.EncryptedClientHelloConfigList) == 0 {
message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{
{
Name: mDNS.Fqdn(s.config.ServerName),
Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET,
},
},
}
dnsRouter := service.FromContext[adapter.Router](ctx)
response, err := dnsRouter.Exchange(ctx, message)
if err != nil {
return nil, E.Cause(err, "fetch ECH config list")
}
if response.Rcode != mDNS.RcodeSuccess {
return nil, E.Cause(dns.RCodeError(response.Rcode), "fetch ECH config list")
}
for _, rr := range response.Answer {
switch resource := rr.(type) {
case *mDNS.HTTPS:
for _, value := range resource.Value {
if value.Key().String() == "ech" {
echConfigList, err := base64.StdEncoding.DecodeString(value.String())
if err != nil {
return nil, E.Cause(err, "decode ECH config")
}
s.config.EncryptedClientHelloConfigList = echConfigList
}
}
}
}
return nil, E.New("no ECH config found in DNS records")
}
tlsConn, err := s.Client(conn)
if err != nil {
return nil, err
}
err = tlsConn.HandshakeContext(ctx)
if err != nil {
return nil, err
}
return tlsConn, nil
}
func (s *STDECHClientConfig) Clone() Config {
return &STDECHClientConfig{STDClientConfig{s.config.Clone()}}
}
func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) { func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
var serverName string var serverName string
if options.ServerName != "" { if options.ServerName != "" {
@@ -128,5 +194,21 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
} }
tlsConfig.RootCAs = certPool tlsConfig.RootCAs = certPool
} }
if options.ECH != nil && options.ECH.Enabled {
var echConfig []byte
if len(options.ECH.Config) > 0 {
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
} else if options.ECH.ConfigPath != "" {
content, err := os.ReadFile(options.ECH.ConfigPath)
if err != nil {
return nil, E.Cause(err, "read ECH config")
}
echConfig = content
}
if echConfig != nil {
tlsConfig.EncryptedClientHelloConfigList = echConfig
}
return &STDECHClientConfig{STDClientConfig{&tlsConfig}}, nil
}
return &STDClientConfig{&tlsConfig}, nil return &STDClientConfig{&tlsConfig}, nil
} }

View File

@@ -3,6 +3,7 @@ package tls
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/pem"
"net" "net"
"os" "os"
"strings" "strings"
@@ -14,6 +15,8 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"golang.org/x/crypto/cryptobyte"
) )
var errInsecureUnused = E.New("tls: insecure unused") var errInsecureUnused = E.New("tls: insecure unused")
@@ -106,6 +109,10 @@ func (c *STDServerConfig) startWatcher() error {
if err != nil { if err != nil {
return err return err
} }
err = watcher.Start()
if err != nil {
return err
}
c.watcher = watcher c.watcher = watcher
return nil return nil
} }
@@ -234,6 +241,31 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
tlsConfig.Certificates = []tls.Certificate{keyPair} tlsConfig.Certificates = []tls.Certificate{keyPair}
} }
} }
if options.ECH != nil && options.ECH.Enabled {
var echKey []byte
if len(options.ECH.Key) > 0 {
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
} else if options.ECH.KeyPath != "" {
content, err := os.ReadFile(options.ECH.KeyPath)
if err != nil {
return nil, E.Cause(err, "read ECH key")
}
echKey = content
} else {
return nil, E.New("missing ECH key")
}
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return nil, E.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil {
return nil, E.Cause(err, "parse ECH keys")
}
tlsConfig.EncryptedClientHelloKeys = echKeys
}
return &STDServerConfig{ return &STDServerConfig{
config: tlsConfig, config: tlsConfig,
logger: logger, logger: logger,
@@ -244,3 +276,22 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
keyPath: options.KeyPath, keyPath: options.KeyPath,
}, nil }, nil
} }
func UnmarshalECHKeys(raw []byte) ([]tls.EncryptedClientHelloKey, error) {
var keys []tls.EncryptedClientHelloKey
rawString := cryptobyte.String(raw)
for !rawString.Empty() {
var key tls.EncryptedClientHelloKey
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
return nil, E.New("error parsing private key")
}
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
return nil, E.New("error parsing config")
}
keys = append(keys, key)
}
if len(keys) == 0 {
return nil, E.New("empty ECH keys")
}
return keys, nil
}

View File

@@ -0,0 +1,8 @@
//go:build android && debug
package constant
// TODO: remove after fixed
// https://github.com/golang/go/issues/68760
const FixAndroidStack = true

View File

@@ -0,0 +1,5 @@
//go:build !(android && debug)
package constant
const FixAndroidStack = false

7
constant/hysteria2.go Normal file
View File

@@ -0,0 +1,7 @@
package constant
const (
Hysterai2MasqueradeTypeFile = "file"
Hysterai2MasqueradeTypeProxy = "proxy"
Hysterai2MasqueradeTypeString = "string"
)

View File

@@ -10,6 +10,7 @@ const (
ProtocolDTLS = "dtls" ProtocolDTLS = "dtls"
ProtocolSSH = "ssh" ProtocolSSH = "ssh"
ProtocolRDP = "rdp" ProtocolRDP = "rdp"
ProtocolNTP = "ntp"
) )
const ( const (

View File

@@ -9,8 +9,6 @@ const (
TCPTimeout = 15 * time.Second TCPTimeout = 15 * time.Second
ReadPayloadTimeout = 300 * time.Millisecond ReadPayloadTimeout = 300 * time.Millisecond
DNSTimeout = 10 * time.Second DNSTimeout = 10 * time.Second
QUICTimeout = 30 * time.Second
STUNTimeout = 15 * time.Second
UDPTimeout = 5 * time.Minute UDPTimeout = 5 * time.Minute
DefaultURLTestInterval = 3 * time.Minute DefaultURLTestInterval = 3 * time.Minute
DefaultURLTestIdleTimeout = 30 * time.Minute DefaultURLTestIdleTimeout = 30 * time.Minute
@@ -19,3 +17,18 @@ const (
FatalStopTimeout = 10 * time.Second FatalStopTimeout = 10 * time.Second
FakeIPMetadataSaveInterval = 10 * time.Second FakeIPMetadataSaveInterval = 10 * time.Second
) )
var PortProtocols = map[uint16]string{
53: ProtocolDNS,
123: ProtocolNTP,
3478: ProtocolSTUN,
443: ProtocolQUIC,
}
var ProtocolTimeouts = map[string]time.Duration{
ProtocolDNS: 10 * time.Second,
ProtocolNTP: 10 * time.Second,
ProtocolSTUN: 10 * time.Second,
ProtocolQUIC: 30 * time.Second,
ProtocolDTLS: 30 * time.Second,
}

View File

@@ -46,7 +46,7 @@ func applyDebugListenOption(options option.DebugOptions) {
encoder := json.NewEncoder(writer) encoder := json.NewEncoder(writer)
encoder.SetIndent("", " ") encoder.SetIndent("", " ")
encoder.Encode(memObject) encoder.Encode(&memObject)
}) })
r.Route("/pprof", func(r chi.Router) { r.Route("/pprof", func(r chi.Router) {
r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { r.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {

View File

@@ -2,6 +2,73 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.11.0-beta.9
* Fixes and improvements
#### 1.11.0-beta.3
* Add more masquerade options for hysteria2 **1**
* Fixes and improvements
**1**:
See [Hysteria2](/configuration/inbound/hysteria2/#masquerade).
### 1.10.3
* Fixes and improvements
#### 1.11.0-alpha.25
* Update quic-go to v0.48.2
* Fixes and improvements
#### 1.11.0-alpha.22
* Add UDP timeout route option **1**
* Fixes and improvements
**1**:
See [Rule Action](/configuration/route/rule_action/#udp_timeout).
#### 1.11.0-alpha.20
* Add UDP GSO support for WireGuard
* Make GSO adaptive **1**
**1**:
For WireGuard outbound and endpoint, GSO will be automatically enabled when available,
see [WireGuard Outbound](/configuration/outbound/wireguard/#gso).
For TUN, GSO has been removed,
see [Deprecated](/deprecated/#gso-option-in-tun).
#### 1.11.0-alpha.19
* Upgrade WireGuard outbound to endpoint **1**
* Fixes and improvements
**1**:
The new WireGuard endpoint combines inbound and outbound capabilities,
and the old outbound will be removed in sing-box 1.13.0.
See [Endpoint](/configuration/endpoint/), [WireGuard Endpoint](/configuration/endpoint/wireguard/)
and [Migrate WireGuard outbound fields to route options](/migration/#migrate-wireguard-outbound-to-endpoint).
### 1.10.2
* Add deprecated warnings
* Fix proxying websocket connections in HTTP/mixed inbounds
* Fixes and improvements
#### 1.11.0-alpha.18
* Fixes and improvements
#### 1.11.0-alpha.16 #### 1.11.0-alpha.16
* Add `cache_capacity` DNS option **1** * Add `cache_capacity` DNS option **1**

View File

@@ -379,7 +379,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! failure "已在 sing-box 1.10.0 废弃" !!! failure "已在 sing-box 1.10.0 废弃"
`rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。 `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 中被移除。
使规则集中的 `ip_cidr` 规则匹配源 IP。 使规则集中的 `ip_cidr` 规则匹配源 IP。

View File

@@ -0,0 +1,32 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.11.0"
# Endpoint
Endpoint is protocols that has both inbound and outbound behavior.
### Structure
```json
{
"endpoints": [
{
"type": "",
"tag": ""
}
]
}
```
### Fields
| Type | Format |
|-------------|---------------------------|
| `wireguard` | [WireGuard](./wireguard/) |
#### tag
The tag of the endpoint.

View File

@@ -0,0 +1,32 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.11.0 起"
# 端点
端点是具有入站和出站行为的协议。
### 结构
```json
{
"endpoints": [
{
"type": "",
"tag": ""
}
]
}
```
### 字段
| 类型 | 格式 |
|-------------|---------------------------|
| `wireguard` | [WireGuard](./wiregaurd/) |
#### tag
端点的标签。

View File

@@ -0,0 +1,133 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.11.0"
### Structure
```json
{
"type": "wireguard",
"tag": "wg-ep",
"system": false,
"name": "",
"mtu": 1408,
"address": [],
"private_key": "",
"listen_port": 10000,
"peers": [
{
"address": "127.0.0.1",
"port": 10001,
"public_key": "",
"pre_shared_key": "",
"allowed_ips": [],
"persistent_keepalive_interval": 0,
"reserved": [0, 0, 0]
}
],
"udp_timeout": "",
"workers": 0,
... // Dial Fields
}
```
!!! note ""
You can ignore the JSON Array [] tag when the content is only one item
### Fields
#### system
Use system interface.
Requires privilege and cannot conflict with exists system interfaces.
#### name
Custom interface name for system interface.
#### mtu
WireGuard MTU.
`1408` will be used by default.
#### address
==Required==
List of IP (v4 or v6) address prefixes to be assigned to the interface.
#### private_key
==Required==
WireGuard requires base64-encoded public and private keys. These can be generated using the wg(8) utility:
```shell
wg genkey
echo "private key" || wg pubkey
```
or `sing-box generate wg-keypair`.
#### peers
==Required==
List of WireGuard peers.
#### peers.address
WireGuard peer address.
#### peers.port
WireGuard peer port.
#### peers.public_key
==Required==
WireGuard peer public key.
#### peers.pre_shared_key
WireGuard peer pre-shared key.
#### peers.allowed_ips
==Required==
WireGuard allowed IPs.
#### peers.persistent_keepalive_interval
WireGuard persistent keepalive interval, in seconds.
Disabled by default.
#### peers.reserved
WireGuard reserved field bytes.
#### udp_timeout
UDP NAT expiration time.
`5m` will be used by default.
#### workers
WireGuard worker count.
CPU count is used by default.
### Dial Fields
See [Dial Fields](/configuration/shared/dial/) for details.

View File

@@ -0,0 +1,135 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.11.0 起"
### 结构
```json
{
"type": "wireguard",
"tag": "wg-ep",
"system": false,
"name": "",
"mtu": 1408,
"address": [],
"private_key": "",
"listen_port": 10000,
"peers": [
{
"address": "127.0.0.1",
"port": 10001,
"public_key": "",
"pre_shared_key": "",
"allowed_ips": [],
"persistent_keepalive_interval": 0,
"reserved": [0, 0, 0]
}
],
"udp_timeout": "",
"workers": 0,
... // 拨号字段
}
```
!!! note ""
当内容只有一项时,可以忽略 JSON 数组 [] 标签
### 字段
#### system_interface
使用系统设备。
需要特权且不能与已有系统接口冲突。
#### name
为系统接口自定义设备名称。
#### mtu
WireGuard MTU。
默认使用 1408。
#### address
==必填==
接口的 IPv4/IPv6 地址或地址段的列表您。
要分配给接口的 IPv4 或 v6地址段列表。
#### private_key
==必填==
WireGuard 需要 base64 编码的公钥和私钥。 这些可以使用 wg(8) 实用程序生成:
```shell
wg genkey
echo "private key" || wg pubkey
```
`sing-box generate wg-keypair`.
#### peers
==必填==
WireGuard 对等方的列表。
#### peers.address
对等方的 IP 地址。
#### peers.port
对等方的 WireGuard 端口。
#### peers.public_key
==必填==
对等方的 WireGuard 公钥。
#### peers.pre_shared_key
对等方的预共享密钥。
#### peers.allowed_ips
==必填==
对等方的允许 IP 地址。
#### peers.persistent_keepalive_interval
对等方的持久性保持活动间隔,以秒为单位。
默认禁用。
#### peers.reserved
对等方的保留字段字节。
#### udp_timeout
UDP NAT 过期时间。
默认使用 `5m`
#### workers
WireGuard worker 数量。
默认使用 CPU 数量。
### 拨号字段
参阅 [拨号字段](/zh/configuration/shared/dial/)。

View File

@@ -1,11 +1,19 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.11.0"
:material-alert: [masquerade](#masquerade)
### Structure ### Structure
```json ```json
{ {
"type": "hysteria2", "type": "hysteria2",
"tag": "hy2-in", "tag": "hy2-in",
...
// Listen Fields ... // Listen Fields
"up_mbps": 100, "up_mbps": 100,
"down_mbps": 100, "down_mbps": 100,
@@ -21,7 +29,7 @@
], ],
"ignore_client_bandwidth": false, "ignore_client_bandwidth": false,
"tls": {}, "tls": {},
"masquerade": "", "masquerade": "", // or {}
"brutal_debug": false "brutal_debug": false
} }
``` ```
@@ -79,14 +87,54 @@ TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
#### masquerade #### masquerade
HTTP3 server behavior when authentication fails. HTTP3 server behavior (URL string configuration) when authentication fails.
| Scheme | Example | Description | | Scheme | Example | Description |
|--------------|-------------------------|--------------------| |--------------|-------------------------|--------------------|
| `file` | `file:///var/www` | As a file server | | `file` | `file:///var/www` | As a file server |
| `http/https` | `http://127.0.0.1:8080` | As a reverse proxy | | `http/https` | `http://127.0.0.1:8080` | As a reverse proxy |
A 404 page will be returned if empty. Conflict with `masquerade.type`.
A 404 page will be returned if masquerade is not configured.
#### masquerade.type
HTTP3 server behavior (Object configuration) when authentication fails.
| Type | Description | Fields |
|----------|-----------------------------|-------------------------------------|
| `file` | As a file server | `directory` |
| `proxy` | As a reverse proxy | `url`, `rewrite_host` |
| `string` | Reply with a fixed response | `status_code`, `headers`, `content` |
Conflict with `masquerade`.
A 404 page will be returned if masquerade is not configured.
#### masquerade.directory
File server root directory.
#### masquerade.url
Reverse proxy target URL.
#### masquerade.rewrite_host
Rewrite the `Host` header to the target URL.
#### masquerade.status_code
Fixed response status code.
#### masquerade.headers
Fixed response headers.
#### masquerade.content
Fixed response content.
#### brutal_debug #### brutal_debug

View File

@@ -1,11 +1,19 @@
---
icon: material/alert-decagram
---
!!! quote "sing-box 1.11.0 中的更改"
:material-alert: [masquerade](#masquerade)
### 结构 ### 结构
```json ```json
{ {
"type": "hysteria2", "type": "hysteria2",
"tag": "hy2-in", "tag": "hy2-in",
...
// 监听字段 ... // 监听字段
"up_mbps": 100, "up_mbps": 100,
"down_mbps": 100, "down_mbps": 100,
@@ -21,7 +29,7 @@
], ],
"ignore_client_bandwidth": false, "ignore_client_bandwidth": false,
"tls": {}, "tls": {},
"masquerade": "", "masquerade": "", // 或 {}
"brutal_debug": false "brutal_debug": false
} }
``` ```
@@ -76,14 +84,54 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
#### masquerade #### masquerade
HTTP3 服务器认证失败时的行为。 HTTP3 服务器认证失败时的行为 URL 字符串配置)
| Scheme | 示例 | 描述 | | Scheme | 示例 | 描述 |
|--------------|-------------------------|---------| |--------------|-------------------------|---------|
| `file` | `file:///var/www` | 作为文件服务器 | | `file` | `file:///var/www` | 作为文件服务器 |
| `http/https` | `http://127.0.0.1:8080` | 作为反向代理 | | `http/https` | `http://127.0.0.1:8080` | 作为反向代理 |
如果为空,则返回 404 页。 如果 masquerade 未配置,则返回 404 页。
`masquerade.type` 冲突。
#### masquerade.type
HTTP3 服务器认证失败时的行为 (对象配置)。
| Type | 描述 | 字段 |
|----------|---------|-------------------------------------|
| `file` | 作为文件服务器 | `directory` |
| `proxy` | 作为反向代理 | `url`, `rewrite_host` |
| `string` | 返回固定响应 | `status_code`, `headers`, `content` |
如果 masquerade 未配置,则返回 404 页。
`masquerade` 冲突。
#### masquerade.directory
文件服务器根目录。
#### masquerade.url
反向代理目标 URL。
#### masquerade.rewrite_host
重写请求头中的 Host 字段到目标 URL。
#### masquerade.status_code
固定响应状态码。
#### masquerade.headers
固定响应头。
#### masquerade.content
固定响应内容。
#### brutal_debug #### brutal_debug

View File

@@ -1,7 +1,11 @@
--- ---
icon: material/new-box icon: material/alert-decagram
--- ---
!!! quote "Changes in sing-box 1.11.0"
:material-delete-alert: [gso](#gso)
!!! quote "Changes in sing-box 1.10.0" !!! quote "Changes in sing-box 1.10.0"
:material-plus: [address](#address) :material-plus: [address](#address)
@@ -46,16 +50,7 @@ icon: material/new-box
"172.18.0.1/30", "172.18.0.1/30",
"fdfe:dcba:9876::1/126" "fdfe:dcba:9876::1/126"
], ],
// deprecated
"inet4_address": [
"172.19.0.1/30"
],
// deprecated
"inet6_address": [
"fdfe:dcba:9876::1/126"
],
"mtu": 9000, "mtu": 9000,
"gso": false,
"auto_route": true, "auto_route": true,
"iproute2_table_index": 2022, "iproute2_table_index": 2022,
"iproute2_rule_index": 9000, "iproute2_rule_index": 9000,
@@ -69,28 +64,11 @@ icon: material/new-box
"::/1", "::/1",
"8000::/1" "8000::/1"
], ],
// deprecated
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
// deprecated
"inet6_route_address": [
"::/1",
"8000::/1"
],
"route_exclude_address": [ "route_exclude_address": [
"192.168.0.0/16", "192.168.0.0/16",
"fc00::/7" "fc00::/7"
], ],
// deprecated
"inet4_route_exclude_address": [
"192.168.0.0/16"
],
// deprecated
"inet6_route_exclude_address": [
"fc00::/7"
],
"route_address_set": [ "route_address_set": [
"geoip-cloudflare" "geoip-cloudflare"
], ],
@@ -137,8 +115,31 @@ icon: material/new-box
"match_domain": [] "match_domain": []
} }
}, },
...
// Listen Fields // Deprecated
"gso": false,
"inet4_address": [
"172.19.0.1/30"
],
"inet6_address": [
"fdfe:dcba:9876::1/126"
],
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
"inet6_route_address": [
"::/1",
"8000::/1"
],
"inet4_route_exclude_address": [
"192.168.0.0/16"
],
"inet6_route_exclude_address": [
"fc00::/7"
],
... // Listen Fields
} }
``` ```
@@ -166,7 +167,7 @@ IPv4 and IPv6 prefix for the tun interface.
!!! failure "Deprecated in sing-box 1.10.0" !!! failure "Deprecated in sing-box 1.10.0"
`inet4_address` is merged to `address` and will be removed in sing-box 1.11.0. `inet4_address` is merged to `address` and will be removed in sing-box 1.12.0.
IPv4 prefix for the tun interface. IPv4 prefix for the tun interface.
@@ -174,7 +175,7 @@ IPv4 prefix for the tun interface.
!!! failure "Deprecated in sing-box 1.10.0" !!! failure "Deprecated in sing-box 1.10.0"
`inet6_address` is merged to `address` and will be removed in sing-box 1.11.0. `inet6_address` is merged to `address` and will be removed in sing-box 1.12.0.
IPv6 prefix for the tun interface. IPv6 prefix for the tun interface.
@@ -184,6 +185,10 @@ The maximum transmission unit.
#### gso #### gso
!!! failure "Deprecated in sing-box 1.11.0"
GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works, and will be removed in sing-box 1.12.0.
!!! question "Since sing-box 1.8.0" !!! question "Since sing-box 1.8.0"
!!! quote "" !!! quote ""
@@ -284,7 +289,7 @@ Use custom routes instead of default when `auto_route` is enabled.
!!! failure "Deprecated in sing-box 1.10.0" !!! failure "Deprecated in sing-box 1.10.0"
`inet4_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) `inet4_route_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_address](#route_address)
instead. instead.
Use custom routes instead of default when `auto_route` is enabled. Use custom routes instead of default when `auto_route` is enabled.
@@ -293,7 +298,7 @@ Use custom routes instead of default when `auto_route` is enabled.
!!! failure "Deprecated in sing-box 1.10.0" !!! failure "Deprecated in sing-box 1.10.0"
`inet6_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) `inet6_route_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_address](#route_address)
instead. instead.
Use custom routes instead of default when `auto_route` is enabled. Use custom routes instead of default when `auto_route` is enabled.
@@ -308,7 +313,7 @@ Exclude custom routes when `auto_route` is enabled.
!!! failure "Deprecated in sing-box 1.10.0" !!! failure "Deprecated in sing-box 1.10.0"
`inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please `inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.12.0, please
use [route_exclude_address](#route_exclude_address) instead. use [route_exclude_address](#route_exclude_address) instead.
Exclude custom routes when `auto_route` is enabled. Exclude custom routes when `auto_route` is enabled.
@@ -317,7 +322,7 @@ Exclude custom routes when `auto_route` is enabled.
!!! failure "Deprecated in sing-box 1.10.0" !!! failure "Deprecated in sing-box 1.10.0"
`inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please `inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.12.0, please
use [route_exclude_address](#route_exclude_address) instead. use [route_exclude_address](#route_exclude_address) instead.
Exclude custom routes when `auto_route` is enabled. Exclude custom routes when `auto_route` is enabled.
@@ -360,7 +365,9 @@ Performance may degrade slightly, so it is not recommended to enable on when it
#### udp_timeout #### udp_timeout
UDP NAT expiration time in seconds, default is 300 (5 minutes). UDP NAT expiration time.
`5m` will be used by default.
#### stack #### stack

View File

@@ -1,8 +1,12 @@
--- ---
icon: material/new-box icon: material/alert-decagram
--- ---
!!! quote "Changes in sing-box 1.10.0" !!! quote "sing-box 1.11.0 中的更改"
:material-delete-alert: [gso](#gso)
!!! quote "sing-box 1.10.0 中的更改"
:material-plus: [address](#address) :material-plus: [address](#address)
:material-delete-clock: [inet4_address](#inet4_address) :material-delete-clock: [inet4_address](#inet4_address)
@@ -46,16 +50,7 @@ icon: material/new-box
"172.18.0.1/30", "172.18.0.1/30",
"fdfe:dcba:9876::1/126" "fdfe:dcba:9876::1/126"
], ],
// 已弃用
"inet4_address": [
"172.19.0.1/30"
],
// 已弃用
"inet6_address": [
"fdfe:dcba:9876::1/126"
],
"mtu": 9000, "mtu": 9000,
"gso": false,
"auto_route": true, "auto_route": true,
"iproute2_table_index": 2022, "iproute2_table_index": 2022,
"iproute2_rule_index": 9000, "iproute2_rule_index": 9000,
@@ -69,28 +64,11 @@ icon: material/new-box
"::/1", "::/1",
"8000::/1" "8000::/1"
], ],
// 已弃用
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
// 已弃用
"inet6_route_address": [
"::/1",
"8000::/1"
],
"route_exclude_address": [ "route_exclude_address": [
"192.168.0.0/16", "192.168.0.0/16",
"fc00::/7" "fc00::/7"
], ],
// 已弃用
"inet4_route_exclude_address": [
"192.168.0.0/16"
],
// 已弃用
"inet6_route_exclude_address": [
"fc00::/7"
],
"route_address_set": [ "route_address_set": [
"geoip-cloudflare" "geoip-cloudflare"
], ],
@@ -137,6 +115,29 @@ icon: material/new-box
"match_domain": [] "match_domain": []
} }
}, },
// 已弃用
"gso": false,
"inet4_address": [
"172.19.0.1/30"
],
"inet6_address": [
"fdfe:dcba:9876::1/126"
],
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
"inet6_route_address": [
"::/1",
"8000::/1"
],
"inet4_route_exclude_address": [
"192.168.0.0/16"
],
"inet6_route_exclude_address": [
"fc00::/7"
],
... // 监听字段 ... // 监听字段
} }
@@ -168,7 +169,7 @@ tun 接口的 IPv4 和 IPv6 前缀。
!!! failure "已在 sing-box 1.10.0 废弃" !!! failure "已在 sing-box 1.10.0 废弃"
`inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。 `inet4_address` 已合并到 `address` 且将在 sing-box 1.12.0 中被移除。
==必填== ==必填==
@@ -178,7 +179,7 @@ tun 接口的 IPv4 前缀。
!!! failure "已在 sing-box 1.10.0 废弃" !!! failure "已在 sing-box 1.10.0 废弃"
`inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。 `inet6_address` 已合并到 `address` 且将在 sing-box 1.12.0 中被移除。
tun 接口的 IPv6 前缀。 tun 接口的 IPv6 前缀。
@@ -188,6 +189,10 @@ tun 接口的 IPv6 前缀。
#### gso #### gso
!!! failure "已在 sing-box 1.11.0 废弃"
GSO 对于透明代理场景没有优势,已废弃和不再生效,且将在 sing-box 1.12.0 中被移除。
!!! question "自 sing-box 1.8.0 起" !!! question "自 sing-box 1.8.0 起"
!!! quote "" !!! quote ""
@@ -288,7 +293,7 @@ tun 接口的 IPv6 前缀。
!!! failure "已在 sing-box 1.10.0 废弃" !!! failure "已在 sing-box 1.10.0 废弃"
`inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。 `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.12.0 中被移除。
启用 `auto_route` 时使用自定义路由而不是默认路由。 启用 `auto_route` 时使用自定义路由而不是默认路由。
@@ -296,7 +301,7 @@ tun 接口的 IPv6 前缀。
!!! failure "已在 sing-box 1.10.0 废弃" !!! failure "已在 sing-box 1.10.0 废弃"
`inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。 `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.12.0 中被移除。
启用 `auto_route` 时使用自定义路由而不是默认路由。 启用 `auto_route` 时使用自定义路由而不是默认路由。
@@ -310,7 +315,7 @@ tun 接口的 IPv6 前缀。
!!! failure "已在 sing-box 1.10.0 废弃" !!! failure "已在 sing-box 1.10.0 废弃"
`inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。 `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.12.0 中被移除。
启用 `auto_route` 时排除自定义路由。 启用 `auto_route` 时排除自定义路由。
@@ -318,7 +323,7 @@ tun 接口的 IPv6 前缀。
!!! failure "已在 sing-box 1.10.0 废弃" !!! failure "已在 sing-box 1.10.0 废弃"
`inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。 `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.12.0 中被移除。
启用 `auto_route` 时排除自定义路由。 启用 `auto_route` 时排除自定义路由。
@@ -356,7 +361,9 @@ tun 接口的 IPv6 前缀。
#### udp_timeout #### udp_timeout
UDP NAT 过期时间,以秒为单位,默认为 3005 分钟) UDP NAT 过期时间。
默认使用 `5m`
#### stack #### stack

View File

@@ -9,6 +9,7 @@ sing-box uses JSON for configuration files.
"log": {}, "log": {},
"dns": {}, "dns": {},
"ntp": {}, "ntp": {},
"endpoints": [],
"inbounds": [], "inbounds": [],
"outbounds": [], "outbounds": [],
"route": {}, "route": {},
@@ -23,6 +24,7 @@ sing-box uses JSON for configuration files.
| `log` | [Log](./log/) | | `log` | [Log](./log/) |
| `dns` | [DNS](./dns/) | | `dns` | [DNS](./dns/) |
| `ntp` | [NTP](./ntp/) | | `ntp` | [NTP](./ntp/) |
| `endpoints` | [Endpoint](./endpoint/) |
| `inbounds` | [Inbound](./inbound/) | | `inbounds` | [Inbound](./inbound/) |
| `outbounds` | [Outbound](./outbound/) | | `outbounds` | [Outbound](./outbound/) |
| `route` | [Route](./route/) | | `route` | [Route](./route/) |

View File

@@ -8,6 +8,7 @@ sing-box 使用 JSON 作为配置文件格式。
{ {
"log": {}, "log": {},
"dns": {}, "dns": {},
"endpoints": [],
"inbounds": [], "inbounds": [],
"outbounds": [], "outbounds": [],
"route": {}, "route": {},
@@ -21,6 +22,7 @@ sing-box 使用 JSON 作为配置文件格式。
|----------------|------------------------| |----------------|------------------------|
| `log` | [日志](./log/) | | `log` | [日志](./log/) |
| `dns` | [DNS](./dns/) | | `dns` | [DNS](./dns/) |
| `endpoints` | [端点](./endpoint/) |
| `inbounds` | [入站](./inbound/) | | `inbounds` | [入站](./inbound/) |
| `outbounds` | [出站](./outbound/) | | `outbounds` | [出站](./outbound/) |
| `route` | [路由](./route/) | | `route` | [路由](./route/) |

View File

@@ -4,8 +4,8 @@ icon: material/alert-decagram
!!! quote "Changes in sing-box 1.11.0" !!! quote "Changes in sing-box 1.11.0"
:material-alert-decagram: [override_address](#override_address) :material-delete-clock: [override_address](#override_address)
:material-alert-decagram: [override_port](#override_port) :material-delete-clock: [override_port](#override_port)
`direct` outbound send requests directly. `direct` outbound send requests directly.

View File

@@ -1,3 +1,15 @@
---
icon: material/delete-clock
---
!!! failure "Deprecated in sing-box 1.11.0"
WireGuard outbound is deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-wireguard-outbound-to-endpoint).
!!! quote "Changes in sing-box 1.11.0"
:material-delete-alert: [gso](#gso)
!!! quote "Changes in sing-box 1.8.0" !!! quote "Changes in sing-box 1.8.0"
:material-plus: [gso](#gso) :material-plus: [gso](#gso)
@@ -12,10 +24,9 @@
"server": "127.0.0.1", "server": "127.0.0.1",
"server_port": 1080, "server_port": 1080,
"system_interface": false, "system_interface": false,
"gso": false,
"interface_name": "wg0", "interface_name": "wg0",
"local_address": [ "local_address": [
"10.0.0.2/32" "10.0.0.1/32"
], ],
"private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=", "private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=",
"peers": [ "peers": [
@@ -37,6 +48,10 @@
"mtu": 1408, "mtu": 1408,
"network": "tcp", "network": "tcp",
// Deprecated
"gso": false,
... // Dial Fields ... // Dial Fields
} }
``` ```
@@ -69,6 +84,10 @@ Custom interface name for system interface.
#### gso #### gso
!!! failure "Deprecated in sing-box 1.11.0"
GSO will be automatically enabled when available since sing-box 1.11.0.
!!! question "Since sing-box 1.8.0" !!! question "Since sing-box 1.8.0"
!!! quote "" !!! quote ""

View File

@@ -1,3 +1,15 @@
---
icon: material/delete-clock
---
!!! failure "已在 sing-box 1.11.0 废弃"
WireGuard 出站已被启用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。
!!! quote "sing-box 1.11.0 中的更改"
:material-delete-alert: [gso](#gso)
!!! quote "sing-box 1.8.0 中的更改" !!! quote "sing-box 1.8.0 中的更改"
:material-plus: [gso](#gso) :material-plus: [gso](#gso)
@@ -12,10 +24,9 @@
"server": "127.0.0.1", "server": "127.0.0.1",
"server_port": 1080, "server_port": 1080,
"system_interface": false, "system_interface": false,
"gso": false,
"interface_name": "wg0", "interface_name": "wg0",
"local_address": [ "local_address": [
"10.0.0.2/32" "10.0.0.1/32"
], ],
"private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=", "private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=",
"peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=", "peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=",
@@ -24,6 +35,10 @@
"workers": 4, "workers": 4,
"mtu": 1408, "mtu": 1408,
"network": "tcp", "network": "tcp",
// 废弃的
"gso": false,
... // 拨号字段 ... // 拨号字段
} }
@@ -57,6 +72,10 @@
#### gso #### gso
!!! failure "已在 sing-box 1.11.0 废弃"
自 sing-box 1.11.0 起GSO 将可用时自动启用。
!!! question "自 sing-box 1.8.0 起" !!! question "自 sing-box 1.8.0 起"
!!! quote "" !!! quote ""

View File

@@ -9,7 +9,7 @@ icon: material/new-box
:material-plus: [default_network_strategy](#default_network_strategy) :material-plus: [default_network_strategy](#default_network_strategy)
:material-plus: [default_network_type](#default_network_type) :material-plus: [default_network_type](#default_network_type)
:material-plus: [default_fallback_network_type](#default_fallback_network_type) :material-plus: [default_fallback_network_type](#default_fallback_network_type)
:material-alert: [default_fallback_delay](#default_fallback_delay) :material-plus: [default_fallback_delay](#default_fallback_delay)
!!! quote "Changes in sing-box 1.8.0" !!! quote "Changes in sing-box 1.8.0"

View File

@@ -9,7 +9,7 @@ icon: material/new-box
:material-plus: [network_strategy](#network_strategy) :material-plus: [network_strategy](#network_strategy)
:material-plus: [default_network_type](#default_network_type) :material-plus: [default_network_type](#default_network_type)
:material-plus: [default_fallback_network_type](#default_fallback_network_type) :material-plus: [default_fallback_network_type](#default_fallback_network_type)
:material-alert: [default_fallback_delay](#default_fallback_delay) :material-plus: [default_fallback_delay](#default_fallback_delay)
!!! quote "sing-box 1.8.0 中的更改" !!! quote "sing-box 1.8.0 中的更改"

View File

@@ -388,7 +388,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
!!! failure "已在 sing-box 1.10.0 废弃" !!! failure "已在 sing-box 1.10.0 废弃"
`rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。 `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 中被移除。
使规则集中的 `ip_cidr` 规则匹配源 IP。 使规则集中的 `ip_cidr` 规则匹配源 IP。

View File

@@ -41,7 +41,8 @@ See `route-options` fields below.
"network_strategy": "", "network_strategy": "",
"fallback_delay": "", "fallback_delay": "",
"udp_disable_domain_unmapping": false, "udp_disable_domain_unmapping": false,
"udp_connect": false "udp_connect": false,
"udp_timeout": ""
} }
``` ```
@@ -86,6 +87,28 @@ do not support receiving UDP packets with domain addresses, such as Surge.
If enabled, attempts to connect UDP connection to the destination instead of listen. If enabled, attempts to connect UDP connection to the destination instead of listen.
#### udp_timeout
Timeout for UDP connections.
Setting a larger value than the UDP timeout in inbounds will have no effect.
Default value for protocol sniffed connections:
| Timeout | Protocol |
|---------|----------------------|
| `10s` | `dns`, `ntp`, `stun` |
| `30s` | `quic`, `dtls` |
If no protocol is sniffed, the following ports will be recognized as protocols by default:
| Port | Protocol |
|------|----------|
| 53 | `dns` |
| 123 | `ntp` |
| 443 | `quic` |
| 3478 | `stun` |
### reject ### reject
```json ```json

View File

@@ -37,7 +37,8 @@ icon: material/new-box
"network_strategy": "", "network_strategy": "",
"fallback_delay": "", "fallback_delay": "",
"udp_disable_domain_unmapping": false, "udp_disable_domain_unmapping": false,
"udp_connect": false "udp_connect": false,
"udp_timeout": ""
} }
``` ```
@@ -84,6 +85,28 @@ icon: material/new-box
如果启用,将尝试将 UDP 连接 connect 到目标而不是 listen。 如果启用,将尝试将 UDP 连接 connect 到目标而不是 listen。
#### udp_timeout
UDP 连接超时时间。
设置比入站 UDP 超时更大的值将无效。
已探测协议连接的默认值:
| 超时 | 协议 |
|-------|----------------------|
| `10s` | `dns`, `ntp`, `stun` |
| `30s` | `quic`, `dtls` |
如果没有探测到协议,以下端口将默认识别为协议:
| 端口 | 协议 |
|------|--------|
| 53 | `dns` |
| 123 | `ntp` |
| 443 | `quic` |
| 3478 | `stun` |
### reject ### reject
```json ```json

View File

@@ -68,9 +68,9 @@ Enable UDP fragmentation.
#### udp_timeout #### udp_timeout
UDP NAT expiration time in seconds. UDP NAT expiration time.
`5m` is used by default. `5m` will be used by default.
#### detour #### detour

View File

@@ -69,7 +69,7 @@ icon: material/delete-clock
#### udp_timeout #### udp_timeout
UDP NAT 过期时间,以秒为单位 UDP NAT 过期时间。
默认使用 `5m` 默认使用 `5m`

View File

@@ -28,6 +28,19 @@ Destination override fields (`override_address` / `override_port`) in direct out
and can be replaced by rule actions, and can be replaced by rule actions,
check [Migration](../migration/#migrate-destination-override-fields-to-route-options). check [Migration](../migration/#migrate-destination-override-fields-to-route-options).
#### WireGuard outbound
WireGuard outbound is deprecated and can be replaced by endpoint,
check [Migration](../migration/#migrate-wireguard-outbound-to-endpoint).
Old outbound will be removed in sing-box 1.13.0.
#### GSO option in TUN
GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works in TUN.
Old fields will be removed in sing-box 1.13.0.
## 1.10.0 ## 1.10.0
#### TUN address fields are merged #### TUN address fields are merged

View File

@@ -27,6 +27,19 @@ direct 出站中的目标地址覆盖字段(`override_address` / `override_por
旧字段将在 sing-box 1.13.0 中被移除。 旧字段将在 sing-box 1.13.0 中被移除。
#### WireGuard 出站
WireGuard 出站已废弃且可以通过端点替代,
参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。
旧出站将在 sing-box 1.13.0 中被移除。
#### TUN 的 GSO 字段
GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用。
旧字段将在 sing-box 1.13.0 中被移除。
## 1.10.0 ## 1.10.0
#### Match source 规则项已重命名 #### Match source 规则项已重命名

View File

@@ -194,6 +194,77 @@ Destination override fields in direct outbound are deprecated and can be replace
} }
``` ```
### Migrate WireGuard outbound to endpoint
WireGuard outbound is deprecated and can be replaced by endpoint.
!!! info "References"
[Endpoint](/configuration/endpoint/) /
[WireGuard Endpoint](/configuration/endpoint/wireguard/) /
[WireGuard Outbound](/configuration/outbound/wireguard/)
=== ":material-card-remove: Deprecated"
```json
{
"outbounds": [
{
"type": "wireguard",
"tag": "wg-out",
"server": "127.0.0.1",
"server_port": 10001,
"system_interface": true,
"gso": true,
"interface_name": "wg0",
"local_address": [
"10.0.0.1/32"
],
"private_key": "<private_key>",
"peer_public_key": "<peer_public_key>",
"pre_shared_key": "<pre_shared_key>",
"reserved": [0, 0, 0],
"mtu": 1408
}
]
}
```
=== ":material-card-multiple: New"
```json
{
"endpoints": [
{
"type": "wireguard",
"tag": "wg-ep",
"system": true,
"name": "wg0",
"mtu": 1408,
"address": [
"10.0.0.2/32"
],
"private_key": "<private_key>",
"listen_port": 10000,
"peers": [
{
"address": "127.0.0.1",
"port": 10001,
"public_key": "<peer_public_key>",
"pre_shared_key": "<pre_shared_key>",
"allowed_ips": [
"0.0.0.0/0"
],
"persistent_keepalive_interval": 30,
"reserved": [0, 0, 0]
}
]
}
]
}
```
## 1.10.0 ## 1.10.0
### TUN address fields are merged ### TUN address fields are merged

View File

@@ -104,7 +104,6 @@ icon: material/arrange-bring-forward
### 迁移旧的入站字段到规则动作 ### 迁移旧的入站字段到规则动作
入站选项已被弃用,且可以被规则动作替代。 入站选项已被弃用,且可以被规则动作替代。
!!! info "参考" !!! info "参考"
@@ -196,6 +195,77 @@ direct 出站中的目标地址覆盖字段已废弃,且可以被路由字段
} }
``` ```
### 迁移 WireGuard 出站到端点
WireGuard 出站已被弃用,且可以被端点替代。
!!! info "参考"
[端点](/zh/configuration/endpoint/) /
[WireGuard 端点](/zh/configuration/endpoint/wireguard/) /
[WireGuard 出站](/zh/configuration/outbound/wireguard/)
=== ":material-card-remove: 弃用的"
```json
{
"outbounds": [
{
"type": "wireguard",
"tag": "wg-out",
"server": "127.0.0.1",
"server_port": 10001,
"system_interface": true,
"gso": true,
"interface_name": "wg0",
"local_address": [
"10.0.0.1/32"
],
"private_key": "<private_key>",
"peer_public_key": "<peer_public_key>",
"pre_shared_key": "<pre_shared_key>",
"reserved": [0, 0, 0],
"mtu": 1408
}
]
}
```
=== ":material-card-multiple: 新的"
```json
{
"endpoints": [
{
"type": "wireguard",
"tag": "wg-ep",
"system": true,
"name": "wg0",
"mtu": 1408,
"address": [
"10.0.0.2/32"
],
"private_key": "<private_key>",
"listen_port": 10000,
"peers": [
{
"address": "127.0.0.1",
"port": 10001,
"public_key": "<peer_public_key>",
"pre_shared_key": "<pre_shared_key>",
"allowed_ips": [
"0.0.0.0/0"
],
"persistent_keepalive_interval": 30,
"reserved": [0, 0, 0]
}
]
}
]
}
```
## 1.10.0 ## 1.10.0
### TUN 地址字段已合并 ### TUN 地址字段已合并

View File

@@ -32,7 +32,7 @@ func groupRouter(server *Server) http.Handler {
func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) { func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
groups := common.Map(common.Filter(server.outboundManager.Outbounds(), func(it adapter.Outbound) bool { groups := common.Map(common.Filter(server.outbound.Outbounds(), func(it adapter.Outbound) bool {
_, isGroup := it.(adapter.OutboundGroup) _, isGroup := it.(adapter.OutboundGroup)
return isGroup return isGroup
}), func(it adapter.Outbound) *badjson.JSONObject { }), func(it adapter.Outbound) *badjson.JSONObject {
@@ -86,7 +86,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
result, err = urlTestGroup.URLTest(ctx) result, err = urlTestGroup.URLTest(ctx)
} else { } else {
outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
itOutbound, _ := server.outboundManager.Outbound(it) itOutbound, _ := server.outbound.Outbound(it)
return itOutbound return itOutbound
})) }))
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10)) b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
@@ -100,7 +100,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
continue continue
} }
checked[realTag] = true checked[realTag] = true
p, loaded := server.outboundManager.Outbound(realTag) p, loaded := server.outbound.Outbound(realTag)
if !loaded { if !loaded {
continue continue
} }

View File

@@ -18,17 +18,19 @@ func configRouter(server *Server, logFactory log.Factory) http.Handler {
} }
type configSchema struct { type configSchema struct {
Port int `json:"port"` Port int `json:"port"`
SocksPort int `json:"socks-port"` SocksPort int `json:"socks-port"`
RedirPort int `json:"redir-port"` RedirPort int `json:"redir-port"`
TProxyPort int `json:"tproxy-port"` TProxyPort int `json:"tproxy-port"`
MixedPort int `json:"mixed-port"` MixedPort int `json:"mixed-port"`
AllowLan bool `json:"allow-lan"` AllowLan bool `json:"allow-lan"`
BindAddress string `json:"bind-address"` BindAddress string `json:"bind-address"`
Mode string `json:"mode"` Mode string `json:"mode"`
LogLevel string `json:"log-level"` // sing-box added
IPv6 bool `json:"ipv6"` ModeList []string `json:"mode-list"`
Tun map[string]any `json:"tun"` LogLevel string `json:"log-level"`
IPv6 bool `json:"ipv6"`
Tun map[string]any `json:"tun"`
} }
func getConfigs(server *Server, logFactory log.Factory) func(w http.ResponseWriter, r *http.Request) { func getConfigs(server *Server, logFactory log.Factory) func(w http.ResponseWriter, r *http.Request) {
@@ -41,6 +43,7 @@ func getConfigs(server *Server, logFactory log.Factory) func(w http.ResponseWrit
} }
render.JSON(w, r, &configSchema{ render.JSON(w, r, &configSchema{
Mode: server.mode, Mode: server.mode,
ModeList: server.modeList,
BindAddress: "*", BindAddress: "*",
LogLevel: log.FormatLevel(logLevel), LogLevel: log.FormatLevel(logLevel),
}) })

View File

@@ -46,7 +46,7 @@ func findProxyByName(server *Server) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := r.Context().Value(CtxKeyProxyName).(string) name := r.Context().Value(CtxKeyProxyName).(string)
proxy, exist := server.outboundManager.Outbound(name) proxy, exist := server.outbound.Outbound(name)
if !exist { if !exist {
render.Status(r, http.StatusNotFound) render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound) render.JSON(w, r, ErrNotFound)
@@ -86,9 +86,14 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) { func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var proxyMap badjson.JSONObject var proxyMap badjson.JSONObject
outbounds := common.Filter(server.outboundManager.Outbounds(), func(detour adapter.Outbound) bool { outbounds := common.Filter(server.outbound.Outbounds(), func(detour adapter.Outbound) bool {
return detour.Tag() != "" return detour.Tag() != ""
}) })
outbounds = append(outbounds, common.Map(common.Filter(server.endpoint.Endpoints(), func(detour adapter.Endpoint) bool {
return detour.Tag() != ""
}), func(it adapter.Endpoint) adapter.Outbound {
return it
})...)
allProxies := make([]string, 0, len(outbounds)) allProxies := make([]string, 0, len(outbounds))
@@ -100,7 +105,7 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) {
allProxies = append(allProxies, detour.Tag()) allProxies = append(allProxies, detour.Tag())
} }
defaultTag := server.outboundManager.Default().Tag() defaultTag := server.outbound.Default().Tag()
sort.SliceStable(allProxies, func(i, j int) bool { sort.SliceStable(allProxies, func(i, j int) bool {
return allProxies[i] == defaultTag return allProxies[i] == defaultTag

View File

@@ -40,16 +40,17 @@ func init() {
var _ adapter.ClashServer = (*Server)(nil) var _ adapter.ClashServer = (*Server)(nil)
type Server struct { type Server struct {
ctx context.Context ctx context.Context
router adapter.Router router adapter.Router
outboundManager adapter.OutboundManager outbound adapter.OutboundManager
logger log.Logger endpoint adapter.EndpointManager
httpServer *http.Server logger log.Logger
trafficManager *trafficontrol.Manager httpServer *http.Server
urlTestHistory *urltest.HistoryStorage trafficManager *trafficontrol.Manager
mode string urlTestHistory *urltest.HistoryStorage
modeList []string mode string
modeUpdateHook chan<- struct{} modeList []string
modeUpdateHook chan<- struct{}
externalController bool externalController bool
externalUI string externalUI string
@@ -61,10 +62,11 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
trafficManager := trafficontrol.NewManager() trafficManager := trafficontrol.NewManager()
chiRouter := chi.NewRouter() chiRouter := chi.NewRouter()
s := &Server{ s := &Server{
ctx: ctx, ctx: ctx,
router: service.FromContext[adapter.Router](ctx), router: service.FromContext[adapter.Router](ctx),
outboundManager: service.FromContext[adapter.OutboundManager](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx),
logger: logFactory.NewLogger("clash-api"), endpoint: service.FromContext[adapter.EndpointManager](ctx),
logger: logFactory.NewLogger("clash-api"),
httpServer: &http.Server{ httpServer: &http.Server{
Addr: options.ExternalController, Addr: options.ExternalController,
Handler: chiRouter, Handler: chiRouter,
@@ -242,11 +244,11 @@ func (s *Server) TrafficManager() *trafficontrol.Manager {
} }
func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn {
return trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule, matchOutbound) return trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outbound, matchedRule, matchOutbound)
} }
func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn { func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn {
return trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule, matchOutbound) return trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outbound, matchedRule, matchOutbound)
} }
func authentication(serverSecret string) func(next http.Handler) http.Handler { func authentication(serverSecret string) func(next http.Handler) http.Handler {
@@ -321,27 +323,29 @@ func traffic(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter,
tick := time.NewTicker(time.Second) tick := time.NewTicker(time.Second)
defer tick.Stop() defer tick.Stop()
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
var err error uploadTotal, downloadTotal := trafficManager.Total()
for range tick.C { for range tick.C {
buf.Reset() buf.Reset()
up, down := trafficManager.Now() uploadTotalNew, downloadTotalNew := trafficManager.Total()
if err := json.NewEncoder(buf).Encode(Traffic{ err := json.NewEncoder(buf).Encode(Traffic{
Up: up, Up: uploadTotalNew - uploadTotal,
Down: down, Down: downloadTotalNew - downloadTotal,
}); err != nil { })
if err != nil {
break break
} }
if conn == nil { if conn == nil {
_, err = w.Write(buf.Bytes()) _, err = w.Write(buf.Bytes())
w.(http.Flusher).Flush() w.(http.Flusher).Flush()
} else { } else {
err = wsutil.WriteServerText(conn, buf.Bytes()) err = wsutil.WriteServerText(conn, buf.Bytes())
} }
if err != nil { if err != nil {
break break
} }
uploadTotal = uploadTotalNew
downloadTotal = downloadTotalNew
} }
} }
} }

View File

@@ -44,13 +44,13 @@ func (s *Server) downloadExternalUI() error {
s.logger.Info("downloading external ui") s.logger.Info("downloading external ui")
var detour adapter.Outbound var detour adapter.Outbound
if s.externalUIDownloadDetour != "" { if s.externalUIDownloadDetour != "" {
outbound, loaded := s.outboundManager.Outbound(s.externalUIDownloadDetour) outbound, loaded := s.outbound.Outbound(s.externalUIDownloadDetour)
if !loaded { if !loaded {
return E.New("detour outbound not found: ", s.externalUIDownloadDetour) return E.New("detour outbound not found: ", s.externalUIDownloadDetour)
} }
detour = outbound detour = outbound
} else { } else {
outbound := s.outboundManager.Default() outbound := s.outbound.Default()
detour = outbound detour = outbound
} }
httpClient := &http.Client{ httpClient := &http.Client{

View File

@@ -16,30 +16,18 @@ import (
) )
type Manager struct { type Manager struct {
uploadTemp atomic.Int64
downloadTemp atomic.Int64
uploadBlip atomic.Int64
downloadBlip atomic.Int64
uploadTotal atomic.Int64 uploadTotal atomic.Int64
downloadTotal atomic.Int64 downloadTotal atomic.Int64
connections compatible.Map[uuid.UUID, Tracker] connections compatible.Map[uuid.UUID, Tracker]
closedConnectionsAccess sync.Mutex closedConnectionsAccess sync.Mutex
closedConnections list.List[TrackerMetadata] closedConnections list.List[TrackerMetadata]
ticker *time.Ticker
done chan struct{}
// process *process.Process // process *process.Process
memory uint64 memory uint64
} }
func NewManager() *Manager { func NewManager() *Manager {
manager := &Manager{ return &Manager{}
ticker: time.NewTicker(time.Second),
done: make(chan struct{}),
// process: &process.Process{Pid: int32(os.Getpid())},
}
go manager.handle()
return manager
} }
func (m *Manager) Join(c Tracker) { func (m *Manager) Join(c Tracker) {
@@ -61,19 +49,13 @@ func (m *Manager) Leave(c Tracker) {
} }
func (m *Manager) PushUploaded(size int64) { func (m *Manager) PushUploaded(size int64) {
m.uploadTemp.Add(size)
m.uploadTotal.Add(size) m.uploadTotal.Add(size)
} }
func (m *Manager) PushDownloaded(size int64) { func (m *Manager) PushDownloaded(size int64) {
m.downloadTemp.Add(size)
m.downloadTotal.Add(size) m.downloadTotal.Add(size)
} }
func (m *Manager) Now() (up int64, down int64) {
return m.uploadBlip.Load(), m.downloadBlip.Load()
}
func (m *Manager) Total() (up int64, down int64) { func (m *Manager) Total() (up int64, down int64) {
return m.uploadTotal.Load(), m.downloadTotal.Load() return m.uploadTotal.Load(), m.downloadTotal.Load()
} }
@@ -127,36 +109,10 @@ func (m *Manager) Snapshot() *Snapshot {
} }
func (m *Manager) ResetStatistic() { func (m *Manager) ResetStatistic() {
m.uploadTemp.Store(0)
m.uploadBlip.Store(0)
m.uploadTotal.Store(0) m.uploadTotal.Store(0)
m.downloadTemp.Store(0)
m.downloadBlip.Store(0)
m.downloadTotal.Store(0) m.downloadTotal.Store(0)
} }
func (m *Manager) handle() {
var uploadTemp int64
var downloadTemp int64
for {
select {
case <-m.done:
return
case <-m.ticker.C:
}
uploadTemp = m.uploadTemp.Swap(0)
downloadTemp = m.downloadTemp.Swap(0)
m.uploadBlip.Store(uploadTemp)
m.downloadBlip.Store(downloadTemp)
}
}
func (m *Manager) Close() error {
m.ticker.Stop()
close(m.done)
return nil
}
type Snapshot struct { type Snapshot struct {
Download int64 Download int64
Upload int64 Upload int64

View File

@@ -33,17 +33,31 @@ func (n Note) Impending() bool {
} }
func (n Note) Message() string { func (n Note) Message() string {
return F.ToString( if n.MigrationLink != "" {
n.Description, " is deprecated in sing-box ", n.DeprecatedVersion, return F.ToString(
" and will be removed in sing-box ", n.ScheduledVersion, ", please checkout documentation for migration.", n.Description, " is deprecated in sing-box ", n.DeprecatedVersion,
) " and will be removed in sing-box ", n.ScheduledVersion, ", please checkout documentation for migration.",
)
} else {
return F.ToString(
n.Description, " is deprecated in sing-box ", n.DeprecatedVersion,
" and will be removed in sing-box ", n.ScheduledVersion, ".",
)
}
} }
func (n Note) MessageWithLink() string { func (n Note) MessageWithLink() string {
return F.ToString( if n.MigrationLink != "" {
n.Description, " is deprecated in sing-box ", n.DeprecatedVersion, return F.ToString(
" and will be removed in sing-box ", n.ScheduledVersion, ", checkout documentation for migration: ", n.MigrationLink, n.Description, " is deprecated in sing-box ", n.DeprecatedVersion,
) " and will be removed in sing-box ", n.ScheduledVersion, ", checkout documentation for migration: ", n.MigrationLink,
)
} else {
return F.ToString(
n.Description, " is deprecated in sing-box ", n.DeprecatedVersion,
" and will be removed in sing-box ", n.ScheduledVersion, ".",
)
}
} }
var OptionBadMatchSource = Note{ var OptionBadMatchSource = Note{
@@ -109,6 +123,32 @@ var OptionDestinationOverrideFields = Note{
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-destination-override-fields-to-route-options", MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-destination-override-fields-to-route-options",
} }
var OptionWireGuardOutbound = Note{
Name: "wireguard-outbound",
Description: "legacy wireguard outbound",
DeprecatedVersion: "1.11.0",
ScheduledVersion: "1.13.0",
EnvName: "WIREGUARD_OUTBOUND",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint",
}
var OptionWireGuardGSO = Note{
Name: "wireguard-gso",
Description: "GSO option in wireguard outbound",
DeprecatedVersion: "1.11.0",
ScheduledVersion: "1.13.0",
EnvName: "WIREGUARD_GSO",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint",
}
var OptionTUNGSO = Note{
Name: "tun-gso",
Description: "GSO option in tun",
DeprecatedVersion: "1.11.0",
ScheduledVersion: "1.12.0",
EnvName: "TUN_GSO",
}
var Options = []Note{ var Options = []Note{
OptionBadMatchSource, OptionBadMatchSource,
OptionGEOIP, OptionGEOIP,
@@ -117,4 +157,7 @@ var Options = []Note{
OptionSpecialOutbounds, OptionSpecialOutbounds,
OptionInboundOptions, OptionInboundOptions,
OptionDestinationOverrideFields, OptionDestinationOverrideFields,
OptionWireGuardOutbound,
OptionWireGuardGSO,
OptionTUNGSO,
} }

View File

@@ -34,5 +34,5 @@ func (f *stderrManager) ReportDeprecated(feature Note) {
return return
} }
f.logger.Error(feature.MessageWithLink()) f.logger.Error(feature.MessageWithLink())
f.logger.Fatal("to continuing using this feature, set ENABLE_DEPRECATED_" + feature.EnvName + "=true") f.logger.Fatal("to continuing using this feature, set environment variable ENABLE_DEPRECATED_" + feature.EnvName + "=true")
} }

View File

@@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"time" "time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
@@ -113,11 +114,24 @@ func (c *CommandClient) Connect() error {
if err != nil { if err != nil {
return err return err
} }
c.handler.Connected() if C.FixAndroidStack {
c.handler.InitializeClashMode(newIterator(modeList), currentMode) go func() {
c.handler.Connected()
c.handler.InitializeClashMode(newIterator(modeList), currentMode)
if len(modeList) == 0 {
conn.Close()
c.handler.Disconnected(os.ErrInvalid.Error())
}
}()
} else {
c.handler.Connected()
c.handler.InitializeClashMode(newIterator(modeList), currentMode)
if len(modeList) == 0 {
conn.Close()
c.handler.Disconnected(os.ErrInvalid.Error())
}
}
if len(modeList) == 0 { if len(modeList) == 0 {
conn.Close()
c.handler.Disconnected(os.ErrInvalid.Error())
return nil return nil
} }
go c.handleModeConn(conn) go c.handleModeConn(conn)

View File

@@ -33,7 +33,6 @@ func (s *CommandServer) readStatus() StatusMessage {
if s.service != nil { if s.service != nil {
message.TrafficAvailable = true message.TrafficAvailable = true
trafficManager := s.service.clashServer.(*clashapi.Server).TrafficManager() trafficManager := s.service.clashServer.(*clashapi.Server).TrafficManager()
message.Uplink, message.Downlink = trafficManager.Now()
message.UplinkTotal, message.DownlinkTotal = trafficManager.Total() message.UplinkTotal, message.DownlinkTotal = trafficManager.Total()
message.ConnectionsIn = int32(trafficManager.ConnectionsLen()) message.ConnectionsIn = int32(trafficManager.ConnectionsLen())
} }
@@ -50,8 +49,11 @@ func (s *CommandServer) handleStatusConn(conn net.Conn) error {
ticker := time.NewTicker(time.Duration(interval)) ticker := time.NewTicker(time.Duration(interval))
defer ticker.Stop() defer ticker.Stop()
ctx := connKeepAlive(conn) ctx := connKeepAlive(conn)
status := s.readStatus()
uploadTotal := status.UplinkTotal
downloadTotal := status.DownlinkTotal
for { for {
err = binary.Write(conn, binary.BigEndian, s.readStatus()) err = binary.Write(conn, binary.BigEndian, status)
if err != nil { if err != nil {
return err return err
} }
@@ -60,6 +62,13 @@ func (s *CommandServer) handleStatusConn(conn net.Conn) error {
return ctx.Err() return ctx.Err()
case <-ticker.C: case <-ticker.C:
} }
status = s.readStatus()
upload := status.UplinkTotal - uploadTotal
download := status.DownlinkTotal - downloadTotal
uploadTotal = status.UplinkTotal
downloadTotal = status.DownlinkTotal
status.Uplink = upload
status.Downlink = download
} }
} }

View File

@@ -30,7 +30,7 @@ func parseConfig(ctx context.Context, configContent string) (option.Options, err
} }
func CheckConfig(configContent string) error { func CheckConfig(configContent string) error {
ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()) ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry())
options, err := parseConfig(ctx, configContent) options, err := parseConfig(ctx, configContent)
if err != nil { if err != nil {
return err return err
@@ -130,17 +130,17 @@ func (s *platformInterfaceStub) SendNotification(notification *platform.Notifica
return nil return nil
} }
func FormatConfig(configContent string) (string, error) { func FormatConfig(configContent string) (*StringBox, error) {
options, err := parseConfig(box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()), configContent) options, err := parseConfig(box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry()), configContent)
if err != nil { if err != nil {
return "", err return nil, err
} }
var buffer bytes.Buffer var buffer bytes.Buffer
encoder := json.NewEncoder(&buffer) encoder := json.NewEncoder(&buffer)
encoder.SetIndent("", " ") encoder.SetIndent("", " ")
err = encoder.Encode(options) err = encoder.Encode(options)
if err != nil { if err != nil {
return "", err return nil, err
} }
return buffer.String(), nil return wrapString(buffer.String()), nil
} }

View File

@@ -50,8 +50,7 @@ type HTTPRequest interface {
} }
type HTTPResponse interface { type HTTPResponse interface {
GetContent() ([]byte, error) GetContent() (*StringBox, error)
GetContentString() (string, error)
WriteTo(path string) error WriteTo(path string) error
} }
@@ -210,27 +209,22 @@ type httpResponse struct {
} }
func (h *httpResponse) errorString() string { func (h *httpResponse) errorString() string {
content, err := h.GetContentString() content, err := h.GetContent()
if err != nil { if err != nil {
return fmt.Sprint("HTTP ", h.Status) return fmt.Sprint("HTTP ", h.Status)
} }
return fmt.Sprint("HTTP ", h.Status, ": ", content) return fmt.Sprint("HTTP ", h.Status, ": ", content)
} }
func (h *httpResponse) GetContent() ([]byte, error) { func (h *httpResponse) GetContent() (*StringBox, error) {
h.getContentOnce.Do(func() { h.getContentOnce.Do(func() {
defer h.Body.Close() defer h.Body.Close()
h.content, h.contentError = io.ReadAll(h.Body) h.content, h.contentError = io.ReadAll(h.Body)
}) })
return h.content, h.contentError if h.contentError != nil {
} return nil, h.contentError
func (h *httpResponse) GetContentString() (string, error) {
content, err := h.GetContent()
if err != nil {
return "", err
} }
return string(content), nil return wrapString(string(h.content)), nil
} }
func (h *httpResponse) WriteTo(path string) error { func (h *httpResponse) WriteTo(path string) error {

View File

@@ -1,6 +1,7 @@
package libbox package libbox
import ( import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -55,6 +56,14 @@ func (m *platformDefaultInterfaceMonitor) UnregisterCallback(element *list.Eleme
} }
func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) { func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) {
if C.FixAndroidStack {
go m.updateDefaultInterface(interfaceName, interfaceIndex32, isExpensive, isConstrained)
} else {
m.updateDefaultInterface(interfaceName, interfaceIndex32, isExpensive, isConstrained)
}
}
func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) {
m.isExpensive = isExpensive m.isExpensive = isExpensive
m.isConstrained = isConstrained m.isConstrained = isConstrained
err := m.networkManager.UpdateInterfaces() err := m.networkManager.UpdateInterfaces()

View File

@@ -0,0 +1,12 @@
package libbox
// https://github.com/golang/go/issues/46893
// TODO: remove after `bulkBarrierPreWrite: unaligned arguments` fixed
type StringBox struct {
Value string
}
func wrapString(value string) *StringBox {
return &StringBox{Value: value}
}

View File

@@ -44,7 +44,7 @@ type BoxService struct {
} }
func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) { func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()) ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry())
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager)) service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
options, err := parseConfig(ctx, configContent) options, err := parseConfig(ctx, configContent)
@@ -81,23 +81,36 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
} }
func (s *BoxService) Start() error { func (s *BoxService) Start() error {
return s.instance.Start() if C.FixAndroidStack {
var err error
done := make(chan struct{})
go func() {
err = s.instance.Start()
close(done)
}()
<-done
return err
} else {
return s.instance.Start()
}
} }
func (s *BoxService) Close() error { func (s *BoxService) Close() error {
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-done:
return
case <-time.After(C.FatalStopTimeout):
os.Exit(1)
}
}()
s.cancel() s.cancel()
s.urlTestHistoryStorage.Close() s.urlTestHistoryStorage.Close()
return s.instance.Close() var err error
done := make(chan struct{})
go func() {
err = s.instance.Close()
close(done)
}()
select {
case <-done:
return err
case <-time.After(C.FatalStopTimeout):
os.Exit(1)
return nil
}
} }
func (s *BoxService) NeedWIFIState() bool { func (s *BoxService) NeedWIFIState() bool {

View File

@@ -13,12 +13,12 @@ func ClearServiceError() {
os.Remove(serviceErrorPath()) os.Remove(serviceErrorPath())
} }
func ReadServiceError() (string, error) { func ReadServiceError() (*StringBox, error) {
data, err := os.ReadFile(serviceErrorPath()) data, err := os.ReadFile(serviceErrorPath())
if err == nil { if err == nil {
os.Remove(serviceErrorPath()) os.Remove(serviceErrorPath())
} }
return string(data), err return wrapString(string(data)), err
} }
func WriteServiceError(message string) error { func WriteServiceError(message string) error {

21
go.mod
View File

@@ -22,21 +22,21 @@ 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-20241021032506-a4324256e4a3 github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff
github.com/sagernet/quic-go v0.48.1-beta.1 github.com/sagernet/quic-go v0.48.2-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.0-alpha.14 github.com/sagernet/sing v0.6.0-beta.6
github.com/sagernet/sing-dns v0.4.0-alpha.2 github.com/sagernet/sing-dns v0.4.0-beta.1
github.com/sagernet/sing-mux v0.3.0-alpha.1 github.com/sagernet/sing-mux v0.3.0-alpha.1
github.com/sagernet/sing-quic v0.4.0-alpha.3 github.com/sagernet/sing-quic v0.4.0-alpha.4
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.1.4 github.com/sagernet/sing-shadowtls v0.2.0-alpha.2
github.com/sagernet/sing-tun v0.6.0-alpha.8 github.com/sagernet/sing-tun v0.6.0-beta.2
github.com/sagernet/sing-vmess v0.1.12 github.com/sagernet/sing-vmess v0.2.0-beta.1
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
github.com/sagernet/utls v1.6.7 github.com/sagernet/utls v1.6.7
github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 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
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
@@ -53,6 +53,8 @@ require (
howett.net/plist v1.0.1 howett.net/plist v1.0.1
) )
//replace github.com/sagernet/sing => ../sing
require ( require (
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
@@ -90,6 +92,7 @@ require (
golang.org/x/text v0.20.0 // indirect golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect golang.org/x/tools v0.24.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

38
go.sum
View File

@@ -99,41 +99,41 @@ 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-20241021032506-a4324256e4a3 h1:RxEz7LhPNiF/gX/Hg+OXr5lqsM9iVAgmaK1L1vzlDRM= github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff h1:mlohw3360Wg1BNGook/UHnISXhUx4Gd/3tVLs5T0nSs=
github.com/sagernet/gvisor v0.0.0-20241021032506-a4324256e4a3/go.mod h1:ehZwnT2UpmOWAHFL48XdBhnd4Qu4hN2O3Ji0us3ZHMw= github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff/go.mod h1:ehZwnT2UpmOWAHFL48XdBhnd4Qu4hN2O3Ji0us3ZHMw=
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=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.48.1-beta.1 h1:ElPaV5yzlXIKZpqFMAcUGax6vddi3zt4AEpT94Z0vwo= github.com/sagernet/quic-go v0.48.2-beta.1 h1:W0plrLWa1XtOWDTdX3CJwxmQuxkya12nN5BRGZ87kEg=
github.com/sagernet/quic-go v0.48.1-beta.1/go.mod h1:1WgdDIVD1Gybp40JTWketeSfKA/+or9YMLaG5VeTk4k= github.com/sagernet/quic-go v0.48.2-beta.1/go.mod h1:1WgdDIVD1Gybp40JTWketeSfKA/+or9YMLaG5VeTk4k=
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.0-alpha.14 h1:ORh6yQwLL+/nv+rklrO2W4k+zgf3ZzaOl/83vQbJUl4= github.com/sagernet/sing v0.6.0-beta.6 h1:IFnTCG06Z5rLMZJqw1ZmDncDl2N9gsVw0MGvgakrpg8=
github.com/sagernet/sing v0.6.0-alpha.14/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.6.0-beta.6/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-dns v0.4.0-alpha.2 h1:0x5WjrO+Ifk9sqJlHRz/tKENHwoEinQ8HQCHAhpJHAQ= github.com/sagernet/sing-dns v0.4.0-beta.1 h1:W1XkdhigwxDOMgMDVB+9kdomCpb7ExsZfB4acPcTZFY=
github.com/sagernet/sing-dns v0.4.0-alpha.2/go.mod h1:ZiXcacKL54jSSYZMbYF3qKNFkkW674Jt+85YCmK64K8= github.com/sagernet/sing-dns v0.4.0-beta.1/go.mod h1:8wuFcoFkWM4vJuQyg8e97LyvDwe0/Vl7G839WLcKDs8=
github.com/sagernet/sing-mux v0.3.0-alpha.1 h1:IgNX5bJBpL41gGbp05pdDOvh/b5eUQ6cv9240+Ngipg= github.com/sagernet/sing-mux v0.3.0-alpha.1 h1:IgNX5bJBpL41gGbp05pdDOvh/b5eUQ6cv9240+Ngipg=
github.com/sagernet/sing-mux v0.3.0-alpha.1/go.mod h1:FTcImmdfW38Lz7b+HQ+mxxOth1lz4ao8uEnz+MwIJQE= github.com/sagernet/sing-mux v0.3.0-alpha.1/go.mod h1:FTcImmdfW38Lz7b+HQ+mxxOth1lz4ao8uEnz+MwIJQE=
github.com/sagernet/sing-quic v0.4.0-alpha.3 h1:2svvOqgQCJg7FNrIrLTaRB6oDzXPiIyWIt9csjZxD6Q= github.com/sagernet/sing-quic v0.4.0-alpha.4 h1:P9xAx3nIfcqb9M8jfgs0uLm+VxCcaY++FCqaBfHY3dQ=
github.com/sagernet/sing-quic v0.4.0-alpha.3/go.mod h1:Fmnpy0XoyYdjJrxNqEyl3LC9uLibMNNbxG7dt6HATY4= github.com/sagernet/sing-quic v0.4.0-alpha.4/go.mod h1:h5RkKTmUhudJKzK7c87FPXD5w1bJjVyxMN9+opZcctA=
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.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 h1:RPrpgAdkP5td0vLfS5ldvYosFjSsZtRPxiyLV6jyKg0=
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= github.com/sagernet/sing-shadowtls v0.2.0-alpha.2/go.mod h1:0j5XlzKxaWRIEjc1uiSKmVoWb0k+L9QgZVb876+thZA=
github.com/sagernet/sing-tun v0.6.0-alpha.8 h1:HhXyUvXxtaXgT+IILZMq6kbrAyDbUwbN+Df/XxpL7Vo= github.com/sagernet/sing-tun v0.6.0-beta.2 h1:GK7r2jWKm7RhlJGTq4QadgFcebQia1c3BO3OlYMcQJ0=
github.com/sagernet/sing-tun v0.6.0-alpha.8/go.mod h1:JkgiLLnQUXln1zLGVoJqUwAulJGT0xoiPU4/pYF1fhU= github.com/sagernet/sing-tun v0.6.0-beta.2/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-vmess v0.1.12 h1:2gFD8JJb+eTFMoa8FIVMnknEi+vCSfaiTXTfEYAYAPg= github.com/sagernet/sing-vmess v0.2.0-beta.1 h1:5sXQ23uwNlZuDvygzi0dFtnG0Csm/SNqTjAHXJkpuj4=
github.com/sagernet/sing-vmess v0.1.12/go.mod h1:luTSsfyBGAc9VhtCqwjR+dt1QgqBhuYBCONB/POhF8I= github.com/sagernet/sing-vmess v0.2.0-beta.1/go.mod h1:fLyE1emIcvQ5DV8reFWnufquZ7MkCSYM5ThodsR9NrQ=
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/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.0-20231215174105-89dec3b2f3e8 h1:R0OMYAScomNAVpTfbHFpxqJpvwuhxSRi+g6z7gZhABs= github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8/go.mod h1:K4J7/npM+VAMUeUmTa2JaA02JmyheP0GpRBOUvn3ecc= github.com/sagernet/wireguard-go v0.0.1-beta.5/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@@ -195,6 +195,8 @@ golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
@@ -82,6 +83,14 @@ func OutboundRegistry() *outbound.Registry {
return registry return registry
} }
func EndpointRegistry() *endpoint.Registry {
registry := endpoint.NewRegistry()
registerWireGuardEndpoint(registry)
return registry
}
func registerStubForRemovedInbounds(registry *inbound.Registry) { func registerStubForRemovedInbounds(registry *inbound.Registry) {
inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0") return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")

View File

@@ -3,6 +3,7 @@
package include package include
import ( import (
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/protocol/wireguard" "github.com/sagernet/sing-box/protocol/wireguard"
) )
@@ -10,3 +11,7 @@ import (
func registerWireGuardOutbound(registry *outbound.Registry) { func registerWireGuardOutbound(registry *outbound.Registry) {
wireguard.RegisterOutbound(registry) wireguard.RegisterOutbound(registry)
} }
func registerWireGuardEndpoint(registry *endpoint.Registry) {
wireguard.RegisterEndpoint(registry)
}

View File

@@ -6,6 +6,7 @@ import (
"context" "context"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
@@ -14,7 +15,13 @@ import (
) )
func registerWireGuardOutbound(registry *outbound.Registry) { func registerWireGuardOutbound(registry *outbound.Registry) {
outbound.Register[option.WireGuardOutboundOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardOutboundOptions) (adapter.Outbound, error) { outbound.Register[option.LegacyWireGuardOutboundOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.LegacyWireGuardOutboundOptions) (adapter.Outbound, error) {
return nil, E.New(`WireGuard is not included in this build, rebuild with -tags with_wireguard`)
})
}
func registerWireGuardEndpoint(registry *endpoint.Registry) {
endpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardEndpointOptions) (adapter.Endpoint, error) {
return nil, E.New(`WireGuard is not included in this build, rebuild with -tags with_wireguard`) return nil, E.New(`WireGuard is not included in this build, rebuild with -tags with_wireguard`)
}) })
} }

View File

@@ -112,6 +112,9 @@ nav:
- V2Ray Transport: configuration/shared/v2ray-transport.md - V2Ray Transport: configuration/shared/v2ray-transport.md
- UDP over TCP: configuration/shared/udp-over-tcp.md - UDP over TCP: configuration/shared/udp-over-tcp.md
- TCP Brutal: configuration/shared/tcp-brutal.md - TCP Brutal: configuration/shared/tcp-brutal.md
- Endpoint:
- configuration/endpoint/index.md
- WireGuard: configuration/endpoint/wireguard.md
- Inbound: - Inbound:
- configuration/inbound/index.md - configuration/inbound/index.md
- Direct: configuration/inbound/direct.md - Direct: configuration/inbound/direct.md
@@ -241,6 +244,7 @@ plugins:
Multiplex: 多路复用 Multiplex: 多路复用
V2Ray Transport: V2Ray 传输层 V2Ray Transport: V2Ray 传输层
Endpoint: 端点
Inbound: 入站 Inbound: 入站
Outbound: 出站 Outbound: 出站

47
option/endpoint.go Normal file
View File

@@ -0,0 +1,47 @@
package option
import (
"context"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/service"
)
type EndpointOptionsRegistry interface {
CreateOptions(endpointType string) (any, bool)
}
type _Endpoint struct {
Type string `json:"type"`
Tag string `json:"tag,omitempty"`
Options any `json:"-"`
}
type Endpoint _Endpoint
func (h *Endpoint) MarshalJSONContext(ctx context.Context) ([]byte, error) {
return badjson.MarshallObjectsContext(ctx, (*_Endpoint)(h), h.Options)
}
func (h *Endpoint) UnmarshalJSONContext(ctx context.Context, content []byte) error {
err := json.UnmarshalContext(ctx, content, (*_Endpoint)(h))
if err != nil {
return err
}
registry := service.FromContext[EndpointOptionsRegistry](ctx)
if registry == nil {
return E.New("missing Endpoint fields registry in context")
}
options, loaded := registry.CreateOptions(h.Type)
if !loaded {
return E.New("unknown inbound type: ", h.Type)
}
err = badjson.UnmarshallExcludedContext(ctx, content, (*_Endpoint)(h), options)
if err != nil {
return err
}
h.Options = options
return nil
}

View File

@@ -1,5 +1,15 @@
package option package option
import (
"net/url"
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 Hysteria2InboundOptions struct { type Hysteria2InboundOptions struct {
ListenOptions ListenOptions
UpMbps int `json:"up_mbps,omitempty"` UpMbps int `json:"up_mbps,omitempty"`
@@ -8,8 +18,8 @@ type Hysteria2InboundOptions struct {
Users []Hysteria2User `json:"users,omitempty"` Users []Hysteria2User `json:"users,omitempty"`
IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"` IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"`
InboundTLSOptionsContainer InboundTLSOptionsContainer
Masquerade string `json:"masquerade,omitempty"` Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"`
BrutalDebug bool `json:"brutal_debug,omitempty"` BrutalDebug bool `json:"brutal_debug,omitempty"`
} }
type Hysteria2Obfs struct { type Hysteria2Obfs struct {
@@ -22,6 +32,82 @@ type Hysteria2User struct {
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
} }
type _Hysteria2Masquerade struct {
Type string `json:"type,omitempty"`
FileOptions Hysteria2MasqueradeFile `json:"-"`
ProxyOptions Hysteria2MasqueradeProxy `json:"-"`
StringOptions Hysteria2MasqueradeString `json:"-"`
}
type Hysteria2Masquerade _Hysteria2Masquerade
func (m Hysteria2Masquerade) MarshalJSON() ([]byte, error) {
var v any
switch m.Type {
case C.Hysterai2MasqueradeTypeFile:
v = m.FileOptions
case C.Hysterai2MasqueradeTypeProxy:
v = m.ProxyOptions
case C.Hysterai2MasqueradeTypeString:
v = m.StringOptions
default:
return nil, E.New("unknown masquerade type: ", m.Type)
}
return badjson.MarshallObjects((_Hysteria2Masquerade)(m), v)
}
func (m *Hysteria2Masquerade) UnmarshalJSON(bytes []byte) error {
var urlString string
err := json.Unmarshal(bytes, &urlString)
if err == nil {
masqueradeURL, err := url.Parse(urlString)
if err != nil {
return E.Cause(err, "invalid masquerade URL")
}
switch masqueradeURL.Scheme {
case "file":
m.Type = C.Hysterai2MasqueradeTypeFile
m.FileOptions.Directory = masqueradeURL.Path
case "http", "https":
m.Type = C.Hysterai2MasqueradeTypeProxy
m.ProxyOptions.URL = urlString
default:
return E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme)
}
}
err = json.Unmarshal(bytes, (*_Hysteria2Masquerade)(m))
if err != nil {
return err
}
var v any
switch m.Type {
case C.Hysterai2MasqueradeTypeFile:
v = &m.FileOptions
case C.Hysterai2MasqueradeTypeProxy:
v = &m.ProxyOptions
case C.Hysterai2MasqueradeTypeString:
v = &m.StringOptions
default:
return E.New("unknown masquerade type: ", m.Type)
}
return badjson.UnmarshallExcluded(bytes, (*_Hysteria2Masquerade)(m), v)
}
type Hysteria2MasqueradeFile struct {
Directory string `json:"directory"`
}
type Hysteria2MasqueradeProxy struct {
URL string `json:"url"`
RewriteHost bool `json:"rewrite_host,omitempty"`
}
type Hysteria2MasqueradeString struct {
StatusCode int `json:"status_code,omitempty"`
Headers badoption.HTTPHeader `json:"headers,omitempty"`
Content string `json:"content"`
}
type Hysteria2OutboundOptions struct { type Hysteria2OutboundOptions struct {
DialerOptions DialerOptions
ServerOptions ServerOptions

View File

@@ -28,7 +28,7 @@ func (h *Inbound) MarshalJSONContext(ctx context.Context) ([]byte, error) {
} }
func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) error { func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) error {
err := json.Unmarshal(content, (*_Inbound)(h)) err := json.UnmarshalContext(ctx, content, (*_Inbound)(h))
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,98 +0,0 @@
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"
)
type _LegacyInbound struct {
Type string `json:"type"`
Tag string `json:"tag,omitempty"`
TunOptions TunInboundOptions `json:"-"`
RedirectOptions RedirectInboundOptions `json:"-"`
TProxyOptions TProxyInboundOptions `json:"-"`
DirectOptions DirectInboundOptions `json:"-"`
SocksOptions SocksInboundOptions `json:"-"`
HTTPOptions HTTPMixedInboundOptions `json:"-"`
MixedOptions HTTPMixedInboundOptions `json:"-"`
ShadowsocksOptions ShadowsocksInboundOptions `json:"-"`
VMessOptions VMessInboundOptions `json:"-"`
TrojanOptions TrojanInboundOptions `json:"-"`
NaiveOptions NaiveInboundOptions `json:"-"`
HysteriaOptions HysteriaInboundOptions `json:"-"`
ShadowTLSOptions ShadowTLSInboundOptions `json:"-"`
VLESSOptions VLESSInboundOptions `json:"-"`
TUICOptions TUICInboundOptions `json:"-"`
Hysteria2Options Hysteria2InboundOptions `json:"-"`
}
type LegacyInbound _LegacyInbound
func (h *LegacyInbound) RawOptions() (any, error) {
var rawOptionsPtr any
switch h.Type {
case C.TypeTun:
rawOptionsPtr = &h.TunOptions
case C.TypeRedirect:
rawOptionsPtr = &h.RedirectOptions
case C.TypeTProxy:
rawOptionsPtr = &h.TProxyOptions
case C.TypeDirect:
rawOptionsPtr = &h.DirectOptions
case C.TypeSOCKS:
rawOptionsPtr = &h.SocksOptions
case C.TypeHTTP:
rawOptionsPtr = &h.HTTPOptions
case C.TypeMixed:
rawOptionsPtr = &h.MixedOptions
case C.TypeShadowsocks:
rawOptionsPtr = &h.ShadowsocksOptions
case C.TypeVMess:
rawOptionsPtr = &h.VMessOptions
case C.TypeTrojan:
rawOptionsPtr = &h.TrojanOptions
case C.TypeNaive:
rawOptionsPtr = &h.NaiveOptions
case C.TypeHysteria:
rawOptionsPtr = &h.HysteriaOptions
case C.TypeShadowTLS:
rawOptionsPtr = &h.ShadowTLSOptions
case C.TypeVLESS:
rawOptionsPtr = &h.VLESSOptions
case C.TypeTUIC:
rawOptionsPtr = &h.TUICOptions
case C.TypeHysteria2:
rawOptionsPtr = &h.Hysteria2Options
case "":
return nil, E.New("missing inbound type")
default:
return nil, E.New("unknown inbound type: ", h.Type)
}
return rawOptionsPtr, nil
}
func (h LegacyInbound) MarshalJSON() ([]byte, error) {
rawOptions, err := h.RawOptions()
if err != nil {
return nil, err
}
return badjson.MarshallObjects((_LegacyInbound)(h), rawOptions)
}
func (h *LegacyInbound) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_LegacyInbound)(h))
if err != nil {
return err
}
rawOptions, err := h.RawOptions()
if err != nil {
return err
}
err = badjson.UnmarshallExcluded(bytes, (*_LegacyInbound)(h), rawOptions)
if err != nil {
return err
}
return nil
}

View File

@@ -13,15 +13,11 @@ type _Options struct {
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"` NTP *NTPOptions `json:"ntp,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"`
// Deprecated: use Inbounds instead
LegacyInbounds []LegacyInbound `json:"-"`
// Deprecated: use Outbounds instead
LegacyOutbounds []LegacyOutbound `json:"-"`
} }
type Options _Options type Options _Options

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