Compare commits

..

11 Commits

Author SHA1 Message Date
世界
47736b27ba refactor: Modular inbounds 2024-11-03 20:24:41 +08:00
世界
0b2c7ec35c refactor: Modular outbounds 2024-11-03 19:56:44 +08:00
世界
e537c56b6b Implement dns-hijack 2024-11-03 19:47:17 +08:00
世界
b456aff4ac Implement resolve(server) 2024-11-03 19:47:15 +08:00
世界
4da652ff02 Implement TCP and ICMP rejects 2024-11-03 19:47:11 +08:00
世界
262727ec6c Crazy sekai overturns the small pond 2024-11-03 19:47:09 +08:00
世界
81dc9e7698 documentation: Bump version 2024-11-02 21:40:38 +08:00
世界
d876077281 Update dependencies 2024-11-02 21:40:28 +08:00
世界
0df81c297b Update quic-go to v0.48.0 2024-11-02 21:40:28 +08:00
世界
b460484e43 Fix "Fix metadata context" 2024-11-02 21:37:04 +08:00
世界
0e511791e8 platform: Add openURL event 2024-11-02 17:13:13 +08:00
281 changed files with 7006 additions and 6109 deletions

View File

@@ -1,599 +0,0 @@
name: Build
on:
workflow_dispatch:
inputs:
version:
description: "Version name"
required: true
type: string
build:
description: "Build type"
required: true
type: choice
default: "All"
options:
- All
- Binary
- Android
- Apple
- app-store
- iOS
- macOS
- tvOS
- macOS-standalone
- publish-android
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 }}
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 "version=${{ inputs.version }}" >> "$GITHUB_ENV"
- name: Calculate version
if: github.event_name != 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/read_tag --nightly
- name: Set outputs
id: outputs
run: |-
echo "version=$version" >> "$GITHUB_OUTPUT"
build:
name: Build binary
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 ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
- name: 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 ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
- name: 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: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
run: |-
cd clients/android
git checkout main
- name: Checkout dev branch
if: github.ref == 'refs/heads/dev-next'
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 ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
- name: 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: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
run: |-
cd clients/android
git checkout main
- name: Checkout dev branch
if: github.ref == 'refs/heads/dev-next'
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:
name: Build Apple clients
runs-on: macos-15
needs:
- calculate_version
strategy:
matrix:
include:
- name: iOS
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'iOS' }}
platform: 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' }}
platform: 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' }}
platform: 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' }}
platform: macos
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 stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.2.app
- name: Setup Xcode beta
if: matrix.if && github.ref == 'refs/heads/dev-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.2.app
- name: Set tag
if: matrix.if
run: |-
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Checkout main branch
if: matrix.if && github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
run: |-
cd clients/apple
git checkout main
- name: Checkout dev branch
if: matrix.if && github.ref == 'refs/heads/dev-next'
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
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
env:
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: Build library
if: matrix.if
run: |-
make lib_install
export PATH="$PATH:$(go env GOPATH)/bin"
go run ./cmd/internal/build_libbox -target apple -platform ${{ matrix.platform }}
mv Libbox.xcframework clients/apple
- name: Update macOS version
if: matrix.if && matrix.name == 'macOS' && github.event_name == 'workflow_dispatch'
run: |-
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
- name: Build
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 $ASC_KEY_PATH \
-authenticationKeyID $ASC_KEY_ID \
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
- name: Upload to App Store Connect
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch'
run: |-
go run -v ./cmd/internal/app_store_connect cancel_app_store ${{ matrix.platform }}
cd clients/apple
xcodebuild -exportArchive \
-archivePath "${{ matrix.archive }}" \
-exportOptionsPlist ${{ matrix.upload }} \
-allowProvisioningUpdates \
-authenticationKeyPath $ASC_KEY_PATH \
-authenticationKeyID $ASC_KEY_ID \
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
- name: Publish to TestFlight
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/dev-next'
run: |-
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
- 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 == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')
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 ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
git tag v${{ needs.calculate_version.outputs.version }} -f
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
- name: Download builds
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Merge builds
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
run: |-
goreleaser continue --merge --skip publish
mkdir -p dist/release
mv dist/*/sing-box*{tar.gz,zip,deb,rpm,_amd64.pkg.tar.zst,_arm64.pkg.tar.zst} dist/release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- name: Upload builds
if: ${{ env.PUBLISHED == 'false' }}
run: |-
export PATH="$PATH:$HOME/go/bin"
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Replace builds
if: ${{ env.PUBLISHED != 'false' }}
run: |-
export PATH="$PATH:$HOME/go/bin"
ghr --replace -p 5 "v${VERSION}" dist/release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

219
.github/workflows/debug.yml vendored Normal file
View File

@@ -0,0 +1,219 @@
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,6 +22,7 @@ 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

@@ -22,16 +22,6 @@ linters-settings:
run: run:
go: "1.23" go: "1.23"
build-tags:
- with_gvisor
- with_quic
- with_dhcp
- with_wireguard
- with_ech
- with_utls
- with_reality_server
- with_acme
- with_clash_api
issues: issues:
exclude-dirs: exclude-dirs:

View File

@@ -200,6 +200,4 @@ 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 5 "v${VERSION}" dist/release ghr --replace --draft --prerelease -p 3 "v${VERSION}" dist/release
rm -r dist/release rm -r dist/release
release_repo: release_repo:
@@ -90,20 +90,22 @@ 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 5 "v${VERSION}" dist/release_android ghr --replace --draft --prerelease -p 3 "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
publish_android: publish_android:
cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle && ./gradlew --stop cd ../sing-box-for-android && ./gradlew :app:publishPlayReleaseBundle
publish_android_appcenter:
cd ../sing-box-for-android && ./gradlew :app:appCenterAssembleAndUploadPlayRelease
# 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:
@@ -145,13 +147,9 @@ build_macos_dmg:
--hide-extension "SFM.app" \ --hide-extension "SFM.app" \
--app-drop-link 0 0 \ --app-drop-link 0 0 \
--skip-jenkins \ --skip-jenkins \
--notarize "notarytool-password" \
"../sing-box/dist/SFM/SFM.dmg" "build/SFM.System/SFM.app" "../sing-box/dist/SFM/SFM.dmg" "build/SFM.System/SFM.app"
notarize_macos_dmg:
xcrun notarytool submit "dist/SFM/SFM.dmg" --wait \
--keychain-profile "notarytool-password" \
--no-s3-acceleration
upload_macos_dmg: upload_macos_dmg:
cd dist/SFM && \ cd dist/SFM && \
cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \ cp SFM.dmg "SFM-${VERSION}-universal.dmg" && \
@@ -166,7 +164,7 @@ upload_macos_dsyms:
cp SFM.dSYMs.zip "SFM-${VERSION}-universal.dSYMs.zip" && \ cp SFM.dSYMs.zip "SFM-${VERSION}-universal.dSYMs.zip" && \
ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dSYMs.zip" ghr --replace --draft --prerelease "v${VERSION}" "SFM-${VERSION}-universal.dSYMs.zip"
release_macos_standalone: build_macos_standalone build_macos_dmg notarize_macos_dmg upload_macos_dmg upload_macos_dsyms release_macos_standalone: build_macos_standalone build_macos_dmg upload_macos_dmg upload_macos_dsyms
build_tvos: build_tvos:
cd ../sing-box-for-apple && \ cd ../sing-box-for-apple && \
@@ -182,22 +180,10 @@ release_tvos: build_tvos upload_tvos_app_store
update_apple_version: update_apple_version:
go run ./cmd/internal/update_apple_version go run ./cmd/internal/update_apple_version
update_macos_version:
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
release_apple_beta: update_apple_version release_ios release_macos release_tvos release_apple_beta: update_apple_version release_ios release_macos release_tvos
publish_testflight:
go run -v ./cmd/internal/app_store_connect publish_testflight
prepare_app_store:
go run -v ./cmd/internal/app_store_connect prepare_app_store
publish_app_store:
go run -v ./cmd/internal/app_store_connect publish_app_store
test: test:
@go test -v ./... && \ @go test -v ./... && \
cd test && \ cd test && \
@@ -213,14 +199,8 @@ 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_apple:
go run ./cmd/internal/build_libbox -target apple
lib_ios: lib_ios:
go run ./cmd/internal/build_libbox -target apple -platform ios -debug go run ./cmd/internal/build_libbox -target ios
lib: lib:
go run ./cmd/internal/build_libbox -target android go run ./cmd/internal/build_libbox -target android

View File

@@ -1,104 +0,0 @@
package adapter
import (
"context"
"net"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type ConnectionRouter interface {
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
}
func NewRouteHandler(
metadata InboundContext,
router ConnectionRouter,
logger logger.ContextLogger,
) UpstreamHandlerAdapter {
return &routeHandlerWrapper{
metadata: metadata,
router: router,
logger: logger,
}
}
func NewRouteContextHandler(
router ConnectionRouter,
logger logger.ContextLogger,
) UpstreamHandlerAdapter {
return &routeContextHandlerWrapper{
router: router,
logger: logger,
}
}
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
type routeHandlerWrapper struct {
metadata InboundContext
router ConnectionRouter
logger logger.ContextLogger
}
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RouteConnection(ctx, conn, myMetadata)
}
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RoutePacketConnection(ctx, conn, myMetadata)
}
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
type routeContextHandlerWrapper struct {
router ConnectionRouter
logger logger.ContextLogger
}
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RouteConnection(ctx, conn, *myMetadata)
}
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
}
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}

View File

@@ -6,27 +6,53 @@ import (
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
// Deprecated
type ConnectionHandler interface { type ConnectionHandler interface {
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
} }
type ConnectionHandlerEx interface {
NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
// Deprecated: use PacketHandlerEx instead
type PacketHandler interface { type PacketHandler interface {
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error
} }
type PacketHandlerEx interface {
NewPacketEx(buffer *buf.Buffer, source M.Socksaddr)
}
// Deprecated: use OOBPacketHandlerEx instead
type OOBPacketHandler interface { type OOBPacketHandler interface {
NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error
} }
type OOBPacketHandlerEx interface {
NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr)
}
// Deprecated
type PacketConnectionHandler interface { type PacketConnectionHandler interface {
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
} }
type PacketConnectionHandlerEx interface {
NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
type UpstreamHandlerAdapter interface { type UpstreamHandlerAdapter interface {
N.TCPConnectionHandler N.TCPConnectionHandler
N.UDPConnectionHandler N.UDPConnectionHandler
E.Handler E.Handler
} }
type UpstreamHandlerAdapterEx interface {
N.TCPConnectionHandlerEx
N.UDPConnectionHandlerEx
}

View File

@@ -2,13 +2,12 @@ package adapter
import ( import (
"context" "context"
"net"
"net/netip" "net/netip"
"github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/common/process"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
) )
type Inbound interface { type Inbound interface {
@@ -17,11 +16,19 @@ type Inbound interface {
Tag() string Tag() string
} }
type InjectableInbound interface { type TCPInjectableInbound interface {
Inbound Inbound
Network() []string ConnectionHandlerEx
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error }
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
type UDPInjectableInbound interface {
Inbound
PacketConnectionHandlerEx
}
type InboundRegistry interface {
option.InboundOptionsRegistry
CreateInbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Inbound, error)
} }
type InboundContext struct { type InboundContext struct {
@@ -43,10 +50,15 @@ type InboundContext struct {
// cache // cache
InboundDetour string // Deprecated: implement in rule action
LastInbound string InboundDetour string
OriginDestination M.Socksaddr LastInbound string
InboundOptions option.InboundOptions OriginDestination M.Socksaddr
// Deprecated
InboundOptions option.InboundOptions
UDPDisableDomainUnmapping bool
DNSServer string
DestinationAddresses []netip.Addr DestinationAddresses []netip.Addr
SourceGeoIPCode string SourceGeoIPCode string
GeoIPCode string GeoIPCode string

View File

@@ -0,0 +1,21 @@
package inbound
type Adapter struct {
inboundType string
inboundTag string
}
func NewAdapter(inboundType string, inboundTag string) Adapter {
return Adapter{
inboundType: inboundType,
inboundTag: inboundTag,
}
}
func (a *Adapter) Type() string {
return a.inboundType
}
func (a *Adapter) Tag() string {
return a.inboundTag
}

View File

@@ -0,0 +1,68 @@
package inbound
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.Inbound, 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, options any) (adapter.Inbound, error) {
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options)))
})
}
var _ adapter.InboundRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructors map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructors: make(map[string]constructorFunc),
}
}
func (r *Registry) CreateOptions(outboundType string) (any, bool) {
r.access.Lock()
defer r.access.Unlock()
optionsConstructor, loaded := r.optionsType[outboundType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (r *Registry) CreateInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) {
r.access.Lock()
defer r.access.Unlock()
constructor, loaded := r.constructors[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
}
return constructor(ctx, router, logger, tag, options)
}
func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
r.access.Lock()
defer r.access.Unlock()
r.optionsType[outboundType] = optionsConstructor
r.constructors[outboundType] = constructor
}

View File

@@ -2,8 +2,9 @@ package adapter
import ( import (
"context" "context"
"net"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
@@ -15,6 +16,9 @@ type Outbound interface {
Network() []string Network() []string
Dependencies() []string Dependencies() []string
N.Dialer N.Dialer
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error }
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
type OutboundRegistry interface {
option.OutboundOptionsRegistry
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
} }

View File

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

View File

@@ -9,8 +9,6 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-dns" "github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
@@ -21,42 +19,6 @@ import (
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
type myOutboundAdapter struct {
protocol string
network []string
router adapter.Router
logger log.ContextLogger
tag string
dependencies []string
}
func (a *myOutboundAdapter) Type() string {
return a.protocol
}
func (a *myOutboundAdapter) Tag() string {
return a.tag
}
func (a *myOutboundAdapter) Network() []string {
return a.network
}
func (a *myOutboundAdapter) Dependencies() []string {
return a.dependencies
}
func (a *myOutboundAdapter) NewError(ctx context.Context, err error) {
NewError(a.logger, ctx, err)
}
func withDialerDependency(options option.DialerOptions) []string {
if options.Detour != "" {
return []string{options.Detour}
}
return nil
}
func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext) error { func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext) error {
ctx = adapter.WithContext(ctx, &metadata) ctx = adapter.WithContext(ctx, &metadata)
var outConn net.Conn var outConn net.Conn
@@ -69,7 +31,7 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a
if err != nil { if err != nil {
return N.ReportHandshakeFailure(conn, err) return N.ReportHandshakeFailure(conn, err)
} }
err = N.ReportHandshakeSuccess(conn) err = N.ReportConnHandshakeSuccess(conn, outConn)
if err != nil { if err != nil {
outConn.Close() outConn.Close()
return err return err
@@ -96,7 +58,7 @@ func NewDirectConnection(ctx context.Context, router adapter.Router, this N.Dial
if err != nil { if err != nil {
return N.ReportHandshakeFailure(conn, err) return N.ReportHandshakeFailure(conn, err)
} }
err = N.ReportHandshakeSuccess(conn) err = N.ReportConnHandshakeSuccess(conn, outConn)
if err != nil { if err != nil {
outConn.Close() outConn.Close()
return err return err
@@ -117,14 +79,14 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn,
if err != nil { if err != nil {
return N.ReportHandshakeFailure(conn, err) return N.ReportHandshakeFailure(conn, err)
} }
err = N.ReportHandshakeSuccess(conn) err = N.ReportPacketConnHandshakeSuccess(conn, outConn)
if err != nil { if err != nil {
outConn.Close() outConn.Close()
return err return err
} }
if destinationAddress.IsValid() { if destinationAddress.IsValid() {
if metadata.Destination.IsFqdn() { if metadata.Destination.IsFqdn() {
if metadata.InboundOptions.UDPDisableDomainUnmapping { if metadata.UDPDisableDomainUnmapping {
outConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) outConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
} else { } else {
outConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) outConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination)
@@ -165,7 +127,7 @@ func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this
if err != nil { if err != nil {
return N.ReportHandshakeFailure(conn, err) return N.ReportHandshakeFailure(conn, err)
} }
err = N.ReportHandshakeSuccess(conn) err = N.ReportPacketConnHandshakeSuccess(conn, outConn)
if err != nil { if err != nil {
outConn.Close() outConn.Close()
return err return err
@@ -233,12 +195,3 @@ func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) erro
} }
return bufio.CopyConn(ctx, conn, serverConn) return bufio.CopyConn(ctx, conn, serverConn)
} }
func NewError(logger log.ContextLogger, ctx context.Context, err error) {
common.Close(err)
if E.IsClosedOrCanceled(err) {
logger.DebugContext(ctx, "connection closed: ", err)
return
}
logger.ErrorContext(ctx, err)
}

View File

@@ -0,0 +1,68 @@
package outbound
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.Outbound, 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, options any) (adapter.Outbound, error) {
return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options)))
})
}
var _ adapter.OutboundRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructors map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructors: make(map[string]constructorFunc),
}
}
func (r *Registry) CreateOptions(outboundType string) (any, bool) {
r.access.Lock()
defer r.access.Unlock()
optionsConstructor, loaded := r.optionsType[outboundType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) {
r.access.Lock()
defer r.access.Unlock()
constructor, loaded := r.constructors[outboundType]
if !loaded {
return nil, E.New("outbound type not found: " + outboundType)
}
return constructor(ctx, router, logger, tag, options)
}
func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
r.access.Lock()
defer r.access.Unlock()
r.optionsType[outboundType] = optionsConstructor
r.constructors[outboundType] = constructor
}

View File

@@ -34,6 +34,8 @@ type Router interface {
FakeIPStore() FakeIPStore FakeIPStore() FakeIPStore
ConnectionRouter ConnectionRouter
PreMatch(metadata InboundContext) error
ConnectionRouterEx
GeoIPReader() *geoip.Reader GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error) LoadGeosite(code string) (Rule, error)
@@ -70,6 +72,18 @@ type Router interface {
ResetNetwork() error ResetNetwork() error
} }
// Deprecated: Use ConnectionRouterEx instead.
type ConnectionRouter interface {
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
}
type ConnectionRouterEx interface {
ConnectionRouter
RouteConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
}
func ContextWithRouter(ctx context.Context, router Router) context.Context { func ContextWithRouter(ctx context.Context, router Router) context.Context {
return service.ContextWith(ctx, router) return service.ContextWith(ctx, router)
} }
@@ -78,28 +92,6 @@ func RouterFromContext(ctx context.Context) Router {
return service.FromContext[Router](ctx) return service.FromContext[Router](ctx)
} }
type HeadlessRule interface {
Match(metadata *InboundContext) bool
String() string
}
type Rule interface {
HeadlessRule
Service
Type() string
UpdateGeosite() error
Outbound() string
}
type DNSRule interface {
Rule
DisableCache() bool
RewriteTTL() *uint32
ClientSubnet() *netip.Prefix
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext) bool
}
type RuleSet interface { type RuleSet interface {
Name() string Name() string
StartContext(ctx context.Context, startContext *HTTPStartContext) error StartContext(ctx context.Context, startContext *HTTPStartContext) error

38
adapter/rule.go Normal file
View File

@@ -0,0 +1,38 @@
package adapter
import (
C "github.com/sagernet/sing-box/constant"
)
type HeadlessRule interface {
Match(metadata *InboundContext) bool
String() string
}
type Rule interface {
HeadlessRule
Service
Type() string
UpdateGeosite() error
Action() RuleAction
}
type DNSRule interface {
Rule
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext) bool
}
type RuleAction interface {
Type() string
String() string
}
func IsFinalAction(action RuleAction) bool {
switch action.Type() {
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
return false
default:
return true
}
}

View File

@@ -4,112 +4,165 @@ import (
"context" "context"
"net" "net"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
type ( type (
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc)
) )
func NewUpstreamHandler( func NewUpstreamHandlerEx(
metadata InboundContext, metadata InboundContext,
connectionHandler ConnectionHandlerFunc, connectionHandler ConnectionHandlerFuncEx,
packetHandler PacketConnectionHandlerFunc, packetHandler PacketConnectionHandlerFuncEx,
errorHandler E.Handler, ) UpstreamHandlerAdapterEx {
) UpstreamHandlerAdapter { return &myUpstreamHandlerWrapperEx{
return &myUpstreamHandlerWrapper{
metadata: metadata, metadata: metadata,
connectionHandler: connectionHandler, connectionHandler: connectionHandler,
packetHandler: packetHandler, packetHandler: packetHandler,
errorHandler: errorHandler,
} }
} }
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil) var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil)
type myUpstreamHandlerWrapper struct { type myUpstreamHandlerWrapperEx struct {
metadata InboundContext metadata InboundContext
connectionHandler ConnectionHandlerFunc connectionHandler ConnectionHandlerFuncEx
packetHandler PacketConnectionHandlerFunc packetHandler PacketConnectionHandlerFuncEx
errorHandler E.Handler
} }
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := w.metadata myMetadata := w.metadata
if metadata.Source.IsValid() { if source.IsValid() {
myMetadata.Source = metadata.Source myMetadata.Source = source
} }
if metadata.Destination.IsValid() { if destination.IsValid() {
myMetadata.Destination = metadata.Destination myMetadata.Destination = destination
} }
return w.connectionHandler(ctx, conn, myMetadata) w.connectionHandler(ctx, conn, myMetadata, onClose)
} }
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := w.metadata myMetadata := w.metadata
if metadata.Source.IsValid() { if source.IsValid() {
myMetadata.Source = metadata.Source myMetadata.Source = source
} }
if metadata.Destination.IsValid() { if destination.IsValid() {
myMetadata.Destination = metadata.Destination myMetadata.Destination = destination
} }
return w.packetHandler(ctx, conn, myMetadata) w.packetHandler(ctx, conn, myMetadata, onClose)
} }
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) { var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil)
w.errorHandler.NewError(ctx, err)
type myUpstreamContextHandlerWrapperEx struct {
connectionHandler ConnectionHandlerFuncEx
packetHandler PacketConnectionHandlerFuncEx
} }
func UpstreamMetadata(metadata InboundContext) M.Metadata { func NewUpstreamContextHandlerEx(
return M.Metadata{ connectionHandler ConnectionHandlerFuncEx,
Source: metadata.Source, packetHandler PacketConnectionHandlerFuncEx,
Destination: metadata.Destination, ) UpstreamHandlerAdapterEx {
} return &myUpstreamContextHandlerWrapperEx{
}
type myUpstreamContextHandlerWrapper struct {
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler
}
func NewUpstreamContextHandler(
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
errorHandler E.Handler,
) UpstreamHandlerAdapter {
return &myUpstreamContextHandlerWrapper{
connectionHandler: connectionHandler, connectionHandler: connectionHandler,
packetHandler: packetHandler, packetHandler: packetHandler,
errorHandler: errorHandler,
} }
} }
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := ContextFrom(ctx) myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() { if source.IsValid() {
myMetadata.Source = metadata.Source myMetadata.Source = source
} }
if metadata.Destination.IsValid() { if destination.IsValid() {
myMetadata.Destination = metadata.Destination myMetadata.Destination = destination
} }
return w.connectionHandler(ctx, conn, *myMetadata) w.connectionHandler(ctx, conn, *myMetadata, onClose)
} }
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := ContextFrom(ctx) myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() { if source.IsValid() {
myMetadata.Source = metadata.Source myMetadata.Source = source
} }
if metadata.Destination.IsValid() { if destination.IsValid() {
myMetadata.Destination = metadata.Destination myMetadata.Destination = destination
} }
return w.packetHandler(ctx, conn, *myMetadata) w.packetHandler(ctx, conn, *myMetadata, onClose)
} }
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) { func NewRouteHandlerEx(
w.errorHandler.NewError(ctx, err) metadata InboundContext,
router ConnectionRouterEx,
) UpstreamHandlerAdapterEx {
return &routeHandlerWrapperEx{
metadata: metadata,
router: router,
}
}
var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil)
type routeHandlerWrapperEx struct {
metadata InboundContext
router ConnectionRouterEx
}
func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
if source.IsValid() {
r.metadata.Source = source
}
if destination.IsValid() {
r.metadata.Destination = destination
}
r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose)
}
func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
if source.IsValid() {
r.metadata.Source = source
}
if destination.IsValid() {
r.metadata.Destination = destination
}
r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose)
}
func NewRouteContextHandlerEx(
router ConnectionRouterEx,
) UpstreamHandlerAdapterEx {
return &routeContextHandlerWrapperEx{
router: router,
}
}
var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil)
type routeContextHandlerWrapperEx struct {
router ConnectionRouterEx
}
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx)
if source.IsValid() {
metadata.Source = source
}
if destination.IsValid() {
metadata.Destination = destination
}
r.router.RouteConnectionEx(ctx, conn, *metadata, onClose)
}
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx)
if source.IsValid() {
metadata.Source = source
}
if destination.IsValid() {
metadata.Destination = destination
}
r.router.RoutePacketConnectionEx(ctx, conn, *metadata, onClose)
} }

216
adapter/upstream_legacy.go Normal file
View File

@@ -0,0 +1,216 @@
package adapter
import (
"context"
"net"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type (
// Deprecated
ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error
// Deprecated
PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
)
// Deprecated
func NewUpstreamHandler(
metadata InboundContext,
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
errorHandler E.Handler,
) UpstreamHandlerAdapter {
return &myUpstreamHandlerWrapper{
metadata: metadata,
connectionHandler: connectionHandler,
packetHandler: packetHandler,
errorHandler: errorHandler,
}
}
var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil)
// Deprecated
type myUpstreamHandlerWrapper struct {
metadata InboundContext
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler
}
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.connectionHandler(ctx, conn, myMetadata)
}
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.packetHandler(ctx, conn, myMetadata)
}
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
w.errorHandler.NewError(ctx, err)
}
// Deprecated
func UpstreamMetadata(metadata InboundContext) M.Metadata {
return M.Metadata{
Source: metadata.Source,
Destination: metadata.Destination,
}
}
// Deprecated
type myUpstreamContextHandlerWrapper struct {
connectionHandler ConnectionHandlerFunc
packetHandler PacketConnectionHandlerFunc
errorHandler E.Handler
}
// Deprecated
func NewUpstreamContextHandler(
connectionHandler ConnectionHandlerFunc,
packetHandler PacketConnectionHandlerFunc,
errorHandler E.Handler,
) UpstreamHandlerAdapter {
return &myUpstreamContextHandlerWrapper{
connectionHandler: connectionHandler,
packetHandler: packetHandler,
errorHandler: errorHandler,
}
}
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.connectionHandler(ctx, conn, *myMetadata)
}
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.packetHandler(ctx, conn, *myMetadata)
}
func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.errorHandler.NewError(ctx, err)
}
// Deprecated: Use ConnectionRouterEx instead.
func NewRouteHandler(
metadata InboundContext,
router ConnectionRouter,
logger logger.ContextLogger,
) UpstreamHandlerAdapter {
return &routeHandlerWrapper{
metadata: metadata,
router: router,
logger: logger,
}
}
// Deprecated: Use ConnectionRouterEx instead.
func NewRouteContextHandler(
router ConnectionRouter,
logger logger.ContextLogger,
) UpstreamHandlerAdapter {
return &routeContextHandlerWrapper{
router: router,
logger: logger,
}
}
var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil)
// Deprecated: Use ConnectionRouterEx instead.
type routeHandlerWrapper struct {
metadata InboundContext
router ConnectionRouter
logger logger.ContextLogger
}
func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RouteConnection(ctx, conn, myMetadata)
}
func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RoutePacketConnection(ctx, conn, myMetadata)
}
func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}
var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil)
// Deprecated: Use ConnectionRouterEx instead.
type routeContextHandlerWrapper struct {
router ConnectionRouter
logger logger.ContextLogger
}
func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RouteConnection(ctx, conn, *myMetadata)
}
func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.router.RoutePacketConnection(ctx, conn, *myMetadata)
}
func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) {
w.logger.ErrorContext(ctx, err)
}

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"net" "net"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
@@ -16,8 +15,7 @@ type V2RayServerTransport interface {
} }
type V2RayServerTransportHandler interface { type V2RayServerTransportHandler interface {
N.TCPConnectionHandler N.TCPConnectionHandlerEx
E.Handler
} }
type V2RayClientTransport interface { type V2RayClientTransport interface {

91
box.go
View File

@@ -14,10 +14,9 @@ import (
"github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/cachefile"
"github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/inbound"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/protocol/direct"
"github.com/sagernet/sing-box/route" "github.com/sagernet/sing-box/route"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -44,16 +43,37 @@ type Box struct {
type Options struct { type Options struct {
option.Options option.Options
Context context.Context Context context.Context
PlatformInterface platform.Interface
PlatformLogWriter log.PlatformWriter PlatformLogWriter log.PlatformWriter
} }
func Context(ctx context.Context, inboundRegistry adapter.InboundRegistry, outboundRegistry adapter.OutboundRegistry) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)
}
if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.OutboundRegistry](ctx) == nil {
ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry)
ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
}
return ctx
}
func New(options Options) (*Box, error) { func New(options Options) (*Box, error) {
createdAt := time.Now() createdAt := time.Now()
ctx := options.Context ctx := options.Context
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
if inboundRegistry == nil {
return nil, E.New("missing inbound registry in context")
}
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context")
}
ctx = service.ContextWithDefaultRegistry(ctx) ctx = service.ContextWithDefaultRegistry(ctx)
ctx = pause.WithDefaultManager(ctx) ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental) experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@@ -70,8 +90,9 @@ func New(options Options) (*Box, error) {
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
needV2RayAPI = true needV2RayAPI = true
} }
platformInterface := service.FromContext[platform.Interface](ctx)
var defaultLogWriter io.Writer var defaultLogWriter io.Writer
if options.PlatformInterface != nil { if platformInterface != nil {
defaultLogWriter = io.Discard defaultLogWriter = io.Discard
} }
logFactory, err := log.New(log.Options{ logFactory, err := log.New(log.Options{
@@ -92,64 +113,92 @@ func New(options Options) (*Box, error) {
common.PtrValueOrDefault(options.DNS), common.PtrValueOrDefault(options.DNS),
common.PtrValueOrDefault(options.NTP), common.PtrValueOrDefault(options.NTP),
options.Inbounds, options.Inbounds,
options.PlatformInterface,
) )
if err != nil { if err != nil {
return nil, E.Cause(err, "parse route options") return nil, E.Cause(err, "parse route options")
} }
//nolint:staticcheck
if len(options.LegacyInbounds) > 0 {
for _, legacyInbound := range options.LegacyInbounds {
options.Inbounds = append(options.Inbounds, option.Inbound{
Type: legacyInbound.Type,
Tag: legacyInbound.Tag,
Options: common.Must1(legacyInbound.RawOptions()),
})
}
}
inbounds := make([]adapter.Inbound, 0, len(options.Inbounds)) inbounds := make([]adapter.Inbound, 0, len(options.Inbounds))
//nolint:staticcheck
if len(options.LegacyOutbounds) > 0 {
for _, legacyOutbound := range options.LegacyOutbounds {
options.Outbounds = append(options.Outbounds, option.Outbound{
Type: legacyOutbound.Type,
Tag: legacyOutbound.Tag,
Options: common.Must1(legacyOutbound.RawOptions()),
})
}
}
outbounds := make([]adapter.Outbound, 0, len(options.Outbounds)) outbounds := make([]adapter.Outbound, 0, len(options.Outbounds))
for i, inboundOptions := range options.Inbounds { for i, inboundOptions := range options.Inbounds {
var in adapter.Inbound var currentInbound adapter.Inbound
var tag string var tag string
if inboundOptions.Tag != "" { if inboundOptions.Tag != "" {
tag = inboundOptions.Tag tag = inboundOptions.Tag
} else { } else {
tag = F.ToString(i) tag = F.ToString(i)
} }
in, err = inbound.New( currentInbound, err = inboundRegistry.CreateInbound(
ctx, ctx,
router, router,
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
tag, tag,
inboundOptions, inboundOptions.Type,
options.PlatformInterface, inboundOptions.Options,
) )
if err != nil { if err != nil {
return nil, E.Cause(err, "parse inbound[", i, "]") return nil, E.Cause(err, "parse inbound[", i, "]")
} }
inbounds = append(inbounds, in) inbounds = append(inbounds, currentInbound)
} }
for i, outboundOptions := range options.Outbounds { for i, outboundOptions := range options.Outbounds {
var out adapter.Outbound var currentOutbound adapter.Outbound
var tag string var tag string
if outboundOptions.Tag != "" { if outboundOptions.Tag != "" {
tag = outboundOptions.Tag tag = outboundOptions.Tag
} else { } else {
tag = F.ToString(i) tag = F.ToString(i)
} }
out, err = outbound.New( outboundCtx := ctx
ctx, if tag != "" {
// TODO: remove this
outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{
Outbound: tag,
})
}
currentOutbound, err = outboundRegistry.CreateOutbound(
outboundCtx,
router, router,
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")), logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
tag, tag,
outboundOptions) outboundOptions.Type,
outboundOptions.Options,
)
if err != nil { if err != nil {
return nil, E.Cause(err, "parse outbound[", i, "]") return nil, E.Cause(err, "parse outbound[", i, "]")
} }
outbounds = append(outbounds, out) outbounds = append(outbounds, currentOutbound)
} }
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound { err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"}) defaultOutbound, cErr := direct.NewOutbound(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.DirectOutboundOptions{})
common.Must(oErr) common.Must(cErr)
outbounds = append(outbounds, out) outbounds = append(outbounds, defaultOutbound)
return out return defaultOutbound
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
if options.PlatformInterface != nil { if platformInterface != nil {
err = options.PlatformInterface.Initialize(ctx, router) err = platformInterface.Initialize(ctx, router)
if err != nil { if err != nil {
return nil, E.Cause(err, "initialize platform interface") return nil, E.Cause(err, "initialize platform interface")
} }

View File

@@ -1,445 +0,0 @@
package main
import (
"context"
"net/http"
"os"
"strconv"
"time"
"github.com/sagernet/asc-go/asc"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
func main() {
ctx := context.Background()
switch os.Args[1] {
case "next_macos_project_version":
err := fetchMacOSVersion(ctx)
if err != nil {
log.Fatal(err)
}
case "publish_testflight":
err := publishTestflight(ctx)
if err != nil {
log.Fatal(err)
}
case "cancel_app_store":
err := cancelAppStore(ctx, os.Args[2])
if err != nil {
log.Fatal(err)
}
case "prepare_app_store":
err := prepareAppStore(ctx)
if err != nil {
log.Fatal(err)
}
case "publish_app_store":
err := publishAppStore(ctx)
if err != nil {
log.Fatal(err)
}
default:
log.Fatal("unknown action: ", os.Args[1])
}
}
const (
appID = "6673731168"
groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda"
)
func createClient(expireDuration time.Duration) *asc.Client {
privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
if err != nil {
log.Fatal(err)
}
tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), expireDuration, privateKey)
if err != nil {
log.Fatal(err)
}
return asc.NewClient(tokenConfig.Client())
}
func fetchMacOSVersion(ctx context.Context) error {
client := createClient(time.Minute)
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{"MAC_OS"},
})
if err != nil {
return err
}
var versionID string
findVersion:
for _, version := range versions.Data {
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStateReadyForSale,
asc.AppStoreVersionStatePendingDeveloperRelease:
versionID = version.ID
break findVersion
}
}
if versionID == "" {
return E.New("no version found")
}
latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{})
if err != nil {
return err
}
versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version)
if err != nil {
return E.Cause(err, "parse version code")
}
os.Stdout.WriteString(F.ToString(versionInt+1, "\n"))
return nil
}
func publishTestflight(ctx context.Context) error {
tagVersion, err := build_shared.ReadTagVersion()
if err != nil {
return err
}
tag := tagVersion.VersionString()
client := createClient(10 * time.Minute)
log.Info(tag, " list build IDs")
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
if err != nil {
return err
}
buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
return it.ID
})
var platforms []asc.Platform
if len(os.Args) == 3 {
switch os.Args[2] {
case "ios":
platforms = []asc.Platform{asc.PlatformIOS}
case "macos":
platforms = []asc.Platform{asc.PlatformMACOS}
case "tvos":
platforms = []asc.Platform{asc.PlatformTVOS}
default:
return E.New("unknown platform: ", os.Args[2])
}
} else {
platforms = []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
}
}
for _, platform := range platforms {
log.Info(string(platform), " list builds")
for {
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
})
if err != nil {
return err
}
build := builds.Data[0]
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 5*time.Minute {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
continue
}
if *build.Attributes.ProcessingState != "VALID" {
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second)
continue
}
log.Info(string(platform), " ", tag, " list localizations")
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
if err != nil {
return err
}
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
return *it.Attributes.Locale == "en-US"
})
if localization.ID == "" {
log.Fatal(string(platform), " ", tag, " no en-US localization found")
}
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(
F.ToString("sing-box ", tagVersion.String()),
))
if err != nil {
return err
}
}
log.Info(string(platform), " ", tag, " publish")
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
if response != nil && response.StatusCode == http.StatusUnprocessableEntity {
log.Info("waiting for process")
time.Sleep(15 * time.Second)
continue
} else if err != nil {
return err
}
log.Info(string(platform), " ", tag, " list submissions")
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
FilterBuild: []string{build.ID},
})
if err != nil {
return err
}
if len(betaSubmissions.Data) == 0 {
log.Info(string(platform), " ", tag, " create submission")
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
if err != nil {
return err
}
}
break
}
}
return nil
}
func cancelAppStore(ctx context.Context, platform string) error {
switch platform {
case "ios":
platform = string(asc.PlatformIOS)
case "macos":
platform = string(asc.PlatformMACOS)
case "tvos":
platform = string(asc.PlatformTVOS)
}
tag, err := build_shared.ReadTag()
if err != nil {
return err
}
client := createClient(time.Minute)
for {
log.Info(platform, " list versions")
versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
if isRetryable(response) {
continue
} else if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
})
if version.ID == "" {
return nil
}
log.Info(platform, " ", tag, " get submission")
submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
if response != nil && response.StatusCode == http.StatusNotFound {
return nil
}
if isRetryable(response) {
continue
} else if err != nil {
return err
}
log.Info(platform, " ", tag, " delete submission")
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
if err != nil {
return err
}
return nil
}
}
func prepareAppStore(ctx context.Context) error {
tag, err := build_shared.ReadTag()
if err != nil {
return err
}
client := createClient(time.Minute)
for _, platform := range []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
} {
log.Info(string(platform), " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
})
log.Info(string(platform), " ", tag, " list builds")
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
})
if err != nil {
return err
}
if len(builds.Data) == 0 {
log.Fatal(platform, " ", tag, " no build found")
}
buildID := common.Ptr(builds.Data[0].ID)
if version.ID == "" {
log.Info(string(platform), " ", tag, " create version")
newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
Platform: platform,
VersionString: tag,
}, appID, buildID)
if err != nil {
return err
}
version = newVersion.Data
} else {
log.Info(string(platform), " ", tag, " check build")
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
if err != nil {
return err
}
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStatePrepareForSubmission,
asc.AppStoreVersionStateRejected,
asc.AppStoreVersionStateDeveloperRejected:
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview,
asc.AppStoreVersionStatePendingDeveloperRelease:
submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
if err != nil {
return err
}
if submission != nil {
log.Info(string(platform), " ", tag, " delete submission")
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
if err != nil {
return err
}
time.Sleep(5 * time.Second)
}
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
log.Info(string(platform), " ", tag, " update build")
response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
if err != nil {
return err
}
if response.StatusCode != http.StatusNoContent {
response.Write(os.Stderr)
log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
}
} else {
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStatePrepareForSubmission,
asc.AppStoreVersionStateRejected,
asc.AppStoreVersionStateDeveloperRejected:
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview,
asc.AppStoreVersionStatePendingDeveloperRelease:
continue
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
}
}
log.Info(string(platform), " ", tag, " list localization")
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
if err != nil {
return err
}
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
return *it.Attributes.Locale == "en-US"
})
if localization.ID == "" {
log.Info(string(platform), " ", tag, " no en-US localization found")
}
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
})
if err != nil {
return err
}
}
log.Info(string(platform), " ", tag, " create submission")
fixSubmit:
for {
_, response, err := client.Submission.CreateSubmission(ctx, version.ID)
if err != nil {
switch response.StatusCode {
case http.StatusInternalServerError:
continue
default:
return err
}
}
switch response.StatusCode {
case http.StatusCreated:
break fixSubmit
default:
return err
}
}
}
return nil
}
func publishAppStore(ctx context.Context) error {
tag, err := build_shared.ReadTag()
if err != nil {
return err
}
client := createClient(time.Minute)
for _, platform := range []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
} {
log.Info(string(platform), " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
})
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
log.Fatal(string(platform), " ", tag, " not submitted")
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview:
log.Warn(string(platform), " ", tag, " waiting for review")
continue
case asc.AppStoreVersionStatePendingDeveloperRelease:
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
if err != nil {
return err
}
}
return nil
}
func isRetryable(response *asc.Response) bool {
if response == nil {
return false
}
switch response.StatusCode {
case http.StatusInternalServerError, http.StatusUnprocessableEntity:
return true
default:
return false
}
}

View File

@@ -10,21 +10,17 @@ 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 (
debugEnabled bool debugEnabled bool
target string target string
platform string
) )
func init() { func init() {
flag.BoolVar(&debugEnabled, "debug", false, "enable debug") flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
flag.StringVar(&target, "target", "android", "target platform") flag.StringVar(&target, "target", "android", "target platform")
flag.StringVar(&platform, "platform", "", "specify platform")
} }
func main() { func main() {
@@ -35,8 +31,8 @@ func main() {
switch target { switch target {
case "android": case "android":
buildAndroid() buildAndroid()
case "apple": case "ios":
buildApple() buildiOS()
} }
} }
@@ -66,35 +62,9 @@ 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 platform != "" {
bindTarget = platform
} else 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",
@@ -116,7 +86,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)
} }
@@ -133,20 +103,11 @@ func buildAndroid() {
} }
} }
func buildApple() { func buildiOS() {
var bindTarget string
if platform != "" {
bindTarget = platform
} else if debugEnabled {
bindTarget = "ios"
} else {
bindTarget = "ios,tvos,macos"
}
args := []string{ args := []string{
"bind", "bind",
"-v", "-v",
"-target", bindTarget, "-target", "ios,iossimulator,tvos,tvossimulator,macos",
"-libname=box", "-libname=box",
} }
if !debugEnabled { if !debugEnabled {

View File

@@ -11,7 +11,9 @@ 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 (
@@ -40,6 +42,14 @@ 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)
@@ -48,16 +58,12 @@ func FindSDK() {
} }
func findNDK() bool { func findNDK() bool {
const fixedVersion = "28.0.12674087" const fixedVersion = "27.2.12479018"
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,11 +20,6 @@ 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())

View File

@@ -1,62 +1,21 @@
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"
) )
var nightly bool
func init() {
flag.BoolVar(&nightly, "nightly", false, "Print nightly tag")
}
func main() { func main() {
flag.Parse() currentTag, err := build_shared.ReadTag()
if nightly { if err != nil {
version, err := build_shared.ReadTagVersionRev() log.Error(err)
if err != nil { _, err = os.Stdout.WriteString("unknown\n")
log.Fatal(err)
}
var versionStr string
if version.PreReleaseIdentifier != "" {
versionStr = version.VersionString() + "-nightly"
} else {
version.Patch++
versionStr = version.VersionString() + "-nightly"
}
err = setGitHubEnv("version", versionStr)
if err != nil {
log.Fatal(err)
}
} else { } else {
tag, err := build_shared.ReadTag() _, err = os.Stdout.WriteString(currentTag + "\n")
if err != nil { }
log.Error(err) if err != nil {
os.Stdout.WriteString("unknown\n") log.Error(err)
} else {
os.Stdout.WriteString(tag + "\n")
}
} }
} }
func setGitHubEnv(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,7 +1,6 @@
package main package main
import ( import (
"flag"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@@ -13,22 +12,9 @@ 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() {
flag.Parse() newVersion := common.Must1(build_shared.ReadTagVersion())
newVersion := common.Must1(build_shared.ReadTag()) androidPath, err := filepath.Abs("../sing-box-for-android")
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)
} }
@@ -45,10 +31,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 { if propPair[1] != newVersion.String() {
versionUpdated = true versionUpdated = true
propPair[1] = newVersion propPair[1] = newVersion.String()
log.Info("updated version to ", newVersion) log.Info("updated version to ", newVersion.String())
} }
case "GO_VERSION": case "GO_VERSION":
if propPair[1] != runtime.Version() { if propPair[1] != runtime.Version() {

View File

@@ -1,7 +1,6 @@
package main package main
import ( import (
"flag"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -14,22 +13,9 @@ 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())
var applePath string applePath, err := filepath.Abs("../sing-box-for-apple")
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

@@ -7,8 +7,9 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/experimental/deprecated"
_ "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager" "github.com/sagernet/sing/service/filemanager"
@@ -67,5 +68,6 @@ func preRun(cmd *cobra.Command, args []string) {
if len(configPaths) == 0 && len(configDirectories) == 0 { if len(configPaths) == 0 && len(configDirectories) == 0 {
configPaths = append(configPaths, "config.json") configPaths = append(configPaths, "config.json")
} }
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())) globalCtx = service.ContextWith(globalCtx, deprecated.NewEnvManager(log.StdLogger()))
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry())
} }

View File

@@ -30,7 +30,7 @@ func check() error {
if err != nil { if err != nil {
return err return err
} }
ctx, cancel := context.WithCancel(globalCtx) ctx, cancel := context.WithCancel(context.Background())
instance, err := box.New(box.Options{ instance, err := box.New(box.Options{
Context: ctx, Context: ctx,
Options: options, Options: options,

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"context"
"os" "os"
"path/filepath" "path/filepath"
@@ -38,7 +39,7 @@ func format() error {
return err return err
} }
for _, optionsEntry := range optionsList { for _, optionsEntry := range optionsList {
optionsEntry.options, err = badjson.Omitempty(optionsEntry.options) optionsEntry.options, err = badjson.Omitempty(context.TODO(), optionsEntry.options)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -68,29 +68,19 @@ func merge(outputPath string) error {
} }
func mergePathResources(options *option.Options) error { func mergePathResources(options *option.Options) error {
for index, inbound := range options.Inbounds { for _, inbound := range options.Inbounds {
rawOptions, err := inbound.RawOptions() if tlsOptions, containsTLSOptions := inbound.Options.(option.InboundTLSOptionsWrapper); containsTLSOptions {
if err != nil {
return err
}
if tlsOptions, containsTLSOptions := rawOptions.(option.InboundTLSOptionsWrapper); containsTLSOptions {
tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions())) tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions()))
} }
options.Inbounds[index] = inbound
} }
for index, outbound := range options.Outbounds { for _, outbound := range options.Outbounds {
rawOptions, err := outbound.RawOptions()
if err != nil {
return err
}
switch outbound.Type { switch outbound.Type {
case C.TypeSSH: case C.TypeSSH:
outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions) mergeSSHOutboundOptions(outbound.Options.(*option.SSHOutboundOptions))
} }
if tlsOptions, containsTLSOptions := rawOptions.(option.OutboundTLSOptionsWrapper); containsTLSOptions { if tlsOptions, containsTLSOptions := outbound.Options.(option.OutboundTLSOptionsWrapper); containsTLSOptions {
tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions())) tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions()))
} }
options.Outbounds[index] = outbound
} }
return nil return nil
} }
@@ -138,13 +128,12 @@ func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.Outboun
return options return options
} }
func mergeSSHOutboundOptions(options option.SSHOutboundOptions) option.SSHOutboundOptions { func mergeSSHOutboundOptions(options *option.SSHOutboundOptions) {
if options.PrivateKeyPath != "" { if options.PrivateKeyPath != "" {
if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil { if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil {
options.PrivateKey = trimStringArray(strings.Split(string(content), "\n")) options.PrivateKey = trimStringArray(strings.Split(string(content), "\n"))
} }
} }
return options
} }
func trimStringArray(array []string) []string { func trimStringArray(array []string) []string {

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
@@ -55,6 +56,10 @@ func compileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
ruleSet, err := plainRuleSet.Upgrade()
if err != nil {
return err
}
var outputPath string var outputPath string
if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput { if flagRuleSetCompileOutput == flagRuleSetCompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".json") { if strings.HasSuffix(sourcePath, ".json") {
@@ -69,7 +74,7 @@ func compileRuleSet(sourcePath string) error {
if err != nil { if err != nil {
return err return err
} }
err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version) err = srs.Write(outputFile, ruleSet, plainRuleSet.Version == C.RuleSetVersion2)
if err != nil { if err != nil {
outputFile.Close() outputFile.Close()
os.Remove(outputPath) os.Remove(outputPath)

View File

@@ -7,7 +7,6 @@ import (
"github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard" "github.com/sagernet/sing-box/cmd/sing-box/internal/convertor/adguard"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -78,7 +77,7 @@ func convertRuleSet(sourcePath string) error {
return err return err
} }
defer outputFile.Close() defer outputFile.Close()
err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, C.RuleSetVersion2) err = srs.Write(outputFile, option.PlainRuleSet{Rules: rules}, true)
if err != nil { if err != nil {
outputFile.Close() outputFile.Close()
os.Remove(outputPath) os.Remove(outputPath)

View File

@@ -6,7 +6,9 @@ import (
"strings" "strings"
"github.com/sagernet/sing-box/common/srs" "github.com/sagernet/sing-box/common/srs"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -46,10 +48,14 @@ func decompileRuleSet(sourcePath string) error {
return err return err
} }
} }
ruleSet, err := srs.Read(reader, true) plainRuleSet, err := srs.Read(reader, true)
if err != nil { if err != nil {
return err return err
} }
ruleSet := option.PlainRuleSetCompat{
Version: C.RuleSetVersion1,
Options: plainRuleSet,
}
var outputPath string var outputPath string
if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput { if flagRuleSetDecompileOutput == flagRuleSetDecompileDefaultOutput {
if strings.HasSuffix(sourcePath, ".srs") { if strings.HasSuffix(sourcePath, ".srs") {

View File

@@ -10,7 +10,7 @@ import (
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"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/route" "github.com/sagernet/sing-box/route/rule"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
@@ -55,25 +55,26 @@ func ruleSetMatch(sourcePath string, domain string) error {
if err != nil { if err != nil {
return E.Cause(err, "read rule-set") return E.Cause(err, "read rule-set")
} }
var ruleSet option.PlainRuleSetCompat var plainRuleSet option.PlainRuleSet
switch flagRuleSetMatchFormat { switch flagRuleSetMatchFormat {
case C.RuleSetFormatSource: case C.RuleSetFormatSource:
ruleSet, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content) var compat option.PlainRuleSetCompat
compat, err = json.UnmarshalExtended[option.PlainRuleSetCompat](content)
if err != nil {
return err
}
plainRuleSet, err = compat.Upgrade()
if err != nil { if err != nil {
return err return err
} }
case C.RuleSetFormatBinary: case C.RuleSetFormatBinary:
ruleSet, err = srs.Read(bytes.NewReader(content), false) plainRuleSet, err = srs.Read(bytes.NewReader(content), false)
if err != nil { if err != nil {
return err return err
} }
default: default:
return E.New("unknown rule-set format: ", flagRuleSetMatchFormat) return E.New("unknown rule-set format: ", flagRuleSetMatchFormat)
} }
plainRuleSet, err := ruleSet.Upgrade()
if err != nil {
return err
}
ipAddress := M.ParseAddr(domain) ipAddress := M.ParseAddr(domain)
var metadata adapter.InboundContext var metadata adapter.InboundContext
if ipAddress.IsValid() { if ipAddress.IsValid() {
@@ -83,7 +84,7 @@ func ruleSetMatch(sourcePath string, domain string) error {
} }
for i, ruleOptions := range plainRuleSet.Rules { for i, ruleOptions := range plainRuleSet.Rules {
var currentRule adapter.HeadlessRule var currentRule adapter.HeadlessRule
currentRule, err = route.NewHeadlessRule(nil, ruleOptions) currentRule, err = rule.NewHeadlessRule(nil, ruleOptions)
if err != nil { if err != nil {
return E.Cause(err, "parse rule_set.rules.[", i, "]") return E.Cause(err, "parse rule_set.rules.[", i, "]")
} }

View File

@@ -57,7 +57,7 @@ func readConfigAt(path string) (*OptionsEntry, error) {
if err != nil { if err != nil {
return nil, E.Cause(err, "read config at ", path) return nil, E.Cause(err, "read config at ", path)
} }
options, err := json.UnmarshalExtended[option.Options](configContent) options, err := json.UnmarshalExtendedContext[option.Options](globalCtx, configContent)
if err != nil { if err != nil {
return nil, E.Cause(err, "decode config at ", path) return nil, E.Cause(err, "decode config at ", path)
} }
@@ -109,13 +109,13 @@ func readConfigAndMerge() (option.Options, error) {
} }
var mergedMessage json.RawMessage var mergedMessage json.RawMessage
for _, options := range optionsList { for _, options := range optionsList {
mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage, false) mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
if err != nil { if err != nil {
return option.Options{}, E.Cause(err, "merge config at ", options.path) return option.Options{}, E.Cause(err, "merge config at ", options.path)
} }
} }
var mergedOptions option.Options var mergedOptions option.Options
err = mergedOptions.UnmarshalJSON(mergedMessage) err = mergedOptions.UnmarshalJSONContext(globalCtx, mergedMessage)
if err != nil { if err != nil {
return option.Options{}, E.Cause(err, "unmarshal merged config") return option.Options{}, E.Cause(err, "unmarshal merged config")
} }

View File

@@ -1,6 +1,9 @@
package main package main
import ( import (
"errors"
"os"
"github.com/sagernet/sing-box" "github.com/sagernet/sing-box"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
@@ -23,7 +26,9 @@ func init() {
func createPreStartedClient() (*box.Box, error) { func createPreStartedClient() (*box.Box, error) {
options, err := readConfigAndMerge() options, err := readConfigAndMerge()
if err != nil { if err != nil {
return nil, err if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" {
return nil, err
}
} }
instance, err := box.New(box.Options{Options: options}) instance, err := box.New(box.Options{Options: options})
if err != nil { if err != nil {

View File

@@ -21,7 +21,7 @@ func initializeHTTP3Client(instance *box.Box) error {
return err return err
} }
http3Client = &http.Client{ http3Client = &http.Client{
Transport: &http3.Transport{ Transport: &http3.RoundTripper{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
destination := M.ParseSocksaddr(addr) destination := M.ParseSocksaddr(addr)
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination) udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)

View File

@@ -5,7 +5,7 @@ import (
"testing" "testing"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/route" "github.com/sagernet/sing-box/route/rule"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -26,7 +26,7 @@ example.arpa
`)) `))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, rules, 1) require.Len(t, rules, 1)
rule, err := route.NewHeadlessRule(nil, rules[0]) rule, err := rule.NewHeadlessRule(nil, rules[0])
require.NoError(t, err) require.NoError(t, err)
matchDomain := []string{ matchDomain := []string{
"example.org", "example.org",
@@ -85,7 +85,7 @@ func TestHosts(t *testing.T) {
`)) `))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, rules, 1) require.Len(t, rules, 1)
rule, err := route.NewHeadlessRule(nil, rules[0]) rule, err := rule.NewHeadlessRule(nil, rules[0])
require.NoError(t, err) require.NoError(t, err)
matchDomain := []string{ matchDomain := []string{
"google.com", "google.com",
@@ -115,7 +115,7 @@ www.example.org
`)) `))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, rules, 1) require.Len(t, rules, 1)
rule, err := route.NewHeadlessRule(nil, rules[0]) rule, err := rule.NewHeadlessRule(nil, rules[0])
require.NoError(t, err) require.NoError(t, err)
matchDomain := []string{ matchDomain := []string{
"example.com", "example.com",

View File

@@ -125,7 +125,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi
setMultiPathTCP(&dialer4) setMultiPathTCP(&dialer4)
} }
if options.IsWireGuardListener { if options.IsWireGuardListener {
for _, controlFn := range wgControlFns { for _, controlFn := range WgControlFns {
listener.Control = control.Append(listener.Control, controlFn) listener.Control = control.Append(listener.Control, controlFn)
} }
} }
@@ -179,7 +179,7 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
} }
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) { func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
return d.udpListener.ListenPacket(context.Background(), network, address) return trackPacketConn(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

@@ -2,8 +2,12 @@ package dialer
import ( import (
"net" "net"
"github.com/sagernet/sing/common/control"
) )
type WireGuardListener interface { type WireGuardListener interface {
ListenPacketCompat(network, address string) (net.PacketConn, error) ListenPacketCompat(network, address string) (net.PacketConn, error)
} }
var WgControlFns []control.Func

View File

@@ -1,11 +0,0 @@
//go:build with_wireguard
package dialer
import (
"github.com/sagernet/wireguard-go/conn"
)
var _ WireGuardListener = (conn.Listener)(nil)
var wgControlFns = conn.ControlFns

View File

@@ -1,9 +0,0 @@
//go:build !with_wireguard
package dialer
import (
"github.com/sagernet/sing/common/control"
)
var wgControlFns []control.Func

136
common/listener/listener.go Normal file
View File

@@ -0,0 +1,136 @@
package listener
import (
"context"
"net"
"sync/atomic"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/settings"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type Listener struct {
ctx context.Context
logger logger.ContextLogger
network []string
listenOptions option.ListenOptions
connHandler adapter.ConnectionHandlerEx
packetHandler adapter.PacketHandlerEx
oobPacketHandler adapter.OOBPacketHandlerEx
threadUnsafePacketWriter bool
disablePacketOutput bool
setSystemProxy bool
systemProxySOCKS bool
tcpListener net.Listener
systemProxy settings.SystemProxy
udpConn *net.UDPConn
udpAddr M.Socksaddr
packetOutbound chan *N.PacketBuffer
packetOutboundClosed chan struct{}
shutdown atomic.Bool
}
type Options struct {
Context context.Context
Logger logger.ContextLogger
Network []string
Listen option.ListenOptions
ConnectionHandler adapter.ConnectionHandlerEx
PacketHandler adapter.PacketHandlerEx
OOBPacketHandler adapter.OOBPacketHandlerEx
ThreadUnsafePacketWriter bool
DisablePacketOutput bool
SetSystemProxy bool
SystemProxySOCKS bool
}
func New(
options Options,
) *Listener {
return &Listener{
ctx: options.Context,
logger: options.Logger,
network: options.Network,
listenOptions: options.Listen,
connHandler: options.ConnectionHandler,
packetHandler: options.PacketHandler,
oobPacketHandler: options.OOBPacketHandler,
threadUnsafePacketWriter: options.ThreadUnsafePacketWriter,
disablePacketOutput: options.DisablePacketOutput,
setSystemProxy: options.SetSystemProxy,
systemProxySOCKS: options.SystemProxySOCKS,
}
}
func (l *Listener) Start() error {
if common.Contains(l.network, N.NetworkTCP) {
_, err := l.ListenTCP()
if err != nil {
return err
}
go l.loopTCPIn()
}
if common.Contains(l.network, N.NetworkUDP) {
_, err := l.ListenUDP()
if err != nil {
return err
}
l.packetOutboundClosed = make(chan struct{})
l.packetOutbound = make(chan *N.PacketBuffer, 64)
go l.loopUDPIn()
if !l.disablePacketOutput {
go l.loopUDPOut()
}
}
if l.setSystemProxy {
listenPort := M.SocksaddrFromNet(l.tcpListener.Addr()).Port
var listenAddrString string
listenAddr := l.listenOptions.Listen.Build()
if listenAddr.IsUnspecified() {
listenAddrString = "127.0.0.1"
} else {
listenAddrString = listenAddr.String()
}
systemProxy, err := settings.NewSystemProxy(l.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), l.systemProxySOCKS)
if err != nil {
return E.Cause(err, "initialize system proxy")
}
err = systemProxy.Enable()
if err != nil {
return E.Cause(err, "set system proxy")
}
l.systemProxy = systemProxy
}
return nil
}
func (l *Listener) Close() error {
l.shutdown.Store(true)
var err error
if l.systemProxy != nil && l.systemProxy.IsEnabled() {
err = l.systemProxy.Disable()
}
return E.Errors(err, common.Close(
l.tcpListener,
common.PtrOrNil(l.udpConn),
))
}
func (l *Listener) TCPListener() net.Listener {
return l.tcpListener
}
func (l *Listener) UDPConn() *net.UDPConn {
return l.udpConn
}
func (l *Listener) ListenOptions() option.ListenOptions {
return l.listenOptions
}

View File

@@ -1,6 +1,6 @@
//go:build go1.21 //go:build go1.21
package inbound package listener
import "net" import "net"

View File

@@ -0,0 +1,16 @@
//go:build go1.23
package listener
import (
"net"
"time"
)
func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
listener.KeepAliveConfig = net.KeepAliveConfig{
Enable: true,
Idle: idle,
Interval: interval,
}
}

View File

@@ -1,6 +1,6 @@
//go:build !go1.21 //go:build !go1.21
package inbound package listener
import "net" import "net"

View File

@@ -0,0 +1,15 @@
//go:build !go1.23
package listener
import (
"net"
"time"
"github.com/sagernet/sing/common/control"
)
func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
listener.KeepAlive = idle
listener.Control = control.Append(listener.Control, control.SetKeepAlivePeriod(idle, interval))
}

View File

@@ -0,0 +1,85 @@
package listener
import (
"net"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/metacubex/tfo-go"
)
func (l *Listener) ListenTCP() (net.Listener, error) {
var err error
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort)
var tcpListener net.Listener
var listenConfig net.ListenConfig
if l.listenOptions.TCPKeepAlive >= 0 {
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial
}
keepInterval := time.Duration(l.listenOptions.TCPKeepAliveInterval)
if keepInterval == 0 {
keepInterval = C.TCPKeepAliveInterval
}
setKeepAliveConfig(&listenConfig, keepIdle, keepInterval)
}
if l.listenOptions.TCPMultiPath {
if !go121Available {
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
}
setMultiPathTCP(&listenConfig)
}
if l.listenOptions.TCPFastOpen {
var tfoConfig tfo.ListenConfig
tfoConfig.ListenConfig = listenConfig
tcpListener, err = tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
} else {
tcpListener, err = listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
}
if err == nil {
l.logger.Info("tcp server started at ", tcpListener.Addr())
}
//nolint:staticcheck
if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader {
return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0")
}
l.tcpListener = tcpListener
return tcpListener, err
}
func (l *Listener) loopTCPIn() {
tcpListener := l.tcpListener
var metadata adapter.InboundContext
for {
conn, err := tcpListener.Accept()
if err != nil {
//nolint:staticcheck
if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() {
l.logger.Error(err)
continue
}
if l.shutdown.Load() && E.IsClosed(err) {
return
}
l.tcpListener.Close()
l.logger.Error("tcp listener closed: ", err)
continue
}
//nolint:staticcheck
metadata.InboundDetour = l.listenOptions.Detour
//nolint:staticcheck
metadata.InboundOptions = l.listenOptions.InboundOptions
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
ctx := log.ContextWithNewID(l.ctx)
l.logger.InfoContext(ctx, "inbound connection from ", metadata.Source)
go l.connHandler.NewConnectionEx(ctx, conn, metadata, nil)
}
}

View File

@@ -0,0 +1,154 @@
package listener
import (
"net"
"os"
"time"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func (l *Listener) ListenUDP() (net.PacketConn, error) {
bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort)
var lc net.ListenConfig
var udpFragment bool
if l.listenOptions.UDPFragment != nil {
udpFragment = *l.listenOptions.UDPFragment
} else {
udpFragment = l.listenOptions.UDPFragmentDefault
}
if !udpFragment {
lc.Control = control.Append(lc.Control, control.DisableUDPFragment())
}
udpConn, err := lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
if err != nil {
return nil, err
}
l.udpConn = udpConn.(*net.UDPConn)
l.udpAddr = bindAddr
l.logger.Info("udp server started at ", udpConn.LocalAddr())
return udpConn, err
}
func (l *Listener) UDPAddr() M.Socksaddr {
return l.udpAddr
}
func (l *Listener) PacketWriter() N.PacketWriter {
return (*packetWriter)(l)
}
func (l *Listener) loopUDPIn() {
defer close(l.packetOutboundClosed)
var buffer *buf.Buffer
if !l.threadUnsafePacketWriter {
buffer = buf.NewPacket()
defer buffer.Release()
}
buffer.IncRef()
defer buffer.DecRef()
if l.oobPacketHandler != nil {
oob := make([]byte, 1024)
for {
if l.threadUnsafePacketWriter {
buffer = buf.NewPacket()
} else {
buffer.Reset()
}
n, oobN, _, addr, err := l.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob)
if err != nil {
if l.threadUnsafePacketWriter {
buffer.Release()
}
if l.shutdown.Load() && E.IsClosed(err) {
return
}
l.udpConn.Close()
l.logger.Error("udp listener closed: ", err)
return
}
buffer.Truncate(n)
l.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap())
}
} else {
for {
if l.threadUnsafePacketWriter {
buffer = buf.NewPacket()
} else {
buffer.Reset()
}
n, addr, err := l.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes())
if err != nil {
if l.threadUnsafePacketWriter {
buffer.Release()
}
if l.shutdown.Load() && E.IsClosed(err) {
return
}
l.udpConn.Close()
l.logger.Error("udp listener closed: ", err)
return
}
buffer.Truncate(n)
l.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap())
}
}
}
func (l *Listener) loopUDPOut() {
for {
select {
case packet := <-l.packetOutbound:
destination := packet.Destination.AddrPort()
_, err := l.udpConn.WriteToUDPAddrPort(packet.Buffer.Bytes(), destination)
packet.Buffer.Release()
N.PutPacketBuffer(packet)
if err != nil {
if l.shutdown.Load() && E.IsClosed(err) {
return
}
l.udpConn.Close()
l.logger.Error("udp listener write back: ", destination, ": ", err)
return
}
continue
case <-l.packetOutboundClosed:
}
for {
select {
case packet := <-l.packetOutbound:
packet.Buffer.Release()
N.PutPacketBuffer(packet)
case <-time.After(time.Second):
return
}
}
}
}
type packetWriter Listener
func (w *packetWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
packet := N.NewPacketBuffer()
packet.Buffer = buffer
packet.Destination = destination
select {
case w.packetOutbound <- packet:
return nil
default:
buffer.Release()
N.PutPacketBuffer(packet)
if w.shutdown.Load() {
return os.ErrClosed
}
w.logger.Trace("dropped packet to ", destination)
return nil
}
}
func (w *packetWriter) WriteIsThreadUnsafe() {
}

View File

@@ -15,11 +15,11 @@ import (
) )
type Router struct { type Router struct {
router adapter.ConnectionRouter router adapter.ConnectionRouterEx
service *mux.Service service *mux.Service
} }
func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouter, error) { func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouterEx, error) {
if !options.Enabled { if !options.Enabled {
return router, nil return router, nil
} }
@@ -54,6 +54,7 @@ func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.Context
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
return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata)) return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata))
} else { } else {
return r.router.RouteConnection(ctx, conn, metadata) return r.router.RouteConnection(ctx, conn, metadata)
@@ -63,3 +64,15 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
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)
} }
func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
if metadata.Destination == mux.Destination {
r.service.NewConnectionEx(adapter.WithContext(ctx, &metadata), conn, metadata.Source, metadata.Destination, onClose)
return
}
r.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}
func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}

View File

@@ -1,32 +0,0 @@
package mux
import (
"context"
"net"
"github.com/sagernet/sing-box/adapter"
vmess "github.com/sagernet/sing-vmess"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
)
type V2RayLegacyRouter struct {
router adapter.ConnectionRouter
logger logger.ContextLogger
}
func NewV2RayLegacyRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) adapter.ConnectionRouter {
return &V2RayLegacyRouter{router, logger}
}
func (r *V2RayLegacyRouter) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
if metadata.Destination.Fqdn == vmess.MuxDestination.Fqdn {
r.logger.InfoContext(ctx, "inbound legacy multiplex connection")
return vmess.HandleMuxConnection(ctx, conn, adapter.NewRouteHandler(metadata, r.router, r.logger))
}
return r.router.RouteConnection(ctx, conn, metadata)
}
func (r *V2RayLegacyRouter) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
return r.router.RoutePacketConnection(ctx, conn, metadata)
}

View File

@@ -18,7 +18,7 @@ type (
PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error
) )
func Skip(metadata adapter.InboundContext) bool { func Skip(metadata *adapter.InboundContext) bool {
// skip server first protocols // skip server first protocols
switch metadata.Destination.Port { switch metadata.Destination.Port {
case 25, 465, 587: case 25, 465, 587:

View File

@@ -41,7 +41,7 @@ const (
ruleItemFinal uint8 = 0xFF ruleItemFinal uint8 = 0xFF
) )
func Read(reader io.Reader, recover bool) (ruleSetCompat option.PlainRuleSetCompat, err error) { func Read(reader io.Reader, recover bool) (ruleSet option.PlainRuleSet, err error) {
var magicBytes [3]byte var magicBytes [3]byte
_, err = io.ReadFull(reader, magicBytes[:]) _, err = io.ReadFull(reader, magicBytes[:])
if err != nil { if err != nil {
@@ -54,10 +54,10 @@ func Read(reader io.Reader, recover bool) (ruleSetCompat option.PlainRuleSetComp
var version uint8 var version uint8
err = binary.Read(reader, binary.BigEndian, &version) err = binary.Read(reader, binary.BigEndian, &version)
if err != nil { if err != nil {
return ruleSetCompat, err return ruleSet, err
} }
if version > C.RuleSetVersionCurrent { if version > C.RuleSetVersion2 {
return ruleSetCompat, E.New("unsupported version: ", version) return ruleSet, E.New("unsupported version: ", version)
} }
compressReader, err := zlib.NewReader(reader) compressReader, err := zlib.NewReader(reader)
if err != nil { if err != nil {
@@ -68,10 +68,9 @@ func Read(reader io.Reader, recover bool) (ruleSetCompat option.PlainRuleSetComp
if err != nil { if err != nil {
return return
} }
ruleSetCompat.Version = version ruleSet.Rules = make([]option.HeadlessRule, length)
ruleSetCompat.Options.Rules = make([]option.HeadlessRule, length)
for i := uint64(0); i < length; i++ { for i := uint64(0); i < length; i++ {
ruleSetCompat.Options.Rules[i], err = readRule(bReader, recover) ruleSet.Rules[i], err = readRule(bReader, recover)
if err != nil { if err != nil {
err = E.Cause(err, "read rule[", i, "]") err = E.Cause(err, "read rule[", i, "]")
return return
@@ -80,12 +79,18 @@ func Read(reader io.Reader, recover bool) (ruleSetCompat option.PlainRuleSetComp
return return
} }
func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateVersion uint8) error { func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateUnstable bool) error {
_, err := writer.Write(MagicBytes[:]) _, err := writer.Write(MagicBytes[:])
if err != nil { if err != nil {
return err return err
} }
err = binary.Write(writer, binary.BigEndian, generateVersion) var version uint8
if generateUnstable {
version = C.RuleSetVersion2
} else {
version = C.RuleSetVersion1
}
err = binary.Write(writer, binary.BigEndian, version)
if err != nil { if err != nil {
return err return err
} }
@@ -99,7 +104,7 @@ func Write(writer io.Writer, ruleSet option.PlainRuleSet, generateVersion uint8)
return err return err
} }
for _, rule := range ruleSet.Rules { for _, rule := range ruleSet.Rules {
err = writeRule(bWriter, rule, generateVersion) err = writeRule(bWriter, rule, generateUnstable)
if err != nil { if err != nil {
return err return err
} }
@@ -130,12 +135,12 @@ func readRule(reader varbin.Reader, recover bool) (rule option.HeadlessRule, err
return return
} }
func writeRule(writer varbin.Writer, rule option.HeadlessRule, generateVersion uint8) error { func writeRule(writer varbin.Writer, rule option.HeadlessRule, generateUnstable bool) error {
switch rule.Type { switch rule.Type {
case C.RuleTypeDefault: case C.RuleTypeDefault:
return writeDefaultRule(writer, rule.DefaultOptions, generateVersion) return writeDefaultRule(writer, rule.DefaultOptions, generateUnstable)
case C.RuleTypeLogical: case C.RuleTypeLogical:
return writeLogicalRule(writer, rule.LogicalOptions, generateVersion) return writeLogicalRule(writer, rule.LogicalOptions, generateUnstable)
default: default:
panic("unknown rule type: " + rule.Type) panic("unknown rule type: " + rule.Type)
} }
@@ -235,7 +240,7 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
} }
} }
func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, generateVersion uint8) error { func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, generateUnstable bool) error {
err := binary.Write(writer, binary.BigEndian, uint8(0)) err := binary.Write(writer, binary.BigEndian, uint8(0))
if err != nil { if err != nil {
return err return err
@@ -259,7 +264,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
if err != nil { if err != nil {
return err return err
} }
err = domain.NewMatcher(rule.Domain, rule.DomainSuffix, generateVersion == C.RuleSetVersion1).Write(writer) err = domain.NewMatcher(rule.Domain, rule.DomainSuffix, !generateUnstable).Write(writer)
if err != nil { if err != nil {
return err return err
} }
@@ -349,9 +354,6 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
} }
} }
if len(rule.AdGuardDomain) > 0 { if len(rule.AdGuardDomain) > 0 {
if generateVersion < C.RuleSetVersion2 {
return E.New("AdGuard rule items is only supported in version 2 or later")
}
err = binary.Write(writer, binary.BigEndian, ruleItemAdGuardDomain) err = binary.Write(writer, binary.BigEndian, ruleItemAdGuardDomain)
if err != nil { if err != nil {
return err return err
@@ -455,7 +457,7 @@ func readLogicalRule(reader varbin.Reader, recovery bool) (logicalRule option.Lo
return return
} }
func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule, generateVersion uint8) error { func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRule, generateUnstable bool) error {
err := binary.Write(writer, binary.BigEndian, uint8(1)) err := binary.Write(writer, binary.BigEndian, uint8(1))
if err != nil { if err != nil {
return err return err
@@ -476,7 +478,7 @@ func writeLogicalRule(writer varbin.Writer, logicalRule option.LogicalHeadlessRu
return err return err
} }
for _, rule := range logicalRule.Rules { for _, rule := range logicalRule.Rules {
err = writeRule(writer, rule, generateVersion) err = writeRule(writer, rule, generateUnstable)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -63,7 +63,6 @@ type echConnWrapper struct {
func (c *echConnWrapper) ConnectionState() tls.ConnectionState { func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
state := c.Conn.ConnectionState() state := c.Conn.ConnectionState()
//nolint:staticcheck
return tls.ConnectionState{ return tls.ConnectionState{
Version: state.Version, Version: state.Version,
HandshakeComplete: state.HandshakeComplete, HandshakeComplete: state.HandshakeComplete,

View File

@@ -147,9 +147,6 @@ func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite [
pair.rawConf = b pair.rawConf = b
secBuf, err := sec.MarshalBinary() secBuf, err := sec.MarshalBinary()
if err != nil {
return nil, E.Cause(err, "serialize ECH private key")
}
sk := []byte{} sk := []byte{}
sk = be.AppendUint16(sk, uint16(len(secBuf))) sk = be.AppendUint16(sk, uint16(len(secBuf)))
sk = append(sk, secBuf...) sk = append(sk, secBuf...)

View File

@@ -28,7 +28,7 @@ func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, ad
} }
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper { func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config) http.RoundTripper {
return &http3.Transport{ return &http3.RoundTripper{
TLSClientConfig: c.config, TLSClientConfig: c.config,
QUICConfig: quicConfig, QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {

View File

@@ -97,10 +97,6 @@ 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
} }
@@ -236,7 +232,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.ECH.KeyPath != "" { } else if options.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

@@ -175,7 +175,6 @@ type realityConnWrapper struct {
func (c *realityConnWrapper) ConnectionState() ConnectionState { func (c *realityConnWrapper) ConnectionState() ConnectionState {
state := c.Conn.ConnectionState() state := c.Conn.ConnectionState()
//nolint:staticcheck
return tls.ConnectionState{ return tls.ConnectionState{
Version: state.Version, Version: state.Version,
HandshakeComplete: state.HandshakeComplete, HandshakeComplete: state.HandshakeComplete,

View File

@@ -106,10 +106,6 @@ 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
} }

View File

@@ -69,7 +69,6 @@ type utlsConnWrapper struct {
func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState { func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState {
state := c.Conn.ConnectionState() state := c.Conn.ConnectionState()
//nolint:staticcheck
return tls.ConnectionState{ return tls.ConnectionState{
Version: state.Version, Version: state.Version,
HandshakeComplete: state.HandshakeComplete, HandshakeComplete: state.HandshakeComplete,

View File

@@ -13,14 +13,14 @@ import (
"github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/common/uot"
) )
var _ adapter.ConnectionRouter = (*Router)(nil) var _ adapter.ConnectionRouterEx = (*Router)(nil)
type Router struct { type Router struct {
router adapter.ConnectionRouter router adapter.ConnectionRouterEx
logger logger.ContextLogger logger logger.ContextLogger
} }
func NewRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) *Router { func NewRouter(router adapter.ConnectionRouterEx, logger logger.ContextLogger) *Router {
return &Router{router, logger} return &Router{router, logger}
} }
@@ -51,3 +51,36 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
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)
} }
func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
switch metadata.Destination.Fqdn {
case uot.MagicAddress:
request, err := uot.ReadRequest(conn)
if err != nil {
err = E.Cause(err, "UoT read request")
r.logger.ErrorContext(ctx, "process connection from ", metadata.Source, ": ", err)
N.CloseOnHandshakeFailure(conn, onClose, err)
return
}
if request.IsConnect {
r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination)
} else {
r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination)
}
metadata.Domain = metadata.Destination.Fqdn
metadata.Destination = request.Destination
r.router.RoutePacketConnectionEx(ctx, uot.NewConn(conn, *request), metadata, onClose)
return
case uot.LegacyMagicAddress:
r.logger.InfoContext(ctx, "inbound legacy UoT connection")
metadata.Domain = metadata.Destination.Fqdn
metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()}
r.RoutePacketConnectionEx(ctx, uot.NewConn(conn, uot.Request{}), metadata, onClose)
return
}
r.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}
func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}

View File

@@ -21,5 +21,22 @@ const (
const ( const (
RuleSetVersion1 = 1 + iota RuleSetVersion1 = 1 + iota
RuleSetVersion2 RuleSetVersion2
RuleSetVersionCurrent = RuleSetVersion2 )
const (
RuleActionTypeRoute = "route"
RuleActionTypeReturn = "return"
RuleActionTypeReject = "reject"
RuleActionTypeHijackDNS = "hijack-dns"
RuleActionTypeSniff = "sniff"
RuleActionTypeResolve = "resolve"
)
const (
RuleActionRejectMethodDefault = "default"
RuleActionRejectMethodReset = "reset"
RuleActionRejectMethodNetworkUnreachable = "network-unreachable"
RuleActionRejectMethodHostUnreachable = "host-unreachable"
RuleActionRejectMethodPortUnreachable = "port-unreachable"
RuleActionRejectMethodDrop = "drop"
) )

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,14 +2,18 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
### 1.10.6 #### 1.11.0-alpha.5
* Fixes and improvements * Fixes and improvements
### 1.10.2 #### 1.11.0-alpha.2
* Add deprecated warnings * Add warnings for usage of deprecated features
* Fix proxying websocket connections in HTTP/mixed inbounds * Fixes and improvements
#### 1.11.0-alpha.1
* Update quic-go to v0.48.0
* Fixes and improvements * Fixes and improvements
### 1.10.1 ### 1.10.1
@@ -43,7 +47,7 @@ Important changes since 1.9:
The new auto-redirect feature allows TUN to automatically The new auto-redirect feature allows TUN to automatically
configure connection redirection to improve proxy performance. configure connection redirection to improve proxy performance.
When auto-redirect is enabled, new route address set options will allow you to When auto-redirect is enabled, new route address set options will allow you to
automatically configure destination IP CIDR rules from a specified rule set to the firewall. automatically configure destination IP CIDR rules from a specified rule set to the firewall.
Specified or unspecified destinations will bypass the sing-box routes to get better performance Specified or unspecified destinations will bypass the sing-box routes to get better performance

View File

@@ -110,13 +110,13 @@ icon: material/new-box
0 0
], ],
"include_uid_range": [ "include_uid_range": [
"1000:99999" "1000-99999"
], ],
"exclude_uid": [ "exclude_uid": [
1000 1000
], ],
"exclude_uid_range": [ "exclude_uid_range": [
"1000:99999" "1000-99999"
], ],
"include_android_user": [ "include_android_user": [
0, 0,

View File

@@ -110,13 +110,13 @@ icon: material/new-box
0 0
], ],
"include_uid_range": [ "include_uid_range": [
"1000:99999" "1000-99999"
], ],
"exclude_uid": [ "exclude_uid": [
1000 1000
], ],
"exclude_uid_range": [ "exclude_uid_range": [
"1000:99999" "1000-99999"
], ],
"include_android_user": [ "include_android_user": [
0, 0,

View File

@@ -10,7 +10,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch" "github.com/sagernet/sing/common/batch"
"github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badjson"
@@ -59,7 +59,7 @@ func getGroup(server *Server) func(w http.ResponseWriter, r *http.Request) {
func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) { func getGroupDelay(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) {
proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
group, ok := proxy.(adapter.OutboundGroup) outboundGroup, ok := proxy.(adapter.OutboundGroup)
if !ok { if !ok {
render.Status(r, http.StatusNotFound) render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound) render.JSON(w, r, ErrNotFound)
@@ -82,10 +82,10 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
defer cancel() defer cancel()
var result map[string]uint16 var result map[string]uint16
if urlTestGroup, isURLTestGroup := group.(adapter.URLTestGroup); isURLTestGroup { if urlTestGroup, isURLTestGroup := outboundGroup.(adapter.URLTestGroup); isURLTestGroup {
result, err = urlTestGroup.URLTest(ctx) result, err = urlTestGroup.URLTest(ctx)
} else { } else {
outbounds := common.FilterNotNil(common.Map(group.All(), func(it string) adapter.Outbound { outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
itOutbound, _ := server.router.Outbound(it) itOutbound, _ := server.router.Outbound(it)
return itOutbound return itOutbound
})) }))
@@ -95,7 +95,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
var resultAccess sync.Mutex var resultAccess sync.Mutex
for _, detour := range outbounds { for _, detour := range outbounds {
tag := detour.Tag() tag := detour.Tag()
realTag := outbound.RealTag(detour) realTag := group.RealTag(detour)
if checked[realTag] { if checked[realTag] {
continue continue
} }

View File

@@ -18,19 +18,17 @@ 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"`
// sing-box added LogLevel string `json:"log-level"`
ModeList []string `json:"mode-list"` IPv6 bool `json:"ipv6"`
LogLevel string `json:"log-level"` Tun map[string]any `json:"tun"`
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) {
@@ -43,7 +41,6 @@ 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

@@ -11,7 +11,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json/badjson" "github.com/sagernet/sing/common/json/badjson"
@@ -168,7 +168,7 @@ func updateProxy(w http.ResponseWriter, r *http.Request) {
} }
proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
selector, ok := proxy.(*outbound.Selector) selector, ok := proxy.(*group.Selector)
if !ok { if !ok {
render.Status(r, http.StatusBadRequest) render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("Must be a Selector")) render.JSON(w, r, newError("Must be a Selector"))
@@ -204,7 +204,7 @@ func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
delay, err := urltest.URLTest(ctx, url, proxy) delay, err := urltest.URLTest(ctx, url, proxy)
defer func() { defer func() {
realTag := outbound.RealTag(proxy) realTag := group.RealTag(proxy)
if err != nil { if err != nil {
server.urlTestHistory.DeleteURLTestHistory(realTag) server.urlTestHistory.DeleteURLTestHistory(realTag)
} else { } else {

View File

@@ -30,10 +30,9 @@ func getRules(router adapter.Router) func(w http.ResponseWriter, r *http.Request
rules = append(rules, Rule{ rules = append(rules, Rule{
Type: rule.Type(), Type: rule.Type(),
Payload: rule.String(), Payload: rule.String(),
Proxy: rule.Outbound(), Proxy: rule.Action().String(),
}) })
} }
render.JSON(w, r, render.M{ render.JSON(w, r, render.M{
"rules": rules, "rules": rules,
}) })

View File

@@ -124,8 +124,11 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
if options.ExternalUI != "" { if options.ExternalUI != "" {
server.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI)) server.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI))
chiRouter.Group(func(r chi.Router) { chiRouter.Group(func(r chi.Router) {
r.Get("/ui", http.RedirectHandler("/ui/", http.StatusMovedPermanently).ServeHTTP) fs := http.StripPrefix("/ui", http.FileServer(http.Dir(server.externalUI)))
r.Handle("/ui/*", http.StripPrefix("/ui/", http.FileServer(http.Dir(server.externalUI)))) r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP)
r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) {
fs.ServeHTTP(w, r)
})
}) })
} }
return server, nil return server, nil
@@ -313,29 +316,27 @@ 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{}
uploadTotal, downloadTotal := trafficManager.Total() var err error
for range tick.C { for range tick.C {
buf.Reset() buf.Reset()
uploadTotalNew, downloadTotalNew := trafficManager.Total() up, down := trafficManager.Now()
err := json.NewEncoder(buf).Encode(Traffic{ if err := json.NewEncoder(buf).Encode(Traffic{
Up: uploadTotalNew - uploadTotal, Up: up,
Down: downloadTotalNew - downloadTotal, Down: down,
}) }); 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

@@ -16,18 +16,30 @@ 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 {
return &Manager{} manager := &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) {
@@ -49,13 +61,19 @@ 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()
} }
@@ -109,10 +127,36 @@ 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

@@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
@@ -60,7 +61,7 @@ func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
} }
var rule string var rule string
if t.Rule != nil { if t.Rule != nil {
rule = F.ToString(t.Rule, " => ", t.Rule.Outbound()) rule = F.ToString(t.Rule, " => ", t.Rule.Action())
} else { } else {
rule = "final" rule = "final"
} }
@@ -131,19 +132,21 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundCont
outbound string outbound string
outboundType string outboundType string
) )
if rule == nil { var action adapter.RuleAction
if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { if rule != nil {
next = defaultOutbound.Tag() action = rule.Action()
} }
} else { if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction {
next = rule.Outbound() next = routeAction.Outbound
} else if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
next = defaultOutbound.Tag()
} }
for { for {
chain = append(chain, next)
detour, loaded := router.Outbound(next) detour, loaded := router.Outbound(next)
if !loaded { if !loaded {
break break
} }
chain = append(chain, next)
outbound = detour.Tag() outbound = detour.Tag()
outboundType = detour.Type() outboundType = detour.Type()
group, isGroup := detour.(adapter.OutboundGroup) group, isGroup := detour.(adapter.OutboundGroup)
@@ -218,19 +221,21 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.Inbound
outbound string outbound string
outboundType string outboundType string
) )
if rule == nil { var action adapter.RuleAction
if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { if rule != nil {
next = defaultOutbound.Tag() action = rule.Action()
} }
} else { if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction {
next = rule.Outbound() next = routeAction.Outbound
} else if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil {
next = defaultOutbound.Tag()
} }
for { for {
chain = append(chain, next)
detour, loaded := router.Outbound(next) detour, loaded := router.Outbound(next)
if !loaded { if !loaded {
break break
} }
chain = append(chain, next)
outbound = detour.Tag() outbound = detour.Tag()
outboundType = detour.Type() outboundType = detour.Type()
group, isGroup := detour.(adapter.OutboundGroup) group, isGroup := detour.(adapter.OutboundGroup)

View File

@@ -1,11 +1,7 @@
package deprecated package deprecated
import ( import (
"fmt"
"github.com/sagernet/sing-box/common/badversion"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/locale"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
"golang.org/x/mod/semver" "golang.org/x/mod/semver"
@@ -27,20 +23,18 @@ func (n Note) Impending() bool {
if !semver.IsValid("v" + C.Version) { if !semver.IsValid("v" + C.Version) {
return false return false
} }
versionCurrent := badversion.Parse(C.Version) versionMinor := semver.Compare(semver.MajorMinor("v"+C.Version), "v"+n.ScheduledVersion)
versionMinor := badversion.Parse(n.ScheduledVersion).Minor - versionCurrent.Minor if versionMinor < 0 {
if versionCurrent.PreReleaseIdentifier == "" && versionMinor < 0 {
panic("invalid deprecated note: " + n.Name) panic("invalid deprecated note: " + n.Name)
} }
return versionMinor <= 1 return versionMinor <= 1
} }
func (n Note) Message() string { func (n Note) Message() string {
if n.MigrationLink != "" { return F.ToString(
return fmt.Sprintf(locale.Current().DeprecatedMessage, n.Description, n.DeprecatedVersion, n.ScheduledVersion) n.Description, " is deprecated in sing-box ", n.DeprecatedVersion,
} else { " and will be removed in sing-box ", n.ScheduledVersion, ", please checkout documentation for migration.",
return fmt.Sprintf(locale.Current().DeprecatedMessageNoLink, n.Description, n.DeprecatedVersion, n.ScheduledVersion) )
}
} }
func (n Note) MessageWithLink() string { func (n Note) MessageWithLink() string {
@@ -55,7 +49,6 @@ var OptionBadMatchSource = Note{
Description: "legacy match source rule item", Description: "legacy match source rule item",
DeprecatedVersion: "1.10.0", DeprecatedVersion: "1.10.0",
ScheduledVersion: "1.11.0", ScheduledVersion: "1.11.0",
EnvName: "BAD_MATCH_SOURCE",
MigrationLink: "https://sing-box.sagernet.org/deprecated/#match-source-rule-items-are-renamed", MigrationLink: "https://sing-box.sagernet.org/deprecated/#match-source-rule-items-are-renamed",
} }
@@ -82,7 +75,6 @@ var OptionTUNAddressX = Note{
Description: "legacy tun address fields", Description: "legacy tun address fields",
DeprecatedVersion: "1.10.0", DeprecatedVersion: "1.10.0",
ScheduledVersion: "1.12.0", ScheduledVersion: "1.12.0",
EnvName: "TUN_ADDRESS_X",
MigrationLink: "https://sing-box.sagernet.org/migration/#tun-address-fields-are-merged", MigrationLink: "https://sing-box.sagernet.org/migration/#tun-address-fields-are-merged",
} }

View File

@@ -7,23 +7,15 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
) )
type stderrManager struct { type envManager struct {
logger logger.Logger logger logger.Logger
reported map[string]bool
} }
func NewStderrManager(logger logger.Logger) Manager { func NewEnvManager(logger logger.Logger) Manager {
return &stderrManager{ return &envManager{logger: logger}
logger: logger,
reported: make(map[string]bool),
}
} }
func (f *stderrManager) ReportDeprecated(feature Note) { func (f *envManager) ReportDeprecated(feature Note) {
if f.reported[feature.Name] {
return
}
f.reported[feature.Name] = true
if !feature.Impending() { if !feature.Impending() {
f.logger.Warn(feature.MessageWithLink()) f.logger.Warn(feature.MessageWithLink())
return return

View File

@@ -20,6 +20,7 @@ type CommandClient struct {
type CommandClientOptions struct { type CommandClientOptions struct {
Command int32 Command int32
StatusInterval int64 StatusInterval int64
IsMainClient bool
} }
type CommandClientHandler interface { type CommandClientHandler interface {
@@ -28,6 +29,7 @@ type CommandClientHandler interface {
ClearLogs() ClearLogs()
WriteLogs(messageList StringIterator) WriteLogs(messageList StringIterator)
WriteStatus(message *StatusMessage) WriteStatus(message *StatusMessage)
OpenURL(url string)
WriteGroups(message OutboundGroupIterator) WriteGroups(message OutboundGroupIterator)
InitializeClashMode(modeList StringIterator, currentMode string) InitializeClashMode(modeList StringIterator, currentMode string)
UpdateClashMode(newMode string) UpdateClashMode(newMode string)
@@ -91,9 +93,13 @@ func (c *CommandClient) Connect() error {
c.handler.Connected() c.handler.Connected()
go c.handleLogConn(conn) go c.handleLogConn(conn)
case CommandStatus: case CommandStatus:
err = binary.Write(conn, binary.BigEndian, c.options.IsMainClient)
if err != nil {
return E.Cause(err, "write is main client")
}
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval) err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
if err != nil { if err != nil {
return E.Cause(err, "write interval") return E.Cause(err, "write header")
} }
c.handler.Connected() c.handler.Connected()
go c.handleStatusConn(conn) go c.handleStatusConn(conn)
@@ -113,24 +119,11 @@ func (c *CommandClient) Connect() error {
if err != nil { if err != nil {
return err return err
} }
if sFixAndroidStack { c.handler.Connected()
go func() { c.handler.InitializeClashMode(newIterator(modeList), currentMode)
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

@@ -0,0 +1,40 @@
package libbox
import (
"encoding/binary"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
)
type myEvent interface {
writeTo(writer varbin.Writer)
}
func readEvent(reader varbin.Reader) (myEvent, error) {
eventType, err := reader.ReadByte()
if err != nil {
return nil, err
}
switch eventType {
case eventTypeEmpty:
return nil, nil
case eventTypeOpenURL:
url, err := varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil {
return nil, err
}
return &eventOpenURL{URL: url}, nil
default:
return nil, E.New("unknown event type: ", eventType)
}
}
type eventOpenURL struct {
URL string
}
func (e *eventOpenURL) writeTo(writer varbin.Writer) {
writer.WriteByte(eventTypeOpenURL)
varbin.Write(writer, binary.BigEndian, e.URL)
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/protocol/group"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/common/varbin"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
@@ -118,14 +118,14 @@ func writeGroups(writer io.Writer, boxService *BoxService) error {
} }
var groups []OutboundGroup var groups []OutboundGroup
for _, iGroup := range iGroups { for _, iGroup := range iGroups {
var group OutboundGroup var outboundGroup OutboundGroup
group.Tag = iGroup.Tag() outboundGroup.Tag = iGroup.Tag()
group.Type = iGroup.Type() outboundGroup.Type = iGroup.Type()
_, group.Selectable = iGroup.(*outbound.Selector) _, outboundGroup.Selectable = iGroup.(*group.Selector)
group.Selected = iGroup.Now() outboundGroup.Selected = iGroup.Now()
if cacheFile != nil { if cacheFile != nil {
if isExpand, loaded := cacheFile.LoadGroupExpand(group.Tag); loaded { if isExpand, loaded := cacheFile.LoadGroupExpand(outboundGroup.Tag); loaded {
group.IsExpand = isExpand outboundGroup.IsExpand = isExpand
} }
} }
@@ -142,12 +142,12 @@ func writeGroups(writer io.Writer, boxService *BoxService) error {
item.URLTestTime = history.Time.Unix() item.URLTestTime = history.Time.Unix()
item.URLTestDelay = int32(history.Delay) item.URLTestDelay = int32(history.Delay)
} }
group.ItemList = append(group.ItemList, &item) outboundGroup.ItemList = append(outboundGroup.ItemList, &item)
} }
if len(group.ItemList) < 2 { if len(outboundGroup.ItemList) < 2 {
continue continue
} }
groups = append(groups, group) groups = append(groups, outboundGroup)
} }
return varbin.Write(writer, binary.BigEndian, groups) return varbin.Write(writer, binary.BigEndian, groups)
} }

View File

@@ -4,7 +4,7 @@ import (
"encoding/binary" "encoding/binary"
"net" "net"
"github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/protocol/group"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/common/varbin"
) )
@@ -47,7 +47,7 @@ func (s *CommandServer) handleSelectOutbound(conn net.Conn) error {
if !isLoaded { if !isLoaded {
return writeError(conn, E.New("selector not found: ", groupTag)) return writeError(conn, E.New("selector not found: ", groupTag))
} }
selector, isSelector := outboundGroup.(*outbound.Selector) selector, isSelector := outboundGroup.(*group.Selector)
if !isSelector { if !isSelector {
return writeError(conn, E.New("outbound is not a selector: ", groupTag)) return writeError(conn, E.New("outbound is not a selector: ", groupTag))
} }

View File

@@ -33,6 +33,7 @@ type CommandServer struct {
urlTestUpdate chan struct{} urlTestUpdate chan struct{}
modeUpdate chan struct{} modeUpdate chan struct{}
logReset chan struct{} logReset chan struct{}
events chan myEvent
closedConnections []Connection closedConnections []Connection
} }
@@ -52,6 +53,7 @@ func NewCommandServer(handler CommandServerHandler, maxLines int32) *CommandServ
urlTestUpdate: make(chan struct{}, 1), urlTestUpdate: make(chan struct{}, 1),
modeUpdate: make(chan struct{}, 1), modeUpdate: make(chan struct{}, 1),
logReset: make(chan struct{}, 1), logReset: make(chan struct{}, 1),
events: make(chan myEvent, 8),
} }
server.observer = observable.NewObserver[string](server.subscriber, 64) server.observer = observable.NewObserver[string](server.subscriber, 64)
return server return server
@@ -61,6 +63,12 @@ func (s *CommandServer) SetService(newService *BoxService) {
if newService != nil { if newService != nil {
service.PtrFromContext[urltest.HistoryStorage](newService.ctx).SetHook(s.urlTestUpdate) service.PtrFromContext[urltest.HistoryStorage](newService.ctx).SetHook(s.urlTestUpdate)
newService.instance.Router().ClashServer().(*clashapi.Server).SetModeUpdateHook(s.modeUpdate) newService.instance.Router().ClashServer().(*clashapi.Server).SetModeUpdateHook(s.modeUpdate)
newService.platformInterface.openURLFunc = func(url string) {
select {
case s.events <- &eventOpenURL{URL: url}:
default:
}
}
} }
s.service = newService s.service = newService
s.notifyURLTestUpdate() s.notifyURLTestUpdate()

View File

@@ -1,6 +1,7 @@
package libbox package libbox
import ( import (
std_bufio "bufio"
"encoding/binary" "encoding/binary"
"net" "net"
"runtime" "runtime"
@@ -9,9 +10,15 @@ import (
"github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/experimental/clashapi" "github.com/sagernet/sing-box/experimental/clashapi"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/memory" "github.com/sagernet/sing/common/memory"
) )
const (
eventTypeEmpty byte = iota
eventTypeOpenURL
)
type StatusMessage struct { type StatusMessage struct {
Memory int64 Memory int64
Goroutines int32 Goroutines int32
@@ -34,6 +41,7 @@ func (s *CommandServer) readStatus() StatusMessage {
if clashServer := s.service.instance.Router().ClashServer(); clashServer != nil { if clashServer := s.service.instance.Router().ClashServer(); clashServer != nil {
message.TrafficAvailable = true message.TrafficAvailable = true
trafficManager := clashServer.(*clashapi.Server).TrafficManager() trafficManager := 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())
} }
@@ -43,41 +51,73 @@ func (s *CommandServer) readStatus() StatusMessage {
} }
func (s *CommandServer) handleStatusConn(conn net.Conn) error { func (s *CommandServer) handleStatusConn(conn net.Conn) error {
var isMainClient bool
err := binary.Read(conn, binary.BigEndian, &isMainClient)
if err != nil {
return E.Cause(err, "read is main client")
}
var interval int64 var interval int64
err := binary.Read(conn, binary.BigEndian, &interval) err = binary.Read(conn, binary.BigEndian, &interval)
if err != nil { if err != nil {
return E.Cause(err, "read interval") return E.Cause(err, "read interval")
} }
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() writer := std_bufio.NewWriter(conn)
uploadTotal := status.UplinkTotal if isMainClient {
downloadTotal := status.DownlinkTotal for {
for { writer.WriteByte(eventTypeEmpty)
err = binary.Write(conn, binary.BigEndian, status) err = binary.Write(conn, binary.BigEndian, s.readStatus())
if err != nil { if err != nil {
return err return err
}
writer.Flush()
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
case event := <-s.events:
event.writeTo(writer)
writer.Flush()
}
} }
select { } else {
case <-ctx.Done(): for {
return ctx.Err() err = binary.Write(conn, binary.BigEndian, s.readStatus())
case <-ticker.C: if err != nil {
return err
}
writer.Flush()
select {
case <-ctx.Done():
return ctx.Err()
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
} }
} }
func (c *CommandClient) handleStatusConn(conn net.Conn) { func (c *CommandClient) handleStatusConn(conn net.Conn) {
reader := std_bufio.NewReader(conn)
for { for {
if c.options.IsMainClient {
rawEvent, err := readEvent(reader)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
switch event := rawEvent.(type) {
case *eventOpenURL:
c.handler.OpenURL(event.URL)
continue
case nil:
default:
panic(F.ToString("unexpected event type: ", event))
}
}
var message StatusMessage var message StatusMessage
err := binary.Read(conn, binary.BigEndian, &message) err := binary.Read(reader, binary.BigEndian, &message)
if err != nil { if err != nil {
c.handler.Disconnected(err.Error()) c.handler.Disconnected(err.Error())
return return

View File

@@ -7,7 +7,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch" "github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@@ -49,7 +49,7 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error {
if !isOutboundGroup { if !isOutboundGroup {
return writeError(conn, E.New("outbound is not a group: ", groupTag)) return writeError(conn, E.New("outbound is not a group: ", groupTag))
} }
urlTest, isURLTest := abstractOutboundGroup.(*outbound.URLTest) urlTest, isURLTest := abstractOutboundGroup.(*group.URLTest)
if isURLTest { if isURLTest {
go urlTest.CheckOutbounds() go urlTest.CheckOutbounds()
} else { } else {

View File

@@ -10,6 +10,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/common/process"
"github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
@@ -17,10 +18,11 @@ import (
"github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
) )
func parseConfig(configContent string) (option.Options, error) { func parseConfig(ctx context.Context, configContent string) (option.Options, error) {
options, err := json.UnmarshalExtended[option.Options]([]byte(configContent)) options, err := json.UnmarshalExtendedContext[option.Options](ctx, []byte(configContent))
if err != nil { if err != nil {
return option.Options{}, E.Cause(err, "decode config") return option.Options{}, E.Cause(err, "decode config")
} }
@@ -28,16 +30,16 @@ func parseConfig(configContent string) (option.Options, error) {
} }
func CheckConfig(configContent string) error { func CheckConfig(configContent string) error {
options, err := parseConfig(configContent) options, err := parseConfig(box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()), configContent)
if err != nil { if err != nil {
return err return err
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
ctx = service.ContextWith[platform.Interface](ctx, (*platformInterfaceStub)(nil))
instance, err := box.New(box.Options{ instance, err := box.New(box.Options{
Context: ctx, Context: ctx,
Options: options, Options: options,
PlatformInterface: (*platformInterfaceStub)(nil),
}) })
if err == nil { if err == nil {
instance.Close() instance.Close()
@@ -55,7 +57,7 @@ func (s *platformInterfaceStub) UsePlatformAutoDetectInterfaceControl() bool {
return true return true
} }
func (s *platformInterfaceStub) AutoDetectInterfaceControl(fd int) error { func (s *platformInterfaceStub) AutoDetectInterfaceControl() control.Func {
return nil return nil
} }
@@ -135,21 +137,20 @@ func (s *interfaceMonitorStub) RegisterCallback(callback tun.DefaultInterfaceUpd
func (s *interfaceMonitorStub) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) { func (s *interfaceMonitorStub) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) {
} }
func (s *platformInterfaceStub) SendNotification(notification *platform.Notification) error { func (s *platformInterfaceStub) OpenURL(url string) {
return nil
} }
func FormatConfig(configContent string) (*StringBox, error) { func FormatConfig(configContent string) (string, error) {
options, err := parseConfig(configContent) options, err := parseConfig(box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()), configContent)
if err != nil { if err != nil {
return nil, err return "", 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 nil, err return "", err
} }
return wrapString(buffer.String()), nil return buffer.String(), nil
} }

View File

@@ -4,28 +4,27 @@ import (
"sync" "sync"
"github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing/common"
) )
var _ deprecated.Manager = (*deprecatedManager)(nil) var _ deprecated.Manager = (*deprecatedManager)(nil)
type deprecatedManager struct { type deprecatedManager struct {
access sync.Mutex access sync.Mutex
notes []deprecated.Note features []deprecated.Note
} }
func (m *deprecatedManager) ReportDeprecated(feature deprecated.Note) { func (m *deprecatedManager) ReportDeprecated(feature deprecated.Note) {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
m.notes = common.Uniq(append(m.notes, feature)) m.features = append(m.features, feature)
} }
func (m *deprecatedManager) Get() []deprecated.Note { func (m *deprecatedManager) Get() []deprecated.Note {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
notes := m.notes features := m.features
m.notes = nil m.features = nil
return notes return features
} }
var _ = deprecated.Note(DeprecatedNote{}) var _ = deprecated.Note(DeprecatedNote{})

View File

@@ -50,7 +50,8 @@ type HTTPRequest interface {
} }
type HTTPResponse interface { type HTTPResponse interface {
GetContent() (*StringBox, error) GetContent() ([]byte, error)
GetContentString() (string, error)
WriteTo(path string) error WriteTo(path string) error
} }
@@ -209,22 +210,27 @@ type httpResponse struct {
} }
func (h *httpResponse) errorString() string { func (h *httpResponse) errorString() string {
content, err := h.GetContent() content, err := h.GetContentString()
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() (*StringBox, error) { func (h *httpResponse) GetContent() ([]byte, 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)
}) })
if h.contentError != nil { return h.content, h.contentError
return nil, h.contentError }
func (h *httpResponse) GetContentString() (string, error) {
content, err := h.GetContent()
if err != nil {
return "", err
} }
return wrapString(string(h.content)), nil return string(content), nil
} }
func (h *httpResponse) WriteTo(path string) error { func (h *httpResponse) WriteTo(path string) error {

View File

@@ -1,30 +0,0 @@
package libbox
import (
"net"
"syscall"
)
// copied from net.linkFlags
func linkFlags(rawFlags uint32) net.Flags {
var f net.Flags
if rawFlags&syscall.IFF_UP != 0 {
f |= net.FlagUp
}
if rawFlags&syscall.IFF_RUNNING != 0 {
f |= net.FlagRunning
}
if rawFlags&syscall.IFF_BROADCAST != 0 {
f |= net.FlagBroadcast
}
if rawFlags&syscall.IFF_LOOPBACK != 0 {
f |= net.FlagLoopback
}
if rawFlags&syscall.IFF_POINTOPOINT != 0 {
f |= net.FlagPointToPoint
}
if rawFlags&syscall.IFF_MULTICAST != 0 {
f |= net.FlagMulticast
}
return f
}

View File

@@ -1,11 +0,0 @@
//go:build !linux
package libbox
import (
"net"
)
func linkFlags(rawFlags uint32) net.Flags {
panic("stub!")
}

View File

@@ -97,14 +97,6 @@ func (m *platformDefaultInterfaceMonitor) UnregisterCallback(element *list.Eleme
} }
func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName string, interfaceIndex32 int32) { func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName string, interfaceIndex32 int32) {
if sFixAndroidStack {
go m.updateDefaultInterface(interfaceName, interfaceIndex32)
} else {
m.updateDefaultInterface(interfaceName, interfaceIndex32)
}
}
func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName string, interfaceIndex32 int32) {
if interfaceName == "" || interfaceIndex32 == -1 { if interfaceName == "" || interfaceIndex32 == -1 {
m.defaultInterfaceName = "" m.defaultInterfaceName = ""
m.defaultInterfaceIndex = -1 m.defaultInterfaceIndex = -1

View File

@@ -1,12 +0,0 @@
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

@@ -22,7 +22,6 @@ type PlatformInterface interface {
IncludeAllNetworks() bool IncludeAllNetworks() bool
ReadWIFIState() *WIFIState ReadWIFIState() *WIFIState
ClearDNSCache() ClearDNSCache()
SendNotification(notification *Notification) error
} }
type TunInterface interface { type TunInterface interface {
@@ -39,7 +38,6 @@ type NetworkInterface struct {
MTU int32 MTU int32
Name string Name string
Addresses StringIterator Addresses StringIterator
Flags int32
} }
type WIFIState struct { type WIFIState struct {
@@ -56,16 +54,6 @@ type NetworkInterfaceIterator interface {
HasNext() bool HasNext() bool
} }
type Notification struct {
Identifier string
TypeName string
TypeID int32
Title string
Subtitle string
Body string
OpenURL string
}
type OnDemandRule interface { type OnDemandRule interface {
Target() int32 Target() int32
DNSSearchDomainMatch() StringIterator DNSSearchDomainMatch() StringIterator

View File

@@ -14,7 +14,7 @@ import (
type Interface interface { type Interface interface {
Initialize(ctx context.Context, router adapter.Router) error Initialize(ctx context.Context, router adapter.Router) error
UsePlatformAutoDetectInterfaceControl() bool UsePlatformAutoDetectInterfaceControl() bool
AutoDetectInterfaceControl(fd int) error AutoDetectInterfaceControl() control.Func
OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
UsePlatformDefaultInterfaceMonitor() bool UsePlatformDefaultInterfaceMonitor() bool
CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
@@ -25,15 +25,5 @@ type Interface interface {
ClearDNSCache() ClearDNSCache()
ReadWIFIState() adapter.WIFIState ReadWIFIState() adapter.WIFIState
process.Searcher process.Searcher
SendNotification(notification *Notification) error OpenURL(url string)
}
type Notification struct {
Identifier string
TypeName string
TypeID int32
Title string
Subtitle string
Body string
OpenURL string
} }

View File

@@ -17,6 +17,7 @@ import (
"github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/experimental/libbox/internal/procfs" "github.com/sagernet/sing-box/experimental/libbox/internal/procfs"
"github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
@@ -34,28 +35,29 @@ type BoxService struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
instance *box.Box instance *box.Box
platformInterface *platformInterfaceWrapper
pauseManager pause.Manager pauseManager pause.Manager
urlTestHistoryStorage *urltest.HistoryStorage urlTestHistoryStorage *urltest.HistoryStorage
servicePauseFields servicePauseFields
} }
func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) { func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
options, err := parseConfig(configContent) ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry())
options, err := parseConfig(ctx, configContent)
if err != nil { if err != nil {
return nil, err return nil, err
} }
runtimeDebug.FreeOSMemory() runtimeDebug.FreeOSMemory()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(ctx)
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
urlTestHistoryStorage := urltest.NewHistoryStorage() urlTestHistoryStorage := urltest.NewHistoryStorage()
ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage) ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
ctx = service.ContextWith[deprecated.Manager](ctx, new(deprecatedManager)) ctx = service.ContextWith[deprecated.Manager](ctx, new(deprecatedManager))
platformWrapper := &platformInterfaceWrapper{iif: platformInterface, useProcFS: platformInterface.UseProcFS()} platformWrapper := &platformInterfaceWrapper{iif: platformInterface, useProcFS: platformInterface.UseProcFS()}
ctx = service.ContextWith[platform.Interface](ctx, platformWrapper)
instance, err := box.New(box.Options{ instance, err := box.New(box.Options{
Context: ctx, Context: ctx,
Options: options, Options: options,
PlatformInterface: platformWrapper,
PlatformLogWriter: platformWrapper, PlatformLogWriter: platformWrapper,
}) })
if err != nil { if err != nil {
@@ -67,42 +69,30 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
instance: instance, instance: instance,
platformInterface: platformWrapper,
urlTestHistoryStorage: urlTestHistoryStorage, urlTestHistoryStorage: urlTestHistoryStorage,
pauseManager: service.FromContext[pause.Manager](ctx), pauseManager: service.FromContext[pause.Manager](ctx),
}, nil }, nil
} }
func (s *BoxService) Start() error { func (s *BoxService) Start() error {
if sFixAndroidStack { return s.instance.Start()
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()
var err error return s.instance.Close()
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 {
@@ -115,9 +105,10 @@ var (
) )
type platformInterfaceWrapper struct { type platformInterfaceWrapper struct {
iif PlatformInterface iif PlatformInterface
useProcFS bool useProcFS bool
router adapter.Router router adapter.Router
openURLFunc func(url string)
} }
func (w *platformInterfaceWrapper) Initialize(ctx context.Context, router adapter.Router) error { func (w *platformInterfaceWrapper) Initialize(ctx context.Context, router adapter.Router) error {
@@ -129,8 +120,12 @@ func (w *platformInterfaceWrapper) UsePlatformAutoDetectInterfaceControl() bool
return w.iif.UsePlatformAutoDetectInterfaceControl() return w.iif.UsePlatformAutoDetectInterfaceControl()
} }
func (w *platformInterfaceWrapper) AutoDetectInterfaceControl(fd int) error { func (w *platformInterfaceWrapper) AutoDetectInterfaceControl() control.Func {
return w.iif.AutoDetectInterfaceControl(int32(fd)) return func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error {
return w.iif.AutoDetectInterfaceControl(int32(fd))
})
}
} }
func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) { func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
@@ -188,7 +183,6 @@ func (w *platformInterfaceWrapper) Interfaces() ([]control.Interface, error) {
MTU: int(netInterface.MTU), MTU: int(netInterface.MTU),
Name: netInterface.Name, Name: netInterface.Name,
Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix), Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix),
Flags: linkFlags(uint32(netInterface.Flags)),
}) })
} }
return interfaces, nil return interfaces, nil
@@ -249,6 +243,8 @@ func (w *platformInterfaceWrapper) WriteMessage(level log.Level, message string)
w.iif.WriteLog(message) w.iif.WriteLog(message)
} }
func (w *platformInterfaceWrapper) SendNotification(notification *platform.Notification) error { func (w *platformInterfaceWrapper) OpenURL(url string) {
return w.iif.SendNotification((*Notification)(notification)) if w.openURLFunc != nil {
w.openURLFunc(url)
}
} }

View File

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

View File

@@ -9,66 +9,49 @@ import (
"github.com/sagernet/sing-box/common/humanize" "github.com/sagernet/sing-box/common/humanize"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/locale"
_ "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
) )
var ( var (
sBasePath string sBasePath string
sWorkingPath string sWorkingPath string
sTempPath string sTempPath string
sUserID int sUserID int
sGroupID int sGroupID int
sTVOS bool sTVOS bool
sFixAndroidStack bool
) )
func init() { func init() {
debug.SetPanicOnFault(true) debug.SetPanicOnFault(true)
debug.SetTraceback("all")
} }
type SetupOptions struct { func Setup(basePath string, workingPath string, tempPath string, isTVOS bool) {
BasePath string sBasePath = basePath
WorkingPath string sWorkingPath = workingPath
TempPath string sTempPath = tempPath
Username string sUserID = os.Getuid()
IsTVOS bool sGroupID = os.Getgid()
FixAndroidStack bool sTVOS = isTVOS
}
func Setup(options *SetupOptions) error {
sBasePath = options.BasePath
sWorkingPath = options.WorkingPath
sTempPath = options.TempPath
if options.Username != "" {
sUser, err := user.Lookup(options.Username)
if err != nil {
return err
}
sUserID, _ = strconv.Atoi(sUser.Uid)
sGroupID, _ = strconv.Atoi(sUser.Gid)
} else {
sUserID = os.Getuid()
sGroupID = os.Getgid()
}
sTVOS = options.IsTVOS
// TODO: remove after fixed
// https://github.com/golang/go/issues/68760
sFixAndroidStack = options.FixAndroidStack
os.MkdirAll(sWorkingPath, 0o777) os.MkdirAll(sWorkingPath, 0o777)
os.MkdirAll(sTempPath, 0o777) os.MkdirAll(sTempPath, 0o777)
if options.Username != "" {
os.Chown(sWorkingPath, sUserID, sGroupID)
os.Chown(sTempPath, sUserID, sGroupID)
}
return nil
} }
func SetLocale(localeId string) { func SetupWithUsername(basePath string, workingPath string, tempPath string, username string) error {
locale.Set(localeId) sBasePath = basePath
sWorkingPath = workingPath
sTempPath = tempPath
sUser, err := user.Lookup(username)
if err != nil {
return err
}
sUserID, _ = strconv.Atoi(sUser.Uid)
sGroupID, _ = strconv.Atoi(sUser.Gid)
os.MkdirAll(sWorkingPath, 0o777)
os.MkdirAll(sTempPath, 0o777)
os.Chown(sWorkingPath, sUserID, sGroupID)
os.Chown(sTempPath, sUserID, sGroupID)
return nil
} }
func Version() string { func Version() string {

View File

@@ -13,7 +13,7 @@ import (
type TunOptions interface { type TunOptions interface {
GetInet4Address() RoutePrefixIterator GetInet4Address() RoutePrefixIterator
GetInet6Address() RoutePrefixIterator GetInet6Address() RoutePrefixIterator
GetDNSServerAddress() (*StringBox, error) GetDNSServerAddress() (string, error)
GetMTU() int32 GetMTU() int32
GetAutoRoute() bool GetAutoRoute() bool
GetStrictRoute() bool GetStrictRoute() bool
@@ -89,11 +89,11 @@ func (o *tunOptions) GetInet6Address() RoutePrefixIterator {
return mapRoutePrefix(o.Inet6Address) return mapRoutePrefix(o.Inet6Address)
} }
func (o *tunOptions) GetDNSServerAddress() (*StringBox, error) { func (o *tunOptions) GetDNSServerAddress() (string, error) {
if len(o.Inet4Address) == 0 || o.Inet4Address[0].Bits() == 32 { if len(o.Inet4Address) == 0 || o.Inet4Address[0].Bits() == 32 {
return nil, E.New("need one more IPv4 address for DNS hijacking") return "", E.New("need one more IPv4 address for DNS hijacking")
} }
return wrapString(o.Inet4Address[0].Addr().Next().String()), nil return o.Inet4Address[0].Addr().Next().String(), nil
} }
func (o *tunOptions) GetMTU() int32 { func (o *tunOptions) GetMTU() int32 {

View File

@@ -1,30 +0,0 @@
package locale
var (
localeRegistry = make(map[string]*Locale)
current = defaultLocal
)
type Locale struct {
// deprecated messages for graphical clients
DeprecatedMessage string
DeprecatedMessageNoLink string
}
var defaultLocal = &Locale{
DeprecatedMessage: "%s is deprecated in sing-box %s and will be removed in sing-box %s please checkout documentation for migration.",
DeprecatedMessageNoLink: "%s is deprecated in sing-box %s and will be removed in sing-box %s.",
}
func Current() *Locale {
return current
}
func Set(localeId string) bool {
locale, loaded := localeRegistry[localeId]
if !loaded {
return false
}
current = locale
return true
}

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