mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 20:28:32 +10:00
Compare commits
36 Commits
dev-mitm
...
v1.11.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5392656bda | ||
|
|
ab6fd326d8 | ||
|
|
cd8c7ee638 | ||
|
|
b7912184a7 | ||
|
|
5c885f719b | ||
|
|
b6e1c1587c | ||
|
|
4e4010ec61 | ||
|
|
7fc6cffd1d | ||
|
|
d4e2396407 | ||
|
|
60cd8b8c6b | ||
|
|
bcb3947fd0 | ||
|
|
5d69be4aa3 | ||
|
|
f89d46e11f | ||
|
|
6fbcd69193 | ||
|
|
579f42161a | ||
|
|
989ee3d050 | ||
|
|
8c05a7fe12 | ||
|
|
61d75dc8e0 | ||
|
|
a8532c541c | ||
|
|
02ac291692 | ||
|
|
ee4f8e8c77 | ||
|
|
e6fc504954 | ||
|
|
c929221519 | ||
|
|
8134fd3a16 | ||
|
|
6862b154b7 | ||
|
|
c3d82a5f26 | ||
|
|
1eebed1f41 | ||
|
|
be239802e1 | ||
|
|
a5bb64e19d | ||
|
|
58c9107289 | ||
|
|
21a6e414fd | ||
|
|
8f288ce168 | ||
|
|
21841c9434 | ||
|
|
7897c872a3 | ||
|
|
ee4e9bf1b3 | ||
|
|
af3d03330d |
148
.github/workflows/build.yml
vendored
148
.github/workflows/build.yml
vendored
@@ -7,6 +7,11 @@ on:
|
|||||||
description: "Version name"
|
description: "Version name"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
prerelease:
|
||||||
|
description: "Is prerelease"
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
build:
|
build:
|
||||||
description: "Build type"
|
description: "Build type"
|
||||||
required: true
|
required: true
|
||||||
@@ -23,6 +28,10 @@ on:
|
|||||||
- tvOS
|
- tvOS
|
||||||
- macOS-standalone
|
- macOS-standalone
|
||||||
- publish-android
|
- publish-android
|
||||||
|
macos_project_version:
|
||||||
|
description: "macOS project version"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main-next
|
- main-next
|
||||||
@@ -38,6 +47,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.outputs.outputs.version }}
|
version: ${{ steps.outputs.outputs.version }}
|
||||||
|
prerelease: ${{ steps.outputs.outputs.prerelease }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
@@ -51,7 +61,9 @@ jobs:
|
|||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
echo "version=${{ inputs.version }}"
|
echo "version=${{ inputs.version }}"
|
||||||
|
echo "prerelease=${{ inputs.prerelease }}"
|
||||||
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
echo "version=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
echo "prerelease=${{ inputs.prerelease }}" >> "$GITHUB_ENV"
|
||||||
- name: Calculate version
|
- name: Calculate version
|
||||||
if: github.event_name != 'workflow_dispatch'
|
if: github.event_name != 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -60,6 +72,7 @@ jobs:
|
|||||||
id: outputs
|
id: outputs
|
||||||
run: |-
|
run: |-
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
|
||||||
build:
|
build:
|
||||||
name: Build binary
|
name: Build binary
|
||||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Binary'
|
||||||
@@ -144,7 +157,7 @@ jobs:
|
|||||||
~/go/go1.20.14
|
~/go/go1.20.14
|
||||||
key: go120
|
key: go120
|
||||||
- name: Setup legacy Go
|
- name: Setup legacy Go
|
||||||
if: matrix.require_legacy_go && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
if: matrix.require_legacy_go == 'true' && steps.cache-legacy-go.outputs.cache-hit != 'true'
|
||||||
run: |-
|
run: |-
|
||||||
wget https://dl.google.com/go/go1.20.14.linux-amd64.tar.gz
|
wget https://dl.google.com/go/go1.20.14.linux-amd64.tar.gz
|
||||||
tar -xzf go1.20.14.linux-amd64.tar.gz
|
tar -xzf go1.20.14.linux-amd64.tar.gz
|
||||||
@@ -159,7 +172,7 @@ jobs:
|
|||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
distribution: goreleaser-pro
|
||||||
version: 2.5.1
|
version: latest
|
||||||
install-only: true
|
install-only: true
|
||||||
- name: Extract signing key
|
- name: Extract signing key
|
||||||
run: |-
|
run: |-
|
||||||
@@ -170,8 +183,7 @@ jobs:
|
|||||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
run: |-
|
run: |-
|
||||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
git tag v${{ needs.calculate_version.outputs.version }}
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: matrix.goos != 'android'
|
if: matrix.goos != 'android'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -224,15 +236,14 @@ jobs:
|
|||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
with:
|
with:
|
||||||
ndk-version: r28-beta3
|
ndk-version: r28-beta2
|
||||||
- name: Setup OpenJDK
|
- name: Setup OpenJDK
|
||||||
run: |-
|
run: |-
|
||||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||||
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
run: |-
|
run: |-
|
||||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
git tag v${{ needs.calculate_version.outputs.version }}
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
|
||||||
- name: Build library
|
- name: Build library
|
||||||
run: |-
|
run: |-
|
||||||
make lib_install
|
make lib_install
|
||||||
@@ -242,12 +253,12 @@ jobs:
|
|||||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
if: needs.calculate_version.outputs.prerelease == 'false'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: github.ref == 'refs/heads/dev-next'
|
if: needs.calculate_version.outputs.prerelease == 'true'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -299,15 +310,14 @@ jobs:
|
|||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
with:
|
with:
|
||||||
ndk-version: r28-beta3
|
ndk-version: r28-beta2
|
||||||
- name: Setup OpenJDK
|
- name: Setup OpenJDK
|
||||||
run: |-
|
run: |-
|
||||||
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
sudo apt update && sudo apt install -y openjdk-17-jdk-headless
|
||||||
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
/usr/lib/jvm/java-17-openjdk-amd64/bin/java --version
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
run: |-
|
run: |-
|
||||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
git tag v${{ needs.calculate_version.outputs.version }}
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
|
||||||
- name: Build library
|
- name: Build library
|
||||||
run: |-
|
run: |-
|
||||||
make lib_install
|
make lib_install
|
||||||
@@ -317,12 +327,12 @@ jobs:
|
|||||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
if: needs.calculate_version.outputs.prerelease == 'false'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: github.ref == 'refs/heads/dev-next'
|
if: needs.calculate_version.outputs.prerelease == 'true'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/android
|
cd clients/android
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -337,45 +347,72 @@ jobs:
|
|||||||
mkdir clients/android/app/libs
|
mkdir clients/android/app/libs
|
||||||
cp libbox.aar clients/android/app/libs
|
cp libbox.aar clients/android/app/libs
|
||||||
cd clients/android
|
cd clients/android
|
||||||
echo -n "$SERVICE_ACCOUNT_CREDENTIALS" | base64 --decode > service-account-credentials.json
|
|
||||||
./gradlew :app:publishPlayReleaseBundle
|
./gradlew :app:publishPlayReleaseBundle
|
||||||
env:
|
env:
|
||||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||||
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }}
|
||||||
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
|
build_apple_library:
|
||||||
|
name: Build Apple library
|
||||||
|
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone'
|
||||||
|
runs-on: macos-15
|
||||||
|
needs:
|
||||||
|
- calculate_version
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: 'recursive'
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ^1.23
|
||||||
|
- name: Setup Xcode
|
||||||
|
run: |-
|
||||||
|
sudo xcode-select -s /Applications/Xcode_16.2_beta_3.app
|
||||||
|
- name: Set tag
|
||||||
|
run: |-
|
||||||
|
git tag v${{ needs.calculate_version.outputs.version }}
|
||||||
|
- name: Build library
|
||||||
|
run: |-
|
||||||
|
make lib_install
|
||||||
|
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||||
|
make lib_ios
|
||||||
|
- name: Upload library
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: library-apple
|
||||||
|
path: 'Libbox.xcframework'
|
||||||
build_apple:
|
build_apple:
|
||||||
name: Build Apple clients
|
name: Build Apple clients
|
||||||
runs-on: macos-15
|
runs-on: macos-15
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
|
- build_apple_library
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- name: iOS
|
- name: iOS
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'iOS' }}
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'iOS' }}
|
||||||
platform: ios
|
|
||||||
scheme: SFI
|
scheme: SFI
|
||||||
destination: 'generic/platform=iOS'
|
destination: 'generic/platform=iOS'
|
||||||
archive: build/SFI.xcarchive
|
archive: build/SFI.xcarchive
|
||||||
upload: SFI/Upload.plist
|
upload: SFI/Upload.plist
|
||||||
- name: macOS
|
- name: macOS
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'macOS' }}
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'macOS' }}
|
||||||
platform: macos
|
|
||||||
scheme: SFM
|
scheme: SFM
|
||||||
destination: 'generic/platform=macOS'
|
destination: 'generic/platform=macOS'
|
||||||
archive: build/SFM.xcarchive
|
archive: build/SFM.xcarchive
|
||||||
upload: SFI/Upload.plist
|
upload: SFI/Upload.plist
|
||||||
- name: tvOS
|
- name: tvOS
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'tvOS' }}
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'app-store'|| inputs.build == 'tvOS' }}
|
||||||
platform: tvos
|
|
||||||
scheme: SFT
|
scheme: SFT
|
||||||
destination: 'generic/platform=tvOS'
|
destination: 'generic/platform=tvOS'
|
||||||
archive: build/SFT.xcarchive
|
archive: build/SFT.xcarchive
|
||||||
upload: SFI/Upload.plist
|
upload: SFI/Upload.plist
|
||||||
- name: macOS-standalone
|
- name: macOS-standalone
|
||||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone' }}
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone' }}
|
||||||
platform: macos
|
|
||||||
scheme: SFM.System
|
scheme: SFM.System
|
||||||
destination: 'generic/platform=macOS'
|
destination: 'generic/platform=macOS'
|
||||||
archive: build/SFM.System.xcarchive
|
archive: build/SFM.System.xcarchive
|
||||||
@@ -393,27 +430,22 @@ jobs:
|
|||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23
|
go-version: ^1.23
|
||||||
- name: Setup Xcode stable
|
- name: Setup Xcode
|
||||||
if: matrix.if && github.ref == 'refs/heads/main-next'
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
sudo xcode-select -s /Applications/Xcode_16.2.app
|
sudo xcode-select -s /Applications/Xcode_16.2_beta_3.app
|
||||||
- name: Setup Xcode beta
|
|
||||||
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
|
||||||
run: |-
|
|
||||||
sudo xcode-select -s /Applications/Xcode_16.2.app
|
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
git tag v${{ needs.calculate_version.outputs.version }}
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
|
||||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
if: matrix.if && github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
if: matrix.if && needs.calculate_version.outputs.prerelease == 'false'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
git checkout main
|
git checkout main
|
||||||
- name: Checkout dev branch
|
- name: Checkout dev branch
|
||||||
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
if: matrix.if && needs.calculate_version.outputs.prerelease == 'true'
|
||||||
run: |-
|
run: |-
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
git checkout dev
|
git checkout dev
|
||||||
@@ -444,10 +476,6 @@ jobs:
|
|||||||
--key $ASC_KEY_PATH \
|
--key $ASC_KEY_PATH \
|
||||||
--key-id $ASC_KEY_ID \
|
--key-id $ASC_KEY_ID \
|
||||||
--issuer $ASC_KEY_ISSUER_ID
|
--issuer $ASC_KEY_ISSUER_ID
|
||||||
|
|
||||||
echo "ASC_KEY_PATH=$ASC_KEY_PATH" >> "$GITHUB_ENV"
|
|
||||||
echo "ASC_KEY_ID=$ASC_KEY_ID" >> "$GITHUB_ENV"
|
|
||||||
echo "ASC_KEY_ISSUER_ID=$ASC_KEY_ISSUER_ID" >> "$GITHUB_ENV"
|
|
||||||
env:
|
env:
|
||||||
CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }}
|
CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }}
|
||||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||||
@@ -456,19 +484,12 @@ jobs:
|
|||||||
ASC_KEY: ${{ secrets.ASC_KEY }}
|
ASC_KEY: ${{ secrets.ASC_KEY }}
|
||||||
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||||
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
|
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
|
||||||
- name: Build library
|
- name: Download library
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
uses: actions/download-artifact@v4
|
||||||
make lib_install
|
with:
|
||||||
export PATH="$PATH:$(go env GOPATH)/bin"
|
name: library-apple
|
||||||
go run ./cmd/internal/build_libbox -target apple -platform ${{ matrix.platform }}
|
path: clients/apple/Libbox.xcframework
|
||||||
mv Libbox.xcframework clients/apple
|
|
||||||
- name: Update macOS version
|
|
||||||
if: matrix.if && matrix.name == 'macOS' && github.event_name == 'workflow_dispatch'
|
|
||||||
run: |-
|
|
||||||
MACOS_PROJECT_VERSION=$(go run -v ./cmd/internal/app_store_connect next_macos_project_version)
|
|
||||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION"
|
|
||||||
echo "MACOS_PROJECT_VERSION=$MACOS_PROJECT_VERSION" >> "$GITHUB_ENV"
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
@@ -480,25 +501,27 @@ jobs:
|
|||||||
-destination "${{ matrix.destination }}" \
|
-destination "${{ matrix.destination }}" \
|
||||||
-archivePath "${{ matrix.archive }}" \
|
-archivePath "${{ matrix.archive }}" \
|
||||||
-allowProvisioningUpdates \
|
-allowProvisioningUpdates \
|
||||||
-authenticationKeyPath $ASC_KEY_PATH \
|
-authenticationKeyPath $RUNNER_TEMP/Key.p12 \
|
||||||
-authenticationKeyID $ASC_KEY_ID \
|
-authenticationKeyID $ASC_KEY_ID \
|
||||||
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||||
|
env:
|
||||||
|
MACOS_PROJECT_VERSION: ${{ inputs.macos_project_version }}
|
||||||
|
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||||
|
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
|
||||||
- name: Upload to App Store Connect
|
- name: Upload to App Store Connect
|
||||||
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
go run -v ./cmd/internal/app_store_connect cancel_app_store ${{ matrix.platform }}
|
|
||||||
cd clients/apple
|
cd clients/apple
|
||||||
xcodebuild -exportArchive \
|
xcodebuild -exportArchive \
|
||||||
-archivePath "${{ matrix.archive }}" \
|
-archivePath "${{ matrix.archive }}" \
|
||||||
-exportOptionsPlist ${{ matrix.upload }} \
|
-exportOptionsPlist ${{ matrix.upload }} \
|
||||||
-allowProvisioningUpdates \
|
-allowProvisioningUpdates \
|
||||||
-authenticationKeyPath $ASC_KEY_PATH \
|
-authenticationKeyPath $RUNNER_TEMP/Key.p12 \
|
||||||
-authenticationKeyID $ASC_KEY_ID \
|
-authenticationKeyID $ASC_KEY_ID \
|
||||||
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||||
- name: Publish to TestFlight
|
env:
|
||||||
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/dev-next'
|
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||||
run: |-
|
ASC_KEY_ISSUER_ID: ${{ secrets.ASC_KEY_ISSUER_ID }}
|
||||||
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
|
|
||||||
- name: Build image
|
- name: Build image
|
||||||
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
if: matrix.if && matrix.name == 'macOS-standalone' && github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -532,7 +555,7 @@ jobs:
|
|||||||
path: 'dist'
|
path: 'dist'
|
||||||
upload:
|
upload:
|
||||||
name: Upload builds
|
name: Upload builds
|
||||||
if: always() && github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Binary' || inputs.build == 'Android' || inputs.build == 'Apple' || inputs.build == 'macOS-standalone')
|
if: always() && github.event_name == 'workflow_dispatch'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- calculate_version
|
- calculate_version
|
||||||
@@ -548,7 +571,7 @@ jobs:
|
|||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
distribution: goreleaser-pro
|
||||||
version: 2.5.1
|
version: latest
|
||||||
install-only: true
|
install-only: true
|
||||||
- name: Cache ghr
|
- name: Cache ghr
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -566,8 +589,7 @@ jobs:
|
|||||||
go install -v .
|
go install -v .
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
run: |-
|
run: |-
|
||||||
git ls-remote --exit-code --tags origin v${{ needs.calculate_version.outputs.version }} || echo "PUBLISHED=false" >> "$GITHUB_ENV"
|
git tag v${{ needs.calculate_version.outputs.version }}
|
||||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
|
||||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||||
- name: Download builds
|
- name: Download builds
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
@@ -584,16 +606,8 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||||
- name: Upload builds
|
- name: Upload builds
|
||||||
if: ${{ env.PUBLISHED == 'false' }}
|
|
||||||
run: |-
|
run: |-
|
||||||
export PATH="$PATH:$HOME/go/bin"
|
export PATH="$PATH:$HOME/go/bin"
|
||||||
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
ghr --replace --draft --prerelease -p 5 "v${VERSION}" dist/release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Replace builds
|
|
||||||
if: ${{ env.PUBLISHED != 'false' }}
|
|
||||||
run: |-
|
|
||||||
export PATH="$PATH:$HOME/go/bin"
|
|
||||||
ghr --replace -p 5 "v${VERSION}" dist/release
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ builds:
|
|||||||
- -v
|
- -v
|
||||||
- -trimpath
|
- -trimpath
|
||||||
ldflags:
|
ldflags:
|
||||||
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }}
|
- -X github.com/sagernet/sing-box/constant.Version={{ .Version }} -s -w -buildid=
|
||||||
- -s
|
|
||||||
- -buildid=
|
|
||||||
tags:
|
tags:
|
||||||
- with_gvisor
|
- with_gvisor
|
||||||
- with_quic
|
- with_quic
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ builds:
|
|||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
- GOROOT={{ .Env.GOPATH }}/go1.20.14
|
- GOROOT={{ .Env.GOPATH }}/go1.20.14
|
||||||
tool: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
|
gobinary: "{{ .Env.GOPATH }}/go1.20.14/bin/go"
|
||||||
targets:
|
targets:
|
||||||
- windows_amd64_v1
|
- windows_amd64_v1
|
||||||
- windows_386
|
- windows_386
|
||||||
|
|||||||
28
Makefile
28
Makefile
@@ -28,7 +28,7 @@ ci_build:
|
|||||||
go build $(MAIN_PARAMS) $(MAIN)
|
go build $(MAIN_PARAMS) $(MAIN)
|
||||||
|
|
||||||
generate_completions:
|
generate_completions:
|
||||||
go run -v --tags $(TAGS),generate,generate_completions $(MAIN)
|
go run -v --tags generate,generate_completions $(MAIN)
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN)
|
||||||
@@ -61,12 +61,6 @@ proto_install:
|
|||||||
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||||
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
|
|
||||||
update_public_suffix:
|
|
||||||
go generate common/tlsfragment/public_suffix.go
|
|
||||||
|
|
||||||
update_certificates:
|
|
||||||
go run ./cmd/internal/update_certificates
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
go run ./cmd/internal/build goreleaser release --clean --skip publish
|
||||||
mkdir dist/release
|
mkdir dist/release
|
||||||
@@ -188,22 +182,10 @@ release_tvos: build_tvos upload_tvos_app_store
|
|||||||
update_apple_version:
|
update_apple_version:
|
||||||
go run ./cmd/internal/update_apple_version
|
go run ./cmd/internal/update_apple_version
|
||||||
|
|
||||||
update_macos_version:
|
|
||||||
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
|
|
||||||
|
|
||||||
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
|
release_apple: lib_ios update_apple_version release_ios release_macos release_tvos release_macos_standalone
|
||||||
|
|
||||||
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
release_apple_beta: update_apple_version release_ios release_macos release_tvos
|
||||||
|
|
||||||
publish_testflight:
|
|
||||||
go run -v ./cmd/internal/app_store_connect publish_testflight
|
|
||||||
|
|
||||||
prepare_app_store:
|
|
||||||
go run -v ./cmd/internal/app_store_connect prepare_app_store
|
|
||||||
|
|
||||||
publish_app_store:
|
|
||||||
go run -v ./cmd/internal/app_store_connect publish_app_store
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@go test -v ./... && \
|
@go test -v ./... && \
|
||||||
cd test && \
|
cd test && \
|
||||||
@@ -222,11 +204,11 @@ lib_android:
|
|||||||
lib_android_debug:
|
lib_android_debug:
|
||||||
go run ./cmd/internal/build_libbox -target android -debug
|
go run ./cmd/internal/build_libbox -target android -debug
|
||||||
|
|
||||||
lib_apple:
|
|
||||||
go run ./cmd/internal/build_libbox -target apple
|
|
||||||
|
|
||||||
lib_ios:
|
lib_ios:
|
||||||
go run ./cmd/internal/build_libbox -target apple -platform ios -debug
|
go run ./cmd/internal/build_libbox -target ios
|
||||||
|
|
||||||
|
lib_ios_debug:
|
||||||
|
go run ./cmd/internal/build_libbox -target ios -debug
|
||||||
|
|
||||||
lib:
|
lib:
|
||||||
go run ./cmd/internal/build_libbox -target android
|
go run ./cmd/internal/build_libbox -target android
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/x509"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CertificateStore interface {
|
|
||||||
LifecycleService
|
|
||||||
Pool() *x509.CertPool
|
|
||||||
}
|
|
||||||
|
|
||||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
|
||||||
store := service.FromContext[CertificateStore](ctx)
|
|
||||||
if store == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return store.Pool()
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DNSRouter interface {
|
|
||||||
Lifecycle
|
|
||||||
Exchange(ctx context.Context, message *dns.Msg, options DNSQueryOptions) (*dns.Msg, error)
|
|
||||||
Lookup(ctx context.Context, domain string, options DNSQueryOptions) ([]netip.Addr, error)
|
|
||||||
ClearCache()
|
|
||||||
LookupReverseMapping(ip netip.Addr) (string, bool)
|
|
||||||
ResetNetwork()
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSClient interface {
|
|
||||||
Start()
|
|
||||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
|
||||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
|
||||||
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
|
|
||||||
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
|
|
||||||
ClearCache()
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSQueryOptions struct {
|
|
||||||
Transport DNSTransport
|
|
||||||
Strategy C.DomainStrategy
|
|
||||||
DisableCache bool
|
|
||||||
RewriteTTL *uint32
|
|
||||||
ClientSubnet netip.Prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
type RDRCStore interface {
|
|
||||||
LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
|
|
||||||
SaveRDRC(transportName string, qName string, qType uint16) error
|
|
||||||
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSTransport interface {
|
|
||||||
Type() string
|
|
||||||
Tag() string
|
|
||||||
Dependencies() []string
|
|
||||||
Reset()
|
|
||||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LegacyDNSTransport interface {
|
|
||||||
LegacyStrategy() C.DomainStrategy
|
|
||||||
LegacyClientSubnet() netip.Prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSTransportRegistry interface {
|
|
||||||
option.DNSTransportOptionsRegistry
|
|
||||||
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSTransportManager interface {
|
|
||||||
Lifecycle
|
|
||||||
Transports() []DNSTransport
|
|
||||||
Transport(tag string) (DNSTransport, bool)
|
|
||||||
Default() DNSTransport
|
|
||||||
FakeIP() FakeIPTransport
|
|
||||||
Remove(tag string) error
|
|
||||||
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/urltest"
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
"github.com/sagernet/sing/common/varbin"
|
"github.com/sagernet/sing/common/varbin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,20 +16,7 @@ type ClashServer interface {
|
|||||||
ConnectionTracker
|
ConnectionTracker
|
||||||
Mode() string
|
Mode() string
|
||||||
ModeList() []string
|
ModeList() []string
|
||||||
HistoryStorage() URLTestHistoryStorage
|
HistoryStorage() *urltest.HistoryStorage
|
||||||
}
|
|
||||||
|
|
||||||
type URLTestHistory struct {
|
|
||||||
Time time.Time `json:"time"`
|
|
||||||
Delay uint16 `json:"delay"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type URLTestHistoryStorage interface {
|
|
||||||
SetHook(hook chan<- struct{})
|
|
||||||
LoadURLTestHistory(tag string) *URLTestHistory
|
|
||||||
DeleteURLTestHistory(tag string)
|
|
||||||
StoreURLTestHistory(tag string, history *URLTestHistory)
|
|
||||||
Close() error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type V2RayServer interface {
|
type V2RayServer interface {
|
||||||
@@ -42,7 +31,7 @@ type CacheFile interface {
|
|||||||
FakeIPStorage
|
FakeIPStorage
|
||||||
|
|
||||||
StoreRDRC() bool
|
StoreRDRC() bool
|
||||||
RDRCStore
|
dns.RDRCStore
|
||||||
|
|
||||||
LoadMode() string
|
LoadMode() string
|
||||||
StoreMode(mode string) error
|
StoreMode(mode string) error
|
||||||
@@ -50,21 +39,17 @@ type CacheFile interface {
|
|||||||
StoreSelected(group string, selected string) error
|
StoreSelected(group string, selected string) error
|
||||||
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
||||||
StoreGroupExpand(group string, expand bool) error
|
StoreGroupExpand(group string, expand bool) error
|
||||||
LoadRuleSet(tag string) *SavedBinary
|
LoadRuleSet(tag string) *SavedRuleSet
|
||||||
SaveRuleSet(tag string, set *SavedBinary) error
|
SaveRuleSet(tag string, set *SavedRuleSet) error
|
||||||
LoadScript(tag string) *SavedBinary
|
|
||||||
SaveScript(tag string, script *SavedBinary) error
|
|
||||||
SurgePersistentStoreRead(key string) string
|
|
||||||
SurgePersistentStoreWrite(key string, value string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedBinary struct {
|
type SavedRuleSet struct {
|
||||||
Content []byte
|
Content []byte
|
||||||
LastUpdated time.Time
|
LastUpdated time.Time
|
||||||
LastEtag string
|
LastEtag string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -85,7 +70,7 @@ func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
|||||||
return buffer.Bytes(), nil
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
|
||||||
reader := bytes.NewReader(data)
|
reader := bytes.NewReader(data)
|
||||||
var version uint8
|
var version uint8
|
||||||
err := binary.Read(reader, binary.BigEndian, &version)
|
err := binary.Read(reader, binary.BigEndian, &version)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package adapter
|
|||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,6 +27,6 @@ type FakeIPStorage interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FakeIPTransport interface {
|
type FakeIPTransport interface {
|
||||||
DNSTransport
|
dns.Transport
|
||||||
Store() FakeIPStore
|
Store() FakeIPStore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -59,8 +57,6 @@ type InboundContext struct {
|
|||||||
Domain string
|
Domain string
|
||||||
Client string
|
Client string
|
||||||
SniffContext any
|
SniffContext any
|
||||||
HTTPRequest *http.Request
|
|
||||||
ClientHello *tls.ClientHelloInfo
|
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|
||||||
@@ -75,15 +71,14 @@ type InboundContext struct {
|
|||||||
UDPDisableDomainUnmapping bool
|
UDPDisableDomainUnmapping bool
|
||||||
UDPConnect bool
|
UDPConnect bool
|
||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
TLSFragment bool
|
|
||||||
TLSFragmentFallbackDelay time.Duration
|
|
||||||
MITM *option.MITMRouteOptions
|
|
||||||
|
|
||||||
NetworkStrategy *C.NetworkStrategy
|
NetworkStrategy C.NetworkStrategy
|
||||||
NetworkType []C.InterfaceType
|
NetworkType []C.InterfaceType
|
||||||
FallbackNetworkType []C.InterfaceType
|
FallbackNetworkType []C.InterfaceType
|
||||||
FallbackDelay time.Duration
|
FallbackDelay time.Duration
|
||||||
|
|
||||||
|
DNSServer string
|
||||||
|
|
||||||
DestinationAddresses []netip.Addr
|
DestinationAddresses []netip.Addr
|
||||||
SourceGeoIPCode string
|
SourceGeoIPCode string
|
||||||
GeoIPCode string
|
GeoIPCode string
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import (
|
import E "github.com/sagernet/sing/common/exceptions"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StartStage uint8
|
type StartStage uint8
|
||||||
|
|
||||||
@@ -47,9 +45,6 @@ type LifecycleService interface {
|
|||||||
|
|
||||||
func Start(stage StartStage, services ...Lifecycle) error {
|
func Start(stage StartStage, services ...Lifecycle) error {
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
if service == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := service.Start(stage)
|
err := service.Start(stage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/x509"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MITMEngine interface {
|
|
||||||
Lifecycle
|
|
||||||
ExportCertificate() *x509.Certificate
|
|
||||||
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
|
||||||
}
|
|
||||||
@@ -28,14 +28,12 @@ type NetworkManager interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NetworkOptions struct {
|
type NetworkOptions struct {
|
||||||
BindInterface string
|
NetworkStrategy C.NetworkStrategy
|
||||||
RoutingMark uint32
|
NetworkType []C.InterfaceType
|
||||||
DomainResolver string
|
FallbackNetworkType []C.InterfaceType
|
||||||
DomainResolveOptions DNSQueryOptions
|
FallbackDelay time.Duration
|
||||||
NetworkStrategy *C.NetworkStrategy
|
BindInterface string
|
||||||
NetworkType []C.InterfaceType
|
RoutingMark uint32
|
||||||
FallbackNetworkType []C.InterfaceType
|
|
||||||
FallbackDelay time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceUpdateListener interface {
|
type InterfaceUpdateListener interface {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type Manager struct {
|
|||||||
registry adapter.OutboundRegistry
|
registry adapter.OutboundRegistry
|
||||||
endpoint adapter.EndpointManager
|
endpoint adapter.EndpointManager
|
||||||
defaultTag string
|
defaultTag string
|
||||||
access sync.RWMutex
|
access sync.Mutex
|
||||||
started bool
|
started bool
|
||||||
stage adapter.StartStage
|
stage adapter.StartStage
|
||||||
outbounds []adapter.Outbound
|
outbounds []adapter.Outbound
|
||||||
@@ -169,15 +169,15 @@ func (m *Manager) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Outbounds() []adapter.Outbound {
|
func (m *Manager) Outbounds() []adapter.Outbound {
|
||||||
m.access.RLock()
|
m.access.Lock()
|
||||||
defer m.access.RUnlock()
|
defer m.access.Unlock()
|
||||||
return m.outbounds
|
return m.outbounds
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
||||||
m.access.RLock()
|
m.access.Lock()
|
||||||
outbound, found := m.outboundByTag[tag]
|
outbound, found := m.outboundByTag[tag]
|
||||||
m.access.RUnlock()
|
m.access.Unlock()
|
||||||
if found {
|
if found {
|
||||||
return outbound, true
|
return outbound, true
|
||||||
}
|
}
|
||||||
@@ -185,8 +185,8 @@ func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Default() adapter.Outbound {
|
func (m *Manager) Default() adapter.Outbound {
|
||||||
m.access.RLock()
|
m.access.Lock()
|
||||||
defer m.access.RUnlock()
|
defer m.access.Unlock()
|
||||||
if m.defaultOutbound != nil {
|
if m.defaultOutbound != nil {
|
||||||
return m.defaultOutbound
|
return m.defaultOutbound
|
||||||
} else {
|
} else {
|
||||||
@@ -196,9 +196,9 @@ func (m *Manager) Default() adapter.Outbound {
|
|||||||
|
|
||||||
func (m *Manager) Remove(tag string) error {
|
func (m *Manager) Remove(tag string) error {
|
||||||
m.access.Lock()
|
m.access.Lock()
|
||||||
defer m.access.Unlock()
|
|
||||||
outbound, found := m.outboundByTag[tag]
|
outbound, found := m.outboundByTag[tag]
|
||||||
if !found {
|
if !found {
|
||||||
|
m.access.Unlock()
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
delete(m.outboundByTag, tag)
|
delete(m.outboundByTag, tag)
|
||||||
@@ -232,6 +232,7 @@ func (m *Manager) Remove(tag string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.access.Unlock()
|
||||||
if started {
|
if started {
|
||||||
return common.Close(outbound)
|
return common.Close(outbound)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,44 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/geoip"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
|
||||||
|
mdns "github.com/miekg/dns"
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Router interface {
|
type Router interface {
|
||||||
Lifecycle
|
Lifecycle
|
||||||
|
|
||||||
|
FakeIPStore() FakeIPStore
|
||||||
|
|
||||||
ConnectionRouter
|
ConnectionRouter
|
||||||
PreMatch(metadata InboundContext) error
|
PreMatch(metadata InboundContext) error
|
||||||
ConnectionRouterEx
|
ConnectionRouterEx
|
||||||
|
|
||||||
|
GeoIPReader() *geoip.Reader
|
||||||
|
LoadGeosite(code string) (Rule, error)
|
||||||
RuleSet(tag string) (RuleSet, bool)
|
RuleSet(tag string) (RuleSet, bool)
|
||||||
NeedWIFIState() bool
|
NeedWIFIState() bool
|
||||||
|
|
||||||
|
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
|
||||||
|
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
|
||||||
|
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
|
||||||
|
ClearDNSCache()
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
|
|
||||||
SetTracker(tracker ConnectionTracker)
|
SetTracker(tracker ConnectionTracker)
|
||||||
|
|
||||||
ResetNetwork()
|
ResetNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,14 +83,12 @@ type RuleSetMetadata struct {
|
|||||||
ContainsIPCIDRRule bool
|
ContainsIPCIDRRule bool
|
||||||
}
|
}
|
||||||
type HTTPStartContext struct {
|
type HTTPStartContext struct {
|
||||||
ctx context.Context
|
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
httpClientCache map[string]*http.Client
|
httpClientCache map[string]*http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
func NewHTTPStartContext() *HTTPStartContext {
|
||||||
return &HTTPStartContext{
|
return &HTTPStartContext{
|
||||||
ctx: ctx,
|
|
||||||
httpClientCache: make(map[string]*http.Client),
|
httpClientCache: make(map[string]*http.Client),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,10 +106,6 @@ func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Clie
|
|||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
},
|
},
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
Time: ntp.TimeFuncFromContext(c.ctx),
|
|
||||||
RootCAs: RootPoolFromContext(c.ctx),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
c.httpClientCache[detour] = httpClient
|
c.httpClientCache[detour] = httpClient
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Rule interface {
|
|||||||
HeadlessRule
|
HeadlessRule
|
||||||
Service
|
Service
|
||||||
Type() string
|
Type() string
|
||||||
|
UpdateGeosite() error
|
||||||
Action() RuleAction
|
Action() RuleAction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ScriptManager interface {
|
|
||||||
Lifecycle
|
|
||||||
Scripts() []Script
|
|
||||||
Script(name string) (Script, bool)
|
|
||||||
SurgeCache() *SurgeInMemoryCache
|
|
||||||
}
|
|
||||||
|
|
||||||
type SurgeInMemoryCache struct {
|
|
||||||
sync.RWMutex
|
|
||||||
Data map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Script interface {
|
|
||||||
Type() string
|
|
||||||
Tag() string
|
|
||||||
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
|
||||||
PostStart() error
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type SurgeScript interface {
|
|
||||||
Script
|
|
||||||
ExecuteGeneric(ctx context.Context, scriptType string, timeout time.Duration, arguments []string) error
|
|
||||||
ExecuteHTTPRequest(ctx context.Context, timeout time.Duration, request *http.Request, body []byte, binaryBody bool, arguments []string) (*HTTPRequestScriptResult, error)
|
|
||||||
ExecuteHTTPResponse(ctx context.Context, timeout time.Duration, request *http.Request, response *http.Response, body []byte, binaryBody bool, arguments []string) (*HTTPResponseScriptResult, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPRequestScriptResult struct {
|
|
||||||
URL string
|
|
||||||
Headers http.Header
|
|
||||||
Body []byte
|
|
||||||
Response *HTTPRequestScriptResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPRequestScriptResponse struct {
|
|
||||||
Status int
|
|
||||||
Headers http.Header
|
|
||||||
Body []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPResponseScriptResult struct {
|
|
||||||
Status int
|
|
||||||
Headers http.Header
|
|
||||||
Body []byte
|
|
||||||
}
|
|
||||||
168
box.go
168
box.go
@@ -12,22 +12,16 @@ import (
|
|||||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
"github.com/sagernet/sing-box/adapter/outbound"
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
"github.com/sagernet/sing-box/common/certificate"
|
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/dns/transport/local"
|
|
||||||
"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/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/mitm"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/protocol/direct"
|
"github.com/sagernet/sing-box/protocol/direct"
|
||||||
"github.com/sagernet/sing-box/route"
|
"github.com/sagernet/sing-box/route"
|
||||||
"github.com/sagernet/sing-box/script"
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
@@ -39,21 +33,17 @@ import (
|
|||||||
var _ adapter.Service = (*Box)(nil)
|
var _ adapter.Service = (*Box)(nil)
|
||||||
|
|
||||||
type Box struct {
|
type Box struct {
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
logFactory log.Factory
|
logFactory log.Factory
|
||||||
logger log.ContextLogger
|
logger log.ContextLogger
|
||||||
network *route.NetworkManager
|
network *route.NetworkManager
|
||||||
endpoint *endpoint.Manager
|
endpoint *endpoint.Manager
|
||||||
inbound *inbound.Manager
|
inbound *inbound.Manager
|
||||||
outbound *outbound.Manager
|
outbound *outbound.Manager
|
||||||
dnsTransport *dns.TransportManager
|
connection *route.ConnectionManager
|
||||||
dnsRouter *dns.Router
|
router *route.Router
|
||||||
connection *route.ConnectionManager
|
services []adapter.LifecycleService
|
||||||
router *route.Router
|
done chan struct{}
|
||||||
script *script.Manager
|
|
||||||
mitm adapter.MITMEngine //*mitm.Engine
|
|
||||||
services []adapter.LifecycleService
|
|
||||||
done chan struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -67,7 +57,6 @@ func Context(
|
|||||||
inboundRegistry adapter.InboundRegistry,
|
inboundRegistry adapter.InboundRegistry,
|
||||||
outboundRegistry adapter.OutboundRegistry,
|
outboundRegistry adapter.OutboundRegistry,
|
||||||
endpointRegistry adapter.EndpointRegistry,
|
endpointRegistry adapter.EndpointRegistry,
|
||||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
|
||||||
) context.Context {
|
) context.Context {
|
||||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||||
@@ -84,10 +73,6 @@ func Context(
|
|||||||
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
||||||
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
||||||
}
|
}
|
||||||
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
|
|
||||||
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
|
||||||
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
|
||||||
}
|
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +87,6 @@ func New(options Options) (*Box, error) {
|
|||||||
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
||||||
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
|
||||||
|
|
||||||
if endpointRegistry == nil {
|
if endpointRegistry == nil {
|
||||||
return nil, E.New("missing endpoint registry in context")
|
return nil, E.New("missing endpoint registry in context")
|
||||||
@@ -146,73 +130,25 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "create log factory")
|
return nil, E.Cause(err, "create log factory")
|
||||||
}
|
}
|
||||||
|
|
||||||
var services []adapter.LifecycleService
|
|
||||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
|
||||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
|
||||||
len(certificateOptions.Certificate) > 0 ||
|
|
||||||
len(certificateOptions.CertificatePath) > 0 ||
|
|
||||||
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
|
||||||
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
|
||||||
services = append(services, certificateStore)
|
|
||||||
}
|
|
||||||
|
|
||||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
|
||||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
|
||||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
|
||||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
|
||||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
|
||||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
|
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize network manager")
|
return nil, E.Cause(err, "initialize network manager")
|
||||||
}
|
}
|
||||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||||
connectionManager := route.NewConnectionManager(ctx, logFactory.NewLogger("connection"))
|
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS))
|
||||||
service.MustRegister[adapter.Router](ctx, router)
|
|
||||||
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize router")
|
return nil, E.Cause(err, "initialize router")
|
||||||
}
|
}
|
||||||
var timeService *tls.TimeServiceWrapper
|
|
||||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
|
||||||
if ntpOptions.Enabled {
|
|
||||||
timeService = new(tls.TimeServiceWrapper)
|
|
||||||
service.MustRegister[ntp.TimeService](ctx, timeService)
|
|
||||||
}
|
|
||||||
for i, transportOptions := range dnsOptions.Servers {
|
|
||||||
var tag string
|
|
||||||
if transportOptions.Tag != "" {
|
|
||||||
tag = transportOptions.Tag
|
|
||||||
} else {
|
|
||||||
tag = F.ToString(i)
|
|
||||||
}
|
|
||||||
err = dnsTransportManager.Create(
|
|
||||||
ctx,
|
|
||||||
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
|
|
||||||
tag,
|
|
||||||
transportOptions.Type,
|
|
||||||
transportOptions.Options,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "initialize DNS server[", i, "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = dnsRouter.Initialize(dnsOptions.Rules)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "initialize dns router")
|
|
||||||
}
|
|
||||||
for i, endpointOptions := range options.Endpoints {
|
for i, endpointOptions := range options.Endpoints {
|
||||||
var tag string
|
var tag string
|
||||||
if endpointOptions.Tag != "" {
|
if endpointOptions.Tag != "" {
|
||||||
@@ -220,8 +156,7 @@ func New(options Options) (*Box, error) {
|
|||||||
} else {
|
} else {
|
||||||
tag = F.ToString(i)
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
err = endpointManager.Create(
|
err = endpointManager.Create(ctx,
|
||||||
ctx,
|
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
||||||
tag,
|
tag,
|
||||||
@@ -229,7 +164,7 @@ func New(options Options) (*Box, error) {
|
|||||||
endpointOptions.Options,
|
endpointOptions.Options,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize endpoint[", i, "]")
|
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, inboundOptions := range options.Inbounds {
|
for i, inboundOptions := range options.Inbounds {
|
||||||
@@ -239,8 +174,7 @@ func New(options Options) (*Box, error) {
|
|||||||
} else {
|
} else {
|
||||||
tag = F.ToString(i)
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
err = inboundManager.Create(
|
err = inboundManager.Create(ctx,
|
||||||
ctx,
|
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
||||||
tag,
|
tag,
|
||||||
@@ -286,24 +220,13 @@ func New(options Options) (*Box, error) {
|
|||||||
option.DirectOutboundOptions{},
|
option.DirectOutboundOptions{},
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
dnsTransportManager.Initialize(common.Must1(
|
|
||||||
local.NewTransport(
|
|
||||||
ctx,
|
|
||||||
logFactory.NewLogger("dns/local"),
|
|
||||||
"local",
|
|
||||||
option.LocalDNSServerOptions{},
|
|
||||||
)))
|
|
||||||
scriptManager, err := script.NewManager(ctx, logFactory, options.Scripts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "initialize script manager")
|
|
||||||
}
|
|
||||||
service.MustRegister[adapter.ScriptManager](ctx, scriptManager)
|
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
err = platformInterface.Initialize(networkManager)
|
err = platformInterface.Initialize(networkManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize platform interface")
|
return nil, E.Cause(err, "initialize platform interface")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var services []adapter.LifecycleService
|
||||||
if needCacheFile {
|
if needCacheFile {
|
||||||
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||||
@@ -331,12 +254,13 @@ func New(options Options) (*Box, error) {
|
|||||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||||
if ntpOptions.Enabled {
|
if ntpOptions.Enabled {
|
||||||
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "create NTP service")
|
return nil, E.Cause(err, "create NTP service")
|
||||||
}
|
}
|
||||||
ntpService := ntp.NewService(ntp.Options{
|
timeService := ntp.NewService(ntp.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Dialer: ntpDialer,
|
Dialer: ntpDialer,
|
||||||
Logger: logFactory.NewLogger("ntp"),
|
Logger: logFactory.NewLogger("ntp"),
|
||||||
@@ -344,35 +268,21 @@ func New(options Options) (*Box, error) {
|
|||||||
Interval: time.Duration(ntpOptions.Interval),
|
Interval: time.Duration(ntpOptions.Interval),
|
||||||
WriteToSystem: ntpOptions.WriteToSystem,
|
WriteToSystem: ntpOptions.WriteToSystem,
|
||||||
})
|
})
|
||||||
timeService.TimeService = ntpService
|
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||||
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
services = append(services, adapter.NewLifecycleService(timeService, "ntp service"))
|
||||||
}
|
|
||||||
mitmOptions := common.PtrValueOrDefault(options.MITM)
|
|
||||||
var mitmEngine adapter.MITMEngine
|
|
||||||
if mitmOptions.Enabled {
|
|
||||||
engine, err := mitm.NewEngine(ctx, logFactory.NewLogger("mitm"), mitmOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create MITM engine")
|
|
||||||
}
|
|
||||||
service.MustRegister[adapter.MITMEngine](ctx, engine)
|
|
||||||
mitmEngine = engine
|
|
||||||
}
|
}
|
||||||
return &Box{
|
return &Box{
|
||||||
network: networkManager,
|
network: networkManager,
|
||||||
endpoint: endpointManager,
|
endpoint: endpointManager,
|
||||||
inbound: inboundManager,
|
inbound: inboundManager,
|
||||||
outbound: outboundManager,
|
outbound: outboundManager,
|
||||||
dnsTransport: dnsTransportManager,
|
connection: connectionManager,
|
||||||
dnsRouter: dnsRouter,
|
router: router,
|
||||||
connection: connectionManager,
|
createdAt: createdAt,
|
||||||
router: router,
|
logFactory: logFactory,
|
||||||
script: scriptManager,
|
logger: logFactory.Logger(),
|
||||||
mitm: mitmEngine,
|
services: services,
|
||||||
createdAt: createdAt,
|
done: make(chan struct{}),
|
||||||
logFactory: logFactory,
|
|
||||||
logger: logFactory.Logger(),
|
|
||||||
services: services,
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,11 +336,11 @@ func (s *Box) preStart() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStateInitialize, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router, s.script, s.mitm)
|
err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.connection, s.router)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -454,7 +364,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.connection, s.router, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -462,7 +372,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.script, s.mitm, s.outbound, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStateStarted, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -481,7 +391,7 @@ func (s *Box) Close() error {
|
|||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
err := common.Close(
|
err := common.Close(
|
||||||
s.inbound, s.outbound, s.endpoint, s.mitm, s.script, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
s.inbound, s.outbound, s.router, s.connection, s.network,
|
||||||
)
|
)
|
||||||
for _, lifecycleService := range s.services {
|
for _, lifecycleService := range s.services {
|
||||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||||
|
|||||||
Submodule clients/android updated: 599d8cecac...cff12c57dd
Submodule clients/apple updated: e43e3a3f64...fa107e3b7c
@@ -1,450 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/asc-go/asc"
|
|
||||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
ctx := context.Background()
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "next_macos_project_version":
|
|
||||||
err := fetchMacOSVersion(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
case "publish_testflight":
|
|
||||||
err := publishTestflight(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
case "cancel_app_store":
|
|
||||||
err := cancelAppStore(ctx, os.Args[2])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
case "prepare_app_store":
|
|
||||||
err := prepareAppStore(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
case "publish_app_store":
|
|
||||||
err := publishAppStore(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Fatal("unknown action: ", os.Args[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
appID = "6673731168"
|
|
||||||
groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createClient(expireDuration time.Duration) *asc.Client {
|
|
||||||
privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), expireDuration, privateKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
return asc.NewClient(tokenConfig.Client())
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchMacOSVersion(ctx context.Context) error {
|
|
||||||
client := createClient(time.Minute)
|
|
||||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
|
||||||
FilterPlatform: []string{"MAC_OS"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var versionID string
|
|
||||||
findVersion:
|
|
||||||
for _, version := range versions.Data {
|
|
||||||
switch *version.Attributes.AppStoreState {
|
|
||||||
case asc.AppStoreVersionStateReadyForSale,
|
|
||||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
|
||||||
versionID = version.ID
|
|
||||||
break findVersion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if versionID == "" {
|
|
||||||
return E.New("no version found")
|
|
||||||
}
|
|
||||||
latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "parse version code")
|
|
||||||
}
|
|
||||||
os.Stdout.WriteString(F.ToString(versionInt+1, "\n"))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func publishTestflight(ctx context.Context) error {
|
|
||||||
tagVersion, err := build_shared.ReadTagVersion()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tag := tagVersion.VersionString()
|
|
||||||
client := createClient(10 * time.Minute)
|
|
||||||
|
|
||||||
log.Info(tag, " list build IDs")
|
|
||||||
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
|
|
||||||
return it.ID
|
|
||||||
})
|
|
||||||
var platforms []asc.Platform
|
|
||||||
if len(os.Args) == 3 {
|
|
||||||
switch os.Args[2] {
|
|
||||||
case "ios":
|
|
||||||
platforms = []asc.Platform{asc.PlatformIOS}
|
|
||||||
case "macos":
|
|
||||||
platforms = []asc.Platform{asc.PlatformMACOS}
|
|
||||||
case "tvos":
|
|
||||||
platforms = []asc.Platform{asc.PlatformTVOS}
|
|
||||||
default:
|
|
||||||
return E.New("unknown platform: ", os.Args[2])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
platforms = []asc.Platform{
|
|
||||||
asc.PlatformIOS,
|
|
||||||
asc.PlatformMACOS,
|
|
||||||
asc.PlatformTVOS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, platform := range platforms {
|
|
||||||
log.Info(string(platform), " list builds")
|
|
||||||
for {
|
|
||||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
|
||||||
FilterApp: []string{appID},
|
|
||||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
build := builds.Data[0]
|
|
||||||
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 5*time.Minute {
|
|
||||||
log.Info(string(platform), " ", tag, " waiting for process")
|
|
||||||
time.Sleep(15 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if *build.Attributes.ProcessingState != "VALID" {
|
|
||||||
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
|
|
||||||
time.Sleep(15 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " list localizations")
|
|
||||||
localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
|
|
||||||
return *it.Attributes.Locale == "en-US"
|
|
||||||
})
|
|
||||||
if localization.ID == "" {
|
|
||||||
log.Fatal(string(platform), " ", tag, " no en-US localization found")
|
|
||||||
}
|
|
||||||
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
|
||||||
log.Info(string(platform), " ", tag, " update localization")
|
|
||||||
_, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(
|
|
||||||
F.ToString("sing-box ", tagVersion.String()),
|
|
||||||
))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " publish")
|
|
||||||
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
|
|
||||||
if response != nil && response.StatusCode == http.StatusUnprocessableEntity {
|
|
||||||
log.Info("waiting for process")
|
|
||||||
time.Sleep(15 * time.Second)
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " list submissions")
|
|
||||||
betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
|
|
||||||
FilterBuild: []string{build.ID},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(betaSubmissions.Data) == 0 {
|
|
||||||
log.Info(string(platform), " ", tag, " create submission")
|
|
||||||
_, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
|
|
||||||
log.Error(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancelAppStore(ctx context.Context, platform string) error {
|
|
||||||
switch platform {
|
|
||||||
case "ios":
|
|
||||||
platform = string(asc.PlatformIOS)
|
|
||||||
case "macos":
|
|
||||||
platform = string(asc.PlatformMACOS)
|
|
||||||
case "tvos":
|
|
||||||
platform = string(asc.PlatformTVOS)
|
|
||||||
}
|
|
||||||
tag, err := build_shared.ReadTag()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
client := createClient(time.Minute)
|
|
||||||
for {
|
|
||||||
log.Info(platform, " list versions")
|
|
||||||
versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
|
||||||
FilterPlatform: []string{string(platform)},
|
|
||||||
})
|
|
||||||
if isRetryable(response) {
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
|
||||||
return *it.Attributes.VersionString == tag
|
|
||||||
})
|
|
||||||
if version.ID == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
log.Info(platform, " ", tag, " get submission")
|
|
||||||
submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
|
|
||||||
if response != nil && response.StatusCode == http.StatusNotFound {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if isRetryable(response) {
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info(platform, " ", tag, " delete submission")
|
|
||||||
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareAppStore(ctx context.Context) error {
|
|
||||||
tag, err := build_shared.ReadTag()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
client := createClient(time.Minute)
|
|
||||||
for _, platform := range []asc.Platform{
|
|
||||||
asc.PlatformIOS,
|
|
||||||
asc.PlatformMACOS,
|
|
||||||
asc.PlatformTVOS,
|
|
||||||
} {
|
|
||||||
log.Info(string(platform), " list versions")
|
|
||||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
|
||||||
FilterPlatform: []string{string(platform)},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
|
||||||
return *it.Attributes.VersionString == tag
|
|
||||||
})
|
|
||||||
log.Info(string(platform), " ", tag, " list builds")
|
|
||||||
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
|
|
||||||
FilterApp: []string{appID},
|
|
||||||
FilterPreReleaseVersionPlatform: []string{string(platform)},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(builds.Data) == 0 {
|
|
||||||
log.Fatal(platform, " ", tag, " no build found")
|
|
||||||
}
|
|
||||||
buildID := common.Ptr(builds.Data[0].ID)
|
|
||||||
if version.ID == "" {
|
|
||||||
log.Info(string(platform), " ", tag, " create version")
|
|
||||||
newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
|
|
||||||
Platform: platform,
|
|
||||||
VersionString: tag,
|
|
||||||
}, appID, buildID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version = newVersion.Data
|
|
||||||
|
|
||||||
} else {
|
|
||||||
log.Info(string(platform), " ", tag, " check build")
|
|
||||||
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
|
|
||||||
switch *version.Attributes.AppStoreState {
|
|
||||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
|
||||||
asc.AppStoreVersionStateRejected,
|
|
||||||
asc.AppStoreVersionStateDeveloperRejected:
|
|
||||||
case asc.AppStoreVersionStateWaitingForReview,
|
|
||||||
asc.AppStoreVersionStateInReview,
|
|
||||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
|
||||||
submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if submission != nil {
|
|
||||||
log.Info(string(platform), " ", tag, " delete submission")
|
|
||||||
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " update build")
|
|
||||||
response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if response.StatusCode != http.StatusNoContent {
|
|
||||||
response.Write(os.Stderr)
|
|
||||||
log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch *version.Attributes.AppStoreState {
|
|
||||||
case asc.AppStoreVersionStatePrepareForSubmission,
|
|
||||||
asc.AppStoreVersionStateRejected,
|
|
||||||
asc.AppStoreVersionStateDeveloperRejected:
|
|
||||||
case asc.AppStoreVersionStateWaitingForReview,
|
|
||||||
asc.AppStoreVersionStateInReview,
|
|
||||||
asc.AppStoreVersionStatePendingDeveloperRelease:
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " list localization")
|
|
||||||
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
|
|
||||||
return *it.Attributes.Locale == "en-US"
|
|
||||||
})
|
|
||||||
if localization.ID == "" {
|
|
||||||
log.Info(string(platform), " ", tag, " no en-US localization found")
|
|
||||||
}
|
|
||||||
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
|
|
||||||
log.Info(string(platform), " ", tag, " update localization")
|
|
||||||
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
|
|
||||||
PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
|
|
||||||
WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info(string(platform), " ", tag, " create submission")
|
|
||||||
fixSubmit:
|
|
||||||
for {
|
|
||||||
_, response, err := client.Submission.CreateSubmission(ctx, version.ID)
|
|
||||||
if err != nil {
|
|
||||||
switch response.StatusCode {
|
|
||||||
case http.StatusInternalServerError:
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch response.StatusCode {
|
|
||||||
case http.StatusCreated:
|
|
||||||
break fixSubmit
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func publishAppStore(ctx context.Context) error {
|
|
||||||
tag, err := build_shared.ReadTag()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
client := createClient(time.Minute)
|
|
||||||
for _, platform := range []asc.Platform{
|
|
||||||
asc.PlatformIOS,
|
|
||||||
asc.PlatformMACOS,
|
|
||||||
asc.PlatformTVOS,
|
|
||||||
} {
|
|
||||||
log.Info(string(platform), " list versions")
|
|
||||||
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
|
|
||||||
FilterPlatform: []string{string(platform)},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
|
|
||||||
return *it.Attributes.VersionString == tag
|
|
||||||
})
|
|
||||||
switch *version.Attributes.AppStoreState {
|
|
||||||
case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
|
|
||||||
log.Fatal(string(platform), " ", tag, " not submitted")
|
|
||||||
case asc.AppStoreVersionStateWaitingForReview,
|
|
||||||
asc.AppStoreVersionStateInReview:
|
|
||||||
log.Warn(string(platform), " ", tag, " waiting for review")
|
|
||||||
continue
|
|
||||||
case asc.AppStoreVersionStatePendingDeveloperRelease:
|
|
||||||
default:
|
|
||||||
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
|
|
||||||
}
|
|
||||||
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isRetryable(response *asc.Response) bool {
|
|
||||||
if response == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch response.StatusCode {
|
|
||||||
case http.StatusInternalServerError, http.StatusUnprocessableEntity:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,13 +18,11 @@ import (
|
|||||||
var (
|
var (
|
||||||
debugEnabled bool
|
debugEnabled bool
|
||||||
target string
|
target string
|
||||||
platform string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
flag.BoolVar(&debugEnabled, "debug", false, "enable debug")
|
||||||
flag.StringVar(&target, "target", "android", "target platform")
|
flag.StringVar(&target, "target", "android", "target platform")
|
||||||
flag.StringVar(&platform, "platform", "", "specify platform")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -35,8 +33,8 @@ func main() {
|
|||||||
switch target {
|
switch target {
|
||||||
case "android":
|
case "android":
|
||||||
buildAndroid()
|
buildAndroid()
|
||||||
case "apple":
|
case "ios":
|
||||||
buildApple()
|
buildiOS()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,9 +81,7 @@ func buildAndroid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var bindTarget string
|
var bindTarget string
|
||||||
if platform != "" {
|
if debugEnabled {
|
||||||
bindTarget = platform
|
|
||||||
} else if debugEnabled {
|
|
||||||
bindTarget = "android/arm64"
|
bindTarget = "android/arm64"
|
||||||
} else {
|
} else {
|
||||||
bindTarget = "android"
|
bindTarget = "android"
|
||||||
@@ -133,20 +129,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 {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func FindSDK() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findNDK() bool {
|
func findNDK() bool {
|
||||||
const fixedVersion = "28.0.12916984"
|
const fixedVersion = "28.0.12674087"
|
||||||
const versionFile = "source.properties"
|
const versionFile = "source.properties"
|
||||||
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
|
if fixedPath := filepath.Join(androidSDKPath, "ndk", fixedVersion); rw.IsFile(filepath.Join(fixedPath, versionFile)) {
|
||||||
androidNDKPath = fixedPath
|
androidNDKPath = fixedPath
|
||||||
|
|||||||
@@ -36,3 +36,11 @@ func ReadTagVersion() (badversion.Version, error) {
|
|||||||
}
|
}
|
||||||
return version, nil
|
return version, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsDevBranch() bool {
|
||||||
|
branch, err := shell.Exec("git", "branch", "--show-current").ReadOutput()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return branch == "dev-next"
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
"github.com/sagernet/sing-box/cmd/internal/build_shared"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
var nightly bool
|
var nightly bool
|
||||||
@@ -21,14 +22,25 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
var versionStr string
|
var (
|
||||||
|
versionStr string
|
||||||
|
isPrerelease bool
|
||||||
|
)
|
||||||
if version.PreReleaseIdentifier != "" {
|
if version.PreReleaseIdentifier != "" {
|
||||||
|
isPrerelease = true
|
||||||
versionStr = version.VersionString() + "-nightly"
|
versionStr = version.VersionString() + "-nightly"
|
||||||
} else {
|
} else {
|
||||||
version.Patch++
|
version.Patch++
|
||||||
versionStr = version.VersionString() + "-nightly"
|
versionStr = version.VersionString() + "-nightly"
|
||||||
}
|
}
|
||||||
err = setGitHubEnv("version", versionStr)
|
if build_shared.IsDevBranch() {
|
||||||
|
isPrerelease = true
|
||||||
|
}
|
||||||
|
err = setGitHubOutput("version", versionStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = setGitHubOutput("prerelease", F.ToString(isPrerelease))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -43,7 +55,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setGitHubEnv(name string, value string) error {
|
func setGitHubOutput(name string, value string) error {
|
||||||
outputFile, err := os.OpenFile(os.Getenv("GITHUB_ENV"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
outputFile, err := os.OpenFile(os.Getenv("GITHUB_ENV"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/csv"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
err := updateMozillaIncludedRootCAs()
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateMozillaIncludedRootCAs() error {
|
|
||||||
response, err := http.Get("https://ccadb.my.salesforce-sites.com/mozilla/IncludedCACertificateReportPEMCSV")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
reader := csv.NewReader(response.Body)
|
|
||||||
header, err := reader.Read()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
geoIndex := slices.Index(header, "Geographic Focus")
|
|
||||||
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
|
||||||
certIndex := slices.Index(header, "PEM Info")
|
|
||||||
|
|
||||||
generated := strings.Builder{}
|
|
||||||
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
|
||||||
|
|
||||||
package certificate
|
|
||||||
|
|
||||||
import "crypto/x509"
|
|
||||||
|
|
||||||
var mozillaIncluded *x509.CertPool
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
mozillaIncluded = x509.NewCertPool()
|
|
||||||
`)
|
|
||||||
for {
|
|
||||||
record, err := reader.Read()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if record[geoIndex] == "China" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
generated.WriteString("\n // ")
|
|
||||||
generated.WriteString(record[nameIndex])
|
|
||||||
generated.WriteString("\n")
|
|
||||||
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
|
||||||
generated.WriteString(record[certIndex])
|
|
||||||
generated.WriteString("`))\n")
|
|
||||||
}
|
|
||||||
generated.WriteString("}\n")
|
|
||||||
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
|
||||||
}
|
|
||||||
@@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) {
|
|||||||
configPaths = append(configPaths, "config.json")
|
configPaths = append(configPaths, "config.json")
|
||||||
}
|
}
|
||||||
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
|
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
|
||||||
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry())
|
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateTLSKeyPair(serverName string) error {
|
func generateTLSKeyPair(serverName string) error {
|
||||||
privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
privateKeyPem, publicKeyPem, err := tls.GenerateKeyPair(time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var commandMerge = &cobra.Command{
|
var commandMerge = &cobra.Command{
|
||||||
Use: "merge <output-path>",
|
Use: "merge <output>",
|
||||||
Short: "Merge configurations",
|
Short: "Merge configurations",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
err := merge(args[0])
|
err := merge(args[0])
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/json"
|
|
||||||
"github.com/sagernet/sing/common/json/badjson"
|
|
||||||
"github.com/sagernet/sing/common/rw"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ruleSetPaths []string
|
|
||||||
ruleSetDirectories []string
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandRuleSetMerge = &cobra.Command{
|
|
||||||
Use: "merge <output-path>",
|
|
||||||
Short: "Merge rule-set source files",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := mergeRuleSet(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetPaths, "config", "c", nil, "set input rule-set file path")
|
|
||||||
commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetDirectories, "config-directory", "C", nil, "set input rule-set directory path")
|
|
||||||
commandRuleSet.AddCommand(commandRuleSetMerge)
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleSetEntry struct {
|
|
||||||
content []byte
|
|
||||||
path string
|
|
||||||
options option.PlainRuleSetCompat
|
|
||||||
}
|
|
||||||
|
|
||||||
func readRuleSetAt(path string) (*RuleSetEntry, error) {
|
|
||||||
var (
|
|
||||||
configContent []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if path == "stdin" {
|
|
||||||
configContent, err = io.ReadAll(os.Stdin)
|
|
||||||
} else {
|
|
||||||
configContent, err = os.ReadFile(path)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read config at ", path)
|
|
||||||
}
|
|
||||||
options, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, configContent)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "decode config at ", path)
|
|
||||||
}
|
|
||||||
return &RuleSetEntry{
|
|
||||||
content: configContent,
|
|
||||||
path: path,
|
|
||||||
options: options,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readRuleSet() ([]*RuleSetEntry, error) {
|
|
||||||
var optionsList []*RuleSetEntry
|
|
||||||
for _, path := range ruleSetPaths {
|
|
||||||
optionsEntry, err := readRuleSetAt(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
optionsList = append(optionsList, optionsEntry)
|
|
||||||
}
|
|
||||||
for _, directory := range ruleSetDirectories {
|
|
||||||
entries, err := os.ReadDir(directory)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read rule-set directory at ", directory)
|
|
||||||
}
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
optionsEntry, err := readRuleSetAt(filepath.Join(directory, entry.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
optionsList = append(optionsList, optionsEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Slice(optionsList, func(i, j int) bool {
|
|
||||||
return optionsList[i].path < optionsList[j].path
|
|
||||||
})
|
|
||||||
return optionsList, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readRuleSetAndMerge() (option.PlainRuleSetCompat, error) {
|
|
||||||
optionsList, err := readRuleSet()
|
|
||||||
if err != nil {
|
|
||||||
return option.PlainRuleSetCompat{}, err
|
|
||||||
}
|
|
||||||
if len(optionsList) == 1 {
|
|
||||||
return optionsList[0].options, nil
|
|
||||||
}
|
|
||||||
var optionVersion uint8
|
|
||||||
for _, options := range optionsList {
|
|
||||||
if optionVersion < options.options.Version {
|
|
||||||
optionVersion = options.options.Version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var mergedMessage json.RawMessage
|
|
||||||
for _, options := range optionsList {
|
|
||||||
mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false)
|
|
||||||
if err != nil {
|
|
||||||
return option.PlainRuleSetCompat{}, E.Cause(err, "merge config at ", options.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mergedOptions, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, mergedMessage)
|
|
||||||
if err != nil {
|
|
||||||
return option.PlainRuleSetCompat{}, E.Cause(err, "unmarshal merged config")
|
|
||||||
}
|
|
||||||
mergedOptions.Version = optionVersion
|
|
||||||
return mergedOptions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeRuleSet(outputPath string) error {
|
|
||||||
mergedOptions, err := readRuleSetAndMerge()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
buffer := new(bytes.Buffer)
|
|
||||||
encoder := json.NewEncoder(buffer)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
err = encoder.Encode(mergedOptions)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "encode config")
|
|
||||||
}
|
|
||||||
if existsContent, err := os.ReadFile(outputPath); err != nil {
|
|
||||||
if string(existsContent) == buffer.String() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = rw.MkdirParent(outputPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.WriteFile(outputPath, buffer.Bytes(), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
outputPath, _ = filepath.Abs(outputPath)
|
|
||||||
os.Stderr.WriteString(outputPath + "\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -61,15 +61,14 @@ func upgradeRuleSet(sourcePath string) error {
|
|||||||
log.Info("already up-to-date")
|
log.Info("already up-to-date")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
plainRuleSetCompat.Options, err = plainRuleSetCompat.Upgrade()
|
plainRuleSet, err := plainRuleSetCompat.Upgrade()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
plainRuleSetCompat.Version = C.RuleSetVersionCurrent
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
encoder := json.NewEncoder(buffer)
|
encoder := json.NewEncoder(buffer)
|
||||||
encoder.SetIndent("", " ")
|
encoder.SetIndent("", " ")
|
||||||
err = encoder.Encode(plainRuleSetCompat)
|
err = encoder.Encode(plainRuleSet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "encode config")
|
return E.Cause(err, "encode config")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func initializeHTTP3Client(instance *box.Box) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
http3Client = &http.Client{
|
http3Client = &http.Client{
|
||||||
Transport: &http3.Transport{
|
Transport: &http3.RoundTripper{
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
destination := M.ParseSocksaddr(addr)
|
destination := M.ParseSocksaddr(addr)
|
||||||
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/settings"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -57,7 +58,7 @@ func syncTime() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if commandSyncTimeWrite {
|
if commandSyncTimeWrite {
|
||||||
err = ntp.SetSystemTime(response.Time)
|
err = settings.SetSystemTime(response.Time)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "write time to system")
|
return E.Cause(err, "write time to system")
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,184 +0,0 @@
|
|||||||
package certificate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/x509"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/fswatch"
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.CertificateStore = (*Store)(nil)
|
|
||||||
|
|
||||||
type Store struct {
|
|
||||||
systemPool *x509.CertPool
|
|
||||||
currentPool *x509.CertPool
|
|
||||||
certificate string
|
|
||||||
certificatePaths []string
|
|
||||||
certificateDirectoryPaths []string
|
|
||||||
watcher *fswatch.Watcher
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
|
||||||
var systemPool *x509.CertPool
|
|
||||||
switch options.Store {
|
|
||||||
case C.CertificateStoreSystem, "":
|
|
||||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
|
||||||
systemCertificates := platformInterface.SystemCertificates()
|
|
||||||
if len(systemCertificates) > 0 {
|
|
||||||
systemPool = x509.NewCertPool()
|
|
||||||
for _, cert := range systemCertificates {
|
|
||||||
if !systemPool.AppendCertsFromPEM([]byte(cert)) {
|
|
||||||
return nil, E.New("invalid system certificate PEM: ", cert)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
certPool, err := x509.SystemCertPool()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
systemPool = certPool
|
|
||||||
}
|
|
||||||
case C.CertificateStoreMozilla:
|
|
||||||
systemPool = mozillaIncluded
|
|
||||||
case C.CertificateStoreNone:
|
|
||||||
systemPool = nil
|
|
||||||
default:
|
|
||||||
return nil, E.New("unknown certificate store: ", options.Store)
|
|
||||||
}
|
|
||||||
store := &Store{
|
|
||||||
systemPool: systemPool,
|
|
||||||
certificate: strings.Join(options.Certificate, "\n"),
|
|
||||||
certificatePaths: options.CertificatePath,
|
|
||||||
certificateDirectoryPaths: options.CertificateDirectoryPath,
|
|
||||||
}
|
|
||||||
var watchPaths []string
|
|
||||||
for _, target := range options.CertificatePath {
|
|
||||||
watchPaths = append(watchPaths, target)
|
|
||||||
}
|
|
||||||
for _, target := range options.CertificateDirectoryPath {
|
|
||||||
watchPaths = append(watchPaths, target)
|
|
||||||
}
|
|
||||||
if len(watchPaths) > 0 {
|
|
||||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
|
||||||
Path: watchPaths,
|
|
||||||
Logger: logger,
|
|
||||||
Callback: func(_ string) {
|
|
||||||
err := store.update()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(E.Cause(err, "reload certificates"))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "fswatch: create fsnotify watcher")
|
|
||||||
}
|
|
||||||
store.watcher = watcher
|
|
||||||
}
|
|
||||||
err := store.update()
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "initializing certificate store")
|
|
||||||
}
|
|
||||||
return store, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Name() string {
|
|
||||||
return "certificate"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Start(stage adapter.StartStage) error {
|
|
||||||
if stage != adapter.StartStateStart {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if s.watcher != nil {
|
|
||||||
return s.watcher.Start()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Close() error {
|
|
||||||
if s.watcher != nil {
|
|
||||||
return s.watcher.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) Pool() *x509.CertPool {
|
|
||||||
return s.currentPool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) update() error {
|
|
||||||
var currentPool *x509.CertPool
|
|
||||||
if s.systemPool == nil {
|
|
||||||
currentPool = x509.NewCertPool()
|
|
||||||
} else {
|
|
||||||
currentPool = s.systemPool.Clone()
|
|
||||||
}
|
|
||||||
if s.certificate != "" {
|
|
||||||
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
|
||||||
return E.New("invalid certificate PEM strings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, path := range s.certificatePaths {
|
|
||||||
pemContent, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !currentPool.AppendCertsFromPEM(pemContent) {
|
|
||||||
return E.New("invalid certificate PEM file: ", path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var firstErr error
|
|
||||||
for _, directoryPath := range s.certificateDirectoryPaths {
|
|
||||||
directoryEntries, err := readUniqueDirectoryEntries(directoryPath)
|
|
||||||
if err != nil {
|
|
||||||
if firstErr == nil && !os.IsNotExist(err) {
|
|
||||||
firstErr = E.Cause(err, "invalid certificate directory: ", directoryPath)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, directoryEntry := range directoryEntries {
|
|
||||||
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
|
||||||
if err == nil {
|
|
||||||
currentPool.AppendCertsFromPEM(pemContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if firstErr != nil {
|
|
||||||
return firstErr
|
|
||||||
}
|
|
||||||
s.currentPool = currentPool
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) {
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
uniq := files[:0]
|
|
||||||
for _, f := range files {
|
|
||||||
if !isSameDirSymlink(f, dir) {
|
|
||||||
uniq = append(uniq, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uniq, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
|
||||||
if f.Type()&fs.ModeSymlink == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
|
||||||
return err == nil && !strings.Contains(target, "/")
|
|
||||||
}
|
|
||||||
@@ -2,16 +2,13 @@ package dialer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/conntrack"
|
"github.com/sagernet/sing-box/common/conntrack"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/atomic"
|
"github.com/sagernet/sing/common/atomic"
|
||||||
@@ -19,7 +16,6 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -28,35 +24,31 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DefaultDialer struct {
|
type DefaultDialer struct {
|
||||||
dialer4 tcpDialer
|
dialer4 tcpDialer
|
||||||
dialer6 tcpDialer
|
dialer6 tcpDialer
|
||||||
udpDialer4 net.Dialer
|
udpDialer4 net.Dialer
|
||||||
udpDialer6 net.Dialer
|
udpDialer6 net.Dialer
|
||||||
udpListener net.ListenConfig
|
udpListener net.ListenConfig
|
||||||
udpAddr4 string
|
udpAddr4 string
|
||||||
udpAddr6 string
|
udpAddr6 string
|
||||||
networkManager adapter.NetworkManager
|
isWireGuardListener bool
|
||||||
networkStrategy *C.NetworkStrategy
|
networkManager adapter.NetworkManager
|
||||||
defaultNetworkStrategy bool
|
networkStrategy C.NetworkStrategy
|
||||||
networkType []C.InterfaceType
|
networkType []C.InterfaceType
|
||||||
fallbackNetworkType []C.InterfaceType
|
fallbackNetworkType []C.InterfaceType
|
||||||
networkFallbackDelay time.Duration
|
networkFallbackDelay time.Duration
|
||||||
networkLastFallback atomic.TypedValue[time.Time]
|
networkLastFallback atomic.TypedValue[time.Time]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
func NewDefault(networkManager adapter.NetworkManager, options option.DialerOptions) (*DefaultDialer, error) {
|
||||||
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
|
||||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dialer net.Dialer
|
dialer net.Dialer
|
||||||
listener net.ListenConfig
|
listener net.ListenConfig
|
||||||
interfaceFinder control.InterfaceFinder
|
interfaceFinder control.InterfaceFinder
|
||||||
networkStrategy *C.NetworkStrategy
|
networkStrategy C.NetworkStrategy
|
||||||
defaultNetworkStrategy bool
|
networkType []C.InterfaceType
|
||||||
networkType []C.InterfaceType
|
fallbackNetworkType []C.InterfaceType
|
||||||
fallbackNetworkType []C.InterfaceType
|
networkFallbackDelay time.Duration
|
||||||
networkFallbackDelay time.Duration
|
|
||||||
)
|
)
|
||||||
if networkManager != nil {
|
if networkManager != nil {
|
||||||
interfaceFinder = networkManager.InterfaceFinder()
|
interfaceFinder = networkManager.InterfaceFinder()
|
||||||
@@ -82,38 +74,31 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
|
listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
|
if C.NetworkStrategy(options.NetworkStrategy) != C.NetworkStrategyDefault {
|
||||||
if disableDefaultBind || options.TCPFastOpen {
|
if options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil {
|
||||||
if options.NetworkStrategy != nil || len(options.NetworkType) > 0 && options.FallbackNetworkType == nil && options.FallbackDelay == 0 {
|
return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address` and `inet6_bind_address`")
|
||||||
return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address`, `inet6_bind_address` and `tcp_fast_open`")
|
}
|
||||||
|
networkStrategy = C.NetworkStrategy(options.NetworkStrategy)
|
||||||
|
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
|
||||||
|
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
|
||||||
|
networkFallbackDelay = time.Duration(options.NetworkFallbackDelay)
|
||||||
|
if networkManager == nil || !networkManager.AutoDetectInterface() {
|
||||||
|
return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if networkManager != nil && options.BindInterface == "" && options.Inet4BindAddress == nil && options.Inet6BindAddress == nil {
|
||||||
if networkManager != nil {
|
|
||||||
defaultOptions := networkManager.DefaultOptions()
|
defaultOptions := networkManager.DefaultOptions()
|
||||||
if !disableDefaultBind {
|
if options.BindInterface == "" {
|
||||||
if defaultOptions.BindInterface != "" {
|
if defaultOptions.BindInterface != "" {
|
||||||
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
} else if networkManager.AutoDetectInterface() {
|
} else if networkManager.AutoDetectInterface() {
|
||||||
if platformInterface != nil {
|
if defaultOptions.NetworkStrategy != C.NetworkStrategyDefault && C.NetworkStrategy(options.NetworkStrategy) == C.NetworkStrategyDefault {
|
||||||
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
|
networkStrategy = defaultOptions.NetworkStrategy
|
||||||
if networkStrategy == nil {
|
networkType = defaultOptions.NetworkType
|
||||||
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
|
fallbackNetworkType = defaultOptions.FallbackNetworkType
|
||||||
defaultNetworkStrategy = true
|
networkFallbackDelay = defaultOptions.FallbackDelay
|
||||||
}
|
|
||||||
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
|
|
||||||
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
|
|
||||||
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
|
|
||||||
networkStrategy = defaultOptions.NetworkStrategy
|
|
||||||
networkType = defaultOptions.NetworkType
|
|
||||||
fallbackNetworkType = defaultOptions.FallbackNetworkType
|
|
||||||
}
|
|
||||||
networkFallbackDelay = time.Duration(options.FallbackDelay)
|
|
||||||
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
|
|
||||||
networkFallbackDelay = defaultOptions.FallbackDelay
|
|
||||||
}
|
|
||||||
bindFunc := networkManager.ProtectFunc()
|
bindFunc := networkManager.ProtectFunc()
|
||||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||||
listener.Control = control.Append(listener.Control, bindFunc)
|
listener.Control = control.Append(listener.Control, bindFunc)
|
||||||
@@ -182,6 +167,14 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
}
|
}
|
||||||
setMultiPathTCP(&dialer4)
|
setMultiPathTCP(&dialer4)
|
||||||
}
|
}
|
||||||
|
if options.IsWireGuardListener {
|
||||||
|
for _, controlFn := range WgControlFns {
|
||||||
|
listener.Control = control.Append(listener.Control, controlFn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if networkStrategy != C.NetworkStrategyDefault && options.TCPFastOpen {
|
||||||
|
return nil, E.New("`tcp_fast_open` is conflict with `network_strategy` or `route.default_network_strategy`")
|
||||||
|
}
|
||||||
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -191,19 +184,19 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &DefaultDialer{
|
return &DefaultDialer{
|
||||||
dialer4: tcpDialer4,
|
dialer4: tcpDialer4,
|
||||||
dialer6: tcpDialer6,
|
dialer6: tcpDialer6,
|
||||||
udpDialer4: udpDialer4,
|
udpDialer4: udpDialer4,
|
||||||
udpDialer6: udpDialer6,
|
udpDialer6: udpDialer6,
|
||||||
udpListener: listener,
|
udpListener: listener,
|
||||||
udpAddr4: udpAddr4,
|
udpAddr4: udpAddr4,
|
||||||
udpAddr6: udpAddr6,
|
udpAddr6: udpAddr6,
|
||||||
networkManager: networkManager,
|
isWireGuardListener: options.IsWireGuardListener,
|
||||||
networkStrategy: networkStrategy,
|
networkManager: networkManager,
|
||||||
defaultNetworkStrategy: defaultNetworkStrategy,
|
networkStrategy: networkStrategy,
|
||||||
networkType: networkType,
|
networkType: networkType,
|
||||||
fallbackNetworkType: fallbackNetworkType,
|
fallbackNetworkType: fallbackNetworkType,
|
||||||
networkFallbackDelay: networkFallbackDelay,
|
networkFallbackDelay: networkFallbackDelay,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +204,7 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
|
|||||||
if !address.IsValid() {
|
if !address.IsValid() {
|
||||||
return nil, E.New("invalid address")
|
return nil, E.New("invalid address")
|
||||||
}
|
}
|
||||||
if d.networkStrategy == nil {
|
if d.networkStrategy == C.NetworkStrategyDefault {
|
||||||
switch N.NetworkName(network) {
|
switch N.NetworkName(network) {
|
||||||
case N.NetworkUDP:
|
case N.NetworkUDP:
|
||||||
if !address.IsIPv6() {
|
if !address.IsIPv6() {
|
||||||
@@ -230,21 +223,12 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||||
if strategy == nil {
|
if strategy == C.NetworkStrategyDefault {
|
||||||
strategy = d.networkStrategy
|
|
||||||
}
|
|
||||||
if strategy == nil {
|
|
||||||
return d.DialContext(ctx, network, address)
|
return d.DialContext(ctx, network, address)
|
||||||
}
|
}
|
||||||
if len(interfaceType) == 0 {
|
if !d.networkManager.AutoDetectInterface() {
|
||||||
interfaceType = d.networkType
|
return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`")
|
||||||
}
|
|
||||||
if len(fallbackInterfaceType) == 0 {
|
|
||||||
fallbackInterfaceType = d.fallbackNetworkType
|
|
||||||
}
|
|
||||||
if fallbackDelay == 0 {
|
|
||||||
fallbackDelay = d.networkFallbackDelay
|
|
||||||
}
|
}
|
||||||
var dialer net.Dialer
|
var dialer net.Dialer
|
||||||
if N.NetworkName(network) == N.NetworkTCP {
|
if N.NetworkName(network) == N.NetworkTCP {
|
||||||
@@ -259,18 +243,12 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if !fastFallback {
|
if !fastFallback {
|
||||||
conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
} else {
|
} else {
|
||||||
conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store)
|
conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// bind interface failed on legacy xiaomi systems
|
return nil, err
|
||||||
if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) {
|
|
||||||
d.networkStrategy = nil
|
|
||||||
return d.DialContext(ctx, network, address)
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !fastFallback && !isPrimary {
|
if !fastFallback && !isPrimary {
|
||||||
d.networkLastFallback.Store(time.Now())
|
d.networkLastFallback.Store(time.Now())
|
||||||
@@ -279,7 +257,7 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
if d.networkStrategy == nil {
|
if d.networkStrategy == C.NetworkStrategyDefault {
|
||||||
if destination.IsIPv6() {
|
if destination.IsIPv6() {
|
||||||
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
|
return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6))
|
||||||
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
} else if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||||
@@ -292,37 +270,18 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||||
if strategy == nil {
|
if strategy == C.NetworkStrategyDefault {
|
||||||
strategy = d.networkStrategy
|
|
||||||
}
|
|
||||||
if strategy == nil {
|
|
||||||
return d.ListenPacket(ctx, destination)
|
return d.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
if len(interfaceType) == 0 {
|
if !d.networkManager.AutoDetectInterface() {
|
||||||
interfaceType = d.networkType
|
return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`")
|
||||||
}
|
|
||||||
if len(fallbackInterfaceType) == 0 {
|
|
||||||
fallbackInterfaceType = d.fallbackNetworkType
|
|
||||||
}
|
|
||||||
if fallbackDelay == 0 {
|
|
||||||
fallbackDelay = d.networkFallbackDelay
|
|
||||||
}
|
}
|
||||||
network := N.NetworkUDP
|
network := N.NetworkUDP
|
||||||
if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
if destination.IsIPv4() && !destination.Addr.IsUnspecified() {
|
||||||
network += "4"
|
network += "4"
|
||||||
}
|
}
|
||||||
packetConn, err := d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", *strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return trackPacketConn(d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", strategy, interfaceType, fallbackInterfaceType, fallbackDelay))
|
||||||
if err != nil {
|
|
||||||
// bind interface failed on legacy xiaomi systems
|
|
||||||
if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) {
|
|
||||||
d.networkStrategy = nil
|
|
||||||
return d.ListenPacket(ctx, destination)
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return trackPacketConn(packetConn, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
|
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di
|
|||||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
select {
|
||||||
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}:
|
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Name, ")"), primary: primary}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
select {
|
select {
|
||||||
case results <- dialResult{Conn: conn, primary: primary}:
|
case results <- dialResult{Conn: conn}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
@@ -107,12 +107,12 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d
|
|||||||
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
conn, err := perNetDialer.DialContext(ctx, network, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
select {
|
||||||
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}:
|
case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Name, ")"), primary: primary}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
select {
|
select {
|
||||||
case results <- dialResult{Conn: conn, primary: primary}:
|
case results <- dialResult{Conn: conn}:
|
||||||
case <-returned:
|
case <-returned:
|
||||||
if primary && time.Since(startAt) <= fallbackDelay {
|
if primary && time.Since(startAt) <= fallbackDelay {
|
||||||
resetFastFallback(time.Time{})
|
resetFastFallback(time.Time{})
|
||||||
@@ -157,7 +157,7 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Index, ")"))
|
errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Name, ")"))
|
||||||
}
|
}
|
||||||
for _, fallbackInterface := range fallbackInterfaces {
|
for _, fallbackInterface := range fallbackInterfaces {
|
||||||
perNetListener := listener
|
perNetListener := listener
|
||||||
@@ -166,7 +166,7 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Index, ")"))
|
errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Name, ")"))
|
||||||
}
|
}
|
||||||
return nil, E.Errors(errors...)
|
return nil, E.Errors(errors...)
|
||||||
}
|
}
|
||||||
@@ -177,57 +177,44 @@ func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkS
|
|||||||
case C.NetworkStrategyDefault:
|
case C.NetworkStrategyDefault:
|
||||||
if len(interfaceType) == 0 {
|
if len(interfaceType) == 0 {
|
||||||
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
|
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
|
||||||
if defaultIf != nil {
|
for _, iif := range interfaces {
|
||||||
for _, iif := range interfaces {
|
if iif.Index == defaultIf.Index {
|
||||||
if iif.Index == defaultIf.Index {
|
primaryInterfaces = append(primaryInterfaces, iif)
|
||||||
primaryInterfaces = append(primaryInterfaces, iif)
|
} else {
|
||||||
}
|
fallbackInterfaces = append(fallbackInterfaces, iif)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
primaryInterfaces = interfaces
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
|
||||||
return common.Contains(interfaceType, it.Type)
|
return common.Contains(interfaceType, iif.Type)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case C.NetworkStrategyHybrid:
|
case C.NetworkStrategyHybrid:
|
||||||
if len(interfaceType) == 0 {
|
if len(interfaceType) == 0 {
|
||||||
primaryInterfaces = interfaces
|
primaryInterfaces = interfaces
|
||||||
} else {
|
} else {
|
||||||
primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
|
||||||
return common.Contains(interfaceType, it.Type)
|
return common.Contains(interfaceType, iif.Type)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case C.NetworkStrategyFallback:
|
case C.NetworkStrategyFallback:
|
||||||
if len(interfaceType) == 0 {
|
if len(interfaceType) == 0 {
|
||||||
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
|
defaultIf := networkManager.InterfaceMonitor().DefaultInterface()
|
||||||
if defaultIf != nil {
|
for _, iif := range interfaces {
|
||||||
for _, iif := range interfaces {
|
if iif.Index == defaultIf.Index {
|
||||||
if iif.Index == defaultIf.Index {
|
primaryInterfaces = append(primaryInterfaces, iif)
|
||||||
primaryInterfaces = append(primaryInterfaces, iif)
|
} else {
|
||||||
break
|
fallbackInterfaces = append(fallbackInterfaces, iif)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
primaryInterfaces = interfaces
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
|
||||||
return common.Contains(interfaceType, it.Type)
|
return common.Contains(interfaceType, iif.Type)
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(fallbackInterfaceType) == 0 {
|
|
||||||
fallbackInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool {
|
|
||||||
return !common.Any(primaryInterfaces, func(iif adapter.NetworkInterface) bool {
|
|
||||||
return it.Index == iif.Index
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
fallbackInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
|
|
||||||
return common.Contains(fallbackInterfaceType, iif.Type)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
fallbackInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool {
|
||||||
|
return common.Contains(fallbackInterfaceType, iif.Type)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return primaryInterfaces, fallbackInterfaces
|
return primaryInterfaces, fallbackInterfaces
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,7 @@ import (
|
|||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||||
if len(destinationAddresses) == 0 {
|
|
||||||
if !destination.IsIP() {
|
|
||||||
panic("invalid usage")
|
|
||||||
}
|
|
||||||
destinationAddresses = []netip.Addr{destination.Addr}
|
|
||||||
}
|
|
||||||
if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
|
if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
|
||||||
return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
}
|
}
|
||||||
@@ -44,14 +38,7 @@ func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, des
|
|||||||
return nil, E.Errors(errors...)
|
return nil, E.Errors(errors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||||
if len(destinationAddresses) == 0 {
|
|
||||||
if !destination.IsIP() {
|
|
||||||
panic("invalid usage")
|
|
||||||
}
|
|
||||||
destinationAddresses = []netip.Addr{destination.Addr}
|
|
||||||
}
|
|
||||||
|
|
||||||
if fallbackDelay == 0 {
|
if fallbackDelay == 0 {
|
||||||
fallbackDelay = N.DefaultFallbackDelay
|
fallbackDelay = N.DefaultFallbackDelay
|
||||||
}
|
}
|
||||||
@@ -129,13 +116,7 @@ func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, ne
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListenSerialNetworkPacket(ctx context.Context, dialer N.Dialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {
|
func ListenSerialNetworkPacket(ctx context.Context, dialer N.Dialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) {
|
||||||
if len(destinationAddresses) == 0 {
|
|
||||||
if !destination.IsIP() {
|
|
||||||
panic("invalid usage")
|
|
||||||
}
|
|
||||||
destinationAddresses = []netip.Addr{destination.Addr}
|
|
||||||
}
|
|
||||||
if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
|
if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel {
|
||||||
return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,18 +29,16 @@ func (d *DetourDialer) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
||||||
d.initOnce.Do(d.init)
|
d.initOnce.Do(func() {
|
||||||
|
var loaded bool
|
||||||
|
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
|
||||||
|
if !loaded {
|
||||||
|
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||||
|
}
|
||||||
|
})
|
||||||
return d.dialer, d.initErr
|
return d.dialer, d.initErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DetourDialer) init() {
|
|
||||||
var loaded bool
|
|
||||||
d.dialer, loaded = d.outboundManager.Outbound(d.detour)
|
|
||||||
if !loaded {
|
|
||||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d *DetourDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
dialer, err := d.Dialer()
|
dialer, err := d.Dialer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,120 +8,80 @@ 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/experimental/deprecated"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) {
|
||||||
Context context.Context
|
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||||
Options option.DialerOptions
|
if options.IsWireGuardListener {
|
||||||
RemoteIsDomain bool
|
return NewDefault(networkManager, options)
|
||||||
DirectResolver bool
|
}
|
||||||
ResolverOnDetour bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: merge with NewWithOptions
|
|
||||||
func New(ctx context.Context, options option.DialerOptions, remoteIsDomain bool) (N.Dialer, error) {
|
|
||||||
return NewWithOptions(Options{
|
|
||||||
Context: ctx,
|
|
||||||
Options: options,
|
|
||||||
RemoteIsDomain: remoteIsDomain,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWithOptions(options Options) (N.Dialer, error) {
|
|
||||||
dialOptions := options.Options
|
|
||||||
var (
|
var (
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if dialOptions.Detour != "" {
|
if options.Detour == "" {
|
||||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
dialer, err = NewDefault(networkManager, options)
|
||||||
if outboundManager == nil {
|
|
||||||
return nil, E.New("missing outbound manager")
|
|
||||||
}
|
|
||||||
dialer = NewDetour(outboundManager, dialOptions.Detour)
|
|
||||||
} else {
|
|
||||||
dialer, err = NewDefault(options.Context, dialOptions)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
outboundManager := service.FromContext[adapter.OutboundManager](ctx)
|
||||||
|
if outboundManager == nil {
|
||||||
|
return nil, E.New("missing outbound manager")
|
||||||
|
}
|
||||||
|
dialer = NewDetour(outboundManager, options.Detour)
|
||||||
}
|
}
|
||||||
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour) {
|
if networkManager == nil {
|
||||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
return NewDefault(networkManager, options)
|
||||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
}
|
||||||
var defaultOptions adapter.NetworkOptions
|
if options.Detour == "" {
|
||||||
if networkManager != nil {
|
router := service.FromContext[adapter.Router](ctx)
|
||||||
defaultOptions = networkManager.DefaultOptions()
|
if router != nil {
|
||||||
|
dialer = NewResolveDialer(
|
||||||
|
router,
|
||||||
|
dialer,
|
||||||
|
options.Detour == "" && !options.TCPFastOpen,
|
||||||
|
dns.DomainStrategy(options.DomainStrategy),
|
||||||
|
time.Duration(options.FallbackDelay))
|
||||||
}
|
}
|
||||||
var (
|
|
||||||
server string
|
|
||||||
dnsQueryOptions adapter.DNSQueryOptions
|
|
||||||
resolveFallbackDelay time.Duration
|
|
||||||
)
|
|
||||||
if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" {
|
|
||||||
var transport adapter.DNSTransport
|
|
||||||
if !options.DirectResolver {
|
|
||||||
var loaded bool
|
|
||||||
transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server)
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var strategy C.DomainStrategy
|
|
||||||
if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
|
||||||
strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy)
|
|
||||||
} else if
|
|
||||||
//nolint:staticcheck
|
|
||||||
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
|
|
||||||
//nolint:staticcheck
|
|
||||||
strategy = C.DomainStrategy(dialOptions.DomainStrategy)
|
|
||||||
}
|
|
||||||
server = dialOptions.DomainResolver.Server
|
|
||||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
|
||||||
Transport: transport,
|
|
||||||
Strategy: strategy,
|
|
||||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
|
||||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
|
||||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
|
||||||
}
|
|
||||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
|
||||||
} else if options.DirectResolver {
|
|
||||||
return nil, E.New("missing domain resolver for domain server address")
|
|
||||||
} else if defaultOptions.DomainResolver != "" {
|
|
||||||
dnsQueryOptions = defaultOptions.DomainResolveOptions
|
|
||||||
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
|
|
||||||
}
|
|
||||||
dnsQueryOptions.Transport = transport
|
|
||||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
|
||||||
} else {
|
|
||||||
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
|
||||||
}
|
|
||||||
dialer = NewResolveDialer(
|
|
||||||
options.Context,
|
|
||||||
dialer,
|
|
||||||
dialOptions.Detour == "" && !dialOptions.TCPFastOpen,
|
|
||||||
server,
|
|
||||||
dnsQueryOptions,
|
|
||||||
resolveFallbackDelay,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return dialer, nil
|
return dialer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInterfaceDialer, error) {
|
||||||
|
if options.Detour != "" {
|
||||||
|
return nil, E.New("`detour` is not supported in direct context")
|
||||||
|
}
|
||||||
|
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||||
|
if options.IsWireGuardListener {
|
||||||
|
return NewDefault(networkManager, options)
|
||||||
|
}
|
||||||
|
dialer, err := NewDefault(networkManager, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewResolveParallelInterfaceDialer(
|
||||||
|
service.FromContext[adapter.Router](ctx),
|
||||||
|
dialer,
|
||||||
|
true,
|
||||||
|
dns.DomainStrategy(options.DomainStrategy),
|
||||||
|
time.Duration(options.FallbackDelay),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
type ParallelInterfaceDialer interface {
|
type ParallelInterfaceDialer interface {
|
||||||
N.Dialer
|
N.Dialer
|
||||||
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||||
ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error)
|
ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParallelNetworkDialer interface {
|
type ParallelNetworkDialer interface {
|
||||||
DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||||
ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
|
ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,16 @@ package dialer
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -21,37 +20,21 @@ var (
|
|||||||
_ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil)
|
_ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResolveDialer interface {
|
|
||||||
N.Dialer
|
|
||||||
QueryOptions() adapter.DNSQueryOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParallelInterfaceResolveDialer interface {
|
|
||||||
ParallelInterfaceDialer
|
|
||||||
QueryOptions() adapter.DNSQueryOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
type resolveDialer struct {
|
type resolveDialer struct {
|
||||||
transport adapter.DNSTransportManager
|
|
||||||
router adapter.DNSRouter
|
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
parallel bool
|
parallel bool
|
||||||
server string
|
router adapter.Router
|
||||||
initOnce sync.Once
|
strategy dns.DomainStrategy
|
||||||
initErr error
|
|
||||||
queryOptions adapter.DNSQueryOptions
|
|
||||||
fallbackDelay time.Duration
|
fallbackDelay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResolveDialer(ctx context.Context, dialer N.Dialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ResolveDialer {
|
func NewResolveDialer(router adapter.Router, dialer N.Dialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) N.Dialer {
|
||||||
return &resolveDialer{
|
return &resolveDialer{
|
||||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
dialer,
|
||||||
router: service.FromContext[adapter.DNSRouter](ctx),
|
parallel,
|
||||||
dialer: dialer,
|
router,
|
||||||
parallel: parallel,
|
strategy,
|
||||||
server: server,
|
fallbackDelay,
|
||||||
queryOptions: queryOptions,
|
|
||||||
fallbackDelay: fallbackDelay,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,68 +43,59 @@ type resolveParallelNetworkDialer struct {
|
|||||||
dialer ParallelInterfaceDialer
|
dialer ParallelInterfaceDialer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResolveParallelInterfaceDialer(ctx context.Context, dialer ParallelInterfaceDialer, parallel bool, server string, queryOptions adapter.DNSQueryOptions, fallbackDelay time.Duration) ParallelInterfaceResolveDialer {
|
func NewResolveParallelInterfaceDialer(router adapter.Router, dialer ParallelInterfaceDialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) ParallelInterfaceDialer {
|
||||||
return &resolveParallelNetworkDialer{
|
return &resolveParallelNetworkDialer{
|
||||||
resolveDialer{
|
resolveDialer{
|
||||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
dialer,
|
||||||
router: service.FromContext[adapter.DNSRouter](ctx),
|
parallel,
|
||||||
dialer: dialer,
|
router,
|
||||||
parallel: parallel,
|
strategy,
|
||||||
server: server,
|
fallbackDelay,
|
||||||
queryOptions: queryOptions,
|
|
||||||
fallbackDelay: fallbackDelay,
|
|
||||||
},
|
},
|
||||||
dialer,
|
dialer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveDialer) initialize() error {
|
|
||||||
d.initOnce.Do(d.initServer)
|
|
||||||
return d.initErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *resolveDialer) initServer() {
|
|
||||||
if d.server == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
transport, loaded := d.transport.Transport(d.server)
|
|
||||||
if !loaded {
|
|
||||||
d.initErr = E.New("domain resolver not found: " + d.server)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.queryOptions.Transport = transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
err := d.initialize()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
metadata.Destination = destination
|
||||||
|
metadata.Domain = ""
|
||||||
|
var addresses []netip.Addr
|
||||||
|
var err error
|
||||||
|
if d.strategy == dns.DomainStrategyAsIS {
|
||||||
|
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
||||||
|
} else {
|
||||||
|
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if d.parallel {
|
if d.parallel {
|
||||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
|
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||||
} else {
|
} else {
|
||||||
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
|
return N.DialSerial(ctx, d.dialer, network, destination, addresses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
err := d.initialize()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
metadata.Destination = destination
|
||||||
|
metadata.Domain = ""
|
||||||
|
var addresses []netip.Addr
|
||||||
|
var err error
|
||||||
|
if d.strategy == dns.DomainStrategyAsIS {
|
||||||
|
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
||||||
|
} else {
|
||||||
|
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -132,24 +106,21 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
|||||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveDialer) QueryOptions() adapter.DNSQueryOptions {
|
func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
||||||
return d.queryOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *resolveDialer) Upstream() any {
|
|
||||||
return d.dialer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) {
|
|
||||||
err := d.initialize()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
metadata.Destination = destination
|
||||||
|
metadata.Domain = ""
|
||||||
|
var addresses []netip.Addr
|
||||||
|
var err error
|
||||||
|
if d.strategy == dns.DomainStrategyAsIS {
|
||||||
|
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
||||||
|
} else {
|
||||||
|
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -157,28 +128,30 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
|
|||||||
fallbackDelay = d.fallbackDelay
|
fallbackDelay = d.fallbackDelay
|
||||||
}
|
}
|
||||||
if d.parallel {
|
if d.parallel {
|
||||||
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.queryOptions.Strategy == C.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
} else {
|
} else {
|
||||||
return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||||
err := d.initialize()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !destination.IsFqdn() {
|
if !destination.IsFqdn() {
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||||
addresses, err := d.router.Lookup(ctx, destination.Fqdn, d.queryOptions)
|
metadata.Destination = destination
|
||||||
|
metadata.Domain = ""
|
||||||
|
var addresses []netip.Addr
|
||||||
|
var err error
|
||||||
|
if d.strategy == dns.DomainStrategyAsIS {
|
||||||
|
addresses, err = d.router.LookupDefault(ctx, destination.Fqdn)
|
||||||
|
} else {
|
||||||
|
addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if fallbackDelay == 0 {
|
|
||||||
fallbackDelay = d.fallbackDelay
|
|
||||||
}
|
|
||||||
conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -186,10 +159,6 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
|
|||||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *resolveParallelNetworkDialer) QueryOptions() adapter.DNSQueryOptions {
|
func (d *resolveDialer) Upstream() any {
|
||||||
return d.queryOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *resolveParallelNetworkDialer) Upstream() any {
|
|
||||||
return d.dialer
|
return d.dialer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,27 +7,24 @@ import (
|
|||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DefaultOutboundDialer struct {
|
type DefaultOutboundDialer struct {
|
||||||
outbound adapter.OutboundManager
|
outboundManager adapter.OutboundManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultOutbound(ctx context.Context) N.Dialer {
|
func NewDefaultOutbound(outboundManager adapter.OutboundManager) N.Dialer {
|
||||||
return &DefaultOutboundDialer{
|
return &DefaultOutboundDialer{outboundManager: outboundManager}
|
||||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
return d.outbound.Default().DialContext(ctx, network, destination)
|
return d.outboundManager.Default().DialContext(ctx, network, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
return d.outbound.Default().ListenPacket(ctx, destination)
|
return d.outboundManager.Default().ListenPacket(ctx, destination)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultOutboundDialer) Upstream() any {
|
func (d *DefaultOutboundDialer) Upstream() any {
|
||||||
return d.outbound.Default()
|
return d.outboundManager.Default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
ProcessID uint32
|
|
||||||
ProcessPath string
|
ProcessPath string
|
||||||
PackageName string
|
PackageName string
|
||||||
User string
|
User string
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ package process
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/winiphlpapi"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
@@ -23,39 +26,209 @@ func NewSearcher(_ Config) (Searcher, error) {
|
|||||||
return &windowsSearcher{}, nil
|
return &windowsSearcher{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
|
||||||
|
procGetExtendedTcpTable = modiphlpapi.NewProc("GetExtendedTcpTable")
|
||||||
|
procGetExtendedUdpTable = modiphlpapi.NewProc("GetExtendedUdpTable")
|
||||||
|
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||||
|
procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW")
|
||||||
|
)
|
||||||
|
|
||||||
func initWin32API() error {
|
func initWin32API() error {
|
||||||
return winiphlpapi.LoadExtendedTable()
|
err := modiphlpapi.Load()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "load iphlpapi.dll")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = procGetExtendedTcpTable.Find()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "load iphlpapi::GetExtendedTcpTable")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = procGetExtendedUdpTable.Find()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "load iphlpapi::GetExtendedUdpTable")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = modkernel32.Load()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "load kernel32.dll")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = procQueryFullProcessImageNameW.Find()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "load kernel32::QueryFullProcessImageNameW")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||||
pid, err := winiphlpapi.FindPid(network, source)
|
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
path, err := getProcessPath(pid)
|
return &Info{ProcessPath: processName, UserId: -1}, nil
|
||||||
if err != nil {
|
|
||||||
return &Info{ProcessID: pid, UserId: -1}, err
|
|
||||||
}
|
|
||||||
return &Info{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getProcessPath(pid uint32) (string, error) {
|
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
|
||||||
|
family := windows.AF_INET
|
||||||
|
if ip.Is6() {
|
||||||
|
family = windows.AF_INET6
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
tcpTablePidConn = 4
|
||||||
|
udpTablePid = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var class int
|
||||||
|
var fn uintptr
|
||||||
|
switch network {
|
||||||
|
case N.NetworkTCP:
|
||||||
|
fn = procGetExtendedTcpTable.Addr()
|
||||||
|
class = tcpTablePidConn
|
||||||
|
case N.NetworkUDP:
|
||||||
|
fn = procGetExtendedUdpTable.Addr()
|
||||||
|
class = udpTablePid
|
||||||
|
default:
|
||||||
|
return "", os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := getTransportTable(fn, family, class)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
s := newSearcher(family == windows.AF_INET, network == N.NetworkTCP)
|
||||||
|
|
||||||
|
pid, err := s.Search(buf, ip, uint16(srcPort))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return getExecPathFromPID(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
type searcher struct {
|
||||||
|
itemSize int
|
||||||
|
port int
|
||||||
|
ip int
|
||||||
|
ipSize int
|
||||||
|
pid int
|
||||||
|
tcpState int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) {
|
||||||
|
n := int(readNativeUint32(b[:4]))
|
||||||
|
itemSize := s.itemSize
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
row := b[4+itemSize*i : 4+itemSize*(i+1)]
|
||||||
|
|
||||||
|
if s.tcpState >= 0 {
|
||||||
|
tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4])
|
||||||
|
// MIB_TCP_STATE_ESTAB, only check established connections for TCP
|
||||||
|
if tcpState != 5 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian.
|
||||||
|
// this field can be illustrated as follows depends on different machine endianess:
|
||||||
|
// little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB)
|
||||||
|
// big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB)
|
||||||
|
// so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32
|
||||||
|
srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4])))
|
||||||
|
if srcPort != port {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize])
|
||||||
|
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
|
||||||
|
if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pid := readNativeUint32(row[s.pid : s.pid+4])
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
return 0, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSearcher(isV4, isTCP bool) *searcher {
|
||||||
|
var itemSize, port, ip, ipSize, pid int
|
||||||
|
tcpState := -1
|
||||||
|
switch {
|
||||||
|
case isV4 && isTCP:
|
||||||
|
// struct MIB_TCPROW_OWNER_PID
|
||||||
|
itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0
|
||||||
|
case isV4 && !isTCP:
|
||||||
|
// struct MIB_UDPROW_OWNER_PID
|
||||||
|
itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8
|
||||||
|
case !isV4 && isTCP:
|
||||||
|
// struct MIB_TCP6ROW_OWNER_PID
|
||||||
|
itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48
|
||||||
|
case !isV4 && !isTCP:
|
||||||
|
// struct MIB_UDP6ROW_OWNER_PID
|
||||||
|
itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24
|
||||||
|
}
|
||||||
|
|
||||||
|
return &searcher{
|
||||||
|
itemSize: itemSize,
|
||||||
|
port: port,
|
||||||
|
ip: ip,
|
||||||
|
ipSize: ipSize,
|
||||||
|
pid: pid,
|
||||||
|
tcpState: tcpState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
|
||||||
|
for size, buf := uint32(8), make([]byte, 8); ; {
|
||||||
|
ptr := unsafe.Pointer(&buf[0])
|
||||||
|
err, _, _ := syscall.SyscallN(fn, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
|
||||||
|
|
||||||
|
switch err {
|
||||||
|
case 0:
|
||||||
|
return buf, nil
|
||||||
|
case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER):
|
||||||
|
buf = make([]byte, size)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("syscall error: %d", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNativeUint32(b []byte) uint32 {
|
||||||
|
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExecPathFromPID(pid uint32) (string, error) {
|
||||||
|
// kernel process starts with a colon in order to distinguish with normal processes
|
||||||
switch pid {
|
switch pid {
|
||||||
case 0:
|
case 0:
|
||||||
|
// reserved pid for system idle process
|
||||||
return ":System Idle Process", nil
|
return ":System Idle Process", nil
|
||||||
case 4:
|
case 4:
|
||||||
|
// reserved pid for windows kernel image
|
||||||
return ":System", nil
|
return ":System", nil
|
||||||
}
|
}
|
||||||
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer windows.CloseHandle(handle)
|
defer windows.CloseHandle(h)
|
||||||
size := uint32(syscall.MAX_LONG_PATH)
|
|
||||||
buf := make([]uint16, syscall.MAX_LONG_PATH)
|
buf := make([]uint16, syscall.MAX_LONG_PATH)
|
||||||
err = windows.QueryFullProcessImageName(handle, 0, &buf[0], &size)
|
size := uint32(len(buf))
|
||||||
if err != nil {
|
r1, _, err := syscall.SyscallN(
|
||||||
|
procQueryFullProcessImageNameW.Addr(),
|
||||||
|
uintptr(h),
|
||||||
|
uintptr(0),
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
)
|
||||||
|
if r1 == 0 {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return windows.UTF16ToString(buf[:size]), nil
|
return syscall.UTF16ToString(buf[:size]), nil
|
||||||
}
|
}
|
||||||
|
|||||||
12
common/settings/time_stub.go
Normal file
12
common/settings/time_stub.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !(windows || linux || darwin)
|
||||||
|
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetSystemTime(nowTime time.Time) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
14
common/settings/time_unix.go
Normal file
14
common/settings/time_unix.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build linux || darwin
|
||||||
|
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetSystemTime(nowTime time.Time) error {
|
||||||
|
timeVal := unix.NsecToTimeval(nowTime.UnixNano())
|
||||||
|
return unix.Settimeofday(&timeVal)
|
||||||
|
}
|
||||||
32
common/settings/time_windows.go
Normal file
32
common/settings/time_windows.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetSystemTime(nowTime time.Time) error {
|
||||||
|
var systemTime windows.Systemtime
|
||||||
|
systemTime.Year = uint16(nowTime.Year())
|
||||||
|
systemTime.Month = uint16(nowTime.Month())
|
||||||
|
systemTime.Day = uint16(nowTime.Day())
|
||||||
|
systemTime.Hour = uint16(nowTime.Hour())
|
||||||
|
systemTime.Minute = uint16(nowTime.Minute())
|
||||||
|
systemTime.Second = uint16(nowTime.Second())
|
||||||
|
systemTime.Milliseconds = uint16(nowTime.UnixMilli() - nowTime.Unix()*1000)
|
||||||
|
|
||||||
|
dllKernel32 := windows.NewLazySystemDLL("kernel32.dll")
|
||||||
|
proc := dllKernel32.NewProc("SetSystemTime")
|
||||||
|
|
||||||
|
_, _, err := proc.Call(
|
||||||
|
uintptr(unsafe.Pointer(&systemTime)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil && err.Error() != "The operation completed successfully." {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -18,6 +18,5 @@ func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Rea
|
|||||||
}
|
}
|
||||||
metadata.Protocol = C.ProtocolHTTP
|
metadata.Protocol = C.ProtocolHTTP
|
||||||
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||||
metadata.HTTPRequest = request
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
|
|||||||
if clientHello != nil {
|
if clientHello != nil {
|
||||||
metadata.Protocol = C.ProtocolTLS
|
metadata.Protocol = C.ProtocolTLS
|
||||||
metadata.Domain = clientHello.ServerName
|
metadata.Domain = clientHello.ServerName
|
||||||
metadata.ClientHello = clientHello
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
|
|
||||||
cftls "github.com/sagernet/cloudflare-tls"
|
cftls "github.com/sagernet/cloudflare-tls"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-dns"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
@@ -64,7 +64,6 @@ type echConnWrapper struct {
|
|||||||
|
|
||||||
func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
|
func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
|
||||||
state := c.Conn.ConnectionState()
|
state := c.Conn.ConnectionState()
|
||||||
//nolint:staticcheck
|
|
||||||
return tls.ConnectionState{
|
return tls.ConnectionState{
|
||||||
Version: state.Version,
|
Version: state.Version,
|
||||||
HandshakeComplete: state.HandshakeComplete,
|
HandshakeComplete: state.HandshakeComplete,
|
||||||
@@ -100,7 +99,6 @@ func NewECHClient(ctx context.Context, serverAddress string, options option.Outb
|
|||||||
|
|
||||||
var tlsConfig cftls.Config
|
var tlsConfig cftls.Config
|
||||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
|
||||||
if options.DisableSNI {
|
if options.DisableSNI {
|
||||||
tlsConfig.ServerName = "127.0.0.1"
|
tlsConfig.ServerName = "127.0.0.1"
|
||||||
} else {
|
} else {
|
||||||
@@ -216,7 +214,7 @@ func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverNam
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
response, err := service.FromContext[adapter.DNSRouter](ctx).Exchange(ctx, message, adapter.DNSQueryOptions{})
|
response, err := service.FromContext[adapter.Router](ctx).Exchange(ctx, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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...)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func (c *echServerConfig) startWatcher() error {
|
|||||||
Callback: func(path string) {
|
Callback: func(path string) {
|
||||||
err := c.credentialsUpdated(path)
|
err := c.credentialsUpdated(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error(E.Cause(err, "reload credentials"))
|
c.logger.Error(E.Cause(err, "reload credentials from ", path))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,14 +8,11 @@ import (
|
|||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
func GenerateCertificate(timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
||||||
privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour))
|
privateKeyPem, publicKeyPem, err := GenerateKeyPair(timeFunc, serverName, timeFunc().Add(time.Hour))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -26,7 +23,7 @@ func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() ti
|
|||||||
return &certificate, err
|
return &certificate, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
|
func GenerateKeyPair(timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
|
||||||
if timeFunc == nil {
|
if timeFunc == nil {
|
||||||
timeFunc = time.Now
|
timeFunc = time.Now
|
||||||
}
|
}
|
||||||
@@ -38,36 +35,19 @@ func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var template *x509.Certificate
|
template := &x509.Certificate{
|
||||||
if serverAddress := M.ParseAddr(serverName); serverAddress.IsValid() {
|
SerialNumber: serialNumber,
|
||||||
template = &x509.Certificate{
|
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||||
SerialNumber: serialNumber,
|
NotAfter: expire,
|
||||||
IPAddresses: []net.IP{serverAddress.AsSlice()},
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
NotAfter: expire,
|
BasicConstraintsValid: true,
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
Subject: pkix.Name{
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
CommonName: serverName,
|
||||||
BasicConstraintsValid: true,
|
},
|
||||||
}
|
DNSNames: []string{serverName},
|
||||||
} else {
|
|
||||||
template = &x509.Certificate{
|
|
||||||
SerialNumber: serialNumber,
|
|
||||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
|
||||||
NotAfter: expire,
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
Subject: pkix.Name{
|
|
||||||
CommonName: serverName,
|
|
||||||
},
|
|
||||||
DNSNames: []string{serverName},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if parent == nil {
|
publicDer, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
|
||||||
parent = template
|
|
||||||
parentKey = key
|
|
||||||
}
|
|
||||||
publicDer, err := x509.CreateCertificate(rand.Reader, template, parent, key.Public(), parentKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common/debug"
|
"github.com/sagernet/sing/common/debug"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
|
||||||
aTLS "github.com/sagernet/sing/common/tls"
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
utls "github.com/sagernet/utls"
|
utls "github.com/sagernet/utls"
|
||||||
|
|
||||||
@@ -42,7 +40,6 @@ import (
|
|||||||
var _ ConfigCompat = (*RealityClientConfig)(nil)
|
var _ ConfigCompat = (*RealityClientConfig)(nil)
|
||||||
|
|
||||||
type RealityClientConfig struct {
|
type RealityClientConfig struct {
|
||||||
ctx context.Context
|
|
||||||
uClient *UTLSClientConfig
|
uClient *UTLSClientConfig
|
||||||
publicKey []byte
|
publicKey []byte
|
||||||
shortID [8]byte
|
shortID [8]byte
|
||||||
@@ -73,7 +70,7 @@ func NewRealityClient(ctx context.Context, serverAddress string, options option.
|
|||||||
if decodedLen > 8 {
|
if decodedLen > 8 {
|
||||||
return nil, E.New("invalid short_id")
|
return nil, E.New("invalid short_id")
|
||||||
}
|
}
|
||||||
return &RealityClientConfig{ctx, uClient, publicKey, shortID}, nil
|
return &RealityClientConfig{uClient, publicKey, shortID}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *RealityClientConfig) ServerName() string {
|
func (e *RealityClientConfig) ServerName() string {
|
||||||
@@ -183,24 +180,20 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !verifier.verified {
|
if !verifier.verified {
|
||||||
go realityClientFallback(e.ctx, uConn, e.uClient.ServerName(), e.uClient.id)
|
go realityClientFallback(uConn, e.uClient.ServerName(), e.uClient.id)
|
||||||
return nil, E.New("reality verification failed")
|
return nil, E.New("reality verification failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &realityClientConnWrapper{uConn}, nil
|
return &utlsConnWrapper{uConn}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func realityClientFallback(ctx context.Context, uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
|
func realityClientFallback(uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
|
||||||
defer uConn.Close()
|
defer uConn.Close()
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: &http2.Transport{
|
Transport: &http2.Transport{
|
||||||
DialTLSContext: func(ctx context.Context, network, addr string, config *tls.Config) (net.Conn, error) {
|
DialTLSContext: func(ctx context.Context, network, addr string, config *tls.Config) (net.Conn, error) {
|
||||||
return uConn, nil
|
return uConn, nil
|
||||||
},
|
},
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
Time: ntp.TimeFuncFromContext(ctx),
|
|
||||||
RootCAs: adapter.RootPoolFromContext(ctx),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
request, _ := http.NewRequest("GET", "https://"+serverName, nil)
|
request, _ := http.NewRequest("GET", "https://"+serverName, nil)
|
||||||
@@ -220,7 +213,6 @@ func (e *RealityClientConfig) SetSessionIDGenerator(generator func(clientHello [
|
|||||||
|
|
||||||
func (e *RealityClientConfig) Clone() Config {
|
func (e *RealityClientConfig) Clone() Config {
|
||||||
return &RealityClientConfig{
|
return &RealityClientConfig{
|
||||||
e.ctx,
|
|
||||||
e.uClient.Clone().(*UTLSClientConfig),
|
e.uClient.Clone().(*UTLSClientConfig),
|
||||||
e.publicKey,
|
e.publicKey,
|
||||||
e.shortID,
|
e.shortID,
|
||||||
@@ -257,36 +249,3 @@ func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChain
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type realityClientConnWrapper struct {
|
|
||||||
*utls.UConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *realityClientConnWrapper) ConnectionState() tls.ConnectionState {
|
|
||||||
state := c.Conn.ConnectionState()
|
|
||||||
//nolint:staticcheck
|
|
||||||
return tls.ConnectionState{
|
|
||||||
Version: state.Version,
|
|
||||||
HandshakeComplete: state.HandshakeComplete,
|
|
||||||
DidResume: state.DidResume,
|
|
||||||
CipherSuite: state.CipherSuite,
|
|
||||||
NegotiatedProtocol: state.NegotiatedProtocol,
|
|
||||||
NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual,
|
|
||||||
ServerName: state.ServerName,
|
|
||||||
PeerCertificates: state.PeerCertificates,
|
|
||||||
VerifiedChains: state.VerifiedChains,
|
|
||||||
SignedCertificateTimestamps: state.SignedCertificateTimestamps,
|
|
||||||
OCSPResponse: state.OCSPResponse,
|
|
||||||
TLSUnique: state.TLSUnique,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *realityClientConnWrapper) Upstream() any {
|
|
||||||
return c.UConn
|
|
||||||
}
|
|
||||||
|
|
||||||
// Due to low implementation quality, the reality server intercepted half close and caused memory leaks.
|
|
||||||
// We fixed it by calling Close() directly.
|
|
||||||
func (c *realityClientConnWrapper) CloseWrite() error {
|
|
||||||
return c.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
|
|||||||
tlsConfig.ShortIds[shortID] = true
|
tlsConfig.ShortIds[shortID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions, options.Reality.Handshake.ServerIsDomain())
|
handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,6 @@ type realityConnWrapper struct {
|
|||||||
|
|
||||||
func (c *realityConnWrapper) ConnectionState() ConnectionState {
|
func (c *realityConnWrapper) ConnectionState() ConnectionState {
|
||||||
state := c.Conn.ConnectionState()
|
state := c.Conn.ConnectionState()
|
||||||
//nolint:staticcheck
|
|
||||||
return tls.ConnectionState{
|
return tls.ConnectionState{
|
||||||
Version: state.Version,
|
Version: state.Version,
|
||||||
HandshakeComplete: state.HandshakeComplete,
|
HandshakeComplete: state.HandshakeComplete,
|
||||||
@@ -194,9 +193,3 @@ func (c *realityConnWrapper) ConnectionState() ConnectionState {
|
|||||||
func (c *realityConnWrapper) Upstream() any {
|
func (c *realityConnWrapper) Upstream() any {
|
||||||
return c.Conn
|
return c.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Due to low implementation quality, the reality server intercepted half close and caused memory leaks.
|
|
||||||
// We fixed it by calling Close() directly.
|
|
||||||
func (c *realityConnWrapper) CloseWrite() error {
|
|
||||||
return c.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"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"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
@@ -51,7 +51,9 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
|||||||
if options.ServerName != "" {
|
if options.ServerName != "" {
|
||||||
serverName = options.ServerName
|
serverName = options.ServerName
|
||||||
} else if serverAddress != "" {
|
} else if serverAddress != "" {
|
||||||
serverName = serverAddress
|
if _, err := netip.ParseAddr(serverName); err != nil {
|
||||||
|
serverName = serverAddress
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if serverName == "" && !options.Insecure {
|
if serverName == "" && !options.Insecure {
|
||||||
return nil, E.New("missing server_name or insecure=true")
|
return nil, E.New("missing server_name or insecure=true")
|
||||||
@@ -59,7 +61,6 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
|||||||
|
|
||||||
var tlsConfig tls.Config
|
var tlsConfig tls.Config
|
||||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
|
||||||
if options.DisableSNI {
|
if options.DisableSNI {
|
||||||
tlsConfig.ServerName = "127.0.0.1"
|
tlsConfig.ServerName = "127.0.0.1"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func (c *STDServerConfig) startWatcher() error {
|
|||||||
Callback: func(path string) {
|
Callback: func(path string) {
|
||||||
err := c.certificateUpdated(path)
|
err := c.certificateUpdated(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Error(E.Cause(err, "reload certificate"))
|
c.logger.Error(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -222,7 +222,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
|||||||
}
|
}
|
||||||
if certificate == nil && key == nil && options.Insecure {
|
if certificate == nil && key == nil && options.Insecure {
|
||||||
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
return GenerateKeyPair(nil, nil, ntp.TimeFuncFromContext(ctx), info.ServerName)
|
return GenerateCertificate(ntp.TimeFuncFromContext(ctx), info.ServerName)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if certificate == nil {
|
if certificate == nil {
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/ntp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TimeServiceWrapper struct {
|
|
||||||
ntp.TimeService
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *TimeServiceWrapper) TimeFunc() func() time.Time {
|
|
||||||
if w.TimeService == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return w.TimeService.TimeFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *TimeServiceWrapper) Upstream() any {
|
|
||||||
return w.TimeService
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"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"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
@@ -70,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,
|
||||||
@@ -131,7 +129,6 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
|
|||||||
|
|
||||||
var tlsConfig utls.Config
|
var tlsConfig utls.Config
|
||||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
|
||||||
if options.DisableSNI {
|
if options.DisableSNI {
|
||||||
tlsConfig.ServerName = "127.0.0.1"
|
tlsConfig.ServerName = "127.0.0.1"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
package tf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"golang.org/x/net/publicsuffix"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Conn struct {
|
|
||||||
net.Conn
|
|
||||||
tcpConn *net.TCPConn
|
|
||||||
ctx context.Context
|
|
||||||
firstPacketWritten bool
|
|
||||||
fallbackDelay time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConn(conn net.Conn, ctx context.Context, fallbackDelay time.Duration) (*Conn, error) {
|
|
||||||
tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
|
|
||||||
return &Conn{
|
|
||||||
Conn: conn,
|
|
||||||
tcpConn: tcpConn,
|
|
||||||
ctx: ctx,
|
|
||||||
fallbackDelay: fallbackDelay,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) Write(b []byte) (n int, err error) {
|
|
||||||
if !c.firstPacketWritten {
|
|
||||||
defer func() {
|
|
||||||
c.firstPacketWritten = true
|
|
||||||
}()
|
|
||||||
serverName := indexTLSServerName(b)
|
|
||||||
if serverName != nil {
|
|
||||||
if c.tcpConn != nil {
|
|
||||||
err = c.tcpConn.SetNoDelay(true)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
splits := strings.Split(serverName.ServerName, ".")
|
|
||||||
currentIndex := serverName.Index
|
|
||||||
if publicSuffix := publicsuffix.List.PublicSuffix(serverName.ServerName); publicSuffix != "" {
|
|
||||||
splits = splits[:len(splits)-strings.Count(serverName.ServerName, ".")]
|
|
||||||
}
|
|
||||||
if len(splits) > 1 && splits[0] == "..." {
|
|
||||||
currentIndex += len(splits[0]) + 1
|
|
||||||
splits = splits[1:]
|
|
||||||
}
|
|
||||||
var splitIndexes []int
|
|
||||||
for i, split := range splits {
|
|
||||||
splitAt := rand.Intn(len(split))
|
|
||||||
splitIndexes = append(splitIndexes, currentIndex+splitAt)
|
|
||||||
currentIndex += len(split)
|
|
||||||
if i != len(splits)-1 {
|
|
||||||
currentIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 0; i <= len(splitIndexes); i++ {
|
|
||||||
var payload []byte
|
|
||||||
if i == 0 {
|
|
||||||
payload = b[:splitIndexes[i]]
|
|
||||||
} else if i == len(splitIndexes) {
|
|
||||||
payload = b[splitIndexes[i-1]:]
|
|
||||||
} else {
|
|
||||||
payload = b[splitIndexes[i-1]:splitIndexes[i]]
|
|
||||||
}
|
|
||||||
if c.tcpConn != nil && i != len(splitIndexes) {
|
|
||||||
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_, err = c.Conn.Write(payload)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.tcpConn != nil {
|
|
||||||
err = c.tcpConn.SetNoDelay(false)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c.Conn.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) ReaderReplaceable() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) WriterReplaceable() bool {
|
|
||||||
return c.firstPacketWritten
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) Upstream() any {
|
|
||||||
return c.Conn
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package tf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
recordLayerHeaderLen int = 5
|
|
||||||
handshakeHeaderLen int = 6
|
|
||||||
randomDataLen int = 32
|
|
||||||
sessionIDHeaderLen int = 1
|
|
||||||
cipherSuiteHeaderLen int = 2
|
|
||||||
compressMethodHeaderLen int = 1
|
|
||||||
extensionsHeaderLen int = 2
|
|
||||||
extensionHeaderLen int = 4
|
|
||||||
sniExtensionHeaderLen int = 5
|
|
||||||
contentType uint8 = 22
|
|
||||||
handshakeType uint8 = 1
|
|
||||||
sniExtensionType uint16 = 0
|
|
||||||
sniNameDNSHostnameType uint8 = 0
|
|
||||||
tlsVersionBitmask uint16 = 0xFFFC
|
|
||||||
tls13 uint16 = 0x0304
|
|
||||||
)
|
|
||||||
|
|
||||||
type myServerName struct {
|
|
||||||
Index int
|
|
||||||
Length int
|
|
||||||
ServerName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func indexTLSServerName(payload []byte) *myServerName {
|
|
||||||
if len(payload) < recordLayerHeaderLen || payload[0] != contentType {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
segmentLen := binary.BigEndian.Uint16(payload[3:5])
|
|
||||||
if len(payload) < recordLayerHeaderLen+int(segmentLen) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
serverName := indexTLSServerNameFromHandshake(payload[recordLayerHeaderLen : recordLayerHeaderLen+int(segmentLen)])
|
|
||||||
if serverName == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
serverName.Length += recordLayerHeaderLen
|
|
||||||
return serverName
|
|
||||||
}
|
|
||||||
|
|
||||||
func indexTLSServerNameFromHandshake(hs []byte) *myServerName {
|
|
||||||
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if hs[0] != handshakeType {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
handshakeLen := uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])
|
|
||||||
if len(hs[4:]) != int(handshakeLen) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tlsVersion := uint16(hs[4])<<8 | uint16(hs[5])
|
|
||||||
if tlsVersion&tlsVersionBitmask != 0x0300 && tlsVersion != tls13 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sessionIDLen := hs[38]
|
|
||||||
if len(hs) < handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cs := hs[handshakeHeaderLen+randomDataLen+sessionIDHeaderLen+int(sessionIDLen):]
|
|
||||||
if len(cs) < cipherSuiteHeaderLen {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
csLen := uint16(cs[0])<<8 | uint16(cs[1])
|
|
||||||
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
compressMethodLen := uint16(cs[cipherSuiteHeaderLen+int(csLen)])
|
|
||||||
if len(cs) < cipherSuiteHeaderLen+int(csLen)+compressMethodHeaderLen+int(compressMethodLen) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
currentIndex := cipherSuiteHeaderLen + int(csLen) + compressMethodHeaderLen + int(compressMethodLen)
|
|
||||||
serverName := indexTLSServerNameFromExtensions(cs[currentIndex:])
|
|
||||||
if serverName == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
serverName.Index += currentIndex
|
|
||||||
return serverName
|
|
||||||
}
|
|
||||||
|
|
||||||
func indexTLSServerNameFromExtensions(exs []byte) *myServerName {
|
|
||||||
if len(exs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(exs) < extensionsHeaderLen {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
exsLen := uint16(exs[0])<<8 | uint16(exs[1])
|
|
||||||
exs = exs[extensionsHeaderLen:]
|
|
||||||
if len(exs) < int(exsLen) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for currentIndex := extensionsHeaderLen; len(exs) > 0; {
|
|
||||||
if len(exs) < extensionHeaderLen {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
exType := uint16(exs[0])<<8 | uint16(exs[1])
|
|
||||||
exLen := uint16(exs[2])<<8 | uint16(exs[3])
|
|
||||||
if len(exs) < extensionHeaderLen+int(exLen) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sex := exs[extensionHeaderLen : extensionHeaderLen+int(exLen)]
|
|
||||||
|
|
||||||
switch exType {
|
|
||||||
case sniExtensionType:
|
|
||||||
if len(sex) < sniExtensionHeaderLen {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sniType := sex[2]
|
|
||||||
if sniType != sniNameDNSHostnameType {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sniLen := uint16(sex[3])<<8 | uint16(sex[4])
|
|
||||||
sex = sex[sniExtensionHeaderLen:]
|
|
||||||
return &myServerName{
|
|
||||||
Index: currentIndex + extensionHeaderLen + sniExtensionHeaderLen,
|
|
||||||
Length: int(sniLen),
|
|
||||||
ServerName: string(sex),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exs = exs[4+exLen:]
|
|
||||||
currentIndex += 4 + int(exLen)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package tf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/control"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
const tcpMaxNotifyAck = 10
|
|
||||||
|
|
||||||
type tcpNotifyAckID uint32
|
|
||||||
|
|
||||||
type tcpNotifyAckComplete struct {
|
|
||||||
NotifyPending uint32
|
|
||||||
NotifyCompleteCount uint32
|
|
||||||
NotifyCompleteID [tcpMaxNotifyAck]tcpNotifyAckID
|
|
||||||
}
|
|
||||||
|
|
||||||
var sizeOfTCPNotifyAckComplete = int(unsafe.Sizeof(tcpNotifyAckComplete{}))
|
|
||||||
|
|
||||||
func getsockoptTCPNotifyAckComplete(fd, level, opt int) (*tcpNotifyAckComplete, error) {
|
|
||||||
var value tcpNotifyAckComplete
|
|
||||||
vallen := uint32(sizeOfTCPNotifyAckComplete)
|
|
||||||
err := getsockopt(fd, level, opt, unsafe.Pointer(&value), &vallen)
|
|
||||||
return &value, err
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname getsockopt golang.org/x/sys/unix.getsockopt
|
|
||||||
func getsockopt(s int, level int, name int, val unsafe.Pointer, vallen *uint32) error
|
|
||||||
|
|
||||||
func waitAck(ctx context.Context, conn *net.TCPConn, _ time.Duration) error {
|
|
||||||
const TCP_NOTIFY_ACKNOWLEDGEMENT = 0x212
|
|
||||||
return control.Conn(conn, func(fd uintptr) error {
|
|
||||||
err := unix.SetsockoptInt(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT, 1)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, unix.EINVAL) {
|
|
||||||
return waitAckFallback(ctx, conn, 0)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
var ackComplete *tcpNotifyAckComplete
|
|
||||||
ackComplete, err = getsockoptTCPNotifyAckComplete(int(fd), unix.IPPROTO_TCP, TCP_NOTIFY_ACKNOWLEDGEMENT)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if ackComplete.NotifyPending == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
|
||||||
_, err := conn.Write(payload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return control.Conn(conn, func(fd uintptr) error {
|
|
||||||
start := time.Now()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
unacked, err := unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_NWRITE)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if unacked == 0 {
|
|
||||||
if time.Since(start) <= 20*time.Millisecond {
|
|
||||||
// under transparent proxy
|
|
||||||
time.Sleep(fallbackDelay)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package tf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/control"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
|
||||||
_, err := conn.Write(payload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return control.Conn(conn, func(fd uintptr) error {
|
|
||||||
start := time.Now()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
tcpInfo, err := unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if tcpInfo.Unacked == 0 {
|
|
||||||
if time.Since(start) <= 20*time.Millisecond {
|
|
||||||
// under transparent proxy
|
|
||||||
time.Sleep(fallbackDelay)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
//go:build !(linux || darwin || windows)
|
|
||||||
|
|
||||||
package tf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
|
||||||
time.Sleep(fallbackDelay)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package tf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/winiphlpapi"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
func writeAndWaitAck(ctx context.Context, conn *net.TCPConn, payload []byte, fallbackDelay time.Duration) error {
|
|
||||||
start := time.Now()
|
|
||||||
err := winiphlpapi.WriteAndWaitAck(ctx, conn, payload)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
|
||||||
time.Sleep(fallbackDelay)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if time.Since(start) <= 20*time.Millisecond {
|
|
||||||
time.Sleep(fallbackDelay)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -2,32 +2,32 @@ package urltest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.URLTestHistoryStorage = (*HistoryStorage)(nil)
|
type History struct {
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Delay uint16 `json:"delay"`
|
||||||
|
}
|
||||||
|
|
||||||
type HistoryStorage struct {
|
type HistoryStorage struct {
|
||||||
access sync.RWMutex
|
access sync.RWMutex
|
||||||
delayHistory map[string]*adapter.URLTestHistory
|
delayHistory map[string]*History
|
||||||
updateHook chan<- struct{}
|
updateHook chan<- struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHistoryStorage() *HistoryStorage {
|
func NewHistoryStorage() *HistoryStorage {
|
||||||
return &HistoryStorage{
|
return &HistoryStorage{
|
||||||
delayHistory: make(map[string]*adapter.URLTestHistory),
|
delayHistory: make(map[string]*History),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ func (s *HistoryStorage) SetHook(hook chan<- struct{}) {
|
|||||||
s.updateHook = hook
|
s.updateHook = hook
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory {
|
func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
|||||||
s.notifyUpdated()
|
s.notifyUpdated()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
|
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
s.delayHistory[tag] = history
|
s.delayHistory[tag] = history
|
||||||
s.access.Unlock()
|
s.access.Unlock()
|
||||||
@@ -110,10 +110,6 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
|
|||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return instance, nil
|
return instance, nil
|
||||||
},
|
},
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
Time: ntp.TimeFuncFromContext(ctx),
|
|
||||||
RootCAs: adapter.RootPoolFromContext(ctx),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package constant
|
|
||||||
|
|
||||||
const (
|
|
||||||
CertificateStoreSystem = "system"
|
|
||||||
CertificateStoreMozilla = "mozilla"
|
|
||||||
CertificateStoreNone = "none"
|
|
||||||
)
|
|
||||||
8
constant/cgo_android_fix.go
Normal file
8
constant/cgo_android_fix.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build android && debug
|
||||||
|
|
||||||
|
package constant
|
||||||
|
|
||||||
|
// TODO: remove after fixed
|
||||||
|
// https://github.com/golang/go/issues/68760
|
||||||
|
|
||||||
|
const FixAndroidStack = true
|
||||||
5
constant/cgo_android_fix_stub.go
Normal file
5
constant/cgo_android_fix_stub.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build !(android && debug)
|
||||||
|
|
||||||
|
package constant
|
||||||
|
|
||||||
|
const FixAndroidStack = false
|
||||||
@@ -1,34 +1,5 @@
|
|||||||
package constant
|
package constant
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultDNSTTL = 600
|
|
||||||
)
|
|
||||||
|
|
||||||
type DomainStrategy = uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
DomainStrategyAsIS DomainStrategy = iota
|
|
||||||
DomainStrategyPreferIPv4
|
|
||||||
DomainStrategyPreferIPv6
|
|
||||||
DomainStrategyIPv4Only
|
|
||||||
DomainStrategyIPv6Only
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DNSTypeLegacy = "legacy"
|
|
||||||
DNSTypeUDP = "udp"
|
|
||||||
DNSTypeTCP = "tcp"
|
|
||||||
DNSTypeTLS = "tls"
|
|
||||||
DNSTypeHTTPS = "https"
|
|
||||||
DNSTypeQUIC = "quic"
|
|
||||||
DNSTypeHTTP3 = "h3"
|
|
||||||
DNSTypeHosts = "hosts"
|
|
||||||
DNSTypeLocal = "local"
|
|
||||||
DNSTypePreDefined = "predefined"
|
|
||||||
DNSTypeFakeIP = "fakeip"
|
|
||||||
DNSTypeDHCP = "dhcp"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DNSProviderAliDNS = "alidns"
|
DNSProviderAliDNS = "alidns"
|
||||||
DNSProviderCloudflare = "cloudflare"
|
DNSProviderCloudflare = "cloudflare"
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package constant
|
|
||||||
|
|
||||||
const (
|
|
||||||
ScriptTypeSurge = "surge"
|
|
||||||
ScriptSourceTypeLocal = "local"
|
|
||||||
ScriptSourceTypeRemote = "remote"
|
|
||||||
)
|
|
||||||
@@ -16,7 +16,6 @@ const (
|
|||||||
StopTimeout = 5 * time.Second
|
StopTimeout = 5 * time.Second
|
||||||
FatalStopTimeout = 10 * time.Second
|
FatalStopTimeout = 10 * time.Second
|
||||||
FakeIPMetadataSaveInterval = 10 * time.Second
|
FakeIPMetadataSaveInterval = 10 * time.Second
|
||||||
TLSFragmentFallbackDelay = 500 * time.Millisecond
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var PortProtocols = map[uint16]string{
|
var PortProtocols = map[uint16]string{
|
||||||
|
|||||||
563
dns/client.go
563
dns/client.go
@@ -1,563 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"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"
|
|
||||||
"github.com/sagernet/sing/common/task"
|
|
||||||
"github.com/sagernet/sing/contrab/freelru"
|
|
||||||
"github.com/sagernet/sing/contrab/maphash"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNoRawSupport = E.New("no raw query support by current transport")
|
|
||||||
ErrNotCached = E.New("not cached")
|
|
||||||
ErrResponseRejected = E.New("response rejected")
|
|
||||||
ErrResponseRejectedCached = E.Extend(ErrResponseRejected, "cached")
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.DNSClient = (*Client)(nil)
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
timeout time.Duration
|
|
||||||
disableCache bool
|
|
||||||
disableExpire bool
|
|
||||||
independentCache bool
|
|
||||||
rdrc adapter.RDRCStore
|
|
||||||
initRDRCFunc func() adapter.RDRCStore
|
|
||||||
logger logger.ContextLogger
|
|
||||||
cache freelru.Cache[dns.Question, *dns.Msg]
|
|
||||||
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientOptions struct {
|
|
||||||
Timeout time.Duration
|
|
||||||
DisableCache bool
|
|
||||||
DisableExpire bool
|
|
||||||
IndependentCache bool
|
|
||||||
CacheCapacity uint32
|
|
||||||
RDRC func() adapter.RDRCStore
|
|
||||||
Logger logger.ContextLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(options ClientOptions) *Client {
|
|
||||||
client := &Client{
|
|
||||||
timeout: options.Timeout,
|
|
||||||
disableCache: options.DisableCache,
|
|
||||||
disableExpire: options.DisableExpire,
|
|
||||||
independentCache: options.IndependentCache,
|
|
||||||
initRDRCFunc: options.RDRC,
|
|
||||||
logger: options.Logger,
|
|
||||||
}
|
|
||||||
if client.timeout == 0 {
|
|
||||||
client.timeout = C.DNSTimeout
|
|
||||||
}
|
|
||||||
cacheCapacity := options.CacheCapacity
|
|
||||||
if cacheCapacity < 1024 {
|
|
||||||
cacheCapacity = 1024
|
|
||||||
}
|
|
||||||
if !client.disableCache {
|
|
||||||
if !client.independentCache {
|
|
||||||
client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32))
|
|
||||||
} else {
|
|
||||||
client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
type transportCacheKey struct {
|
|
||||||
dns.Question
|
|
||||||
transportTag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Start() {
|
|
||||||
if c.initRDRCFunc != nil {
|
|
||||||
c.rdrc = c.initRDRCFunc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
|
|
||||||
if len(message.Question) == 0 {
|
|
||||||
if c.logger != nil {
|
|
||||||
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
|
||||||
}
|
|
||||||
responseMessage := dns.Msg{
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Response: true,
|
|
||||||
Rcode: dns.RcodeFormatError,
|
|
||||||
},
|
|
||||||
Question: message.Question,
|
|
||||||
}
|
|
||||||
return &responseMessage, nil
|
|
||||||
}
|
|
||||||
question := message.Question[0]
|
|
||||||
if options.ClientSubnet.IsValid() {
|
|
||||||
message = SetClientSubnet(message, options.ClientSubnet, true)
|
|
||||||
}
|
|
||||||
isSimpleRequest := len(message.Question) == 1 &&
|
|
||||||
len(message.Ns) == 0 &&
|
|
||||||
len(message.Extra) == 0 &&
|
|
||||||
!options.ClientSubnet.IsValid()
|
|
||||||
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
|
||||||
if !disableCache {
|
|
||||||
response, ttl := c.loadResponse(question, transport)
|
|
||||||
if response != nil {
|
|
||||||
logCachedResponse(c.logger, ctx, response, ttl)
|
|
||||||
response.Id = message.Id
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if question.Qtype == dns.TypeA && options.Strategy == C.DomainStrategyIPv6Only || question.Qtype == dns.TypeAAAA && options.Strategy == C.DomainStrategyIPv4Only {
|
|
||||||
responseMessage := dns.Msg{
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Response: true,
|
|
||||||
Rcode: dns.RcodeSuccess,
|
|
||||||
},
|
|
||||||
Question: []dns.Question{question},
|
|
||||||
}
|
|
||||||
if c.logger != nil {
|
|
||||||
c.logger.DebugContext(ctx, "strategy rejected")
|
|
||||||
}
|
|
||||||
return &responseMessage, nil
|
|
||||||
}
|
|
||||||
messageId := message.Id
|
|
||||||
contextTransport, clientSubnetLoaded := transportTagFromContext(ctx)
|
|
||||||
if clientSubnetLoaded && transport.Tag() == contextTransport {
|
|
||||||
return nil, E.New("DNS query loopback in transport[", contextTransport, "]")
|
|
||||||
}
|
|
||||||
ctx = contextWithTransportTag(ctx, transport.Tag())
|
|
||||||
if responseChecker != nil && c.rdrc != nil {
|
|
||||||
rejected := c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype)
|
|
||||||
if rejected {
|
|
||||||
return nil, ErrResponseRejectedCached
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
|
||||||
response, err := transport.Exchange(ctx, message)
|
|
||||||
cancel()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
|
|
||||||
validResponse := response
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
var (
|
|
||||||
addresses int
|
|
||||||
queryCNAME string
|
|
||||||
)
|
|
||||||
for _, rawRR := range validResponse.Answer {
|
|
||||||
switch rr := rawRR.(type) {
|
|
||||||
case *dns.A:
|
|
||||||
break loop
|
|
||||||
case *dns.AAAA:
|
|
||||||
break loop
|
|
||||||
case *dns.CNAME:
|
|
||||||
queryCNAME = rr.Target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if queryCNAME == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
exMessage := *message
|
|
||||||
exMessage.Question = []dns.Question{{
|
|
||||||
Name: queryCNAME,
|
|
||||||
Qtype: question.Qtype,
|
|
||||||
}}
|
|
||||||
validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if validResponse != response {
|
|
||||||
response.Answer = append(response.Answer, validResponse.Answer...)
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
if responseChecker != nil {
|
|
||||||
addr, addrErr := MessageToAddresses(response)
|
|
||||||
if addrErr != nil || !responseChecker(addr) {
|
|
||||||
if c.rdrc != nil {
|
|
||||||
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
|
|
||||||
}
|
|
||||||
logRejectedResponse(c.logger, ctx, response)
|
|
||||||
return response, ErrResponseRejected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if question.Qtype == dns.TypeHTTPS {
|
|
||||||
if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only {
|
|
||||||
for _, rr := range response.Answer {
|
|
||||||
https, isHTTPS := rr.(*dns.HTTPS)
|
|
||||||
if !isHTTPS {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
content := https.SVCB
|
|
||||||
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
|
|
||||||
if options.Strategy == C.DomainStrategyIPv4Only {
|
|
||||||
return it.Key() != dns.SVCB_IPV6HINT
|
|
||||||
} else {
|
|
||||||
return it.Key() != dns.SVCB_IPV4HINT
|
|
||||||
}
|
|
||||||
})
|
|
||||||
https.SVCB = content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var timeToLive uint32
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
|
||||||
timeToLive = record.Header().Ttl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options.RewriteTTL != nil {
|
|
||||||
timeToLive = *options.RewriteTTL
|
|
||||||
}
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
record.Header().Ttl = timeToLive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response.Id = messageId
|
|
||||||
if !disableCache {
|
|
||||||
c.storeCache(transport, question, response, timeToLive)
|
|
||||||
}
|
|
||||||
logExchangedResponse(c.logger, ctx, response, timeToLive)
|
|
||||||
return response, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
|
||||||
domain = FqdnToDomain(domain)
|
|
||||||
dnsName := dns.Fqdn(domain)
|
|
||||||
if options.Strategy == C.DomainStrategyIPv4Only {
|
|
||||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
|
|
||||||
} else if options.Strategy == C.DomainStrategyIPv6Only {
|
|
||||||
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
|
|
||||||
}
|
|
||||||
var response4 []netip.Addr
|
|
||||||
var response6 []netip.Addr
|
|
||||||
var group task.Group
|
|
||||||
group.Append("exchange4", func(ctx context.Context) error {
|
|
||||||
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
response4 = response
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
group.Append("exchange6", func(ctx context.Context) error {
|
|
||||||
response, err := c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
response6 = response
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
err := group.Run(ctx)
|
|
||||||
if len(response4) == 0 && len(response6) == 0 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return sortAddresses(response4, response6, options.Strategy), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) ClearCache() {
|
|
||||||
if c.cache != nil {
|
|
||||||
c.cache.Purge()
|
|
||||||
}
|
|
||||||
if c.transportCache != nil {
|
|
||||||
c.transportCache.Purge()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool) {
|
|
||||||
if c.disableCache || c.independentCache {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if dns.IsFqdn(domain) {
|
|
||||||
domain = domain[:len(domain)-1]
|
|
||||||
}
|
|
||||||
dnsName := dns.Fqdn(domain)
|
|
||||||
if strategy == C.DomainStrategyIPv4Only {
|
|
||||||
response, err := c.questionCache(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
if err != ErrNotCached {
|
|
||||||
return response, true
|
|
||||||
}
|
|
||||||
} else if strategy == C.DomainStrategyIPv6Only {
|
|
||||||
response, err := c.questionCache(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeAAAA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
if err != ErrNotCached {
|
|
||||||
return response, true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
response4, _ := c.questionCache(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
response6, _ := c.questionCache(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeAAAA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
if len(response4) > 0 || len(response6) > 0 {
|
|
||||||
return sortAddresses(response4, response6, strategy), true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool) {
|
|
||||||
if c.disableCache || c.independentCache || len(message.Question) != 1 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
question := message.Question[0]
|
|
||||||
response, ttl := c.loadResponse(question, nil)
|
|
||||||
if response == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
logCachedResponse(c.logger, ctx, response, ttl)
|
|
||||||
response.Id = message.Id
|
|
||||||
return response, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
|
|
||||||
if strategy == C.DomainStrategyPreferIPv6 {
|
|
||||||
return append(response6, response4...)
|
|
||||||
} else {
|
|
||||||
return append(response4, response6...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, timeToLive uint32) {
|
|
||||||
if timeToLive == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.disableExpire {
|
|
||||||
if !c.independentCache {
|
|
||||||
c.cache.Add(question, message)
|
|
||||||
} else {
|
|
||||||
c.transportCache.Add(transportCacheKey{
|
|
||||||
Question: question,
|
|
||||||
transportTag: transport.Tag(),
|
|
||||||
}, message)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !c.independentCache {
|
|
||||||
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
|
|
||||||
} else {
|
|
||||||
c.transportCache.AddWithLifetime(transportCacheKey{
|
|
||||||
Question: question,
|
|
||||||
transportTag: transport.Tag(),
|
|
||||||
}, message, time.Second*time.Duration(timeToLive))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
|
||||||
question := dns.Question{
|
|
||||||
Name: name,
|
|
||||||
Qtype: qType,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}
|
|
||||||
disableCache := c.disableCache || options.DisableCache
|
|
||||||
if !disableCache {
|
|
||||||
cachedAddresses, err := c.questionCache(question, transport)
|
|
||||||
if err != ErrNotCached {
|
|
||||||
return cachedAddresses, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
message := dns.Msg{
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
RecursionDesired: true,
|
|
||||||
},
|
|
||||||
Question: []dns.Question{question},
|
|
||||||
}
|
|
||||||
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return MessageToAddresses(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
|
|
||||||
response, _ := c.loadResponse(question, transport)
|
|
||||||
if response == nil {
|
|
||||||
return nil, ErrNotCached
|
|
||||||
}
|
|
||||||
return MessageToAddresses(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) {
|
|
||||||
var (
|
|
||||||
response *dns.Msg
|
|
||||||
loaded bool
|
|
||||||
)
|
|
||||||
if c.disableExpire {
|
|
||||||
if !c.independentCache {
|
|
||||||
response, loaded = c.cache.Get(question)
|
|
||||||
} else {
|
|
||||||
response, loaded = c.transportCache.Get(transportCacheKey{
|
|
||||||
Question: question,
|
|
||||||
transportTag: transport.Tag(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if !loaded {
|
|
||||||
return nil, 0
|
|
||||||
}
|
|
||||||
return response.Copy(), 0
|
|
||||||
} else {
|
|
||||||
var expireAt time.Time
|
|
||||||
if !c.independentCache {
|
|
||||||
response, expireAt, loaded = c.cache.GetWithLifetime(question)
|
|
||||||
} else {
|
|
||||||
response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{
|
|
||||||
Question: question,
|
|
||||||
transportTag: transport.Tag(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if !loaded {
|
|
||||||
return nil, 0
|
|
||||||
}
|
|
||||||
timeNow := time.Now()
|
|
||||||
if timeNow.After(expireAt) {
|
|
||||||
if !c.independentCache {
|
|
||||||
c.cache.Remove(question)
|
|
||||||
} else {
|
|
||||||
c.transportCache.Remove(transportCacheKey{
|
|
||||||
Question: question,
|
|
||||||
transportTag: transport.Tag(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil, 0
|
|
||||||
}
|
|
||||||
var originTTL int
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
|
|
||||||
originTTL = int(record.Header().Ttl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
|
||||||
if nowTTL < 0 {
|
|
||||||
nowTTL = 0
|
|
||||||
}
|
|
||||||
response = response.Copy()
|
|
||||||
if originTTL > 0 {
|
|
||||||
duration := uint32(originTTL - nowTTL)
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
record.Header().Ttl = record.Header().Ttl - duration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
record.Header().Ttl = uint32(nowTTL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response, nowTTL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
|
|
||||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
|
||||||
return nil, RCodeError(response.Rcode)
|
|
||||||
}
|
|
||||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
|
||||||
for _, rawAnswer := range response.Answer {
|
|
||||||
switch answer := rawAnswer.(type) {
|
|
||||||
case *dns.A:
|
|
||||||
addresses = append(addresses, M.AddrFromIP(answer.A))
|
|
||||||
case *dns.AAAA:
|
|
||||||
addresses = append(addresses, M.AddrFromIP(answer.AAAA))
|
|
||||||
case *dns.HTTPS:
|
|
||||||
for _, value := range answer.SVCB.Value {
|
|
||||||
if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT {
|
|
||||||
addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return addresses, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapError(err error) error {
|
|
||||||
switch dnsErr := err.(type) {
|
|
||||||
case *net.DNSError:
|
|
||||||
if dnsErr.IsNotFound {
|
|
||||||
return RCodeNameError
|
|
||||||
}
|
|
||||||
case *net.AddrError:
|
|
||||||
return RCodeNameError
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type transportKey struct{}
|
|
||||||
|
|
||||||
func contextWithTransportTag(ctx context.Context, transportTag string) context.Context {
|
|
||||||
return context.WithValue(ctx, transportKey{}, transportTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func transportTagFromContext(ctx context.Context) (string, bool) {
|
|
||||||
value, loaded := ctx.Value(transportKey{}).(string)
|
|
||||||
return value, loaded
|
|
||||||
}
|
|
||||||
|
|
||||||
func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg {
|
|
||||||
response := dns.Msg{
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Id: id,
|
|
||||||
Rcode: dns.RcodeSuccess,
|
|
||||||
Response: true,
|
|
||||||
},
|
|
||||||
Question: []dns.Question{question},
|
|
||||||
}
|
|
||||||
for _, address := range addresses {
|
|
||||||
if address.Is4() {
|
|
||||||
response.Answer = append(response.Answer, &dns.A{
|
|
||||||
Hdr: dns.RR_Header{
|
|
||||||
Name: question.Name,
|
|
||||||
Rrtype: dns.TypeA,
|
|
||||||
Class: dns.ClassINET,
|
|
||||||
Ttl: timeToLive,
|
|
||||||
},
|
|
||||||
A: address.AsSlice(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
response.Answer = append(response.Answer, &dns.AAAA{
|
|
||||||
Hdr: dns.RR_Header{
|
|
||||||
Name: question.Name,
|
|
||||||
Rrtype: dns.TypeAAAA,
|
|
||||||
Class: dns.ClassINET,
|
|
||||||
Ttl: timeToLive,
|
|
||||||
},
|
|
||||||
AAAA: address.AsSlice(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &response
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
func logCachedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl int) {
|
|
||||||
if logger == nil || len(response.Question) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
domain := FqdnToDomain(response.Question[0].Name)
|
|
||||||
logger.DebugContext(ctx, "cached ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl)
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
logger.InfoContext(ctx, "cached ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
|
|
||||||
if logger == nil || len(response.Question) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
domain := FqdnToDomain(response.Question[0].Name)
|
|
||||||
logger.DebugContext(ctx, "exchanged ", domain, " ", dns.RcodeToString[response.Rcode], " ", ttl)
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
logger.InfoContext(ctx, "exchanged ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func logRejectedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) {
|
|
||||||
if logger == nil || len(response.Question) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
logger.InfoContext(ctx, "rejected ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func FqdnToDomain(fqdn string) string {
|
|
||||||
if dns.IsFqdn(fqdn) {
|
|
||||||
return fqdn[:len(fqdn)-1]
|
|
||||||
}
|
|
||||||
return fqdn
|
|
||||||
}
|
|
||||||
|
|
||||||
func FormatQuestion(string string) string {
|
|
||||||
for strings.HasPrefix(string, ";") {
|
|
||||||
string = string[1:]
|
|
||||||
}
|
|
||||||
string = strings.ReplaceAll(string, "\t", " ")
|
|
||||||
string = strings.ReplaceAll(string, "\n", " ")
|
|
||||||
string = strings.ReplaceAll(string, ";; ", " ")
|
|
||||||
string = strings.ReplaceAll(string, "; ", " ")
|
|
||||||
|
|
||||||
for strings.Contains(string, " ") {
|
|
||||||
string = strings.ReplaceAll(string, " ", " ")
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string)
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf.Buffer, error) {
|
|
||||||
maxLen := 512
|
|
||||||
if edns0Option := request.IsEdns0(); edns0Option != nil {
|
|
||||||
if udpSize := int(edns0Option.UDPSize()); udpSize > 512 {
|
|
||||||
maxLen = udpSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
responseLen := response.Len()
|
|
||||||
if responseLen > maxLen {
|
|
||||||
response.Truncate(maxLen)
|
|
||||||
}
|
|
||||||
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
|
||||||
buffer.Resize(headroom, 0)
|
|
||||||
rawMessage, err := response.PackBuffer(buffer.FreeBytes())
|
|
||||||
if err != nil {
|
|
||||||
buffer.Release()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.Truncate(len(rawMessage))
|
|
||||||
return buffer, nil
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SetClientSubnet(message *dns.Msg, clientSubnet netip.Prefix, override bool) *dns.Msg {
|
|
||||||
var (
|
|
||||||
optRecord *dns.OPT
|
|
||||||
subnetOption *dns.EDNS0_SUBNET
|
|
||||||
)
|
|
||||||
findExists:
|
|
||||||
for _, record := range message.Extra {
|
|
||||||
var isOPTRecord bool
|
|
||||||
if optRecord, isOPTRecord = record.(*dns.OPT); isOPTRecord {
|
|
||||||
for _, option := range optRecord.Option {
|
|
||||||
var isEDNS0Subnet bool
|
|
||||||
subnetOption, isEDNS0Subnet = option.(*dns.EDNS0_SUBNET)
|
|
||||||
if isEDNS0Subnet {
|
|
||||||
if !override {
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
break findExists
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if optRecord == nil {
|
|
||||||
exMessage := *message
|
|
||||||
message = &exMessage
|
|
||||||
optRecord = &dns.OPT{
|
|
||||||
Hdr: dns.RR_Header{
|
|
||||||
Name: ".",
|
|
||||||
Rrtype: dns.TypeOPT,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
message.Extra = append(message.Extra, optRecord)
|
|
||||||
} else {
|
|
||||||
message = message.Copy()
|
|
||||||
}
|
|
||||||
if subnetOption == nil {
|
|
||||||
subnetOption = new(dns.EDNS0_SUBNET)
|
|
||||||
optRecord.Option = append(optRecord.Option, subnetOption)
|
|
||||||
}
|
|
||||||
subnetOption.Code = dns.EDNS0SUBNET
|
|
||||||
if clientSubnet.Addr().Is4() {
|
|
||||||
subnetOption.Family = 1
|
|
||||||
} else {
|
|
||||||
subnetOption.Family = 2
|
|
||||||
}
|
|
||||||
subnetOption.SourceNetmask = uint8(clientSubnet.Bits())
|
|
||||||
subnetOption.Address = clientSubnet.Addr().AsSlice()
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
33
dns/rcode.go
33
dns/rcode.go
@@ -1,33 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import F "github.com/sagernet/sing/common/format"
|
|
||||||
|
|
||||||
const (
|
|
||||||
RCodeSuccess RCodeError = 0 // NoError
|
|
||||||
RCodeFormatError RCodeError = 1 // FormErr
|
|
||||||
RCodeServerFailure RCodeError = 2 // ServFail
|
|
||||||
RCodeNameError RCodeError = 3 // NXDomain
|
|
||||||
RCodeNotImplemented RCodeError = 4 // NotImp
|
|
||||||
RCodeRefused RCodeError = 5 // Refused
|
|
||||||
)
|
|
||||||
|
|
||||||
type RCodeError uint16
|
|
||||||
|
|
||||||
func (e RCodeError) Error() string {
|
|
||||||
switch e {
|
|
||||||
case RCodeSuccess:
|
|
||||||
return "success"
|
|
||||||
case RCodeFormatError:
|
|
||||||
return "format error"
|
|
||||||
case RCodeServerFailure:
|
|
||||||
return "server failure"
|
|
||||||
case RCodeNameError:
|
|
||||||
return "name error"
|
|
||||||
case RCodeNotImplemented:
|
|
||||||
return "not implemented"
|
|
||||||
case RCodeRefused:
|
|
||||||
return "refused"
|
|
||||||
default:
|
|
||||||
return F.ToString("unknown error: ", uint16(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
437
dns/router.go
437
dns/router.go
@@ -1,437 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net/netip"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
R "github.com/sagernet/sing-box/route/rule"
|
|
||||||
"github.com/sagernet/sing-tun"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
"github.com/sagernet/sing/contrab/freelru"
|
|
||||||
"github.com/sagernet/sing/contrab/maphash"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.DNSRouter = (*Router)(nil)
|
|
||||||
|
|
||||||
type Router struct {
|
|
||||||
ctx context.Context
|
|
||||||
logger logger.ContextLogger
|
|
||||||
transport adapter.DNSTransportManager
|
|
||||||
outbound adapter.OutboundManager
|
|
||||||
client adapter.DNSClient
|
|
||||||
rules []adapter.DNSRule
|
|
||||||
defaultDomainStrategy C.DomainStrategy
|
|
||||||
dnsReverseMapping freelru.Cache[netip.Addr, string]
|
|
||||||
platformInterface platform.Interface
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
|
|
||||||
router := &Router{
|
|
||||||
ctx: ctx,
|
|
||||||
logger: logFactory.NewLogger("dns"),
|
|
||||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
|
||||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
|
||||||
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
|
|
||||||
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
|
|
||||||
}
|
|
||||||
router.client = NewClient(ClientOptions{
|
|
||||||
DisableCache: options.DNSClientOptions.DisableCache,
|
|
||||||
DisableExpire: options.DNSClientOptions.DisableExpire,
|
|
||||||
IndependentCache: options.DNSClientOptions.IndependentCache,
|
|
||||||
CacheCapacity: options.DNSClientOptions.CacheCapacity,
|
|
||||||
RDRC: func() adapter.RDRCStore {
|
|
||||||
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
|
||||||
if cacheFile == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !cacheFile.StoreRDRC() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return cacheFile
|
|
||||||
},
|
|
||||||
Logger: router.logger,
|
|
||||||
})
|
|
||||||
if options.ReverseMapping {
|
|
||||||
router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32))
|
|
||||||
}
|
|
||||||
return router
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) Initialize(rules []option.DNSRule) error {
|
|
||||||
for i, ruleOptions := range rules {
|
|
||||||
dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "parse dns rule[", i, "]")
|
|
||||||
}
|
|
||||||
r.rules = append(r.rules, dnsRule)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) Start(stage adapter.StartStage) error {
|
|
||||||
monitor := taskmonitor.New(r.logger, C.StartTimeout)
|
|
||||||
switch stage {
|
|
||||||
case adapter.StartStateStart:
|
|
||||||
monitor.Start("initialize DNS client")
|
|
||||||
r.client.Start()
|
|
||||||
monitor.Finish()
|
|
||||||
|
|
||||||
for i, rule := range r.rules {
|
|
||||||
monitor.Start("initialize DNS rule[", i, "]")
|
|
||||||
err := rule.Start()
|
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "initialize DNS rule[", i, "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) Close() error {
|
|
||||||
monitor := taskmonitor.New(r.logger, C.StopTimeout)
|
|
||||||
var err error
|
|
||||||
for i, rule := range r.rules {
|
|
||||||
monitor.Start("close dns rule[", i, "]")
|
|
||||||
err = E.Append(err, rule.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close dns rule[", i, "]")
|
|
||||||
})
|
|
||||||
monitor.Finish()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
|
|
||||||
metadata := adapter.ContextFrom(ctx)
|
|
||||||
if metadata == nil {
|
|
||||||
panic("no context")
|
|
||||||
}
|
|
||||||
var currentRuleIndex int
|
|
||||||
if ruleIndex != -1 {
|
|
||||||
currentRuleIndex = ruleIndex + 1
|
|
||||||
}
|
|
||||||
for ; currentRuleIndex < len(r.rules); currentRuleIndex++ {
|
|
||||||
currentRule := r.rules[currentRuleIndex]
|
|
||||||
if currentRule.WithAddressLimit() && !isAddressQuery {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
metadata.ResetRuleCache()
|
|
||||||
if currentRule.Match(metadata) {
|
|
||||||
displayRuleIndex := currentRuleIndex
|
|
||||||
if displayRuleIndex != -1 {
|
|
||||||
displayRuleIndex += displayRuleIndex + 1
|
|
||||||
}
|
|
||||||
ruleDescription := currentRule.String()
|
|
||||||
if ruleDescription != "" {
|
|
||||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action())
|
|
||||||
} else {
|
|
||||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
|
||||||
}
|
|
||||||
switch action := currentRule.Action().(type) {
|
|
||||||
case *R.RuleActionDNSRoute:
|
|
||||||
transport, loaded := r.transport.Transport(action.Server)
|
|
||||||
if !loaded {
|
|
||||||
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
isFakeIP := transport.Type() == C.DNSTypeFakeIP
|
|
||||||
if isFakeIP && !allowFakeIP {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if action.Strategy != C.DomainStrategyAsIS {
|
|
||||||
options.Strategy = action.Strategy
|
|
||||||
}
|
|
||||||
if isFakeIP || action.DisableCache {
|
|
||||||
options.DisableCache = true
|
|
||||||
}
|
|
||||||
if action.RewriteTTL != nil {
|
|
||||||
options.RewriteTTL = action.RewriteTTL
|
|
||||||
}
|
|
||||||
if action.ClientSubnet.IsValid() {
|
|
||||||
options.ClientSubnet = action.ClientSubnet
|
|
||||||
}
|
|
||||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
|
||||||
options.Strategy = legacyTransport.LegacyStrategy()
|
|
||||||
}
|
|
||||||
if !options.ClientSubnet.IsValid() {
|
|
||||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
|
||||||
return transport, currentRule, currentRuleIndex
|
|
||||||
case *R.RuleActionDNSRouteOptions:
|
|
||||||
if action.Strategy != C.DomainStrategyAsIS {
|
|
||||||
options.Strategy = action.Strategy
|
|
||||||
}
|
|
||||||
if action.DisableCache {
|
|
||||||
options.DisableCache = true
|
|
||||||
}
|
|
||||||
if action.RewriteTTL != nil {
|
|
||||||
options.RewriteTTL = action.RewriteTTL
|
|
||||||
}
|
|
||||||
if action.ClientSubnet.IsValid() {
|
|
||||||
options.ClientSubnet = action.ClientSubnet
|
|
||||||
}
|
|
||||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
|
||||||
case *R.RuleActionReject:
|
|
||||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
|
||||||
return nil, currentRule, currentRuleIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r.transport.Default(), nil, -1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
|
|
||||||
if len(message.Question) != 1 {
|
|
||||||
r.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
|
||||||
responseMessage := mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Response: true,
|
|
||||||
Rcode: mDNS.RcodeFormatError,
|
|
||||||
},
|
|
||||||
Question: message.Question,
|
|
||||||
}
|
|
||||||
return &responseMessage, nil
|
|
||||||
}
|
|
||||||
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
|
||||||
var (
|
|
||||||
transport adapter.DNSTransport
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
response, cached := r.client.ExchangeCache(ctx, message)
|
|
||||||
if !cached {
|
|
||||||
var metadata *adapter.InboundContext
|
|
||||||
ctx, metadata = adapter.ExtendContext(ctx)
|
|
||||||
metadata.Destination = M.Socksaddr{}
|
|
||||||
metadata.QueryType = message.Question[0].Qtype
|
|
||||||
switch metadata.QueryType {
|
|
||||||
case mDNS.TypeA:
|
|
||||||
metadata.IPVersion = 4
|
|
||||||
case mDNS.TypeAAAA:
|
|
||||||
metadata.IPVersion = 6
|
|
||||||
}
|
|
||||||
metadata.Domain = FqdnToDomain(message.Question[0].Name)
|
|
||||||
if options.Transport != nil {
|
|
||||||
transport = options.Transport
|
|
||||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
|
||||||
options.Strategy = legacyTransport.LegacyStrategy()
|
|
||||||
}
|
|
||||||
if !options.ClientSubnet.IsValid() {
|
|
||||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
|
||||||
options.Strategy = r.defaultDomainStrategy
|
|
||||||
}
|
|
||||||
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
|
||||||
} else {
|
|
||||||
var (
|
|
||||||
rule adapter.DNSRule
|
|
||||||
ruleIndex int
|
|
||||||
)
|
|
||||||
ruleIndex = -1
|
|
||||||
for {
|
|
||||||
dnsCtx := adapter.OverrideContext(ctx)
|
|
||||||
dnsOptions := options
|
|
||||||
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
|
||||||
if rule != nil {
|
|
||||||
switch action := rule.Action().(type) {
|
|
||||||
case *R.RuleActionReject:
|
|
||||||
switch action.Method {
|
|
||||||
case C.RuleActionRejectMethodDefault:
|
|
||||||
return FixedResponse(message.Id, message.Question[0], nil, 0), nil
|
|
||||||
case C.RuleActionRejectMethodDrop:
|
|
||||||
return nil, tun.ErrDrop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var responseCheck func(responseAddrs []netip.Addr) bool
|
|
||||||
if rule != nil && rule.WithAddressLimit() {
|
|
||||||
responseCheck = func(responseAddrs []netip.Addr) bool {
|
|
||||||
metadata.DestinationAddresses = responseAddrs
|
|
||||||
return rule.MatchAddressLimit(metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
|
||||||
dnsOptions.Strategy = r.defaultDomainStrategy
|
|
||||||
}
|
|
||||||
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
|
|
||||||
var rejected bool
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, ErrResponseRejectedCached) {
|
|
||||||
rejected = true
|
|
||||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
|
|
||||||
} else if errors.Is(err, ErrResponseRejected) {
|
|
||||||
rejected = true
|
|
||||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
|
||||||
} else if len(message.Question) > 0 {
|
|
||||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
|
||||||
} else {
|
|
||||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if responseCheck != nil && rejected {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 {
|
|
||||||
if transport.Type() != C.DNSTypeFakeIP {
|
|
||||||
for _, answer := range response.Answer {
|
|
||||||
switch record := answer.(type) {
|
|
||||||
case *mDNS.A:
|
|
||||||
r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.A), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second)
|
|
||||||
case *mDNS.AAAA:
|
|
||||||
r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.AAAA), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
|
||||||
var (
|
|
||||||
responseAddrs []netip.Addr
|
|
||||||
cached bool
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
printResult := func() {
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, ErrResponseRejectedCached) {
|
|
||||||
r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
|
|
||||||
} else if errors.Is(err, ErrResponseRejected) {
|
|
||||||
r.logger.DebugContext(ctx, "response rejected for ", domain)
|
|
||||||
} else {
|
|
||||||
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
|
|
||||||
}
|
|
||||||
} else if len(responseAddrs) == 0 {
|
|
||||||
r.logger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
|
|
||||||
err = RCodeNameError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
|
|
||||||
if cached {
|
|
||||||
if len(responseAddrs) == 0 {
|
|
||||||
return nil, RCodeNameError
|
|
||||||
}
|
|
||||||
return responseAddrs, nil
|
|
||||||
}
|
|
||||||
r.logger.DebugContext(ctx, "lookup domain ", domain)
|
|
||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
|
||||||
metadata.Destination = M.Socksaddr{}
|
|
||||||
metadata.Domain = domain
|
|
||||||
if options.Transport != nil {
|
|
||||||
transport := options.Transport
|
|
||||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
|
||||||
options.Strategy = r.defaultDomainStrategy
|
|
||||||
}
|
|
||||||
if !options.ClientSubnet.IsValid() {
|
|
||||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
|
||||||
options.Strategy = r.defaultDomainStrategy
|
|
||||||
}
|
|
||||||
responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil)
|
|
||||||
} else {
|
|
||||||
var (
|
|
||||||
transport adapter.DNSTransport
|
|
||||||
rule adapter.DNSRule
|
|
||||||
ruleIndex int
|
|
||||||
)
|
|
||||||
ruleIndex = -1
|
|
||||||
for {
|
|
||||||
dnsCtx := adapter.OverrideContext(ctx)
|
|
||||||
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &options)
|
|
||||||
if rule != nil {
|
|
||||||
switch action := rule.Action().(type) {
|
|
||||||
case *R.RuleActionReject:
|
|
||||||
switch action.Method {
|
|
||||||
case C.RuleActionRejectMethodDefault:
|
|
||||||
return nil, nil
|
|
||||||
case C.RuleActionRejectMethodDrop:
|
|
||||||
return nil, tun.ErrDrop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var responseCheck func(responseAddrs []netip.Addr) bool
|
|
||||||
if rule != nil && rule.WithAddressLimit() {
|
|
||||||
responseCheck = func(responseAddrs []netip.Addr) bool {
|
|
||||||
metadata.DestinationAddresses = responseAddrs
|
|
||||||
return rule.MatchAddressLimit(metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
|
||||||
options.Strategy = r.defaultDomainStrategy
|
|
||||||
}
|
|
||||||
responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, options, responseCheck)
|
|
||||||
if responseCheck == nil || err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
printResult()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
printResult()
|
|
||||||
if len(responseAddrs) > 0 {
|
|
||||||
r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " "))
|
|
||||||
}
|
|
||||||
return responseAddrs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAddressQuery(message *mDNS.Msg) bool {
|
|
||||||
for _, question := range message.Question {
|
|
||||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA || question.Qtype == mDNS.TypeHTTPS {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) ClearCache() {
|
|
||||||
r.client.ClearCache()
|
|
||||||
if r.platformInterface != nil {
|
|
||||||
r.platformInterface.ClearDNSCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) {
|
|
||||||
if r.dnsReverseMapping == nil {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
domain, loaded := r.dnsReverseMapping.Get(ip)
|
|
||||||
return domain, loaded
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) ResetNetwork() {
|
|
||||||
r.ClearCache()
|
|
||||||
for _, transport := range r.transport.Transports() {
|
|
||||||
transport.Reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
package fakeip
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
|
||||||
dns.RegisterTransport[option.FakeIPDNSServerOptions](registry, C.DNSTypeFakeIP, NewTransport)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ adapter.FakeIPTransport = (*Transport)(nil)
|
|
||||||
|
|
||||||
type Transport struct {
|
|
||||||
dns.TransportAdapter
|
|
||||||
logger logger.ContextLogger
|
|
||||||
store adapter.FakeIPStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.FakeIPDNSServerOptions) (adapter.DNSTransport, error) {
|
|
||||||
store := NewStore(ctx, logger, options.Inet4Range.Build(netip.Prefix{}), options.Inet6Range.Build(netip.Prefix{}))
|
|
||||||
return &Transport{
|
|
||||||
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeFakeIP, tag, nil),
|
|
||||||
logger: logger,
|
|
||||||
store: store,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Start() error {
|
|
||||||
return t.store.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Close() error {
|
|
||||||
return t.store.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Reset() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
question := message.Question[0]
|
|
||||||
if question.Qtype != mDNS.TypeA && question.Qtype != mDNS.TypeAAAA {
|
|
||||||
return nil, E.New("only IP queries are supported by fakeip")
|
|
||||||
}
|
|
||||||
address, err := t.store.Create(question.Name, question.Qtype == mDNS.TypeAAAA)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dns.FixedResponse(message.Id, question, []netip.Addr{address}, C.DefaultDNSTTL), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Store() adapter.FakeIPStore {
|
|
||||||
return t.store
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package hosts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
|
||||||
dns.RegisterTransport[option.HostsDNSServerOptions](registry, C.DNSTypeHosts, NewTransport)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ adapter.DNSTransport = (*Transport)(nil)
|
|
||||||
|
|
||||||
type Transport struct {
|
|
||||||
dns.TransportAdapter
|
|
||||||
files []*File
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.HostsDNSServerOptions) (adapter.DNSTransport, error) {
|
|
||||||
var files []*File
|
|
||||||
if len(options.Path) == 0 {
|
|
||||||
files = append(files, NewFile(DefaultPath))
|
|
||||||
} else {
|
|
||||||
for _, path := range options.Path {
|
|
||||||
files = append(files, NewFile(path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &Transport{
|
|
||||||
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeHosts, tag, nil),
|
|
||||||
files: files,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Reset() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
question := message.Question[0]
|
|
||||||
domain := dns.FqdnToDomain(question.Name)
|
|
||||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
|
||||||
for _, file := range t.files {
|
|
||||||
addresses := file.Lookup(domain)
|
|
||||||
if len(addresses) > 0 {
|
|
||||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Rcode: mDNS.RcodeNameError,
|
|
||||||
Response: true,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{question},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package hosts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
const cacheMaxAge = 5 * time.Second
|
|
||||||
|
|
||||||
type File struct {
|
|
||||||
path string
|
|
||||||
access sync.Mutex
|
|
||||||
byName map[string][]netip.Addr
|
|
||||||
expire time.Time
|
|
||||||
modTime time.Time
|
|
||||||
size int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFile(path string) *File {
|
|
||||||
return &File{
|
|
||||||
path: path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *File) Lookup(name string) []netip.Addr {
|
|
||||||
f.access.Lock()
|
|
||||||
defer f.access.Unlock()
|
|
||||||
f.update()
|
|
||||||
return f.byName[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *File) update() {
|
|
||||||
now := time.Now()
|
|
||||||
if now.Before(f.expire) && len(f.byName) > 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stat, err := os.Stat(f.path)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if f.modTime.Equal(stat.ModTime()) && f.size == stat.Size() {
|
|
||||||
f.expire = now.Add(cacheMaxAge)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
byName := make(map[string][]netip.Addr)
|
|
||||||
file, err := os.Open(f.path)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
reader := bufio.NewReader(file)
|
|
||||||
var (
|
|
||||||
prefix []byte
|
|
||||||
line []byte
|
|
||||||
isPrefix bool
|
|
||||||
)
|
|
||||||
for {
|
|
||||||
line, isPrefix, err = reader.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if isPrefix {
|
|
||||||
prefix = append(prefix, line...)
|
|
||||||
continue
|
|
||||||
} else if len(prefix) > 0 {
|
|
||||||
line = append(prefix, line...)
|
|
||||||
prefix = nil
|
|
||||||
}
|
|
||||||
commentIndex := strings.IndexRune(string(line), '#')
|
|
||||||
if commentIndex != -1 {
|
|
||||||
line = line[:commentIndex]
|
|
||||||
}
|
|
||||||
fields := strings.Fields(string(line))
|
|
||||||
if len(fields) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var addr netip.Addr
|
|
||||||
addr, err = netip.ParseAddr(fields[0])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for index := 1; index < len(fields); index++ {
|
|
||||||
canonicalName := dns.CanonicalName(fields[index])
|
|
||||||
byName[canonicalName] = append(byName[canonicalName], addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.expire = now.Add(cacheMaxAge)
|
|
||||||
f.modTime = stat.ModTime()
|
|
||||||
f.size = stat.Size()
|
|
||||||
f.byName = byName
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package hosts_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/dns/transport/hosts"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHosts(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
require.Equal(t, []netip.Addr{netip.AddrFrom4([4]byte{127, 0, 0, 1}), netip.IPv6Loopback()}, hosts.NewFile("testdata/hosts").Lookup("localhost."))
|
|
||||||
require.NotEmpty(t, hosts.NewFile(hosts.DefaultPath).Lookup("localhost."))
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package hosts
|
|
||||||
|
|
||||||
var DefaultPath = "/etc/hosts"
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package hosts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
var DefaultPath string
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
systemDirectory, err := windows.GetSystemDirectory()
|
|
||||||
if err != nil {
|
|
||||||
systemDirectory = "C:\\Windows\\System32"
|
|
||||||
}
|
|
||||||
DefaultPath = filepath.Join(systemDirectory, "Drivers/etc/hosts")
|
|
||||||
}
|
|
||||||
2
dns/transport/hosts/testdata/hosts
vendored
2
dns/transport/hosts/testdata/hosts
vendored
@@ -1,2 +0,0 @@
|
|||||||
127.0.0.1 localhost
|
|
||||||
::1 localhost
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
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"
|
|
||||||
aTLS "github.com/sagernet/sing/common/tls"
|
|
||||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const MimeType = "application/dns-message"
|
|
||||||
|
|
||||||
var _ adapter.DNSTransport = (*HTTPSTransport)(nil)
|
|
||||||
|
|
||||||
func RegisterHTTPS(registry *dns.TransportRegistry) {
|
|
||||||
dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTPS, NewHTTPS)
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPSTransport struct {
|
|
||||||
dns.TransportAdapter
|
|
||||||
logger logger.ContextLogger
|
|
||||||
dialer N.Dialer
|
|
||||||
destination *url.URL
|
|
||||||
headers http.Header
|
|
||||||
transport *http.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
|
||||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
|
||||||
tlsOptions.Enabled = true
|
|
||||||
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if common.Error(tlsConfig.Config()) == nil && !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
|
||||||
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), http2.NextProtoTLS))
|
|
||||||
}
|
|
||||||
if !common.Contains(tlsConfig.NextProtos(), "http/1.1") {
|
|
||||||
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), "http/1.1"))
|
|
||||||
}
|
|
||||||
headers := options.Headers.Build()
|
|
||||||
host := headers.Get("Host")
|
|
||||||
if host != "" {
|
|
||||||
headers.Del("Host")
|
|
||||||
} else {
|
|
||||||
if tlsConfig.ServerName() != "" {
|
|
||||||
host = tlsConfig.ServerName()
|
|
||||||
} else {
|
|
||||||
host = options.Server
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destinationURL := url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: host,
|
|
||||||
}
|
|
||||||
if destinationURL.Host == "" {
|
|
||||||
destinationURL.Host = options.Server
|
|
||||||
}
|
|
||||||
if options.ServerPort != 0 && options.ServerPort != 443 {
|
|
||||||
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
|
|
||||||
}
|
|
||||||
path := options.Path
|
|
||||||
if path == "" {
|
|
||||||
path = "/dns-query"
|
|
||||||
}
|
|
||||||
err = sHTTP.URLSetPath(&destinationURL, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
serverAddr := options.ServerOptions.Build()
|
|
||||||
if serverAddr.Port == 0 {
|
|
||||||
serverAddr.Port = 443
|
|
||||||
}
|
|
||||||
return NewHTTPSRaw(
|
|
||||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
|
|
||||||
logger,
|
|
||||||
transportDialer,
|
|
||||||
&destinationURL,
|
|
||||||
headers,
|
|
||||||
serverAddr,
|
|
||||||
tlsConfig,
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTPSRaw(
|
|
||||||
adapter dns.TransportAdapter,
|
|
||||||
logger log.ContextLogger,
|
|
||||||
dialer N.Dialer,
|
|
||||||
destination *url.URL,
|
|
||||||
headers http.Header,
|
|
||||||
serverAddr M.Socksaddr,
|
|
||||||
tlsConfig tls.Config,
|
|
||||||
) *HTTPSTransport {
|
|
||||||
var transport *http.Transport
|
|
||||||
if tlsConfig != nil {
|
|
||||||
transport = &http.Transport{
|
|
||||||
ForceAttemptHTTP2: true,
|
|
||||||
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
tcpConn, hErr := dialer.DialContext(ctx, network, serverAddr)
|
|
||||||
if hErr != nil {
|
|
||||||
return nil, hErr
|
|
||||||
}
|
|
||||||
tlsConn, hErr := aTLS.ClientHandshake(ctx, tcpConn, tlsConfig)
|
|
||||||
if hErr != nil {
|
|
||||||
tcpConn.Close()
|
|
||||||
return nil, hErr
|
|
||||||
}
|
|
||||||
return tlsConn, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
transport = &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return dialer.DialContext(ctx, network, serverAddr)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &HTTPSTransport{
|
|
||||||
TransportAdapter: adapter,
|
|
||||||
logger: logger,
|
|
||||||
dialer: dialer,
|
|
||||||
destination: destination,
|
|
||||||
headers: headers,
|
|
||||||
transport: transport,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HTTPSTransport) Reset() {
|
|
||||||
t.transport.CloseIdleConnections()
|
|
||||||
t.transport = t.transport.Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
exMessage := *message
|
|
||||||
exMessage.Id = 0
|
|
||||||
exMessage.Compress = true
|
|
||||||
requestBuffer := buf.NewSize(1 + message.Len())
|
|
||||||
rawMessage, err := exMessage.PackBuffer(requestBuffer.FreeBytes())
|
|
||||||
if err != nil {
|
|
||||||
requestBuffer.Release()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
|
|
||||||
if err != nil {
|
|
||||||
requestBuffer.Release()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
request.Header = t.headers.Clone()
|
|
||||||
request.Header.Set("Content-Type", MimeType)
|
|
||||||
request.Header.Set("Accept", MimeType)
|
|
||||||
response, err := t.transport.RoundTrip(request)
|
|
||||||
requestBuffer.Release()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
if response.StatusCode != http.StatusOK {
|
|
||||||
return nil, E.New("unexpected status: ", response.Status)
|
|
||||||
}
|
|
||||||
var responseMessage mDNS.Msg
|
|
||||||
if response.ContentLength > 0 {
|
|
||||||
responseBuffer := buf.NewSize(int(response.ContentLength))
|
|
||||||
_, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = responseMessage.Unpack(responseBuffer.Bytes())
|
|
||||||
responseBuffer.Release()
|
|
||||||
} else {
|
|
||||||
rawMessage, err = io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = responseMessage.Unpack(rawMessage)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &responseMessage, nil
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/dns/transport/hosts"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
|
||||||
dns.RegisterTransport[option.LocalDNSServerOptions](registry, C.DNSTypeLocal, NewTransport)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ adapter.DNSTransport = (*Transport)(nil)
|
|
||||||
|
|
||||||
type Transport struct {
|
|
||||||
dns.TransportAdapter
|
|
||||||
hosts *hosts.File
|
|
||||||
dialer N.Dialer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
|
|
||||||
transportDialer, err := dns.NewLocalDialer(ctx, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Transport{
|
|
||||||
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
|
|
||||||
hosts: hosts.NewFile(hosts.DefaultPath),
|
|
||||||
dialer: transportDialer,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Reset() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
question := message.Question[0]
|
|
||||||
domain := dns.FqdnToDomain(question.Name)
|
|
||||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
|
||||||
addresses := t.hosts.Lookup(domain)
|
|
||||||
if len(addresses) > 0 {
|
|
||||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
systemConfig := getSystemDNSConfig()
|
|
||||||
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
|
|
||||||
return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
|
|
||||||
} else {
|
|
||||||
return t.exchangeParallel(ctx, systemConfig, message, domain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) exchangeSingleRequest(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
|
||||||
var lastErr error
|
|
||||||
for _, fqdn := range systemConfig.nameList(domain) {
|
|
||||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
return nil, lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfig, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
|
|
||||||
returned := make(chan struct{})
|
|
||||||
defer close(returned)
|
|
||||||
type queryResult struct {
|
|
||||||
response *mDNS.Msg
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
results := make(chan queryResult)
|
|
||||||
startRacer := func(ctx context.Context, fqdn string) {
|
|
||||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
|
||||||
addresses, _ := dns.MessageToAddresses(response)
|
|
||||||
if len(addresses) == 0 {
|
|
||||||
err = E.New(fqdn, ": empty result")
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case results <- queryResult{response, err}:
|
|
||||||
case <-returned:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
queryCtx, queryCancel := context.WithCancel(ctx)
|
|
||||||
defer queryCancel()
|
|
||||||
var nameCount int
|
|
||||||
for _, fqdn := range systemConfig.nameList(domain) {
|
|
||||||
nameCount++
|
|
||||||
go startRacer(queryCtx, fqdn)
|
|
||||||
}
|
|
||||||
var errors []error
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
case result := <-results:
|
|
||||||
if result.err == nil {
|
|
||||||
return result.response, nil
|
|
||||||
}
|
|
||||||
errors = append(errors, result.err)
|
|
||||||
if len(errors) == nameCount {
|
|
||||||
return nil, E.Errors(errors...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) tryOneName(ctx context.Context, config *dnsConfig, fqdn string, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
serverOffset := config.serverOffset()
|
|
||||||
sLen := uint32(len(config.servers))
|
|
||||||
var lastErr error
|
|
||||||
for i := 0; i < config.attempts; i++ {
|
|
||||||
for j := uint32(0); j < sLen; j++ {
|
|
||||||
server := config.servers[(serverOffset+j)%sLen]
|
|
||||||
question := message.Question[0]
|
|
||||||
question.Name = fqdn
|
|
||||||
response, err := t.exchangeOne(ctx, M.ParseSocksaddr(server), question, config.timeout, config.useTCP, config.trustAD)
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, E.Cause(lastErr, fqdn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) {
|
|
||||||
var networks []string
|
|
||||||
if useTCP {
|
|
||||||
networks = []string{N.NetworkTCP}
|
|
||||||
} else {
|
|
||||||
networks = []string{N.NetworkUDP, N.NetworkTCP}
|
|
||||||
}
|
|
||||||
request := &mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
Id: uint16(rand.Uint32()),
|
|
||||||
RecursionDesired: true,
|
|
||||||
AuthenticatedData: ad,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{question},
|
|
||||||
Compress: true,
|
|
||||||
}
|
|
||||||
request.SetEdns0(maxDNSPacketSize, false)
|
|
||||||
buffer := buf.Get(buf.UDPBufferSize)
|
|
||||||
defer buf.Put(buffer)
|
|
||||||
for _, network := range networks {
|
|
||||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
|
|
||||||
defer cancel()
|
|
||||||
conn, err := t.dialer.DialContext(ctx, network, server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
|
||||||
conn.SetDeadline(deadline)
|
|
||||||
}
|
|
||||||
rawMessage, err := request.PackBuffer(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "pack request")
|
|
||||||
}
|
|
||||||
_, err = conn.Write(rawMessage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "write request")
|
|
||||||
}
|
|
||||||
n, err := conn.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read response")
|
|
||||||
}
|
|
||||||
var response mDNS.Msg
|
|
||||||
err = response.Unpack(buffer[:n])
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "unpack response")
|
|
||||||
}
|
|
||||||
if response.Truncated && network == N.NetworkUDP {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return &response, nil
|
|
||||||
}
|
|
||||||
panic("unexpected")
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// net.maxDNSPacketSize
|
|
||||||
maxDNSPacketSize = 1232
|
|
||||||
)
|
|
||||||
|
|
||||||
type resolverConfig struct {
|
|
||||||
initOnce sync.Once
|
|
||||||
ch chan struct{}
|
|
||||||
lastChecked time.Time
|
|
||||||
dnsConfig atomic.Pointer[dnsConfig]
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolvConf resolverConfig
|
|
||||||
|
|
||||||
func getSystemDNSConfig() *dnsConfig {
|
|
||||||
resolvConf.tryUpdate("/etc/resolv.conf")
|
|
||||||
return resolvConf.dnsConfig.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (conf *resolverConfig) init() {
|
|
||||||
conf.dnsConfig.Store(dnsReadConfig("/etc/resolv.conf"))
|
|
||||||
conf.lastChecked = time.Now()
|
|
||||||
conf.ch = make(chan struct{}, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (conf *resolverConfig) tryUpdate(name string) {
|
|
||||||
conf.initOnce.Do(conf.init)
|
|
||||||
|
|
||||||
if conf.dnsConfig.Load().noReload {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !conf.tryAcquireSema() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conf.releaseSema()
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
if conf.lastChecked.After(now.Add(-5 * time.Second)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conf.lastChecked = now
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
var mtime time.Time
|
|
||||||
if fi, err := os.Stat(name); err == nil {
|
|
||||||
mtime = fi.ModTime()
|
|
||||||
}
|
|
||||||
if mtime.Equal(conf.dnsConfig.Load().mtime) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dnsConf := dnsReadConfig(name)
|
|
||||||
conf.dnsConfig.Store(dnsConf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (conf *resolverConfig) tryAcquireSema() bool {
|
|
||||||
select {
|
|
||||||
case conf.ch <- struct{}{}:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (conf *resolverConfig) releaseSema() {
|
|
||||||
<-conf.ch
|
|
||||||
}
|
|
||||||
|
|
||||||
type dnsConfig struct {
|
|
||||||
servers []string
|
|
||||||
search []string
|
|
||||||
ndots int
|
|
||||||
timeout time.Duration
|
|
||||||
attempts int
|
|
||||||
rotate bool
|
|
||||||
unknownOpt bool
|
|
||||||
lookup []string
|
|
||||||
err error
|
|
||||||
mtime time.Time
|
|
||||||
soffset uint32
|
|
||||||
singleRequest bool
|
|
||||||
useTCP bool
|
|
||||||
trustAD bool
|
|
||||||
noReload bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *dnsConfig) serverOffset() uint32 {
|
|
||||||
if c.rotate {
|
|
||||||
return atomic.AddUint32(&c.soffset, 1) - 1 // return 0 to start
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (conf *dnsConfig) nameList(name string) []string {
|
|
||||||
l := len(name)
|
|
||||||
rooted := l > 0 && name[l-1] == '.'
|
|
||||||
if l > 254 || l == 254 && !rooted {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if rooted {
|
|
||||||
if avoidDNS(name) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []string{name}
|
|
||||||
}
|
|
||||||
|
|
||||||
hasNdots := strings.Count(name, ".") >= conf.ndots
|
|
||||||
name += "."
|
|
||||||
// l++
|
|
||||||
|
|
||||||
names := make([]string, 0, 1+len(conf.search))
|
|
||||||
if hasNdots && !avoidDNS(name) {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
for _, suffix := range conf.search {
|
|
||||||
fqdn := name + suffix
|
|
||||||
if !avoidDNS(fqdn) && len(fqdn) <= 254 {
|
|
||||||
names = append(names, fqdn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasNdots && !avoidDNS(name) {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
func avoidDNS(name string) bool {
|
|
||||||
if name == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if name[len(name)-1] == '.' {
|
|
||||||
name = name[:len(name)-1]
|
|
||||||
}
|
|
||||||
return strings.HasSuffix(name, ".onion")
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
_ "unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dnsReadConfig(name string) *dnsConfig {
|
|
||||||
conf := &dnsConfig{
|
|
||||||
ndots: 1,
|
|
||||||
timeout: 5 * time.Second,
|
|
||||||
attempts: 2,
|
|
||||||
}
|
|
||||||
file, err := os.Open(name)
|
|
||||||
if err != nil {
|
|
||||||
conf.servers = defaultNS
|
|
||||||
conf.search = dnsDefaultSearch()
|
|
||||||
conf.err = err
|
|
||||||
return conf
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
fi, err := file.Stat()
|
|
||||||
if err == nil {
|
|
||||||
conf.mtime = fi.ModTime()
|
|
||||||
} else {
|
|
||||||
conf.servers = defaultNS
|
|
||||||
conf.search = dnsDefaultSearch()
|
|
||||||
conf.err = err
|
|
||||||
return conf
|
|
||||||
}
|
|
||||||
reader := bufio.NewReader(file)
|
|
||||||
var (
|
|
||||||
prefix []byte
|
|
||||||
line []byte
|
|
||||||
isPrefix bool
|
|
||||||
)
|
|
||||||
for {
|
|
||||||
line, isPrefix, err = reader.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if isPrefix {
|
|
||||||
prefix = append(prefix, line...)
|
|
||||||
continue
|
|
||||||
} else if len(prefix) > 0 {
|
|
||||||
line = append(prefix, line...)
|
|
||||||
prefix = nil
|
|
||||||
}
|
|
||||||
if len(line) > 0 && (line[0] == ';' || line[0] == '#') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f := strings.Fields(string(line))
|
|
||||||
if len(f) < 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch f[0] {
|
|
||||||
case "nameserver":
|
|
||||||
if len(f) > 1 && len(conf.servers) < 3 {
|
|
||||||
if _, err := netip.ParseAddr(f[1]); err == nil {
|
|
||||||
conf.servers = append(conf.servers, net.JoinHostPort(f[1], "53"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "domain":
|
|
||||||
if len(f) > 1 {
|
|
||||||
conf.search = []string{ensureRooted(f[1])}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "search":
|
|
||||||
conf.search = make([]string, 0, len(f)-1)
|
|
||||||
for i := 1; i < len(f); i++ {
|
|
||||||
name := ensureRooted(f[i])
|
|
||||||
if name == "." {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
conf.search = append(conf.search, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "options":
|
|
||||||
for _, s := range f[1:] {
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(s, "ndots:"):
|
|
||||||
n, _, _ := dtoi(s[6:])
|
|
||||||
if n < 0 {
|
|
||||||
n = 0
|
|
||||||
} else if n > 15 {
|
|
||||||
n = 15
|
|
||||||
}
|
|
||||||
conf.ndots = n
|
|
||||||
case strings.HasPrefix(s, "timeout:"):
|
|
||||||
n, _, _ := dtoi(s[8:])
|
|
||||||
if n < 1 {
|
|
||||||
n = 1
|
|
||||||
}
|
|
||||||
conf.timeout = time.Duration(n) * time.Second
|
|
||||||
case strings.HasPrefix(s, "attempts:"):
|
|
||||||
n, _, _ := dtoi(s[9:])
|
|
||||||
if n < 1 {
|
|
||||||
n = 1
|
|
||||||
}
|
|
||||||
conf.attempts = n
|
|
||||||
case s == "rotate":
|
|
||||||
conf.rotate = true
|
|
||||||
case s == "single-request" || s == "single-request-reopen":
|
|
||||||
conf.singleRequest = true
|
|
||||||
case s == "use-vc" || s == "usevc" || s == "tcp":
|
|
||||||
conf.useTCP = true
|
|
||||||
case s == "trust-ad":
|
|
||||||
conf.trustAD = true
|
|
||||||
case s == "edns0":
|
|
||||||
case s == "no-reload":
|
|
||||||
conf.noReload = true
|
|
||||||
default:
|
|
||||||
conf.unknownOpt = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "lookup":
|
|
||||||
conf.lookup = f[1:]
|
|
||||||
|
|
||||||
default:
|
|
||||||
conf.unknownOpt = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(conf.servers) == 0 {
|
|
||||||
conf.servers = defaultNS
|
|
||||||
}
|
|
||||||
if len(conf.search) == 0 {
|
|
||||||
conf.search = dnsDefaultSearch()
|
|
||||||
}
|
|
||||||
return conf
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname defaultNS net.defaultNS
|
|
||||||
var defaultNS []string
|
|
||||||
|
|
||||||
func dnsDefaultSearch() []string {
|
|
||||||
hn, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if i := strings.IndexRune(hn, '.'); i >= 0 && i < len(hn)-1 {
|
|
||||||
return []string{ensureRooted(hn[i+1:])}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureRooted(s string) string {
|
|
||||||
if len(s) > 0 && s[len(s)-1] == '.' {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return s + "."
|
|
||||||
}
|
|
||||||
|
|
||||||
const big = 0xFFFFFF
|
|
||||||
|
|
||||||
func dtoi(s string) (n int, i int, ok bool) {
|
|
||||||
n = 0
|
|
||||||
for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
|
|
||||||
n = n*10 + int(s[i]-'0')
|
|
||||||
if n >= big {
|
|
||||||
return big, i, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if i == 0 {
|
|
||||||
return 0, 0, false
|
|
||||||
}
|
|
||||||
return n, i, true
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dnsReadConfig(_ string) *dnsConfig {
|
|
||||||
conf := &dnsConfig{
|
|
||||||
ndots: 1,
|
|
||||||
timeout: 5 * time.Second,
|
|
||||||
attempts: 2,
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if len(conf.servers) == 0 {
|
|
||||||
conf.servers = defaultNS
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
aas, err := adapterAddresses()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, aa := range aas {
|
|
||||||
// Only take interfaces whose OperStatus is IfOperStatusUp(0x01) into DNS configs.
|
|
||||||
if aa.OperStatus != windows.IfOperStatusUp {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only take interfaces which have at least one gateway
|
|
||||||
if aa.FirstGatewayAddress == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next {
|
|
||||||
sa, err := dns.Address.Sockaddr.Sockaddr()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var ip netip.Addr
|
|
||||||
switch sa := sa.(type) {
|
|
||||||
case *syscall.SockaddrInet4:
|
|
||||||
ip = netip.AddrFrom4([4]byte{sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3]})
|
|
||||||
case *syscall.SockaddrInet6:
|
|
||||||
var addr16 [16]byte
|
|
||||||
copy(addr16[:], sa.Addr[:])
|
|
||||||
if addr16[0] == 0xfe && addr16[1] == 0xc0 {
|
|
||||||
// fec0/10 IPv6 addresses are site local anycast DNS
|
|
||||||
// addresses Microsoft sets by default if no other
|
|
||||||
// IPv6 DNS address is set. Site local anycast is
|
|
||||||
// deprecated since 2004, see
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc3879
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ip = netip.AddrFrom16(addr16)
|
|
||||||
default:
|
|
||||||
// Unexpected type.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
conf.servers = append(conf.servers, net.JoinHostPort(ip.String(), "53"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return conf
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname defaultNS net.defaultNS
|
|
||||||
var defaultNS []string
|
|
||||||
|
|
||||||
func adapterAddresses() ([]*windows.IpAdapterAddresses, error) {
|
|
||||||
var b []byte
|
|
||||||
l := uint32(15000) // recommended initial size
|
|
||||||
for {
|
|
||||||
b = make([]byte, l)
|
|
||||||
const flags = windows.GAA_FLAG_INCLUDE_PREFIX | windows.GAA_FLAG_INCLUDE_GATEWAYS
|
|
||||||
err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, flags, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l)
|
|
||||||
if err == nil {
|
|
||||||
if l == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW {
|
|
||||||
return nil, os.NewSyscallError("getadaptersaddresses", err)
|
|
||||||
}
|
|
||||||
if l <= uint32(len(b)) {
|
|
||||||
return nil, os.NewSyscallError("getadaptersaddresses", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var aas []*windows.IpAdapterAddresses
|
|
||||||
for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next {
|
|
||||||
aas = append(aas, aa)
|
|
||||||
}
|
|
||||||
return aas, nil
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.DNSTransport = (*PredefinedTransport)(nil)
|
|
||||||
|
|
||||||
func RegisterPredefined(registry *dns.TransportRegistry) {
|
|
||||||
dns.RegisterTransport[option.PredefinedDNSServerOptions](registry, C.DNSTypePreDefined, NewPredefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PredefinedTransport struct {
|
|
||||||
dns.TransportAdapter
|
|
||||||
responses []*predefinedResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
type predefinedResponse struct {
|
|
||||||
questions []mDNS.Question
|
|
||||||
answer *mDNS.Msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPredefined(ctx context.Context, logger log.ContextLogger, tag string, options option.PredefinedDNSServerOptions) (adapter.DNSTransport, error) {
|
|
||||||
var responses []*predefinedResponse
|
|
||||||
for _, response := range options.Responses {
|
|
||||||
questions, msg, err := response.Build()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
responses = append(responses, &predefinedResponse{
|
|
||||||
questions: questions,
|
|
||||||
answer: msg,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(responses) == 0 {
|
|
||||||
return nil, E.New("empty predefined responses")
|
|
||||||
}
|
|
||||||
return &PredefinedTransport{
|
|
||||||
TransportAdapter: dns.NewTransportAdapter(C.DNSTypePreDefined, tag, nil),
|
|
||||||
responses: responses,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PredefinedTransport) Reset() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *PredefinedTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
for _, response := range t.responses {
|
|
||||||
for _, question := range response.questions {
|
|
||||||
if func() bool {
|
|
||||||
if question.Name == "" && question.Qtype == mDNS.TypeNone {
|
|
||||||
return true
|
|
||||||
} else if question.Name == "" {
|
|
||||||
return common.Any(message.Question, func(it mDNS.Question) bool {
|
|
||||||
return it.Qtype == question.Qtype
|
|
||||||
})
|
|
||||||
} else if question.Qtype == mDNS.TypeNone {
|
|
||||||
return common.Any(message.Question, func(it mDNS.Question) bool {
|
|
||||||
return it.Name == question.Name
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return common.Contains(message.Question, question)
|
|
||||||
}
|
|
||||||
}() {
|
|
||||||
copyAnswer := *response.answer
|
|
||||||
copyAnswer.Id = message.Id
|
|
||||||
return ©Answer, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, dns.RCodeNameError
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
package quic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/sagernet/quic-go"
|
|
||||||
"github.com/sagernet/quic-go/http3"
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/dns/transport"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
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"
|
|
||||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.DNSTransport = (*HTTP3Transport)(nil)
|
|
||||||
|
|
||||||
func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
|
|
||||||
dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTP3, NewHTTP3)
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTP3Transport struct {
|
|
||||||
dns.TransportAdapter
|
|
||||||
logger logger.ContextLogger
|
|
||||||
dialer N.Dialer
|
|
||||||
destination *url.URL
|
|
||||||
headers http.Header
|
|
||||||
transport *http3.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
|
||||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
|
||||||
tlsOptions.Enabled = true
|
|
||||||
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stdConfig, err := tlsConfig.Config()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
headers := options.Headers.Build()
|
|
||||||
host := headers.Get("Host")
|
|
||||||
if host != "" {
|
|
||||||
headers.Del("Host")
|
|
||||||
} else {
|
|
||||||
if tlsConfig.ServerName() != "" {
|
|
||||||
host = tlsConfig.ServerName()
|
|
||||||
} else {
|
|
||||||
host = options.Server
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destinationURL := url.URL{
|
|
||||||
Scheme: "HTTP3",
|
|
||||||
Host: host,
|
|
||||||
}
|
|
||||||
if destinationURL.Host == "" {
|
|
||||||
destinationURL.Host = options.Server
|
|
||||||
}
|
|
||||||
if options.ServerPort != 0 && options.ServerPort != 443 {
|
|
||||||
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
|
|
||||||
}
|
|
||||||
path := options.Path
|
|
||||||
if path == "" {
|
|
||||||
path = "/dns-query"
|
|
||||||
}
|
|
||||||
err = sHTTP.URLSetPath(&destinationURL, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
serverAddr := options.ServerOptions.Build()
|
|
||||||
if serverAddr.Port == 0 {
|
|
||||||
serverAddr.Port = 443
|
|
||||||
}
|
|
||||||
return &HTTP3Transport{
|
|
||||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
|
|
||||||
logger: logger,
|
|
||||||
dialer: transportDialer,
|
|
||||||
destination: &destinationURL,
|
|
||||||
headers: headers,
|
|
||||||
transport: &http3.Transport{
|
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (quic.EarlyConnection, error) {
|
|
||||||
destinationAddr := M.ParseSocksaddr(addr)
|
|
||||||
conn, dialErr := transportDialer.DialContext(ctx, N.NetworkUDP, destinationAddr)
|
|
||||||
if dialErr != nil {
|
|
||||||
return nil, dialErr
|
|
||||||
}
|
|
||||||
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsCfg, cfg)
|
|
||||||
},
|
|
||||||
TLSClientConfig: stdConfig,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HTTP3Transport) Reset() {
|
|
||||||
t.transport.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *HTTP3Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
exMessage := *message
|
|
||||||
exMessage.Id = 0
|
|
||||||
exMessage.Compress = true
|
|
||||||
requestBuffer := buf.NewSize(1 + message.Len())
|
|
||||||
rawMessage, err := exMessage.PackBuffer(requestBuffer.FreeBytes())
|
|
||||||
if err != nil {
|
|
||||||
requestBuffer.Release()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
|
|
||||||
if err != nil {
|
|
||||||
requestBuffer.Release()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
request.Header = t.headers.Clone()
|
|
||||||
request.Header.Set("Content-Type", transport.MimeType)
|
|
||||||
request.Header.Set("Accept", transport.MimeType)
|
|
||||||
response, err := t.transport.RoundTrip(request)
|
|
||||||
requestBuffer.Release()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
if response.StatusCode != http.StatusOK {
|
|
||||||
return nil, E.New("unexpected status: ", response.Status)
|
|
||||||
}
|
|
||||||
var responseMessage mDNS.Msg
|
|
||||||
if response.ContentLength > 0 {
|
|
||||||
responseBuffer := buf.NewSize(int(response.ContentLength))
|
|
||||||
_, err = responseBuffer.ReadFullFrom(response.Body, int(response.ContentLength))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = responseMessage.Unpack(responseBuffer.Bytes())
|
|
||||||
responseBuffer.Release()
|
|
||||||
} else {
|
|
||||||
rawMessage, err = io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = responseMessage.Unpack(rawMessage)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &responseMessage, nil
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
package quic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sagernet/quic-go"
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/dns/transport"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
sQUIC "github.com/sagernet/sing-quic"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.DNSTransport = (*Transport)(nil)
|
|
||||||
|
|
||||||
func RegisterTransport(registry *dns.TransportRegistry) {
|
|
||||||
dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeQUIC, NewQUIC)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Transport struct {
|
|
||||||
dns.TransportAdapter
|
|
||||||
ctx context.Context
|
|
||||||
logger logger.ContextLogger
|
|
||||||
dialer N.Dialer
|
|
||||||
serverAddr M.Socksaddr
|
|
||||||
tlsConfig tls.Config
|
|
||||||
access sync.Mutex
|
|
||||||
connection quic.EarlyConnection
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
|
|
||||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
|
||||||
tlsOptions.Enabled = true
|
|
||||||
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(tlsConfig.NextProtos()) == 0 {
|
|
||||||
tlsConfig.SetNextProtos([]string{"doq"})
|
|
||||||
}
|
|
||||||
serverAddr := options.ServerOptions.Build()
|
|
||||||
if serverAddr.Port == 0 {
|
|
||||||
serverAddr.Port = 853
|
|
||||||
}
|
|
||||||
return &Transport{
|
|
||||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
|
|
||||||
ctx: ctx,
|
|
||||||
logger: logger,
|
|
||||||
dialer: transportDialer,
|
|
||||||
serverAddr: serverAddr,
|
|
||||||
tlsConfig: tlsConfig,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Reset() {
|
|
||||||
t.access.Lock()
|
|
||||||
defer t.access.Unlock()
|
|
||||||
connection := t.connection
|
|
||||||
if connection != nil {
|
|
||||||
connection.CloseWithError(0, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
var (
|
|
||||||
conn quic.Connection
|
|
||||||
err error
|
|
||||||
response *mDNS.Msg
|
|
||||||
)
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
conn, err = t.openConnection()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
response, err = t.exchange(ctx, message, conn)
|
|
||||||
if err == nil {
|
|
||||||
return response, nil
|
|
||||||
} else if !isQUICRetryError(err) {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
conn.CloseWithError(quic.ApplicationErrorCode(0), "")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) openConnection() (quic.EarlyConnection, error) {
|
|
||||||
connection := t.connection
|
|
||||||
if connection != nil && !common.Done(connection.Context()) {
|
|
||||||
return connection, nil
|
|
||||||
}
|
|
||||||
t.access.Lock()
|
|
||||||
defer t.access.Unlock()
|
|
||||||
connection = t.connection
|
|
||||||
if connection != nil && !common.Done(connection.Context()) {
|
|
||||||
return connection, nil
|
|
||||||
}
|
|
||||||
conn, err := t.dialer.DialContext(t.ctx, N.NetworkUDP, t.serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
earlyConnection, err := sQUIC.DialEarly(
|
|
||||||
t.ctx,
|
|
||||||
bufio.NewUnbindPacketConn(conn),
|
|
||||||
t.serverAddr.UDPAddr(),
|
|
||||||
t.tlsConfig,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t.connection = earlyConnection
|
|
||||||
return earlyConnection, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn quic.Connection) (*mDNS.Msg, error) {
|
|
||||||
stream, err := conn.OpenStreamSync(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer stream.Close()
|
|
||||||
defer stream.CancelRead(0)
|
|
||||||
err = transport.WriteMessage(stream, 0, message)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return transport.ReadMessage(stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/AdguardTeam/dnsproxy/blob/fd1868577652c639cce3da00e12ca548f421baf1/upstream/upstream_quic.go#L394
|
|
||||||
func isQUICRetryError(err error) (ok bool) {
|
|
||||||
var qAppErr *quic.ApplicationError
|
|
||||||
if errors.As(err, &qAppErr) && qAppErr.ErrorCode == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var qIdleErr *quic.IdleTimeoutError
|
|
||||||
if errors.As(err, &qIdleErr) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var resetErr *quic.StatelessResetError
|
|
||||||
if errors.As(err, &resetErr) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var qTransportError *quic.TransportError
|
|
||||||
if errors.As(err, &qTransportError) && qTransportError.ErrorCode == quic.NoError {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, quic.Err0RTTRejected) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.DNSTransport = (*TCPTransport)(nil)
|
|
||||||
|
|
||||||
func RegisterTCP(registry *dns.TransportRegistry) {
|
|
||||||
dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeTCP, NewTCP)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPTransport struct {
|
|
||||||
dns.TransportAdapter
|
|
||||||
dialer N.Dialer
|
|
||||||
serverAddr M.Socksaddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTCP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) {
|
|
||||||
transportDialer, err := dns.NewRemoteDialer(ctx, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
serverAddr := options.ServerOptions.Build()
|
|
||||||
if serverAddr.Port == 0 {
|
|
||||||
serverAddr.Port = 53
|
|
||||||
}
|
|
||||||
return &TCPTransport{
|
|
||||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTCP, tag, options),
|
|
||||||
dialer: transportDialer,
|
|
||||||
serverAddr: serverAddr,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TCPTransport) Reset() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
err = WriteMessage(conn, 0, message)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ReadMessage(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadMessage(reader io.Reader) (*mDNS.Msg, error) {
|
|
||||||
var responseLen uint16
|
|
||||||
err := binary.Read(reader, binary.BigEndian, &responseLen)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if responseLen < 10 {
|
|
||||||
return nil, mDNS.ErrShortRead
|
|
||||||
}
|
|
||||||
buffer := buf.NewSize(int(responseLen))
|
|
||||||
defer buffer.Release()
|
|
||||||
_, err = buffer.ReadFullFrom(reader, int(responseLen))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var message mDNS.Msg
|
|
||||||
err = message.Unpack(buffer.Bytes())
|
|
||||||
return &message, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteMessage(writer io.Writer, messageId uint16, message *mDNS.Msg) error {
|
|
||||||
requestLen := message.Len()
|
|
||||||
buffer := buf.NewSize(3 + requestLen)
|
|
||||||
defer buffer.Release()
|
|
||||||
common.Must(binary.Write(buffer, binary.BigEndian, uint16(requestLen)))
|
|
||||||
exMessage := *message
|
|
||||||
exMessage.Id = messageId
|
|
||||||
exMessage.Compress = true
|
|
||||||
rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
buffer.Truncate(2 + len(rawMessage))
|
|
||||||
return common.Error(writer.Write(buffer.Bytes()))
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"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"
|
|
||||||
"github.com/sagernet/sing/common/x/list"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.DNSTransport = (*TLSTransport)(nil)
|
|
||||||
|
|
||||||
func RegisterTLS(registry *dns.TransportRegistry) {
|
|
||||||
dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeTLS, NewTLS)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TLSTransport struct {
|
|
||||||
dns.TransportAdapter
|
|
||||||
logger logger.ContextLogger
|
|
||||||
dialer N.Dialer
|
|
||||||
serverAddr M.Socksaddr
|
|
||||||
tlsConfig tls.Config
|
|
||||||
access sync.Mutex
|
|
||||||
connections list.List[*tlsDNSConn]
|
|
||||||
}
|
|
||||||
|
|
||||||
type tlsDNSConn struct {
|
|
||||||
tls.Conn
|
|
||||||
queryId uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteTLSDNSServerOptions) (adapter.DNSTransport, error) {
|
|
||||||
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
|
||||||
tlsOptions.Enabled = true
|
|
||||||
tlsConfig, err := tls.NewClient(ctx, options.Server, tlsOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
serverAddr := options.ServerOptions.Build()
|
|
||||||
if serverAddr.Port == 0 {
|
|
||||||
serverAddr.Port = 853
|
|
||||||
}
|
|
||||||
return &TLSTransport{
|
|
||||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions),
|
|
||||||
logger: logger,
|
|
||||||
dialer: transportDialer,
|
|
||||||
serverAddr: serverAddr,
|
|
||||||
tlsConfig: tlsConfig,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TLSTransport) Reset() {
|
|
||||||
t.access.Lock()
|
|
||||||
defer t.access.Unlock()
|
|
||||||
for connection := t.connections.Front(); connection != nil; connection = connection.Next() {
|
|
||||||
connection.Value.Close()
|
|
||||||
}
|
|
||||||
t.connections.Init()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
t.access.Lock()
|
|
||||||
conn := t.connections.PopFront()
|
|
||||||
t.access.Unlock()
|
|
||||||
if conn != nil {
|
|
||||||
response, err := t.exchange(message, conn)
|
|
||||||
if err == nil {
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tcpConn, err := t.dialer.DialContext(ctx, N.NetworkTCP, t.serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsConn, err := tls.ClientHandshake(ctx, tcpConn, t.tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
tcpConn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return t.exchange(message, &tlsDNSConn{Conn: tlsConn})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TLSTransport) exchange(message *mDNS.Msg, conn *tlsDNSConn) (*mDNS.Msg, error) {
|
|
||||||
conn.queryId++
|
|
||||||
err := WriteMessage(conn, conn.queryId, message)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, E.Cause(err, "write request")
|
|
||||||
}
|
|
||||||
response, err := ReadMessage(conn)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, E.Cause(err, "read response")
|
|
||||||
}
|
|
||||||
t.access.Lock()
|
|
||||||
t.connections.PushBack(conn)
|
|
||||||
t.access.Unlock()
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
package transport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common/buf"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.DNSTransport = (*UDPTransport)(nil)
|
|
||||||
|
|
||||||
func RegisterUDP(registry *dns.TransportRegistry) {
|
|
||||||
dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeUDP, NewUDP)
|
|
||||||
}
|
|
||||||
|
|
||||||
type UDPTransport struct {
|
|
||||||
dns.TransportAdapter
|
|
||||||
logger logger.ContextLogger
|
|
||||||
dialer N.Dialer
|
|
||||||
serverAddr M.Socksaddr
|
|
||||||
udpSize int
|
|
||||||
tcpTransport *TCPTransport
|
|
||||||
access sync.Mutex
|
|
||||||
conn *dnsConnection
|
|
||||||
done chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteDNSServerOptions) (adapter.DNSTransport, error) {
|
|
||||||
transportDialer, err := dns.NewRemoteDialer(ctx, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
serverAddr := options.ServerOptions.Build()
|
|
||||||
if serverAddr.Port == 0 {
|
|
||||||
serverAddr.Port = 53
|
|
||||||
}
|
|
||||||
return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr) *UDPTransport {
|
|
||||||
return &UDPTransport{
|
|
||||||
TransportAdapter: adapter,
|
|
||||||
logger: logger,
|
|
||||||
dialer: dialer,
|
|
||||||
serverAddr: serverAddr,
|
|
||||||
udpSize: 512,
|
|
||||||
tcpTransport: &TCPTransport{
|
|
||||||
dialer: dialer,
|
|
||||||
serverAddr: serverAddr,
|
|
||||||
},
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPTransport) Reset() {
|
|
||||||
t.access.Lock()
|
|
||||||
defer t.access.Unlock()
|
|
||||||
close(t.done)
|
|
||||||
t.done = make(chan struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
response, err := t.exchange(ctx, message)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if response.Truncated {
|
|
||||||
t.logger.InfoContext(ctx, "response truncated, retrying with TCP")
|
|
||||||
return t.tcpTransport.Exchange(ctx, message)
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
conn, err := t.open(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if edns0Opt := message.IsEdns0(); edns0Opt != nil {
|
|
||||||
if udpSize := int(edns0Opt.UDPSize()); udpSize > t.udpSize {
|
|
||||||
t.udpSize = udpSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buffer := buf.NewSize(1 + message.Len())
|
|
||||||
defer buffer.Release()
|
|
||||||
exMessage := *message
|
|
||||||
exMessage.Compress = true
|
|
||||||
messageId := message.Id
|
|
||||||
callback := &dnsCallback{
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
conn.access.Lock()
|
|
||||||
conn.queryId++
|
|
||||||
exMessage.Id = conn.queryId
|
|
||||||
conn.callbacks[exMessage.Id] = callback
|
|
||||||
conn.access.Unlock()
|
|
||||||
defer func() {
|
|
||||||
conn.access.Lock()
|
|
||||||
delete(conn.callbacks, messageId)
|
|
||||||
conn.access.Unlock()
|
|
||||||
callback.access.Lock()
|
|
||||||
select {
|
|
||||||
case <-callback.done:
|
|
||||||
default:
|
|
||||||
close(callback.done)
|
|
||||||
}
|
|
||||||
callback.access.Unlock()
|
|
||||||
}()
|
|
||||||
rawMessage, err := exMessage.PackBuffer(buffer.FreeBytes())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = conn.Write(rawMessage)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-callback.done:
|
|
||||||
callback.message.Id = messageId
|
|
||||||
return callback.message, nil
|
|
||||||
case <-conn.done:
|
|
||||||
return nil, conn.err
|
|
||||||
case <-t.done:
|
|
||||||
return nil, os.ErrClosed
|
|
||||||
case <-ctx.Done():
|
|
||||||
conn.Close(ctx.Err())
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPTransport) open(ctx context.Context) (*dnsConnection, error) {
|
|
||||||
t.access.Lock()
|
|
||||||
defer t.access.Unlock()
|
|
||||||
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dnsConn := &dnsConnection{
|
|
||||||
Conn: conn,
|
|
||||||
done: make(chan struct{}),
|
|
||||||
callbacks: make(map[uint16]*dnsCallback),
|
|
||||||
}
|
|
||||||
go t.recvLoop(dnsConn)
|
|
||||||
return dnsConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPTransport) recvLoop(conn *dnsConnection) {
|
|
||||||
for {
|
|
||||||
buffer := buf.NewSize(t.udpSize)
|
|
||||||
_, err := buffer.ReadOnceFrom(conn)
|
|
||||||
if err != nil {
|
|
||||||
buffer.Release()
|
|
||||||
conn.Close(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var message mDNS.Msg
|
|
||||||
err = message.Unpack(buffer.Bytes())
|
|
||||||
buffer.Release()
|
|
||||||
if err != nil {
|
|
||||||
conn.Close(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conn.access.RLock()
|
|
||||||
callback, loaded := conn.callbacks[message.Id]
|
|
||||||
conn.access.RUnlock()
|
|
||||||
if !loaded {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
callback.access.Lock()
|
|
||||||
select {
|
|
||||||
case <-callback.done:
|
|
||||||
default:
|
|
||||||
callback.message = &message
|
|
||||||
close(callback.done)
|
|
||||||
}
|
|
||||||
callback.access.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type dnsConnection struct {
|
|
||||||
net.Conn
|
|
||||||
access sync.RWMutex
|
|
||||||
done chan struct{}
|
|
||||||
closeOnce sync.Once
|
|
||||||
err error
|
|
||||||
queryId uint16
|
|
||||||
callbacks map[uint16]*dnsCallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *dnsConnection) Close(err error) {
|
|
||||||
c.access.Lock()
|
|
||||||
defer c.access.Unlock()
|
|
||||||
c.closeOnce.Do(func() {
|
|
||||||
close(c.done)
|
|
||||||
c.err = err
|
|
||||||
})
|
|
||||||
c.Conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
type dnsCallback struct {
|
|
||||||
access sync.Mutex
|
|
||||||
message *mDNS.Msg
|
|
||||||
done chan struct{}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.LegacyDNSTransport = (*TransportAdapter)(nil)
|
|
||||||
|
|
||||||
type TransportAdapter struct {
|
|
||||||
transportType string
|
|
||||||
transportTag string
|
|
||||||
dependencies []string
|
|
||||||
strategy C.DomainStrategy
|
|
||||||
clientSubnet netip.Prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
|
|
||||||
return TransportAdapter{
|
|
||||||
transportType: transportType,
|
|
||||||
transportTag: transportTag,
|
|
||||||
dependencies: dependencies,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTransportAdapterWithLocalOptions(transportType string, transportTag string, localOptions option.LocalDNSServerOptions) TransportAdapter {
|
|
||||||
var dependencies []string
|
|
||||||
if localOptions.DomainResolver != nil && localOptions.DomainResolver.Server != "" {
|
|
||||||
dependencies = append(dependencies, localOptions.DomainResolver.Server)
|
|
||||||
}
|
|
||||||
return TransportAdapter{
|
|
||||||
transportType: transportType,
|
|
||||||
transportTag: transportTag,
|
|
||||||
dependencies: dependencies,
|
|
||||||
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
|
|
||||||
clientSubnet: localOptions.LegacyClientSubnet,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTransportAdapterWithRemoteOptions(transportType string, transportTag string, remoteOptions option.RemoteDNSServerOptions) TransportAdapter {
|
|
||||||
var dependencies []string
|
|
||||||
if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" {
|
|
||||||
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
|
|
||||||
}
|
|
||||||
if remoteOptions.LegacyAddressResolver != "" {
|
|
||||||
dependencies = append(dependencies, remoteOptions.LegacyAddressResolver)
|
|
||||||
}
|
|
||||||
return TransportAdapter{
|
|
||||||
transportType: transportType,
|
|
||||||
transportTag: transportTag,
|
|
||||||
dependencies: dependencies,
|
|
||||||
strategy: C.DomainStrategy(remoteOptions.LegacyStrategy),
|
|
||||||
clientSubnet: remoteOptions.LegacyClientSubnet,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *TransportAdapter) Type() string {
|
|
||||||
return a.transportType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *TransportAdapter) Tag() string {
|
|
||||||
return a.transportTag
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *TransportAdapter) Dependencies() []string {
|
|
||||||
return a.dependencies
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy {
|
|
||||||
return a.strategy
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *TransportAdapter) LegacyClientSubnet() netip.Prefix {
|
|
||||||
return a.clientSubnet
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) {
|
|
||||||
if options.LegacyDefaultDialer {
|
|
||||||
return dialer.NewDefaultOutbound(ctx), nil
|
|
||||||
} else {
|
|
||||||
return dialer.NewWithOptions(dialer.Options{
|
|
||||||
Context: ctx,
|
|
||||||
Options: options.DialerOptions,
|
|
||||||
DirectResolver: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
|
|
||||||
if options.LegacyDefaultDialer {
|
|
||||||
transportDialer := dialer.NewDefaultOutbound(ctx)
|
|
||||||
if options.LegacyAddressResolver != "" {
|
|
||||||
transport := service.FromContext[adapter.DNSTransportManager](ctx)
|
|
||||||
resolverTransport, loaded := transport.Transport(options.LegacyAddressResolver)
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("address resolver not found: ", options.LegacyAddressResolver)
|
|
||||||
}
|
|
||||||
transportDialer = newTransportDialer(transportDialer, service.FromContext[adapter.DNSRouter](ctx), resolverTransport, C.DomainStrategy(options.LegacyAddressStrategy), time.Duration(options.LegacyAddressFallbackDelay))
|
|
||||||
} else if options.ServerIsDomain() {
|
|
||||||
return nil, E.New("missing address resolver for server: ", options.Server)
|
|
||||||
}
|
|
||||||
return transportDialer, nil
|
|
||||||
} else {
|
|
||||||
return dialer.NewWithOptions(dialer.Options{
|
|
||||||
Context: ctx,
|
|
||||||
Options: options.DialerOptions,
|
|
||||||
RemoteIsDomain: options.ServerIsDomain(),
|
|
||||||
DirectResolver: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type legacyTransportDialer struct {
|
|
||||||
dialer N.Dialer
|
|
||||||
dnsRouter adapter.DNSRouter
|
|
||||||
transport adapter.DNSTransport
|
|
||||||
strategy C.DomainStrategy
|
|
||||||
fallbackDelay time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *legacyTransportDialer {
|
|
||||||
return &legacyTransportDialer{
|
|
||||||
dialer,
|
|
||||||
dnsRouter,
|
|
||||||
transport,
|
|
||||||
strategy,
|
|
||||||
fallbackDelay,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *legacyTransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
||||||
if destination.IsIP() {
|
|
||||||
return d.dialer.DialContext(ctx, network, destination)
|
|
||||||
}
|
|
||||||
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
|
|
||||||
Transport: d.transport,
|
|
||||||
Strategy: d.strategy,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *legacyTransportDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
|
||||||
if destination.IsIP() {
|
|
||||||
return d.dialer.ListenPacket(ctx, destination)
|
|
||||||
}
|
|
||||||
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
|
|
||||||
Transport: d.transport,
|
|
||||||
Strategy: d.strategy,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
conn, _, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
|
|
||||||
return conn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *legacyTransportDialer) Upstream() any {
|
|
||||||
return d.dialer
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.DNSTransportManager = (*TransportManager)(nil)
|
|
||||||
|
|
||||||
type TransportManager struct {
|
|
||||||
logger log.ContextLogger
|
|
||||||
registry adapter.DNSTransportRegistry
|
|
||||||
outbound adapter.OutboundManager
|
|
||||||
defaultTag string
|
|
||||||
access sync.RWMutex
|
|
||||||
started bool
|
|
||||||
stage adapter.StartStage
|
|
||||||
transports []adapter.DNSTransport
|
|
||||||
transportByTag map[string]adapter.DNSTransport
|
|
||||||
dependByTag map[string][]string
|
|
||||||
defaultTransport adapter.DNSTransport
|
|
||||||
defaultTransportFallback adapter.DNSTransport
|
|
||||||
fakeIPTransport adapter.FakeIPTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTransportManager(logger logger.ContextLogger, registry adapter.DNSTransportRegistry, outbound adapter.OutboundManager, defaultTag string) *TransportManager {
|
|
||||||
return &TransportManager{
|
|
||||||
logger: logger,
|
|
||||||
registry: registry,
|
|
||||||
outbound: outbound,
|
|
||||||
defaultTag: defaultTag,
|
|
||||||
transportByTag: make(map[string]adapter.DNSTransport),
|
|
||||||
dependByTag: make(map[string][]string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TransportManager) Initialize(defaultTransportFallback adapter.DNSTransport) {
|
|
||||||
m.defaultTransportFallback = defaultTransportFallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TransportManager) Start(stage adapter.StartStage) error {
|
|
||||||
m.access.Lock()
|
|
||||||
if m.started && m.stage >= stage {
|
|
||||||
panic("already started")
|
|
||||||
}
|
|
||||||
m.started = true
|
|
||||||
m.stage = stage
|
|
||||||
outbounds := m.transports
|
|
||||||
m.access.Unlock()
|
|
||||||
if stage == adapter.StartStateStart {
|
|
||||||
return m.startTransports(m.transports)
|
|
||||||
} else {
|
|
||||||
for _, outbound := range outbounds {
|
|
||||||
err := adapter.LegacyStart(outbound, stage)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, stage, " dns/", outbound.Type(), "[", outbound.Tag(), "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TransportManager) startTransports(transports []adapter.DNSTransport) error {
|
|
||||||
monitor := taskmonitor.New(m.logger, C.StartTimeout)
|
|
||||||
started := make(map[string]bool)
|
|
||||||
for {
|
|
||||||
canContinue := false
|
|
||||||
startOne:
|
|
||||||
for _, transportToStart := range transports {
|
|
||||||
transportTag := transportToStart.Tag()
|
|
||||||
if started[transportTag] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dependencies := transportToStart.Dependencies()
|
|
||||||
for _, dependency := range dependencies {
|
|
||||||
if !started[dependency] {
|
|
||||||
continue startOne
|
|
||||||
}
|
|
||||||
}
|
|
||||||
started[transportTag] = true
|
|
||||||
canContinue = true
|
|
||||||
if starter, isStarter := transportToStart.(adapter.Lifecycle); isStarter {
|
|
||||||
monitor.Start("start dns/", transportToStart.Type(), "[", transportTag, "]")
|
|
||||||
err := starter.Start(adapter.StartStateStart)
|
|
||||||
monitor.Finish()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "start dns/", transportToStart.Type(), "[", transportTag, "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(started) == len(transports) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if canContinue {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
currentTransport := common.Find(transports, func(it adapter.DNSTransport) bool {
|
|
||||||
return !started[it.Tag()]
|
|
||||||
})
|
|
||||||
var lintTransport func(oTree []string, oCurrent adapter.DNSTransport) error
|
|
||||||
lintTransport = func(oTree []string, oCurrent adapter.DNSTransport) error {
|
|
||||||
problemTransportTag := common.Find(oCurrent.Dependencies(), func(it string) bool {
|
|
||||||
return !started[it]
|
|
||||||
})
|
|
||||||
if common.Contains(oTree, problemTransportTag) {
|
|
||||||
return E.New("circular server dependency: ", strings.Join(oTree, " -> "), " -> ", problemTransportTag)
|
|
||||||
}
|
|
||||||
m.access.Lock()
|
|
||||||
problemTransport := m.transportByTag[problemTransportTag]
|
|
||||||
m.access.Unlock()
|
|
||||||
if problemTransport == nil {
|
|
||||||
return E.New("dependency[", problemTransportTag, "] not found for server[", oCurrent.Tag(), "]")
|
|
||||||
}
|
|
||||||
return lintTransport(append(oTree, problemTransportTag), problemTransport)
|
|
||||||
}
|
|
||||||
return lintTransport([]string{currentTransport.Tag()}, currentTransport)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TransportManager) Close() error {
|
|
||||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
|
||||||
m.access.Lock()
|
|
||||||
if !m.started {
|
|
||||||
m.access.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
m.started = false
|
|
||||||
transports := m.transports
|
|
||||||
m.transports = nil
|
|
||||||
m.access.Unlock()
|
|
||||||
var err error
|
|
||||||
for _, transport := range transports {
|
|
||||||
if closer, isCloser := transport.(io.Closer); isCloser {
|
|
||||||
monitor.Start("close server/", transport.Type(), "[", transport.Tag(), "]")
|
|
||||||
err = E.Append(err, closer.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close server/", transport.Type(), "[", transport.Tag(), "]")
|
|
||||||
})
|
|
||||||
monitor.Finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TransportManager) Transports() []adapter.DNSTransport {
|
|
||||||
m.access.RLock()
|
|
||||||
defer m.access.RUnlock()
|
|
||||||
return m.transports
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TransportManager) Transport(tag string) (adapter.DNSTransport, bool) {
|
|
||||||
m.access.RLock()
|
|
||||||
outbound, found := m.transportByTag[tag]
|
|
||||||
m.access.RUnlock()
|
|
||||||
return outbound, found
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TransportManager) Default() adapter.DNSTransport {
|
|
||||||
m.access.RLock()
|
|
||||||
defer m.access.RUnlock()
|
|
||||||
if m.defaultTransport != nil {
|
|
||||||
return m.defaultTransport
|
|
||||||
} else {
|
|
||||||
return m.defaultTransportFallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TransportManager) FakeIP() adapter.FakeIPTransport {
|
|
||||||
m.access.RLock()
|
|
||||||
defer m.access.RUnlock()
|
|
||||||
return m.fakeIPTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TransportManager) Remove(tag string) error {
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
transport, found := m.transportByTag[tag]
|
|
||||||
if !found {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
delete(m.transportByTag, tag)
|
|
||||||
index := common.Index(m.transports, func(it adapter.DNSTransport) bool {
|
|
||||||
return it == transport
|
|
||||||
})
|
|
||||||
if index == -1 {
|
|
||||||
panic("invalid inbound index")
|
|
||||||
}
|
|
||||||
m.transports = append(m.transports[:index], m.transports[index+1:]...)
|
|
||||||
started := m.started
|
|
||||||
if m.defaultTransport == transport {
|
|
||||||
if len(m.transports) > 0 {
|
|
||||||
nextTransport := m.transports[0]
|
|
||||||
if nextTransport.Type() != C.DNSTypeFakeIP {
|
|
||||||
return E.New("default server cannot be fakeip")
|
|
||||||
}
|
|
||||||
m.defaultTransport = nextTransport
|
|
||||||
m.logger.Info("updated default server to ", m.defaultTransport.Tag())
|
|
||||||
} else {
|
|
||||||
m.defaultTransport = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dependBy := m.dependByTag[tag]
|
|
||||||
if len(dependBy) > 0 {
|
|
||||||
return E.New("server[", tag, "] is depended by ", strings.Join(dependBy, ", "))
|
|
||||||
}
|
|
||||||
dependencies := transport.Dependencies()
|
|
||||||
for _, dependency := range dependencies {
|
|
||||||
if len(m.dependByTag[dependency]) == 1 {
|
|
||||||
delete(m.dependByTag, dependency)
|
|
||||||
} else {
|
|
||||||
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
|
|
||||||
return it != tag
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if started {
|
|
||||||
transport.Reset()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TransportManager) Create(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) error {
|
|
||||||
if tag == "" {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
transport, err := m.registry.CreateDNSTransport(ctx, logger, tag, transportType, options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
if m.started {
|
|
||||||
for _, stage := range adapter.ListStartStages {
|
|
||||||
err = adapter.LegacyStart(transport, stage)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, stage, " dns/", transport.Type(), "[", transport.Tag(), "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if existsTransport, loaded := m.transportByTag[tag]; loaded {
|
|
||||||
if m.started {
|
|
||||||
err = common.Close(existsTransport)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "close dns/", existsTransport.Type(), "[", existsTransport.Tag(), "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
existsIndex := common.Index(m.transports, func(it adapter.DNSTransport) bool {
|
|
||||||
return it == existsTransport
|
|
||||||
})
|
|
||||||
if existsIndex == -1 {
|
|
||||||
panic("invalid inbound index")
|
|
||||||
}
|
|
||||||
m.transports = append(m.transports[:existsIndex], m.transports[existsIndex+1:]...)
|
|
||||||
}
|
|
||||||
m.transports = append(m.transports, transport)
|
|
||||||
m.transportByTag[tag] = transport
|
|
||||||
dependencies := transport.Dependencies()
|
|
||||||
for _, dependency := range dependencies {
|
|
||||||
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
|
|
||||||
}
|
|
||||||
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultTransport == nil) {
|
|
||||||
if transport.Type() == C.DNSTypeFakeIP {
|
|
||||||
return E.New("default server cannot be fakeip")
|
|
||||||
}
|
|
||||||
m.defaultTransport = transport
|
|
||||||
if m.started {
|
|
||||||
m.logger.Info("updated default server to ", transport.Tag())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if transport.Type() == C.DNSTypeFakeIP {
|
|
||||||
if m.fakeIPTransport != nil {
|
|
||||||
return E.New("multiple fakeip server are not supported")
|
|
||||||
}
|
|
||||||
m.fakeIPTransport = transport.(adapter.FakeIPTransport)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user