Compare commits

..
Author SHA1 Message Date
Fangliding a0bf239fc1 Add maxReuseTimes 2026-02-18 00:30:51 +08:00
Fangliding dee64ef240 little refactor 2026-02-18 00:30:51 +08:00
Fangliding 0a1b5bfb51 Bench 2026-02-05 20:25:32 +08:00
515 changed files with 13345 additions and 32491 deletions
+2 -1
View File
@@ -29,5 +29,6 @@
"openbsd-arm7": { "friendlyName": "openbsd-arm32-v7a" }, "openbsd-arm7": { "friendlyName": "openbsd-arm32-v7a" },
"windows-386": { "friendlyName": "windows-32" }, "windows-386": { "friendlyName": "windows-32" },
"windows-amd64": { "friendlyName": "windows-64" }, "windows-amd64": { "friendlyName": "windows-64" },
"windows-arm64": { "friendlyName": "windows-arm64-v8a" } "windows-arm64": { "friendlyName": "windows-arm64-v8a" },
"windows-arm7": { "friendlyName": "windows-arm32-v7a" }
} }
+5 -8
View File
@@ -32,12 +32,9 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/03_routing.json
RUN echo '{}' >/tmp/usr/local/etc/xray/04_policy.json RUN echo '{}' >/tmp/usr/local/etc/xray/04_policy.json
RUN echo '{}' >/tmp/usr/local/etc/xray/05_inbounds.json RUN echo '{}' >/tmp/usr/local/etc/xray/05_inbounds.json
RUN echo '{}' >/tmp/usr/local/etc/xray/06_outbounds.json RUN echo '{}' >/tmp/usr/local/etc/xray/06_outbounds.json
RUN echo '{}' >/tmp/usr/local/etc/xray/07_stats.json RUN echo '{}' >/tmp/usr/local/etc/xray/07_transport.json
RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json RUN echo '{}' >/tmp/usr/local/etc/xray/08_stats.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json RUN echo '{}' >/tmp/usr/local/etc/xray/09_reverse.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files # Create log files
RUN mkdir -p /tmp/var/log/xray && touch \ RUN mkdir -p /tmp/var/log/xray && touch \
@@ -48,8 +45,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
FROM gcr.io/distroless/static:nonroot FROM gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+5 -8
View File
@@ -32,12 +32,9 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/03_routing.json
RUN echo '{}' >/tmp/usr/local/etc/xray/04_policy.json RUN echo '{}' >/tmp/usr/local/etc/xray/04_policy.json
RUN echo '{}' >/tmp/usr/local/etc/xray/05_inbounds.json RUN echo '{}' >/tmp/usr/local/etc/xray/05_inbounds.json
RUN echo '{}' >/tmp/usr/local/etc/xray/06_outbounds.json RUN echo '{}' >/tmp/usr/local/etc/xray/06_outbounds.json
RUN echo '{}' >/tmp/usr/local/etc/xray/07_stats.json RUN echo '{}' >/tmp/usr/local/etc/xray/07_transport.json
RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json RUN echo '{}' >/tmp/usr/local/etc/xray/08_stats.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json RUN echo '{}' >/tmp/usr/local/etc/xray/09_reverse.json
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files # Create log files
RUN mkdir -p /tmp/var/log/xray && touch \ RUN mkdir -p /tmp/var/log/xray && touch \
@@ -57,8 +54,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+5 -5
View File
@@ -68,13 +68,13 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -82,7 +82,7 @@ jobs:
- name: Build Docker image (main architectures) - name: Build Docker image (main architectures)
id: build_main_arches id: build_main_arches
uses: docker/build-push-action@v7 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: .github/docker/Dockerfile file: .github/docker/Dockerfile
@@ -97,7 +97,7 @@ jobs:
- name: Build Docker image (additional architectures) - name: Build Docker image (additional architectures)
id: build_additional_arches id: build_additional_arches
uses: docker/build-push-action@v7 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: .github/docker/Dockerfile.usa file: .github/docker/Dockerfile.usa
+1 -3
View File
@@ -11,7 +11,6 @@ on:
jobs: jobs:
check-assets: check-assets:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps: steps:
- name: Restore Geodat Cache - name: Restore Geodat Cache
uses: actions/cache/restore@v5 uses: actions/cache/restore@v5
@@ -76,7 +75,6 @@ jobs:
fail-fast: false fail-fast: false
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
env: env:
GOOS: ${{ matrix.goos}} GOOS: ${{ matrix.goos}}
GOARCH: ${{ matrix.goarch }} GOARCH: ${{ matrix.goarch }}
@@ -175,7 +173,7 @@ jobs:
file_glob: true file_glob: true
- name: Upload files to Artifacts - name: Upload files to Artifacts
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: Xray-${{ env.ASSET_NAME }} name: Xray-${{ env.ASSET_NAME }}
path: | path: |
+9 -8
View File
@@ -11,7 +11,6 @@ on:
jobs: jobs:
check-assets: check-assets:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps: steps:
- name: Restore Geodat Cache - name: Restore Geodat Cache
uses: actions/cache/restore@v5 uses: actions/cache/restore@v5
@@ -41,7 +40,7 @@ jobs:
break break
fi fi
done done
LIST=('amd64' 'x86' 'arm64') LIST=('amd64' 'x86' 'arm64' 'arm')
for ARCHITECTURE in "${LIST[@]}" for ARCHITECTURE in "${LIST[@]}"
do do
echo -e "Checking wintun.dll for ${ARCHITECTURE}..." echo -e "Checking wintun.dll for ${ARCHITECTURE}..."
@@ -56,7 +55,7 @@ jobs:
- name: Trigger Asset Update Workflow if Assets Missing - name: Trigger Asset Update Workflow if Assets Missing
if: steps.check-assets.outputs.missing == 'true' if: steps.check-assets.outputs.missing == 'true'
uses: actions/github-script@v9 uses: actions/github-script@v8
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |
@@ -115,6 +114,9 @@ jobs:
# Windows ARM # Windows ARM
- goos: windows - goos: windows
goarch: arm64 goarch: arm64
- goos: windows
goarch: arm
goarm: 7
# BEGIN Other architectures # BEGIN Other architectures
# BEGIN riscv64 & ARM64 & LOONG64 # BEGIN riscv64 & ARM64 & LOONG64
- goos: linux - goos: linux
@@ -162,7 +164,6 @@ jobs:
fail-fast: false fail-fast: false
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
env: env:
GOOS: ${{ matrix.goos }} GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }} GOARCH: ${{ matrix.goarch }}
@@ -210,9 +211,6 @@ jobs:
go build -o build_assets/xray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main go build -o build_assets/xray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
# The line below is for without running conhost.exe version. Commented for not being used. Provided for reference. # The line below is for without running conhost.exe version. Commented for not being used. Provided for reference.
# go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main # go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
elif [[ ${GOOS} == 'android' ]]; then
echo 'Building Xray for Android...'
go build -o build_assets/xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid= -checklinkname=0" -v ./main
else else
echo 'Building Xray...' echo 'Building Xray...'
if [[ ${GOARCH} == 'mips' || ${GOARCH} == 'mipsle' ]]; then if [[ ${GOARCH} == 'mips' || ${GOARCH} == 'mipsle' ]]; then
@@ -252,6 +250,9 @@ jobs:
if [[ ${GOARCH} == 'arm64' ]]; then if [[ ${GOARCH} == 'arm64' ]]; then
mv resources/wintun/bin/arm64/wintun.dll build_assets/ mv resources/wintun/bin/arm64/wintun.dll build_assets/
fi fi
if [[ ${GOARCH} == 'arm' ]]; then
mv resources/wintun/bin/arm/wintun.dll build_assets/
fi
mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt
fi fi
@@ -285,7 +286,7 @@ jobs:
file_glob: true file_glob: true
- name: Upload files to Artifacts - name: Upload files to Artifacts
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v6
with: with:
name: Xray-${{ env.ASSET_NAME }} name: Xray-${{ env.ASSET_NAME }}
path: | path: |
@@ -33,7 +33,7 @@ jobs:
- name: Update Geodat - name: Update Geodat
id: update id: update
uses: nick-fields/retry@v4 uses: nick-fields/retry@v3
with: with:
timeout_minutes: 60 timeout_minutes: 60
retry_wait_seconds: 60 retry_wait_seconds: 60
@@ -82,14 +82,14 @@ jobs:
- name: Update Wintun - name: Update Wintun
id: update id: update
uses: nick-fields/retry@v4 uses: nick-fields/retry@v3
with: with:
timeout_minutes: 60 timeout_minutes: 60
retry_wait_seconds: 60 retry_wait_seconds: 60
max_attempts: 60 max_attempts: 60
command: | command: |
[ -d 'resources' ] || mkdir resources [ -d 'resources' ] || mkdir resources
LIST=('amd64' 'x86' 'arm64') LIST=('amd64' 'x86' 'arm64' 'arm')
for ARCHITECTURE in "${LIST[@]}" for ARCHITECTURE in "${LIST[@]}"
do do
FILE_PATH="resources/wintun/bin/${ARCHITECTURE}/wintun.dll" FILE_PATH="resources/wintun/bin/${ARCHITECTURE}/wintun.dll"
+1 -39
View File
@@ -1,4 +1,4 @@
name: Tests and Checkings name: Test
on: on:
push: push:
@@ -8,7 +8,6 @@ on:
jobs: jobs:
check-assets: check-assets:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps: steps:
- name: Restore Geodat Cache - name: Restore Geodat Cache
uses: actions/cache/restore@v5 uses: actions/cache/restore@v5
@@ -35,45 +34,8 @@ jobs:
if: steps.check-assets.outputs.missing == 'true' if: steps.check-assets.outputs.missing == 'true'
run: sleep 90 run: sleep 90
check-proto:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Checkout codebase
uses: actions/checkout@v6
- name: Check Proto Version Header
run: |
head -n 4 core/config.pb.go > ref.txt
find . -name "*.pb.go" ! -name "*_grpc.pb.go" -print0 | while IFS= read -r -d '' file; do
if ! cmp -s ref.txt <(head -n 4 "$file"); then
echo "Error: Header mismatch in $file"
head -n 4 "$file"
exit 1
fi
done
check-format:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
permissions:
contents: read
steps:
- name: Checkout codebase
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
cache: false
- name: Check Format
run: |
go install -v mvdan.cc/gofumpt@latest
go run ./infra/vformat/main.go -mode check -pwd ./
test: test:
needs: check-assets needs: check-assets
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
permissions: permissions:
contents: read contents: read
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
+1 -25
View File
@@ -10,8 +10,6 @@
[![Happ](https://github.com/user-attachments/assets/14055dab-e8bb-48bd-89e8-962709e4098e)](https://happ.su) [![Happ](https://github.com/user-attachments/assets/14055dab-e8bb-48bd-89e8-962709e4098e)](https://happ.su)
[![BlancVPN](https://github.com/user-attachments/assets/9145ea7d-5da3-446e-8143-710dba4292c3)](https://blanc.link/VMTSDqW)
[**Sponsor Xray-core**](https://github.com/XTLS/Xray-core/issues/3668) [**Sponsor Xray-core**](https://github.com/XTLS/Xray-core/issues/3668)
## Donation & NFTs ## Donation & NFTs
@@ -67,12 +65,12 @@
- [Marzban](https://github.com/Gozargah/Marzban) - [Marzban](https://github.com/Gozargah/Marzban)
- [Hiddify](https://github.com/hiddify/Hiddify-Manager) - [Hiddify](https://github.com/hiddify/Hiddify-Manager)
- [TX-UI](https://github.com/AghayeCoder/tx-ui) - [TX-UI](https://github.com/AghayeCoder/tx-ui)
- [CELERITY](https://github.com/ClickDevTech/CELERITY-panel)
- One Click - One Click
- [Xray-REALITY](https://github.com/zxcvos/Xray-script), [xray-reality](https://github.com/sajjaddg/xray-reality), [reality-ezpz](https://github.com/aleskxyz/reality-ezpz) - [Xray-REALITY](https://github.com/zxcvos/Xray-script), [xray-reality](https://github.com/sajjaddg/xray-reality), [reality-ezpz](https://github.com/aleskxyz/reality-ezpz)
- [Xray_bash_onekey](https://github.com/hello-yunshu/Xray_bash_onekey), [XTool](https://github.com/LordPenguin666/XTool), [VPainLess](https://github.com/vpainless/vpainless) - [Xray_bash_onekey](https://github.com/hello-yunshu/Xray_bash_onekey), [XTool](https://github.com/LordPenguin666/XTool), [VPainLess](https://github.com/vpainless/vpainless)
- [v2ray-agent](https://github.com/mack-a/v2ray-agent), [Xray_onekey](https://github.com/wulabing/Xray_onekey), [ProxySU](https://github.com/proxysu/ProxySU) - [v2ray-agent](https://github.com/mack-a/v2ray-agent), [Xray_onekey](https://github.com/wulabing/Xray_onekey), [ProxySU](https://github.com/proxysu/ProxySU)
- Magisk - Magisk
- [Xray4Magisk](https://github.com/Asterisk4Magisk/Xray4Magisk)
- [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk) - [Xray_For_Magisk](https://github.com/E7KMbb/Xray_For_Magisk)
- Homebrew - Homebrew
- `brew install xray` - `brew install xray`
@@ -108,23 +106,16 @@
- [Furious](https://github.com/LorenEteval/Furious) - [Furious](https://github.com/LorenEteval/Furious)
- [Invisible Man - Xray](https://github.com/InvisibleManVPN/InvisibleMan-XRayClient) - [Invisible Man - Xray](https://github.com/InvisibleManVPN/InvisibleMan-XRayClient)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal) - [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [GenyConnect](https://github.com/genyleap/GenyConnect)
- [OneXray](https://github.com/OneXray/OneXray)
- [XrayUI-dev](https://github.com/PhoenixNil/XrayUI-dev)
- Android - Android
- [v2rayNG](https://github.com/2dust/v2rayNG) - [v2rayNG](https://github.com/2dust/v2rayNG)
- [X-flutter](https://github.com/XTLS/X-flutter) - [X-flutter](https://github.com/XTLS/X-flutter)
- [SaeedDev94/Xray](https://github.com/SaeedDev94/Xray) - [SaeedDev94/Xray](https://github.com/SaeedDev94/Xray)
- [SimpleXray](https://github.com/lhear/SimpleXray) - [SimpleXray](https://github.com/lhear/SimpleXray)
- [XrayFA](https://github.com/Q7DF1/XrayFA)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal) - [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [OneXray](https://github.com/OneXray/OneXray)
- [AsteriskNG](https://github.com/Asterisk4Magisk/AsteriskNG)
- iOS & macOS arm64 & tvOS - iOS & macOS arm64 & tvOS
- [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973) | [Happ tvOS](https://apps.apple.com/us/app/happ-proxy-utility-for-tv/id6748297274) - [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973) | [Happ tvOS](https://apps.apple.com/us/app/happ-proxy-utility-for-tv/id6748297274)
- [Streisand](https://apps.apple.com/app/streisand/id6450534064) - [Streisand](https://apps.apple.com/app/streisand/id6450534064)
- [OneXray](https://github.com/OneXray/OneXray) - [OneXray](https://github.com/OneXray/OneXray)
- [INCY](https://apps.apple.com/en/app/incy/id6756943388)
- macOS arm64 & x64 - macOS arm64 & x64
- [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973) - [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215) | [Happ RU](https://apps.apple.com/ru/app/happ-proxy-utility-plus/id6746188973)
- [V2rayU](https://github.com/yanue/V2rayU) - [V2rayU](https://github.com/yanue/V2rayU)
@@ -134,8 +125,6 @@
- [GoXRay](https://github.com/goxray/desktop) - [GoXRay](https://github.com/goxray/desktop)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal) - [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [v2rayN](https://github.com/2dust/v2rayN) - [v2rayN](https://github.com/2dust/v2rayN)
- [GenyConnect](https://github.com/genyleap/GenyConnect)
- [INCY](https://apps.apple.com/en/app/incy/id6756943388)
- Linux - Linux
- [v2rayA](https://github.com/v2rayA/v2rayA) - [v2rayA](https://github.com/v2rayA/v2rayA)
- [Furious](https://github.com/LorenEteval/Furious) - [Furious](https://github.com/LorenEteval/Furious)
@@ -143,15 +132,10 @@
- [GoXRay](https://github.com/goxray/desktop) - [GoXRay](https://github.com/goxray/desktop)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal) - [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [v2rayN](https://github.com/2dust/v2rayN) - [v2rayN](https://github.com/2dust/v2rayN)
- [GenyConnect](https://github.com/genyleap/GenyConnect)
- [OneXray](https://github.com/OneXray/OneXray)
- HarmonyOS
- [Hey](https://github.com/popsiclelmlm/Hey)
## Others that support VLESS, XTLS, REALITY, XUDP, PLUX... ## Others that support VLESS, XTLS, REALITY, XUDP, PLUX...
- iOS & macOS arm64 & tvOS - iOS & macOS arm64 & tvOS
- [Anywhere](https://github.com/NodePassProject/Anywhere)
- [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118) - [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118)
- [Loon](https://apps.apple.com/us/app/loon/id1373567447) - [Loon](https://apps.apple.com/us/app/loon/id1373567447)
- [Egern](https://apps.apple.com/us/app/egern/id1616105820) - [Egern](https://apps.apple.com/us/app/egern/id1616105820)
@@ -164,7 +148,6 @@
- [xtls-sdk](https://github.com/remnawave/xtls-sdk) - [xtls-sdk](https://github.com/remnawave/xtls-sdk)
- [xtlsapi](https://github.com/hiddify/xtlsapi) - [xtlsapi](https://github.com/hiddify/xtlsapi)
- [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite) - [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite)
- [flutter_vless](https://github.com/XIIIFOX/flutter_vless)
- [Xray-core-python](https://github.com/LorenEteval/Xray-core-python) - [Xray-core-python](https://github.com/LorenEteval/Xray-core-python)
- [xray-api](https://github.com/XVGuardian/xray-api) - [xray-api](https://github.com/XVGuardian/xray-api)
- [XrayR](https://github.com/XrayR-project/XrayR) - [XrayR](https://github.com/XrayR-project/XrayR)
@@ -209,13 +192,6 @@ Make sure that you are using the same Go version, and remember to set the git co
CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid=" -v ./main CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid=" -v ./main
``` ```
For Android:
```bash
GOOS=android GOARCH=arm64 CGO_ENABLED=1 CC=/path/to/aarch64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0" -v ./main
GOOS=android GOARCH=amd64 CGO_ENABLED=1 CC=/path/to/x86_64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0" -v ./main
```
If you are compiling a 32-bit MIPS/MIPSLE target, use this command instead: If you are compiling a 32-bit MIPS/MIPSLE target, use this command instead:
```bash ```bash
+5 -19
View File
@@ -3,7 +3,6 @@ package commander
import ( import (
"context" "context"
"net" "net"
"strings"
"sync" "sync"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
@@ -11,7 +10,6 @@ import (
"github.com/xtls/xray-core/common/signal/done" "github.com/xtls/xray-core/common/signal/done"
core "github.com/xtls/xray-core/core" core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/transport/internet"
"google.golang.org/grpc" "google.golang.org/grpc"
) )
@@ -69,32 +67,20 @@ func (c *Commander) Start() error {
} }
c.Unlock() c.Unlock()
listen := func(listener net.Listener) { var listen = func(listener net.Listener) {
if err := c.server.Serve(listener); err != nil { if err := c.server.Serve(listener); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start grpc server") errors.LogErrorInner(context.Background(), err, "failed to start grpc server")
} }
} }
if len(c.listen) > 0 { if len(c.listen) > 0 {
var addr net.Addr if l, err := net.Listen("tcp", c.listen); err != nil {
if strings.HasPrefix(c.listen, "/") || strings.HasPrefix(c.listen, "@") {
addr = &net.UnixAddr{Name: c.listen, Net: "unix"}
} else {
tcpAddr, err := net.ResolveTCPAddr("tcp", c.listen)
if err != nil {
errors.LogErrorInner(context.Background(), err, "API server failed to parse listen address ", c.listen)
return err
}
addr = tcpAddr
}
l, err := internet.ListenSystem(context.Background(), addr, nil)
if err != nil {
errors.LogErrorInner(context.Background(), err, "API server failed to listen on ", c.listen) errors.LogErrorInner(context.Background(), err, "API server failed to listen on ", c.listen)
return err return err
} else {
errors.LogInfo(context.Background(), "API server listening on ", l.Addr())
go listen(l)
} }
errors.LogInfo(context.Background(), "API server listening on ", l.Addr())
go listen(l)
return nil return nil
} }
+34 -15
View File
@@ -2,6 +2,7 @@ package dispatcher
import ( import (
"context" "context"
"regexp"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -180,7 +181,15 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
} }
if p.Stats.UserOnline { if p.Stats.UserOnline {
trackOnlineIP(ctx, d.stats, user.Email, sessionInbound.Source.Address.String()) name := "user>>>" + user.Email + ">>>online"
if om, _ := stats.GetOrRegisterOnlineMap(d.stats, name); om != nil {
sessionInbounds := session.InboundFromContext(ctx)
userIP := sessionInbounds.Source.Address.String()
om.AddIP(userIP)
// log Online user with ips
// errors.LogDebug(ctx, "user>>>" + user.Email + ">>>online", om.Count(), om.List())
}
} }
} }
@@ -214,31 +223,41 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
} }
} }
if p.Stats.UserOnline { if p.Stats.UserOnline {
trackOnlineIP(ctx, statsManager, user.Email, sessionInbound.Source.Address.String()) name := "user>>>" + user.Email + ">>>online"
if om, _ := stats.GetOrRegisterOnlineMap(statsManager, name); om != nil {
sessionInbounds := session.InboundFromContext(ctx)
userIP := sessionInbounds.Source.Address.String()
om.AddIP(userIP)
// log Online user with ips
// errors.LogDebug(ctx, "user>>>" + user.Email + ">>>online", om.Count(), om.List())
}
} }
} }
return link return link
} }
func trackOnlineIP(ctx context.Context, sm stats.Manager, email, ip string) {
name := "user>>>" + email + ">>>online"
if om, _ := stats.GetOrRegisterOnlineMap(sm, name); om != nil {
om.AddIP(ip)
context.AfterFunc(ctx, func() { om.RemoveIP(ip) })
}
}
func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResult, request session.SniffingRequest, destination net.Destination) bool { func (d *DefaultDispatcher) shouldOverride(ctx context.Context, result SniffResult, request session.SniffingRequest, destination net.Destination) bool {
domain := result.Domain() domain := result.Domain()
if domain == "" { if domain == "" {
return false return false
} }
if request.ExcludeForDomain != nil && request.ExcludeForDomain.MatchAny(strings.ToLower(domain)) { for _, d := range request.ExcludeForDomain {
return false if strings.HasPrefix(d, "regexp:") {
} pattern := d[7:]
if request.ExcludeForIP != nil && destination.Address.Family().IsIP() && request.ExcludeForIP.Match(destination.Address.IP()) { re, err := regexp.Compile(pattern)
return false if err != nil {
errors.LogInfo(ctx, "Unable to compile regex")
continue
}
if re.MatchString(domain) {
return false
}
} else {
if strings.ToLower(domain) == d {
return false
}
}
} }
protocolString := result.Protocol() protocolString := result.Protocol()
if resComp, ok := result.(SnifferResultComposite); ok { if resComp, ok := result.(SnifferResultComposite); ok {
+3 -4
View File
@@ -139,8 +139,7 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) {
if lenAfter == 0 { if lenAfter == 0 {
if c.highWatermark >= minSizeForEmptyRebuild { if c.highWatermark >= minSizeForEmptyRebuild {
errors.LogDebug( errors.LogDebug(context.Background(), c.name,
context.Background(), c.name,
" rebuilding empty cache map to reclaim memory.", " rebuilding empty cache map to reclaim memory.",
" size_before_cleanup=", lenBefore, " size_before_cleanup=", lenBefore,
" peak_size_before_rebuild=", c.highWatermark, " peak_size_before_rebuild=", c.highWatermark,
@@ -154,8 +153,7 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) {
if reductionFromPeak := c.highWatermark - lenAfter; reductionFromPeak > shrinkAbsoluteThreshold && if reductionFromPeak := c.highWatermark - lenAfter; reductionFromPeak > shrinkAbsoluteThreshold &&
float64(reductionFromPeak) > float64(c.highWatermark)*shrinkRatioThreshold { float64(reductionFromPeak) > float64(c.highWatermark)*shrinkRatioThreshold {
errors.LogDebug( errors.LogDebug(context.Background(), c.name,
context.Background(), c.name,
" shrinking cache map to reclaim memory.", " shrinking cache map to reclaim memory.",
" new_size=", lenAfter, " new_size=", lenAfter,
" peak_size_before_shrink=", c.highWatermark, " peak_size_before_shrink=", c.highWatermark,
@@ -167,6 +165,7 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) {
c.highWatermark = lenAfter c.highWatermark = lenAfter
go c.migrate() go c.migrate()
} }
} }
type migrationEntry struct { type migrationEntry struct {
+35 -11
View File
@@ -2,24 +2,48 @@ package dns
import ( import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/common/uuid" "github.com/xtls/xray-core/common/uuid"
) )
var typeMap = map[DomainMatchingType]strmatcher.Type{
DomainMatchingType_Full: strmatcher.Full,
DomainMatchingType_Subdomain: strmatcher.Domain,
DomainMatchingType_Keyword: strmatcher.Substr,
DomainMatchingType_Regex: strmatcher.Regex,
}
// References: // References:
// https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml // https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml
// https://unix.stackexchange.com/questions/92441/whats-the-difference-between-local-home-and-lan // https://unix.stackexchange.com/questions/92441/whats-the-difference-between-local-home-and-lan
var localTLDsAndDotlessDomainsRules = []*geodata.DomainRule{ var localTLDsAndDotlessDomains = []*NameServer_PriorityDomain{
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Regex, Value: "^[^.]+$"}}}, // This will only match domains without any dot {Type: DomainMatchingType_Regex, Domain: "^[^.]+$"}, // This will only match domains without any dot
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "local"}}}, {Type: DomainMatchingType_Subdomain, Domain: "local"},
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "localdomain"}}}, {Type: DomainMatchingType_Subdomain, Domain: "localdomain"},
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "localhost"}}}, {Type: DomainMatchingType_Subdomain, Domain: "localhost"},
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "lan"}}}, {Type: DomainMatchingType_Subdomain, Domain: "lan"},
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "home.arpa"}}}, {Type: DomainMatchingType_Subdomain, Domain: "home.arpa"},
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "example"}}}, {Type: DomainMatchingType_Subdomain, Domain: "example"},
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "invalid"}}}, {Type: DomainMatchingType_Subdomain, Domain: "invalid"},
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "test"}}}, {Type: DomainMatchingType_Subdomain, Domain: "test"},
}
var localTLDsAndDotlessDomainsRule = &NameServer_OriginalRule{
Rule: "geosite:private",
Size: uint32(len(localTLDsAndDotlessDomains)),
}
func toStrMatcher(t DomainMatchingType, domain string) (strmatcher.Matcher, error) {
strMType, f := typeMap[t]
if !f {
return nil, errors.New("unknown mapping type", t).AtWarning()
}
matcher, err := strMType.New(domain)
if err != nil {
return nil, errors.New("failed to create str matcher").Base(err)
}
return matcher, nil
} }
func toNetIP(addrs []net.Address) ([]net.IP, error) { func toNetIP(addrs []net.Address) ([]net.IP, error) {
+260 -72
View File
@@ -7,7 +7,7 @@
package dns package dns
import ( import (
geodata "github.com/xtls/xray-core/common/geodata" router "github.com/xtls/xray-core/app/router"
net "github.com/xtls/xray-core/common/net" net "github.com/xtls/xray-core/common/net"
protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
@@ -23,6 +23,58 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
) )
type DomainMatchingType int32
const (
DomainMatchingType_Full DomainMatchingType = 0
DomainMatchingType_Subdomain DomainMatchingType = 1
DomainMatchingType_Keyword DomainMatchingType = 2
DomainMatchingType_Regex DomainMatchingType = 3
)
// Enum value maps for DomainMatchingType.
var (
DomainMatchingType_name = map[int32]string{
0: "Full",
1: "Subdomain",
2: "Keyword",
3: "Regex",
}
DomainMatchingType_value = map[string]int32{
"Full": 0,
"Subdomain": 1,
"Keyword": 2,
"Regex": 3,
}
)
func (x DomainMatchingType) Enum() *DomainMatchingType {
p := new(DomainMatchingType)
*p = x
return p
}
func (x DomainMatchingType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (DomainMatchingType) Descriptor() protoreflect.EnumDescriptor {
return file_app_dns_config_proto_enumTypes[0].Descriptor()
}
func (DomainMatchingType) Type() protoreflect.EnumType {
return &file_app_dns_config_proto_enumTypes[0]
}
func (x DomainMatchingType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use DomainMatchingType.Descriptor instead.
func (DomainMatchingType) EnumDescriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{0}
}
type QueryStrategy int32 type QueryStrategy int32
const ( const (
@@ -59,11 +111,11 @@ func (x QueryStrategy) String() string {
} }
func (QueryStrategy) Descriptor() protoreflect.EnumDescriptor { func (QueryStrategy) Descriptor() protoreflect.EnumDescriptor {
return file_app_dns_config_proto_enumTypes[0].Descriptor() return file_app_dns_config_proto_enumTypes[1].Descriptor()
} }
func (QueryStrategy) Type() protoreflect.EnumType { func (QueryStrategy) Type() protoreflect.EnumType {
return &file_app_dns_config_proto_enumTypes[0] return &file_app_dns_config_proto_enumTypes[1]
} }
func (x QueryStrategy) Number() protoreflect.EnumNumber { func (x QueryStrategy) Number() protoreflect.EnumNumber {
@@ -72,29 +124,30 @@ func (x QueryStrategy) Number() protoreflect.EnumNumber {
// Deprecated: Use QueryStrategy.Descriptor instead. // Deprecated: Use QueryStrategy.Descriptor instead.
func (QueryStrategy) EnumDescriptor() ([]byte, []int) { func (QueryStrategy) EnumDescriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{0} return file_app_dns_config_proto_rawDescGZIP(), []int{1}
} }
type NameServer struct { type NameServer struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Address *net.Endpoint `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` Address *net.Endpoint `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
ClientIp []byte `protobuf:"bytes,5,opt,name=client_ip,json=clientIp,proto3" json:"client_ip,omitempty"` ClientIp []byte `protobuf:"bytes,5,opt,name=client_ip,json=clientIp,proto3" json:"client_ip,omitempty"`
SkipFallback bool `protobuf:"varint,6,opt,name=skipFallback,proto3" json:"skipFallback,omitempty"` SkipFallback bool `protobuf:"varint,6,opt,name=skipFallback,proto3" json:"skipFallback,omitempty"`
Domain []*geodata.DomainRule `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"` PrioritizedDomain []*NameServer_PriorityDomain `protobuf:"bytes,2,rep,name=prioritized_domain,json=prioritizedDomain,proto3" json:"prioritized_domain,omitempty"`
ExpectedIp []*geodata.IPRule `protobuf:"bytes,3,rep,name=expected_ip,json=expectedIp,proto3" json:"expected_ip,omitempty"` ExpectedGeoip []*router.GeoIP `protobuf:"bytes,3,rep,name=expected_geoip,json=expectedGeoip,proto3" json:"expected_geoip,omitempty"`
QueryStrategy QueryStrategy `protobuf:"varint,7,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"` OriginalRules []*NameServer_OriginalRule `protobuf:"bytes,4,rep,name=original_rules,json=originalRules,proto3" json:"original_rules,omitempty"`
ActPrior bool `protobuf:"varint,8,opt,name=actPrior,proto3" json:"actPrior,omitempty"` QueryStrategy QueryStrategy `protobuf:"varint,7,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"`
Tag string `protobuf:"bytes,9,opt,name=tag,proto3" json:"tag,omitempty"` ActPrior bool `protobuf:"varint,8,opt,name=actPrior,proto3" json:"actPrior,omitempty"`
TimeoutMs uint64 `protobuf:"varint,10,opt,name=timeoutMs,proto3" json:"timeoutMs,omitempty"` Tag string `protobuf:"bytes,9,opt,name=tag,proto3" json:"tag,omitempty"`
DisableCache *bool `protobuf:"varint,11,opt,name=disableCache,proto3,oneof" json:"disableCache,omitempty"` TimeoutMs uint64 `protobuf:"varint,10,opt,name=timeoutMs,proto3" json:"timeoutMs,omitempty"`
ServeStale *bool `protobuf:"varint,15,opt,name=serveStale,proto3,oneof" json:"serveStale,omitempty"` DisableCache *bool `protobuf:"varint,11,opt,name=disableCache,proto3,oneof" json:"disableCache,omitempty"`
ServeExpiredTTL *uint32 `protobuf:"varint,16,opt,name=serveExpiredTTL,proto3,oneof" json:"serveExpiredTTL,omitempty"` ServeStale *bool `protobuf:"varint,15,opt,name=serveStale,proto3,oneof" json:"serveStale,omitempty"`
FinalQuery bool `protobuf:"varint,12,opt,name=finalQuery,proto3" json:"finalQuery,omitempty"` ServeExpiredTTL *uint32 `protobuf:"varint,16,opt,name=serveExpiredTTL,proto3,oneof" json:"serveExpiredTTL,omitempty"`
UnexpectedIp []*geodata.IPRule `protobuf:"bytes,13,rep,name=unexpected_ip,json=unexpectedIp,proto3" json:"unexpected_ip,omitempty"` FinalQuery bool `protobuf:"varint,12,opt,name=finalQuery,proto3" json:"finalQuery,omitempty"`
ActUnprior bool `protobuf:"varint,14,opt,name=actUnprior,proto3" json:"actUnprior,omitempty"` UnexpectedGeoip []*router.GeoIP `protobuf:"bytes,13,rep,name=unexpected_geoip,json=unexpectedGeoip,proto3" json:"unexpected_geoip,omitempty"`
PolicyID uint32 `protobuf:"varint,17,opt,name=policyID,proto3" json:"policyID,omitempty"` ActUnprior bool `protobuf:"varint,14,opt,name=actUnprior,proto3" json:"actUnprior,omitempty"`
unknownFields protoimpl.UnknownFields PolicyID uint32 `protobuf:"varint,17,opt,name=policyID,proto3" json:"policyID,omitempty"`
sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *NameServer) Reset() { func (x *NameServer) Reset() {
@@ -148,16 +201,23 @@ func (x *NameServer) GetSkipFallback() bool {
return false return false
} }
func (x *NameServer) GetDomain() []*geodata.DomainRule { func (x *NameServer) GetPrioritizedDomain() []*NameServer_PriorityDomain {
if x != nil { if x != nil {
return x.Domain return x.PrioritizedDomain
} }
return nil return nil
} }
func (x *NameServer) GetExpectedIp() []*geodata.IPRule { func (x *NameServer) GetExpectedGeoip() []*router.GeoIP {
if x != nil { if x != nil {
return x.ExpectedIp return x.ExpectedGeoip
}
return nil
}
func (x *NameServer) GetOriginalRules() []*NameServer_OriginalRule {
if x != nil {
return x.OriginalRules
} }
return nil return nil
} }
@@ -218,9 +278,9 @@ func (x *NameServer) GetFinalQuery() bool {
return false return false
} }
func (x *NameServer) GetUnexpectedIp() []*geodata.IPRule { func (x *NameServer) GetUnexpectedGeoip() []*router.GeoIP {
if x != nil { if x != nil {
return x.UnexpectedIp return x.UnexpectedGeoip
} }
return nil return nil
} }
@@ -369,9 +429,114 @@ func (x *Config) GetEnableParallelQuery() bool {
return false return false
} }
type NameServer_PriorityDomain struct {
state protoimpl.MessageState `protogen:"open.v1"`
Type DomainMatchingType `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.dns.DomainMatchingType" json:"type,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NameServer_PriorityDomain) Reset() {
*x = NameServer_PriorityDomain{}
mi := &file_app_dns_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *NameServer_PriorityDomain) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NameServer_PriorityDomain) ProtoMessage() {}
func (x *NameServer_PriorityDomain) ProtoReflect() protoreflect.Message {
mi := &file_app_dns_config_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NameServer_PriorityDomain.ProtoReflect.Descriptor instead.
func (*NameServer_PriorityDomain) Descriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{0, 0}
}
func (x *NameServer_PriorityDomain) GetType() DomainMatchingType {
if x != nil {
return x.Type
}
return DomainMatchingType_Full
}
func (x *NameServer_PriorityDomain) GetDomain() string {
if x != nil {
return x.Domain
}
return ""
}
type NameServer_OriginalRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
Rule string `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"`
Size uint32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *NameServer_OriginalRule) Reset() {
*x = NameServer_OriginalRule{}
mi := &file_app_dns_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *NameServer_OriginalRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NameServer_OriginalRule) ProtoMessage() {}
func (x *NameServer_OriginalRule) ProtoReflect() protoreflect.Message {
mi := &file_app_dns_config_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NameServer_OriginalRule.ProtoReflect.Descriptor instead.
func (*NameServer_OriginalRule) Descriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{0, 1}
}
func (x *NameServer_OriginalRule) GetRule() string {
if x != nil {
return x.Rule
}
return ""
}
func (x *NameServer_OriginalRule) GetSize() uint32 {
if x != nil {
return x.Size
}
return 0
}
type Config_HostMapping struct { type Config_HostMapping struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Domain *geodata.DomainRule `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"` Type DomainMatchingType `protobuf:"varint,1,opt,name=type,proto3,enum=xray.app.dns.DomainMatchingType" json:"type,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
Ip [][]byte `protobuf:"bytes,3,rep,name=ip,proto3" json:"ip,omitempty"` Ip [][]byte `protobuf:"bytes,3,rep,name=ip,proto3" json:"ip,omitempty"`
// ProxiedDomain indicates the mapped domain has the same IP address on this // ProxiedDomain indicates the mapped domain has the same IP address on this
// domain. Xray will use this domain for IP queries. // domain. Xray will use this domain for IP queries.
@@ -382,7 +547,7 @@ type Config_HostMapping struct {
func (x *Config_HostMapping) Reset() { func (x *Config_HostMapping) Reset() {
*x = Config_HostMapping{} *x = Config_HostMapping{}
mi := &file_app_dns_config_proto_msgTypes[2] mi := &file_app_dns_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -394,7 +559,7 @@ func (x *Config_HostMapping) String() string {
func (*Config_HostMapping) ProtoMessage() {} func (*Config_HostMapping) ProtoMessage() {}
func (x *Config_HostMapping) ProtoReflect() protoreflect.Message { func (x *Config_HostMapping) ProtoReflect() protoreflect.Message {
mi := &file_app_dns_config_proto_msgTypes[2] mi := &file_app_dns_config_proto_msgTypes[4]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -410,11 +575,18 @@ func (*Config_HostMapping) Descriptor() ([]byte, []int) {
return file_app_dns_config_proto_rawDescGZIP(), []int{1, 0} return file_app_dns_config_proto_rawDescGZIP(), []int{1, 0}
} }
func (x *Config_HostMapping) GetDomain() *geodata.DomainRule { func (x *Config_HostMapping) GetType() DomainMatchingType {
if x != nil {
return x.Type
}
return DomainMatchingType_Full
}
func (x *Config_HostMapping) GetDomain() string {
if x != nil { if x != nil {
return x.Domain return x.Domain
} }
return nil return ""
} }
func (x *Config_HostMapping) GetIp() [][]byte { func (x *Config_HostMapping) GetIp() [][]byte {
@@ -435,15 +607,15 @@ var File_app_dns_config_proto protoreflect.FileDescriptor
const file_app_dns_config_proto_rawDesc = "" + const file_app_dns_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x14app/dns/config.proto\x12\fxray.app.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\xde\x05\n" + "\x14app/dns/config.proto\x12\fxray.app.dns\x1a\x1ccommon/net/destination.proto\x1a\x17app/router/config.proto\"\xdf\a\n" +
"\n" + "\n" +
"NameServer\x123\n" + "NameServer\x123\n" +
"\aaddress\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\aaddress\x12\x1b\n" + "\aaddress\x18\x01 \x01(\v2\x19.xray.common.net.EndpointR\aaddress\x12\x1b\n" +
"\tclient_ip\x18\x05 \x01(\fR\bclientIp\x12\"\n" + "\tclient_ip\x18\x05 \x01(\fR\bclientIp\x12\"\n" +
"\fskipFallback\x18\x06 \x01(\bR\fskipFallback\x127\n" + "\fskipFallback\x18\x06 \x01(\bR\fskipFallback\x12V\n" +
"\x06domain\x18\x02 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\x12<\n" + "\x12prioritized_domain\x18\x02 \x03(\v2'.xray.app.dns.NameServer.PriorityDomainR\x11prioritizedDomain\x12=\n" +
"\vexpected_ip\x18\x03 \x03(\v2\x1b.xray.common.geodata.IPRuleR\n" + "\x0eexpected_geoip\x18\x03 \x03(\v2\x16.xray.app.router.GeoIPR\rexpectedGeoip\x12L\n" +
"expectedIp\x12B\n" + "\x0eoriginal_rules\x18\x04 \x03(\v2%.xray.app.dns.NameServer.OriginalRuleR\roriginalRules\x12B\n" +
"\x0equery_strategy\x18\a \x01(\x0e2\x1b.xray.app.dns.QueryStrategyR\rqueryStrategy\x12\x1a\n" + "\x0equery_strategy\x18\a \x01(\x0e2\x1b.xray.app.dns.QueryStrategyR\rqueryStrategy\x12\x1a\n" +
"\bactPrior\x18\b \x01(\bR\bactPrior\x12\x10\n" + "\bactPrior\x18\b \x01(\bR\bactPrior\x12\x10\n" +
"\x03tag\x18\t \x01(\tR\x03tag\x12\x1c\n" + "\x03tag\x18\t \x01(\tR\x03tag\x12\x1c\n" +
@@ -456,15 +628,21 @@ const file_app_dns_config_proto_rawDesc = "" +
"\x0fserveExpiredTTL\x18\x10 \x01(\rH\x02R\x0fserveExpiredTTL\x88\x01\x01\x12\x1e\n" + "\x0fserveExpiredTTL\x18\x10 \x01(\rH\x02R\x0fserveExpiredTTL\x88\x01\x01\x12\x1e\n" +
"\n" + "\n" +
"finalQuery\x18\f \x01(\bR\n" + "finalQuery\x18\f \x01(\bR\n" +
"finalQuery\x12@\n" + "finalQuery\x12A\n" +
"\runexpected_ip\x18\r \x03(\v2\x1b.xray.common.geodata.IPRuleR\funexpectedIp\x12\x1e\n" + "\x10unexpected_geoip\x18\r \x03(\v2\x16.xray.app.router.GeoIPR\x0funexpectedGeoip\x12\x1e\n" +
"\n" + "\n" +
"actUnprior\x18\x0e \x01(\bR\n" + "actUnprior\x18\x0e \x01(\bR\n" +
"actUnprior\x12\x1a\n" + "actUnprior\x12\x1a\n" +
"\bpolicyID\x18\x11 \x01(\rR\bpolicyIDB\x0f\n" + "\bpolicyID\x18\x11 \x01(\rR\bpolicyID\x1a^\n" +
"\x0ePriorityDomain\x124\n" +
"\x04type\x18\x01 \x01(\x0e2 .xray.app.dns.DomainMatchingTypeR\x04type\x12\x16\n" +
"\x06domain\x18\x02 \x01(\tR\x06domain\x1a6\n" +
"\fOriginalRule\x12\x12\n" +
"\x04rule\x18\x01 \x01(\tR\x04rule\x12\x12\n" +
"\x04size\x18\x02 \x01(\rR\x04sizeB\x0f\n" +
"\r_disableCacheB\r\n" + "\r_disableCacheB\r\n" +
"\v_serveStaleB\x12\n" + "\v_serveStaleB\x12\n" +
"\x10_serveExpiredTTLJ\x04\b\x04\x10\x05\"\x82\x05\n" + "\x10_serveExpiredTTL\"\x98\x05\n" +
"\x06Config\x129\n" + "\x06Config\x129\n" +
"\vname_server\x18\x05 \x03(\v2\x18.xray.app.dns.NameServerR\n" + "\vname_server\x18\x05 \x03(\v2\x18.xray.app.dns.NameServerR\n" +
"nameServer\x12\x1b\n" + "nameServer\x12\x1b\n" +
@@ -480,11 +658,17 @@ const file_app_dns_config_proto_rawDesc = "" +
"\x0fdisableFallback\x18\n" + "\x0fdisableFallback\x18\n" +
" \x01(\bR\x0fdisableFallback\x126\n" + " \x01(\bR\x0fdisableFallback\x126\n" +
"\x16disableFallbackIfMatch\x18\v \x01(\bR\x16disableFallbackIfMatch\x120\n" + "\x16disableFallbackIfMatch\x18\v \x01(\bR\x16disableFallbackIfMatch\x120\n" +
"\x13enableParallelQuery\x18\x0e \x01(\bR\x13enableParallelQuery\x1a}\n" + "\x13enableParallelQuery\x18\x0e \x01(\bR\x13enableParallelQuery\x1a\x92\x01\n" +
"\vHostMapping\x127\n" + "\vHostMapping\x124\n" +
"\x06domain\x18\x02 \x01(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\x12\x0e\n" + "\x04type\x18\x01 \x01(\x0e2 .xray.app.dns.DomainMatchingTypeR\x04type\x12\x16\n" +
"\x06domain\x18\x02 \x01(\tR\x06domain\x12\x0e\n" +
"\x02ip\x18\x03 \x03(\fR\x02ip\x12%\n" + "\x02ip\x18\x03 \x03(\fR\x02ip\x12%\n" +
"\x0eproxied_domain\x18\x04 \x01(\tR\rproxiedDomainJ\x04\b\a\x10\b*B\n" + "\x0eproxied_domain\x18\x04 \x01(\tR\rproxiedDomainJ\x04\b\a\x10\b*E\n" +
"\x12DomainMatchingType\x12\b\n" +
"\x04Full\x10\x00\x12\r\n" +
"\tSubdomain\x10\x01\x12\v\n" +
"\aKeyword\x10\x02\x12\t\n" +
"\x05Regex\x10\x03*B\n" +
"\rQueryStrategy\x12\n" + "\rQueryStrategy\x12\n" +
"\n" + "\n" +
"\x06USE_IP\x10\x00\x12\v\n" + "\x06USE_IP\x10\x00\x12\v\n" +
@@ -505,32 +689,36 @@ func file_app_dns_config_proto_rawDescGZIP() []byte {
return file_app_dns_config_proto_rawDescData return file_app_dns_config_proto_rawDescData
} }
var file_app_dns_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_app_dns_config_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_app_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_app_dns_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_app_dns_config_proto_goTypes = []any{ var file_app_dns_config_proto_goTypes = []any{
(QueryStrategy)(0), // 0: xray.app.dns.QueryStrategy (DomainMatchingType)(0), // 0: xray.app.dns.DomainMatchingType
(*NameServer)(nil), // 1: xray.app.dns.NameServer (QueryStrategy)(0), // 1: xray.app.dns.QueryStrategy
(*Config)(nil), // 2: xray.app.dns.Config (*NameServer)(nil), // 2: xray.app.dns.NameServer
(*Config_HostMapping)(nil), // 3: xray.app.dns.Config.HostMapping (*Config)(nil), // 3: xray.app.dns.Config
(*net.Endpoint)(nil), // 4: xray.common.net.Endpoint (*NameServer_PriorityDomain)(nil), // 4: xray.app.dns.NameServer.PriorityDomain
(*geodata.DomainRule)(nil), // 5: xray.common.geodata.DomainRule (*NameServer_OriginalRule)(nil), // 5: xray.app.dns.NameServer.OriginalRule
(*geodata.IPRule)(nil), // 6: xray.common.geodata.IPRule (*Config_HostMapping)(nil), // 6: xray.app.dns.Config.HostMapping
(*net.Endpoint)(nil), // 7: xray.common.net.Endpoint
(*router.GeoIP)(nil), // 8: xray.app.router.GeoIP
} }
var file_app_dns_config_proto_depIdxs = []int32{ var file_app_dns_config_proto_depIdxs = []int32{
4, // 0: xray.app.dns.NameServer.address:type_name -> xray.common.net.Endpoint 7, // 0: xray.app.dns.NameServer.address:type_name -> xray.common.net.Endpoint
5, // 1: xray.app.dns.NameServer.domain:type_name -> xray.common.geodata.DomainRule 4, // 1: xray.app.dns.NameServer.prioritized_domain:type_name -> xray.app.dns.NameServer.PriorityDomain
6, // 2: xray.app.dns.NameServer.expected_ip:type_name -> xray.common.geodata.IPRule 8, // 2: xray.app.dns.NameServer.expected_geoip:type_name -> xray.app.router.GeoIP
0, // 3: xray.app.dns.NameServer.query_strategy:type_name -> xray.app.dns.QueryStrategy 5, // 3: xray.app.dns.NameServer.original_rules:type_name -> xray.app.dns.NameServer.OriginalRule
6, // 4: xray.app.dns.NameServer.unexpected_ip:type_name -> xray.common.geodata.IPRule 1, // 4: xray.app.dns.NameServer.query_strategy:type_name -> xray.app.dns.QueryStrategy
1, // 5: xray.app.dns.Config.name_server:type_name -> xray.app.dns.NameServer 8, // 5: xray.app.dns.NameServer.unexpected_geoip:type_name -> xray.app.router.GeoIP
3, // 6: xray.app.dns.Config.static_hosts:type_name -> xray.app.dns.Config.HostMapping 2, // 6: xray.app.dns.Config.name_server:type_name -> xray.app.dns.NameServer
0, // 7: xray.app.dns.Config.query_strategy:type_name -> xray.app.dns.QueryStrategy 6, // 7: xray.app.dns.Config.static_hosts:type_name -> xray.app.dns.Config.HostMapping
5, // 8: xray.app.dns.Config.HostMapping.domain:type_name -> xray.common.geodata.DomainRule 1, // 8: xray.app.dns.Config.query_strategy:type_name -> xray.app.dns.QueryStrategy
9, // [9:9] is the sub-list for method output_type 0, // 9: xray.app.dns.NameServer.PriorityDomain.type:type_name -> xray.app.dns.DomainMatchingType
9, // [9:9] is the sub-list for method input_type 0, // 10: xray.app.dns.Config.HostMapping.type:type_name -> xray.app.dns.DomainMatchingType
9, // [9:9] is the sub-list for extension type_name 11, // [11:11] is the sub-list for method output_type
9, // [9:9] is the sub-list for extension extendee 11, // [11:11] is the sub-list for method input_type
0, // [0:9] is the sub-list for field type_name 11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
} }
func init() { file_app_dns_config_proto_init() } func init() { file_app_dns_config_proto_init() }
@@ -544,8 +732,8 @@ func file_app_dns_config_proto_init() {
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_dns_config_proto_rawDesc), len(file_app_dns_config_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_dns_config_proto_rawDesc), len(file_app_dns_config_proto_rawDesc)),
NumEnums: 1, NumEnums: 2,
NumMessages: 3, NumMessages: 5,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
+25 -6
View File
@@ -7,15 +7,26 @@ option java_package = "com.xray.app.dns";
option java_multiple_files = true; option java_multiple_files = true;
import "common/net/destination.proto"; import "common/net/destination.proto";
import "common/geodata/geodat.proto"; import "app/router/config.proto";
message NameServer { message NameServer {
xray.common.net.Endpoint address = 1; xray.common.net.Endpoint address = 1;
bytes client_ip = 5; bytes client_ip = 5;
bool skipFallback = 6; bool skipFallback = 6;
repeated xray.common.geodata.DomainRule domain = 2;
repeated xray.common.geodata.IPRule expected_ip = 3; message PriorityDomain {
reserved 4; DomainMatchingType type = 1;
string domain = 2;
}
message OriginalRule {
string rule = 1;
uint32 size = 2;
}
repeated PriorityDomain prioritized_domain = 2;
repeated xray.app.router.GeoIP expected_geoip = 3;
repeated OriginalRule original_rules = 4;
QueryStrategy query_strategy = 7; QueryStrategy query_strategy = 7;
bool actPrior = 8; bool actPrior = 8;
string tag = 9; string tag = 9;
@@ -24,11 +35,18 @@ message NameServer {
optional bool serveStale = 15; optional bool serveStale = 15;
optional uint32 serveExpiredTTL = 16; optional uint32 serveExpiredTTL = 16;
bool finalQuery = 12; bool finalQuery = 12;
repeated xray.common.geodata.IPRule unexpected_ip = 13; repeated xray.app.router.GeoIP unexpected_geoip = 13;
bool actUnprior = 14; bool actUnprior = 14;
uint32 policyID = 17; uint32 policyID = 17;
} }
enum DomainMatchingType {
Full = 0;
Subdomain = 1;
Keyword = 2;
Regex = 3;
}
enum QueryStrategy { enum QueryStrategy {
USE_IP = 0; USE_IP = 0;
USE_IP4 = 1; USE_IP4 = 1;
@@ -46,7 +64,8 @@ message Config {
bytes client_ip = 3; bytes client_ip = 3;
message HostMapping { message HostMapping {
xray.common.geodata.DomainRule domain = 2; DomainMatchingType type = 1;
string domain = 2;
repeated bytes ip = 3; repeated bytes ip = 3;
+142 -75
View File
@@ -5,16 +5,20 @@ import (
"context" "context"
go_errors "errors" go_errors "errors"
"fmt" "fmt"
"os"
"runtime"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
) )
@@ -28,15 +32,15 @@ type DNS struct {
hosts *StaticHosts hosts *StaticHosts
clients []*Client clients []*Client
ctx context.Context ctx context.Context
domainMatcher geodata.DomainMatcher domainMatcher strmatcher.IndexMatcher
matcherInfos []*DomainMatcherInfo matcherInfos []*DomainMatcherInfo
checkSystem bool checkSystem bool
} }
// DomainMatcherInfo contains information attached to index returned by Server.domainMatcher. // DomainMatcherInfo contains information attached to index returned by Server.domainMatcher
type DomainMatcherInfo struct { type DomainMatcherInfo struct {
clientIdx uint16 clientIdx uint16
domainRule string domainRuleIdx uint16
} }
// New creates a new DNS server with given configuration. // New creates a new DNS server with given configuration.
@@ -81,40 +85,56 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
return nil, errors.New("unexpected query strategy ", config.QueryStrategy) return nil, errors.New("unexpected query strategy ", config.QueryStrategy)
} }
hosts, err := NewStaticHosts(config.StaticHosts) var hosts *StaticHosts
if err != nil { mphLoaded := false
return nil, errors.New("failed to create hosts").Base(err) domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" })
if domainMatcherPath != "" {
if f, err := os.Open(domainMatcherPath); err == nil {
defer f.Close()
if m, err := router.LoadGeoSiteMatcher(f, "HOSTS"); err == nil {
f.Seek(0, 0)
if hostIPs, err := router.LoadGeoSiteHosts(f); err == nil {
if sh, err := NewStaticHostsFromCache(m, hostIPs); err == nil {
hosts = sh
mphLoaded = true
errors.LogDebug(ctx, "MphDomainMatcher loaded from cache for DNS hosts, size: ", sh.matchers.Size())
}
}
}
}
} }
defaultTag := config.Tag if !mphLoaded {
sh, err := NewStaticHosts(config.StaticHosts)
if err != nil {
return nil, errors.New("failed to create hosts").Base(err)
}
hosts = sh
}
var clients []*Client
domainRuleCount := 0
var defaultTag = config.Tag
if len(config.Tag) == 0 { if len(config.Tag) == 0 {
defaultTag = generateRandomTag() defaultTag = generateRandomTag()
} }
clients := make([]*Client, 0, len(config.NameServer)) for _, ns := range config.NameServer {
matcherInfos := make([]*DomainMatcherInfo, 0) domainRuleCount += len(ns.PrioritizedDomain)
effectiveRules := make([]*geodata.DomainRule, 0) }
// MatcherInfos is ensured to cover the maximum index domainMatcher could return, where matcher's index starts from 1
matcherInfos := make([]*DomainMatcherInfo, domainRuleCount+1)
domainMatcher := &strmatcher.MatcherGroup{}
for _, ns := range config.NameServer { for _, ns := range config.NameServer {
clientIdx := len(clients) clientIdx := len(clients)
updateRules := func(isLocalNameServer bool) { updateDomain := func(domainRule strmatcher.Matcher, originalRuleIdx int, matcherInfos []*DomainMatcherInfo) {
// Prioritize local domains with specific TLDs or those without any dot for the local DNS midx := domainMatcher.Add(domainRule)
if isLocalNameServer { matcherInfos[midx] = &DomainMatcherInfo{
effectiveRules = append(effectiveRules, localTLDsAndDotlessDomainsRules...) clientIdx: uint16(clientIdx),
for _, rule := range localTLDsAndDotlessDomainsRules { domainRuleIdx: uint16(originalRuleIdx),
matcherInfos = append(matcherInfos, &DomainMatcherInfo{
clientIdx: uint16(clientIdx),
domainRule: rule.String(),
})
}
}
effectiveRules = append(effectiveRules, ns.Domain...)
for _, rule := range ns.Domain {
matcherInfos = append(matcherInfos, &DomainMatcherInfo{
clientIdx: uint16(clientIdx),
domainRule: rule.String(),
})
} }
} }
@@ -139,31 +159,22 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
serveExpiredTTL = *ns.ServeExpiredTTL serveExpiredTTL = *ns.ServeExpiredTTL
} }
tag := defaultTag var tag = defaultTag
if len(ns.Tag) > 0 { if len(ns.Tag) > 0 {
tag = ns.Tag tag = ns.Tag
} }
clientIPOption := ResolveIpOptionOverride(ns.QueryStrategy, ipOption) clientIPOption := ResolveIpOptionOverride(ns.QueryStrategy, ipOption)
if !clientIPOption.IPv4Enable && !clientIPOption.IPv6Enable { if !clientIPOption.IPv4Enable && !clientIPOption.IPv6Enable {
return nil, errors.New("no QueryStrategy available for ", ns.Address) return nil, errors.New("no QueryStrategy available for ", ns.Address)
} }
client, err := NewClient(ctx, ns, myClientIP, disableCache, serveStale, serveExpiredTTL, tag, clientIPOption, updateRules) client, err := NewClient(ctx, ns, myClientIP, disableCache, serveStale, serveExpiredTTL, tag, clientIPOption, &matcherInfos, updateDomain)
if err != nil { if err != nil {
return nil, errors.New("failed to create client").Base(err) return nil, errors.New("failed to create client").Base(err)
} }
clients = append(clients, client) clients = append(clients, client)
} }
var domainMatcher geodata.DomainMatcher
if len(effectiveRules) > 0 {
domainMatcher, err = geodata.DomainReg.BuildDomainMatcher(effectiveRules)
if err != nil {
return nil, err
}
}
// If there is no DNS client in config, add a `localhost` DNS client // If there is no DNS client in config, add a `localhost` DNS client
if len(clients) == 0 { if len(clients) == 0 {
clients = append(clients, NewLocalDNSClient(ipOption)) clients = append(clients, NewLocalDNSClient(ipOption))
@@ -221,7 +232,7 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, er
} }
if s.checkSystem { if s.checkSystem {
supportIPv4, supportIPv6 := utils.CheckRoutes() supportIPv4, supportIPv6 := checkRoutes()
option.IPv4Enable = option.IPv4Enable && supportIPv4 option.IPv4Enable = option.IPv4Enable && supportIPv4
option.IPv6Enable = option.IPv6Enable && supportIPv6 option.IPv6Enable = option.IPv6Enable && supportIPv6
} else { } else {
@@ -272,27 +283,24 @@ func (s *DNS) sortClients(domain string) []*Client {
// Priority domain matching // Priority domain matching
hasMatch := false hasMatch := false
if s.domainMatcher != nil { MatchSlice := s.domainMatcher.Match(domain)
matchSlice := s.domainMatcher.Match(strings.ToLower(domain)) sort.Slice(MatchSlice, func(i, j int) bool {
sort.Slice(matchSlice, func(i, j int) bool { return MatchSlice[i] < MatchSlice[j]
return matchSlice[i] < matchSlice[j] })
}) for _, match := range MatchSlice {
for _, match := range matchSlice { info := s.matcherInfos[match]
info := s.matcherInfos[match] client := s.clients[info.clientIdx]
client := s.clients[info.clientIdx] domainRule := client.domains[info.domainRuleIdx]
domainRule := info.domainRule domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx)) if clientUsed[info.clientIdx] {
if clientUsed[info.clientIdx] { continue
continue }
} clientUsed[info.clientIdx] = true
clientUsed[info.clientIdx] = true clients = append(clients, client)
clients = append(clients, client) clientNames = append(clientNames, client.Name())
clientNames = append(clientNames, client.Name()) hasMatch = true
hasMatch = true if client.finalQuery {
if client.finalQuery { return clients
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
} }
} }
@@ -306,13 +314,17 @@ func (s *DNS) sortClients(domain string) []*Client {
clients = append(clients, client) clients = append(clients, client)
clientNames = append(clientNames, client.Name()) clientNames = append(clientNames, client.Name())
if client.finalQuery { if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients return clients
} }
} }
} }
logDecision(s.ctx, domain, domainRules, clientNames) if len(domainRules) > 0 {
errors.LogDebug(s.ctx, "domain ", domain, " matches following rules: ", domainRules)
}
if len(clientNames) > 0 {
errors.LogDebug(s.ctx, "domain ", domain, " will use DNS in order: ", clientNames)
}
if len(clients) == 0 { if len(clients) == 0 {
if len(s.clients) > 0 { if len(s.clients) > 0 {
@@ -327,15 +339,6 @@ func (s *DNS) sortClients(domain string) []*Client {
return clients return clients
} }
func logDecision(ctx context.Context, domain string, domainRules []string, clientNames []string) {
if len(domainRules) > 0 {
errors.LogDebug(ctx, "domain ", domain, " matches following rules: ", domainRules)
}
if len(clientNames) > 0 {
errors.LogDebug(ctx, "domain ", domain, " will use DNS in order: ", clientNames)
}
}
func mergeQueryErrors(domain string, errs []error) error { func mergeQueryErrors(domain string, errs []error) error {
if len(errs) == 0 { if len(errs) == 0 {
return dns.ErrEmptyResponse return dns.ErrEmptyResponse
@@ -537,3 +540,67 @@ func init() {
return New(ctx, config.(*Config)) return New(ctx, config.(*Config))
})) }))
} }
func probeRoutes() (ipv4 bool, ipv6 bool) {
if conn, err := net.Dial("udp4", "192.33.4.12:53"); err == nil {
ipv4 = true
conn.Close()
}
if conn, err := net.Dial("udp6", "[2001:500:2::c]:53"); err == nil {
ipv6 = true
conn.Close()
}
return
}
var routeCache struct {
sync.Once
sync.RWMutex
expire time.Time
ipv4, ipv6 bool
}
func checkRoutes() (bool, bool) {
if !isGUIPlatform {
routeCache.Once.Do(func() {
routeCache.ipv4, routeCache.ipv6 = probeRoutes()
})
return routeCache.ipv4, routeCache.ipv6
}
routeCache.RWMutex.RLock()
now := time.Now()
if routeCache.expire.After(now) {
routeCache.RWMutex.RUnlock()
return routeCache.ipv4, routeCache.ipv6
}
routeCache.RWMutex.RUnlock()
routeCache.RWMutex.Lock()
defer routeCache.RWMutex.Unlock()
now = time.Now()
if routeCache.expire.After(now) { // double-check
return routeCache.ipv4, routeCache.ipv6
}
routeCache.ipv4, routeCache.ipv6 = probeRoutes() // ~2ms
routeCache.expire = now.Add(100 * time.Millisecond) // ttl
return routeCache.ipv4, routeCache.ipv6
}
var isGUIPlatform = detectGUIPlatform()
func detectGUIPlatform() bool {
switch runtime.GOOS {
case "android", "ios", "windows", "darwin":
return true
case "linux", "freebsd", "openbsd":
if t := os.Getenv("XDG_SESSION_TYPE"); t == "wayland" || t == "x11" {
return true
}
if os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" {
return true
}
}
return false
}
+100 -68
View File
@@ -11,9 +11,9 @@ import (
"github.com/xtls/xray-core/app/policy" "github.com/xtls/xray-core/app/policy"
"github.com/xtls/xray-core/app/proxyman" "github.com/xtls/xray-core/app/proxyman"
_ "github.com/xtls/xray-core/app/proxyman/outbound" _ "github.com/xtls/xray-core/app/proxyman/outbound"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
@@ -147,9 +147,7 @@ func TestUDPServerSubnet(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{ ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -209,9 +207,7 @@ func TestUDPServer(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{ ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -335,9 +331,10 @@ func TestPrioritizedDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
Domain: []*geodata.DomainRule{ PrioritizedDomain: []*NameServer_PriorityDomain{
{ {
Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "google.com"}}, Type: DomainMatchingType_Full,
Domain: "google.com",
}, },
}, },
}, },
@@ -349,9 +346,7 @@ func TestPrioritizedDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{ ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -420,9 +415,7 @@ func TestUDPServerIPv6(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{ ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -478,7 +471,8 @@ func TestStaticHostDomain(t *testing.T) {
}, },
StaticHosts: []*Config_HostMapping{ StaticHosts: []*Config_HostMapping{
{ {
Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "example.com"}}}, Type: DomainMatchingType_Full,
Domain: "example.com",
ProxiedDomain: "google.com", ProxiedDomain: "google.com",
}, },
}, },
@@ -489,9 +483,7 @@ func TestStaticHostDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{ ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -547,9 +539,17 @@ func TestIPMatch(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedGeoip: []*router.GeoIP{
// inner ip, will not match {
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{192, 168, 11, 1}, Prefix: 32}}}}, CountryCode: "local",
Cidr: []*router.CIDR{
{
// inner ip, will not match
Ip: []byte{192, 168, 11, 1},
Prefix: 32,
},
},
},
}, },
}, },
// second dns, match ip // second dns, match ip
@@ -563,9 +563,25 @@ func TestIPMatch(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedGeoip: []*router.GeoIP{
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}}, {
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 4}, Prefix: 32}}}}, CountryCode: "test",
Cidr: []*router.CIDR{
{
Ip: []byte{8, 8, 8, 8},
Prefix: 32,
},
},
},
{
CountryCode: "test",
Cidr: []*router.CIDR{
{
Ip: []byte{8, 8, 8, 4},
Prefix: 32,
},
},
},
}, },
}, },
}, },
@@ -576,9 +592,7 @@ func TestIPMatch(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{ ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -649,15 +663,19 @@ func TestLocalDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
Domain: []*geodata.DomainRule{ PrioritizedDomain: []*NameServer_PriorityDomain{
// Equivalent of dotless:localhost // Equivalent of dotless:localhost
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Regex, Value: "^[^.]*localhost[^.]*$"}}}, {Type: DomainMatchingType_Regex, Domain: "^[^.]*localhost[^.]*$"},
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedGeoip: []*router.GeoIP{
// Will match localhost, localhost-a and localhost-b, { // Will match localhost, localhost-a and localhost-b,
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 2}, Prefix: 32}}}}, CountryCode: "local",
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 3}, Prefix: 32}}}}, Cidr: []*router.CIDR{
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 4}, Prefix: 32}}}}, {Ip: []byte{127, 0, 0, 2}, Prefix: 32},
{Ip: []byte{127, 0, 0, 3}, Prefix: 32},
{Ip: []byte{127, 0, 0, 4}, Prefix: 32},
},
},
}, },
}, },
{ {
@@ -670,21 +688,23 @@ func TestLocalDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
Domain: []*geodata.DomainRule{ PrioritizedDomain: []*NameServer_PriorityDomain{
// Equivalent of dotless: and domain:local // Equivalent of dotless: and domain:local
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Regex, Value: "^[^.]*$"}}}, {Type: DomainMatchingType_Regex, Domain: "^[^.]*$"},
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "local"}}}, {Type: DomainMatchingType_Subdomain, Domain: "local"},
{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "localdomain"}}}, {Type: DomainMatchingType_Subdomain, Domain: "localdomain"},
}, },
}, },
}, },
StaticHosts: []*Config_HostMapping{ StaticHosts: []*Config_HostMapping{
{ {
Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "hostnamestatic"}}}, Type: DomainMatchingType_Full,
Domain: "hostnamestatic",
Ip: [][]byte{{127, 0, 0, 53}}, Ip: [][]byte{{127, 0, 0, 53}},
}, },
{ {
Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "hostnamealias"}}}, Type: DomainMatchingType_Full,
Domain: "hostnamealias",
ProxiedDomain: "hostname.localdomain", ProxiedDomain: "hostname.localdomain",
}, },
}, },
@@ -695,9 +715,7 @@ func TestLocalDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{ ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
@@ -873,15 +891,19 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
Domain: []*geodata.DomainRule{ PrioritizedDomain: []*NameServer_PriorityDomain{
{ {
Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "google.com"}}, Type: DomainMatchingType_Subdomain,
Domain: "google.com",
}, },
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedGeoip: []*router.GeoIP{
// Will only match 8.8.8.8 and 8.8.4.4 { // Will only match 8.8.8.8 and 8.8.4.4
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}}}}, Cidr: []*router.CIDR{
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 4, 4}, Prefix: 32}}}}, {Ip: []byte{8, 8, 8, 8}, Prefix: 32},
{Ip: []byte{8, 8, 4, 4}, Prefix: 32},
},
},
}, },
}, },
{ {
@@ -894,14 +916,18 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
Domain: []*geodata.DomainRule{ PrioritizedDomain: []*NameServer_PriorityDomain{
{ {
Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "google.com"}}, Type: DomainMatchingType_Subdomain,
Domain: "google.com",
}, },
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedGeoip: []*router.GeoIP{
// Will match 8.8.8.8 and 8.8.8.7, etc { // Will match 8.8.8.8 and 8.8.8.7, etc
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 7}, Prefix: 24}}}}, Cidr: []*router.CIDR{
{Ip: []byte{8, 8, 8, 7}, Prefix: 24},
},
},
}, },
}, },
{ {
@@ -914,14 +940,18 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
Domain: []*geodata.DomainRule{ PrioritizedDomain: []*NameServer_PriorityDomain{
{ {
Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "api.google.com"}}, Type: DomainMatchingType_Subdomain,
Domain: "api.google.com",
}, },
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedGeoip: []*router.GeoIP{
// Will only match 8.8.7.7 (api.google.com) { // Will only match 8.8.7.7 (api.google.com)
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 7}, Prefix: 32}}}}, Cidr: []*router.CIDR{
{Ip: []byte{8, 8, 7, 7}, Prefix: 32},
},
},
}, },
}, },
{ {
@@ -934,14 +964,18 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
Port: uint32(port), Port: uint32(port),
}, },
Domain: []*geodata.DomainRule{ PrioritizedDomain: []*NameServer_PriorityDomain{
{ {
Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "v2.api.google.com"}}, Type: DomainMatchingType_Full,
Domain: "v2.api.google.com",
}, },
}, },
ExpectedIp: []*geodata.IPRule{ ExpectedGeoip: []*router.GeoIP{
// Will only match 8.8.7.8 (v2.api.google.com) { // Will only match 8.8.7.8 (v2.api.google.com)
{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{8, 8, 7, 8}, Prefix: 32}}}}, Cidr: []*router.CIDR{
{Ip: []byte{8, 8, 7, 8}, Prefix: 32},
},
},
}, },
}, },
}, },
@@ -952,9 +986,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}, },
Outbound: []*core.OutboundHandlerConfig{ Outbound: []*core.OutboundHandlerConfig{
{ {
ProxySettings: serial.ToTypedMessage(&freedom.Config{ ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
}),
}, },
}, },
} }
+4 -9
View File
@@ -127,20 +127,15 @@ func genEDNS0Options(clientIP net.IP, padding int) *dnsmessage.Resource {
return opt return opt
} }
func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) ([]*dnsRequest, error) { func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) []*dnsRequest {
name, err := dnsmessage.NewName(domain)
if err != nil {
return nil, err
}
qA := dnsmessage.Question{ qA := dnsmessage.Question{
Name: name, Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeA, Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET, Class: dnsmessage.ClassINET,
} }
qAAAA := dnsmessage.Question{ qAAAA := dnsmessage.Question{
Name: name, Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeAAAA, Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET, Class: dnsmessage.ClassINET,
} }
@@ -180,7 +175,7 @@ func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() ui
}) })
} }
return reqs, nil return reqs
} }
// parseResponse parses DNS answers from the returned payload // parseResponse parses DNS answers from the returned payload
+3 -11
View File
@@ -2,7 +2,6 @@ package dns
import ( import (
"math/rand" "math/rand"
"strings"
"testing" "testing"
"time" "time"
@@ -25,8 +24,7 @@ func Test_parseResponse(t *testing.T) {
ans = new(dns.Msg) ans = new(dns.Msg)
ans.Id = 1 ans.Id = 1
ans.Answer = append( ans.Answer = append(ans.Answer,
ans.Answer,
common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")), common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")),
common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")), common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")),
common.Must2(dns.NewRR("google.com. IN A 8.8.8.8")), common.Must2(dns.NewRR("google.com. IN A 8.8.8.8")),
@@ -36,8 +34,7 @@ func Test_parseResponse(t *testing.T) {
ans = new(dns.Msg) ans = new(dns.Msg)
ans.Id = 2 ans.Id = 2
ans.Answer = append( ans.Answer = append(ans.Answer,
ans.Answer,
common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")), common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")),
common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")), common.Must2(dns.NewRR("google.com. IN CNAME fake.google.com")),
common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")), common.Must2(dns.NewRR("google.com. IN CNAME m.test.google.com")),
@@ -134,15 +131,10 @@ func Test_buildReqMsgs(t *testing.T) {
IPv6Enable: false, IPv6Enable: false,
FakeEnable: false, FakeEnable: false,
}, nil}, 0}, }, nil}, 0},
{"name too long", args{strings.Repeat("a", 256), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
}, nil}, 0},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got, _ := buildReqMsgs(tt.args.domain, tt.args.option, stubID, tt.args.reqOpts); !(len(got) == tt.want) { if got := buildReqMsgs(tt.args.domain, tt.args.option, stubID, tt.args.reqOpts); !(len(got) == tt.want) {
t.Errorf("buildReqMsgs() = %v, want %v", got, tt.want) t.Errorf("buildReqMsgs() = %v, want %v", got, tt.want)
} }
}) })
+12 -8
View File
@@ -17,7 +17,7 @@ import (
type Holder struct { type Holder struct {
domainToIP cache.Lru domainToIP cache.Lru
ipRange *net.IPNet ipRange *net.IPNet
mu sync.Mutex mu *sync.Mutex
config *FakeDnsPool config *FakeDnsPool
} }
@@ -49,7 +49,9 @@ func (fkdns *Holder) Start() error {
} }
func (fkdns *Holder) Close() error { func (fkdns *Holder) Close() error {
// nothing to do for now, just wait GC fkdns.domainToIP = nil
fkdns.ipRange = nil
fkdns.mu = nil
return nil return nil
} }
@@ -68,7 +70,7 @@ func NewFakeDNSHolder() (*Holder, error) {
} }
func NewFakeDNSHolderConfigOnly(conf *FakeDnsPool) (*Holder, error) { func NewFakeDNSHolderConfigOnly(conf *FakeDnsPool) (*Holder, error) {
return &Holder{config: conf}, nil return &Holder{nil, nil, nil, conf}, nil
} }
func (fkdns *Holder) initializeFromConfig() error { func (fkdns *Holder) initializeFromConfig() error {
@@ -90,6 +92,7 @@ func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
} }
fkdns.domainToIP = cache.NewLru(lruSize) fkdns.domainToIP = cache.NewLru(lruSize)
fkdns.ipRange = ipRange fkdns.ipRange = ipRange
fkdns.mu = new(sync.Mutex)
return nil return nil
} }
@@ -100,7 +103,7 @@ func (fkdns *Holder) GetFakeIPForDomain(domain string) []net.Address {
if v, ok := fkdns.domainToIP.Get(domain); ok { if v, ok := fkdns.domainToIP.Get(domain); ok {
return []net.Address{v.(net.Address)} return []net.Address{v.(net.Address)}
} }
currentTimeMillis := uint64(time.Now().UnixMilli()) currentTimeMillis := uint64(time.Now().UnixNano() / 1e6)
ones, bits := fkdns.ipRange.Mask.Size() ones, bits := fkdns.ipRange.Mask.Size()
rooms := bits - ones rooms := bits - ones
if rooms < 64 { if rooms < 64 {
@@ -199,11 +202,12 @@ func (h *HolderMulti) Start() error {
} }
func (h *HolderMulti) Close() error { func (h *HolderMulti) Close() error {
var errs []error
for _, v := range h.holders { for _, v := range h.holders {
errs = append(errs, v.Close()) if err := v.Close(); err != nil {
return errors.New("Cannot close all fake dns pools").Base(err)
}
} }
return errors.Combine(errs...) return nil
} }
func (h *HolderMulti) createHolderGroups() error { func (h *HolderMulti) createHolderGroups() error {
@@ -218,7 +222,7 @@ func (h *HolderMulti) createHolderGroups() error {
} }
func NewFakeDNSHolderMulti(conf *FakeDnsPoolMulti) (*HolderMulti, error) { func NewFakeDNSHolderMulti(conf *FakeDnsPoolMulti) (*HolderMulti, error) {
holderMulti := &HolderMulti{config: conf} holderMulti := &HolderMulti{nil, conf}
if err := holderMulti.createHolderGroups(); err != nil { if err := holderMulti.createHolderGroups(); err != nil {
return nil, err return nil, err
} }
+9 -10
View File
@@ -129,16 +129,15 @@ func TestFakeDnsHolderCreateMappingAndRollOver(t *testing.T) {
} }
func TestFakeDNSMulti(t *testing.T) { func TestFakeDNSMulti(t *testing.T) {
fakeMulti, err := NewFakeDNSHolderMulti( fakeMulti, err := NewFakeDNSHolderMulti(&FakeDnsPoolMulti{
&FakeDnsPoolMulti{ Pools: []*FakeDnsPool{{
Pools: []*FakeDnsPool{{ IpPool: "240.0.0.0/12",
IpPool: "240.0.0.0/12", LruSize: 256,
LruSize: 256, }, {
}, { IpPool: "fddd:c5b4:ff5f:f4f0::/64",
IpPool: "fddd:c5b4:ff5f:f4f0::/64", LruSize: 256,
LruSize: 256, }},
}}, },
},
) )
common.Must(err) common.Must(err)
+81 -37
View File
@@ -2,28 +2,39 @@ package dns
import ( import (
"context" "context"
"runtime"
"strconv" "strconv"
"strings"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
) )
// StaticHosts represents static domain-ip mapping in DNS server. // StaticHosts represents static domain-ip mapping in DNS server.
type StaticHosts struct { type StaticHosts struct {
responses [][]net.Address ips [][]net.Address
matcher geodata.DomainMatcher matchers strmatcher.IndexMatcher
} }
// NewStaticHosts creates a new StaticHosts instance. // NewStaticHosts creates a new StaticHosts instance.
func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) { func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
reps := make([][]net.Address, 0, len(hosts)) g := new(strmatcher.MatcherGroup)
rules := make([]*geodata.DomainRule, 0, len(hosts)) sh := &StaticHosts{
ips: make([][]net.Address, len(hosts)+16),
matchers: g,
}
for _, mapping := range hosts { defer runtime.GC()
rep := make([]net.Address, 0, len(mapping.Ip)) for i, mapping := range hosts {
hosts[i] = nil
matcher, err := toStrMatcher(mapping.Type, mapping.Domain)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to create domain matcher, ignore domain rule [type: ", mapping.Type, ", domain: ", mapping.Domain, "]")
continue
}
id := g.Add(matcher)
ips := make([]net.Address, 0, len(mapping.Ip)+1)
switch { switch {
case len(mapping.ProxiedDomain) > 0: case len(mapping.ProxiedDomain) > 0:
if mapping.ProxiedDomain[0] == '#' { if mapping.ProxiedDomain[0] == '#' {
@@ -31,36 +42,28 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
rep = append(rep, dns.RCodeError(rcode)) ips = append(ips, dns.RCodeError(rcode))
} else { } else {
rep = append(rep, net.DomainAddress(mapping.ProxiedDomain)) ips = append(ips, net.DomainAddress(mapping.ProxiedDomain))
} }
case len(mapping.Ip) > 0: case len(mapping.Ip) > 0:
for _, ip := range mapping.Ip { for _, ip := range mapping.Ip {
addr := net.IPAddress(ip) addr := net.IPAddress(ip)
if addr == nil { if addr == nil {
errors.LogError(context.Background(), "invalid IP address in static hosts: ", ip, ", ignore this ip for rule: ", mapping.Domain) errors.LogError(context.Background(), "invalid IP address in static hosts: ", ip, ", ignore this ip for rule [type: ", mapping.Type, ", domain: ", mapping.Domain, "]")
continue continue
} }
rep = append(rep, addr) ips = append(ips, addr)
}
if len(ips) == 0 {
continue
} }
} }
reps = append(reps, rep)
rules = append(rules, mapping.Domain) sh.ips[id] = ips
} }
if len(rules) == 0 { return sh, nil
return &StaticHosts{}, nil
}
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
if err != nil {
return nil, err
}
return &StaticHosts{
responses: reps,
matcher: matcher,
}, nil
} }
func filterIP(ips []net.Address, option dns.IPOption) []net.Address { func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
@@ -76,16 +79,16 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) { func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
ips := make([]net.Address, 0) ips := make([]net.Address, 0)
found := false found := false
for _, idx := range h.matcher.Match(domain) { for _, id := range h.matchers.Match(domain) {
for _, rep := range h.responses[idx] { for _, v := range h.ips[id] {
if err, ok := rep.(dns.RCodeError); ok { if err, ok := v.(dns.RCodeError); ok {
if uint16(err) == 0 { if uint16(err) == 0 {
return nil, dns.ErrEmptyResponse return nil, dns.ErrEmptyResponse
} }
return nil, err return nil, err
} }
} }
ips = append(ips, h.responses[idx]...) ips = append(ips, h.ips[id]...)
found = true found = true
} }
if !found { if !found {
@@ -95,13 +98,10 @@ func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
} }
func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) ([]net.Address, error) { func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) ([]net.Address, error) {
domain = strings.ToLower(domain)
switch addrs, err := h.lookupInternal(domain); { switch addrs, err := h.lookupInternal(domain); {
case err != nil: case err != nil:
return nil, err return nil, err
case addrs == nil: // Not recorded in static hosts, return nil case len(addrs) == 0: // Not recorded in static hosts, return nil
return nil, nil
case len(addrs) == 0: // Domain recorded, but no valid IP returned
return addrs, nil return addrs, nil
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain
errors.LogDebug(context.Background(), "found replaced domain: ", domain, " -> ", addrs[0].Domain(), ". Try to unwrap it") errors.LogDebug(context.Background(), "found replaced domain: ", domain, " -> ", addrs[0].Domain(), ". Try to unwrap it")
@@ -122,8 +122,52 @@ func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) (
// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts. // Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) { func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) {
if h.matcher == nil {
return nil, nil
}
return h.lookup(domain, option, 5) return h.lookup(domain, option, 5)
} }
func NewStaticHostsFromCache(matcher strmatcher.IndexMatcher, hostIPs map[string][]string) (*StaticHosts, error) {
sh := &StaticHosts{
ips: make([][]net.Address, matcher.Size()+1),
matchers: matcher,
}
order := hostIPs["_ORDER"]
var offset uint32
img, ok := matcher.(*strmatcher.IndexMatcherGroup)
if !ok {
// Single matcher (e.g. only manual or only one geosite)
if len(order) > 0 {
pattern := order[0]
ips := parseIPs(hostIPs[pattern])
for i := uint32(1); i <= matcher.Size(); i++ {
sh.ips[i] = ips
}
}
return sh, nil
}
for i, m := range img.Matchers {
if i < len(order) {
pattern := order[i]
ips := parseIPs(hostIPs[pattern])
for j := uint32(1); j <= m.Size(); j++ {
sh.ips[offset+j] = ips
}
offset += m.Size()
}
}
return sh, nil
}
func parseIPs(raw []string) []net.Address {
addrs := make([]net.Address, 0, len(raw))
for _, s := range raw {
if len(s) > 1 && s[0] == '#' {
rcode, _ := strconv.Atoi(s[1:])
addrs = append(addrs, dns.RCodeError(rcode))
} else {
addrs = append(addrs, net.ParseAddress(s))
}
}
return addrs
}
+68 -7
View File
@@ -1,12 +1,13 @@
package dns_test package dns_test
import ( import (
"bytes"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
. "github.com/xtls/xray-core/app/dns" . "github.com/xtls/xray-core/app/dns"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
) )
@@ -14,17 +15,20 @@ import (
func TestStaticHosts(t *testing.T) { func TestStaticHosts(t *testing.T) {
pb := []*Config_HostMapping{ pb := []*Config_HostMapping{
{ {
Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "lan"}}}, Type: DomainMatchingType_Subdomain,
Domain: "lan",
ProxiedDomain: "#3", ProxiedDomain: "#3",
}, },
{ {
Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "example.com"}}}, Type: DomainMatchingType_Full,
Domain: "example.com",
Ip: [][]byte{ Ip: [][]byte{
{1, 1, 1, 1}, {1, 1, 1, 1},
}, },
}, },
{ {
Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "proxy.xray.com"}}}, Type: DomainMatchingType_Full,
Domain: "proxy.xray.com",
Ip: [][]byte{ Ip: [][]byte{
{1, 2, 3, 4}, {1, 2, 3, 4},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
@@ -32,17 +36,20 @@ func TestStaticHosts(t *testing.T) {
ProxiedDomain: "another-proxy.xray.com", ProxiedDomain: "another-proxy.xray.com",
}, },
{ {
Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Full, Value: "proxy2.xray.com"}}}, Type: DomainMatchingType_Full,
Domain: "proxy2.xray.com",
ProxiedDomain: "proxy.xray.com", ProxiedDomain: "proxy.xray.com",
}, },
{ {
Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "example.cn"}}}, Type: DomainMatchingType_Subdomain,
Domain: "example.cn",
Ip: [][]byte{ Ip: [][]byte{
{2, 2, 2, 2}, {2, 2, 2, 2},
}, },
}, },
{ {
Domain: &geodata.DomainRule{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "baidu.com"}}}, Type: DomainMatchingType_Subdomain,
Domain: "baidu.com",
Ip: [][]byte{ Ip: [][]byte{
{127, 0, 0, 1}, {127, 0, 0, 1},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
@@ -125,3 +132,57 @@ func TestStaticHosts(t *testing.T) {
} }
} }
} }
func TestStaticHostsFromCache(t *testing.T) {
sites := []*router.GeoSite{
{
CountryCode: "cloudflare-dns.com",
Domain: []*router.Domain{
{Type: router.Domain_Full, Value: "example.com"},
},
},
{
CountryCode: "geosite:cn",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "baidu.cn"},
},
},
}
deps := map[string][]string{
"HOSTS": {"cloudflare-dns.com", "geosite:cn"},
}
hostIPs := map[string][]string{
"cloudflare-dns.com": {"1.1.1.1"},
"geosite:cn": {"2.2.2.2"},
"_ORDER": {"cloudflare-dns.com", "geosite:cn"},
}
var buf bytes.Buffer
err := router.SerializeGeoSiteList(sites, deps, hostIPs, &buf)
common.Must(err)
// Load matcher
m, err := router.LoadGeoSiteMatcher(bytes.NewReader(buf.Bytes()), "HOSTS")
common.Must(err)
// Load hostIPs
f := bytes.NewReader(buf.Bytes())
hips, err := router.LoadGeoSiteHosts(f)
common.Must(err)
hosts, err := NewStaticHostsFromCache(m, hips)
common.Must(err)
{
ips, _ := hosts.Lookup("example.com", dns.IPOption{IPv4Enable: true})
if len(ips) != 1 || ips[0].String() != "1.1.1.1" {
t.Error("failed to lookup example.com from cache")
}
}
{
ips, _ := hosts.Lookup("baidu.cn", dns.IPOption{IPv4Enable: true})
if len(ips) != 1 || ips[0].String() != "2.2.2.2" {
t.Error("failed to lookup baidu.cn from cache deps")
}
}
}
+110 -19
View File
@@ -3,19 +3,34 @@ package dns
import ( import (
"context" "context"
"net/url" "net/url"
"runtime"
"strings" "strings"
"time" "time"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/utils" "github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
) )
type mphMatcherWrapper struct {
m strmatcher.IndexMatcher
}
func (w *mphMatcherWrapper) Match(s string) bool {
return w.m.Match(s) != nil
}
func (w *mphMatcherWrapper) String() string {
return "mph-matcher"
}
// Server is the interface for Name Server. // Server is the interface for Name Server.
type Server interface { type Server interface {
// Name of the Client. // Name of the Client.
@@ -31,8 +46,9 @@ type Server interface {
type Client struct { type Client struct {
server Server server Server
skipFallback bool skipFallback bool
expectedIPs geodata.IPMatcher domains []string
unexpectedIPs geodata.IPMatcher expectedIPs router.GeoIPMatcher
unexpectedIPs router.GeoIPMatcher
actPrior bool actPrior bool
actUnprior bool actUnprior bool
tag string tag string
@@ -95,9 +111,11 @@ func NewClient(
disableCache bool, serveStale bool, serveExpiredTTL uint32, disableCache bool, serveStale bool, serveExpiredTTL uint32,
tag string, tag string,
ipOption dns.IPOption, ipOption dns.IPOption,
updateRules func(bool), matcherInfos *[]*DomainMatcherInfo,
updateDomainRule func(strmatcher.Matcher, int, []*DomainMatcherInfo),
) (*Client, error) { ) (*Client, error) {
client := &Client{} client := &Client{}
err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error { err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error {
// Create a new server for each client for now // Create a new server for each client for now
server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP) server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP)
@@ -105,25 +123,97 @@ func NewClient(
return errors.New("failed to create nameserver").Base(err).AtWarning() return errors.New("failed to create nameserver").Base(err).AtWarning()
} }
_, isLocalDNS := server.(*LocalNameServer) // Prioritize local domains with specific TLDs or those without any dot for the local DNS
updateRules(isLocalDNS) if _, isLocalDNS := server.(*LocalNameServer); isLocalDNS {
ns.PrioritizedDomain = append(ns.PrioritizedDomain, localTLDsAndDotlessDomains...)
// Establish expected IPs ns.OriginalRules = append(ns.OriginalRules, localTLDsAndDotlessDomainsRule)
var expectedMatcher geodata.IPMatcher // The following lines is a solution to avoid core panics(rule index out of range) when setting `localhost` DNS client in config.
if len(ns.ExpectedIp) > 0 { // Because the `localhost` DNS client will append len(localTLDsAndDotlessDomains) rules into matcherInfos to match `geosite:private` default rule.
expectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.ExpectedIp) // But `matcherInfos` has no enough length to add rules, which leads to core panics (rule index out of range).
if err != nil { // To avoid this, the length of `matcherInfos` must be equal to the expected, so manually append it with Golang default zero value first for later modification.
return errors.New("failed to create expected ip matcher").Base(err).AtWarning() // Related issues:
// https://github.com/v2fly/v2ray-core/issues/529
// https://github.com/v2fly/v2ray-core/issues/719
for i := 0; i < len(localTLDsAndDotlessDomains); i++ {
*matcherInfos = append(*matcherInfos, &DomainMatcherInfo{
clientIdx: uint16(0),
domainRuleIdx: uint16(0),
})
} }
} }
// Establish domain rules
var rules []string
ruleCurr := 0
ruleIter := 0
// Check if domain matcher cache is provided via environment
domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" })
var mphLoaded bool
if domainMatcherPath != "" && ns.Tag != "" {
f, err := filesystem.NewFileReader(domainMatcherPath)
if err == nil {
defer f.Close()
g, err := router.LoadGeoSiteMatcher(f, ns.Tag)
if err == nil {
errors.LogDebug(ctx, "MphDomainMatcher loaded from cache for ", ns.Tag, " dns tag)")
updateDomainRule(&mphMatcherWrapper{m: g}, 0, *matcherInfos)
rules = append(rules, "[MPH Cache]")
mphLoaded = true
}
}
}
if !mphLoaded {
for i, domain := range ns.PrioritizedDomain {
ns.PrioritizedDomain[i] = nil
domainRule, err := toStrMatcher(domain.Type, domain.Domain)
if err != nil {
errors.LogErrorInner(ctx, err, "failed to create domain matcher, ignore domain rule [type: ", domain.Type, ", domain: ", domain.Domain, "]")
domainRule, _ = toStrMatcher(DomainMatchingType_Full, "hack.fix.index.for.illegal.domain.rule")
}
originalRuleIdx := ruleCurr
if ruleCurr < len(ns.OriginalRules) {
rule := ns.OriginalRules[ruleCurr]
if ruleCurr >= len(rules) {
rules = append(rules, rule.Rule)
}
ruleIter++
if ruleIter >= int(rule.Size) {
ruleIter = 0
ruleCurr++
}
} else { // No original rule, generate one according to current domain matcher (majorly for compatibility with tests)
rules = append(rules, domainRule.String())
ruleCurr++
}
updateDomainRule(domainRule, originalRuleIdx, *matcherInfos)
}
}
ns.PrioritizedDomain = nil
runtime.GC()
// Establish expected IPs
var expectedMatcher router.GeoIPMatcher
if len(ns.ExpectedGeoip) > 0 {
expectedMatcher, err = router.BuildOptimizedGeoIPMatcher(ns.ExpectedGeoip...)
if err != nil {
return errors.New("failed to create expected ip matcher").Base(err).AtWarning()
}
ns.ExpectedGeoip = nil
runtime.GC()
}
// Establish unexpected IPs // Establish unexpected IPs
var unexpectedMatcher geodata.IPMatcher var unexpectedMatcher router.GeoIPMatcher
if len(ns.UnexpectedIp) > 0 { if len(ns.UnexpectedGeoip) > 0 {
unexpectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.UnexpectedIp) unexpectedMatcher, err = router.BuildOptimizedGeoIPMatcher(ns.UnexpectedGeoip...)
if err != nil { if err != nil {
return errors.New("failed to create unexpected ip matcher").Base(err).AtWarning() return errors.New("failed to create unexpected ip matcher").Base(err).AtWarning()
} }
ns.UnexpectedGeoip = nil
runtime.GC()
} }
if len(clientIP) > 0 { if len(clientIP) > 0 {
@@ -135,7 +225,7 @@ func NewClient(
} }
} }
timeoutMs := 4000 * time.Millisecond var timeoutMs = 4000 * time.Millisecond
if ns.TimeoutMs > 0 { if ns.TimeoutMs > 0 {
timeoutMs = time.Duration(ns.TimeoutMs) * time.Millisecond timeoutMs = time.Duration(ns.TimeoutMs) * time.Millisecond
} }
@@ -144,6 +234,7 @@ func NewClient(
client.server = server client.server = server
client.skipFallback = ns.SkipFallback client.skipFallback = ns.SkipFallback
client.domains = rules
client.expectedIPs = expectedMatcher client.expectedIPs = expectedMatcher
client.unexpectedIPs = unexpectedMatcher client.unexpectedIPs = unexpectedMatcher
client.actPrior = ns.ActPrior client.actPrior = ns.ActPrior
@@ -167,7 +258,7 @@ func (c *Client) Name() string {
// QueryIP sends DNS query to the name server with the client's IP. // QueryIP sends DNS query to the name server with the client's IP.
func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) { func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) {
if c.checkSystem { if c.checkSystem {
supportIPv4, supportIPv6 := utils.CheckRoutes() supportIPv4, supportIPv6 := checkRoutes()
option.IPv4Enable = option.IPv4Enable && supportIPv4 option.IPv4Enable = option.IPv4Enable && supportIPv4
option.IPv6Enable = option.IPv6Enable && supportIPv6 option.IPv6Enable = option.IPv6Enable && supportIPv6
} else { } else {
+2 -21
View File
@@ -137,32 +137,14 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
if s.Name()+"." == "DOH//"+fqdn { if s.Name()+"." == "DOH//"+fqdn {
errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead") errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead")
if noResponseErrCh != nil { if noResponseErrCh != nil {
err := errors.New("tries to resolve itself!", s.Name()) noResponseErrCh <- errors.New("tries to resolve itself!", s.Name())
if option.IPv4Enable {
noResponseErrCh <- err
}
if option.IPv6Enable {
noResponseErrCh <- err
}
} }
return return
} }
// As we don't want our traffic pattern looks like DoH, we use Random-Length Padding instead of Block-Length Padding recommended in RFC 8467 // As we don't want our traffic pattern looks like DoH, we use Random-Length Padding instead of Block-Length Padding recommended in RFC 8467
// Although DoH server like 1.1.1.1 will pad the response to Block-Length 468, at least it is better than no padding for response at all // Although DoH server like 1.1.1.1 will pad the response to Block-Length 468, at least it is better than no padding for response at all
reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, int(crypto.RandBetween(100, 300)))) reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, int(crypto.RandBetween(100, 300))))
if err != nil {
errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn)
if noResponseErrCh != nil {
if option.IPv4Enable {
noResponseErrCh <- err
}
if option.IPv6Enable {
noResponseErrCh <- err
}
}
return
}
var deadline time.Time var deadline time.Time
if d, ok := ctx.Deadline(); ok { if d, ok := ctx.Deadline(); ok {
@@ -232,7 +214,6 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte,
req.Header.Add("Accept", "application/dns-message") req.Header.Add("Accept", "application/dns-message")
req.Header.Add("Content-Type", "application/dns-message") req.Header.Add("Content-Type", "application/dns-message")
utils.TryDefaultHeadersWith(req.Header, "fetch")
req.Header.Set("X-Padding", utils.H2Base62Pad(crypto.RandBetween(100, 1000))) req.Header.Set("X-Padding", utils.H2Base62Pad(crypto.RandBetween(100, 1000)))
hc := s.httpClient hc := s.httpClient
+1
View File
@@ -18,6 +18,7 @@ type LocalNameServer struct {
// QueryIP implements Server. // QueryIP implements Server.
func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, option dns.IPOption) (ips []net.IP, ttl uint32, err error) { func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, option dns.IPOption) (ips []net.IP, ttl uint32, err error) {
start := time.Now() start := time.Now()
ips, ttl, err = s.client.LookupIP(domain, option) ips, ttl, err = s.client.LookupIP(domain, option)
+1 -13
View File
@@ -78,19 +78,7 @@ func (s *QUICNameServer) getCacheController() *CacheController { return s.cacheC
func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) {
errors.LogInfo(ctx, s.Name(), " querying: ", fqdn) errors.LogInfo(ctx, s.Name(), " querying: ", fqdn)
reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
if err != nil {
errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn)
if noResponseErrCh != nil {
if option.IPv4Enable {
noResponseErrCh <- err
}
if option.IPv6Enable {
noResponseErrCh <- err
}
}
return
}
var deadline time.Time var deadline time.Time
if d, ok := ctx.Deadline(); ok { if d, ok := ctx.Deadline(); ok {
+1 -13
View File
@@ -113,19 +113,7 @@ func (s *TCPNameServer) getCacheController() *CacheController {
func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) {
errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn) errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn)
reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
if err != nil {
errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn)
if noResponseErrCh != nil {
if option.IPv4Enable {
noResponseErrCh <- err
}
if option.IPv6Enable {
noResponseErrCh <- err
}
}
return
}
var deadline time.Time var deadline time.Time
if d, ok := ctx.Deadline(); ok { if d, ok := ctx.Deadline(); ok {
+5 -13
View File
@@ -131,6 +131,8 @@ func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_prot
newReq.msg = &newMsg newReq.msg = &newMsg
s.addPendingRequest(&newReq) s.addPendingRequest(&newReq)
b, _ := dns.PackMessage(newReq.msg) b, _ := dns.PackMessage(newReq.msg)
copyDest := net.UDPDestination(s.address.Address, s.address.Port)
b.UDP = &copyDest
s.udpServer.Dispatch(toDnsContext(newReq.ctx, s.address.String()), *s.address, b) s.udpServer.Dispatch(toDnsContext(newReq.ctx, s.address.String()), *s.address, b)
return return
} }
@@ -161,19 +163,7 @@ func (s *ClassicNameServer) getCacheController() *CacheController {
func (s *ClassicNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) { func (s *ClassicNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, fqdn string, option dns_feature.IPOption) {
errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn) errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn)
reqs, err := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
if err != nil {
errors.LogErrorInner(ctx, err, "failed to build dns query for ", fqdn)
if noResponseErrCh != nil {
if option.IPv4Enable {
noResponseErrCh <- err
}
if option.IPv6Enable {
noResponseErrCh <- err
}
}
return
}
for _, req := range reqs { for _, req := range reqs {
udpReq := &udpDnsRequest{ udpReq := &udpDnsRequest{
@@ -189,6 +179,8 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<
} }
return return
} }
copyDest := net.UDPDestination(s.address.Address, s.address.Port)
b.UDP = &copyDest
s.udpServer.Dispatch(toDnsContext(ctx, s.address.String()), *s.address, b) s.udpServer.Dispatch(toDnsContext(ctx, s.address.String()), *s.address, b)
} }
} }
-198
View File
@@ -1,198 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.5
// source: app/geodata/config.proto
package geodata
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Asset struct {
state protoimpl.MessageState `protogen:"open.v1"`
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
File string `protobuf:"bytes,2,opt,name=file,proto3" json:"file,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Asset) Reset() {
*x = Asset{}
mi := &file_app_geodata_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Asset) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Asset) ProtoMessage() {}
func (x *Asset) ProtoReflect() protoreflect.Message {
mi := &file_app_geodata_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Asset.ProtoReflect.Descriptor instead.
func (*Asset) Descriptor() ([]byte, []int) {
return file_app_geodata_config_proto_rawDescGZIP(), []int{0}
}
func (x *Asset) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *Asset) GetFile() string {
if x != nil {
return x.File
}
return ""
}
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
Cron string `protobuf:"bytes,1,opt,name=cron,proto3" json:"cron,omitempty"`
Outbound string `protobuf:"bytes,2,opt,name=outbound,proto3" json:"outbound,omitempty"`
Assets []*Asset `protobuf:"bytes,3,rep,name=assets,proto3" json:"assets,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_app_geodata_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_app_geodata_config_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_app_geodata_config_proto_rawDescGZIP(), []int{1}
}
func (x *Config) GetCron() string {
if x != nil {
return x.Cron
}
return ""
}
func (x *Config) GetOutbound() string {
if x != nil {
return x.Outbound
}
return ""
}
func (x *Config) GetAssets() []*Asset {
if x != nil {
return x.Assets
}
return nil
}
var File_app_geodata_config_proto protoreflect.FileDescriptor
const file_app_geodata_config_proto_rawDesc = "" +
"\n" +
"\x18app/geodata/config.proto\x12\x10xray.app.geodata\"-\n" +
"\x05Asset\x12\x10\n" +
"\x03url\x18\x01 \x01(\tR\x03url\x12\x12\n" +
"\x04file\x18\x02 \x01(\tR\x04file\"i\n" +
"\x06Config\x12\x12\n" +
"\x04cron\x18\x01 \x01(\tR\x04cron\x12\x1a\n" +
"\boutbound\x18\x02 \x01(\tR\boutbound\x12/\n" +
"\x06assets\x18\x03 \x03(\v2\x17.xray.app.geodata.AssetR\x06assetsBR\n" +
"\x14com.xray.app.geodataP\x01Z%github.com/xtls/xray-core/app/geodata\xaa\x02\x10Xray.App.Geodatab\x06proto3"
var (
file_app_geodata_config_proto_rawDescOnce sync.Once
file_app_geodata_config_proto_rawDescData []byte
)
func file_app_geodata_config_proto_rawDescGZIP() []byte {
file_app_geodata_config_proto_rawDescOnce.Do(func() {
file_app_geodata_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_geodata_config_proto_rawDesc), len(file_app_geodata_config_proto_rawDesc)))
})
return file_app_geodata_config_proto_rawDescData
}
var file_app_geodata_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_app_geodata_config_proto_goTypes = []any{
(*Asset)(nil), // 0: xray.app.geodata.Asset
(*Config)(nil), // 1: xray.app.geodata.Config
}
var file_app_geodata_config_proto_depIdxs = []int32{
0, // 0: xray.app.geodata.Config.assets:type_name -> xray.app.geodata.Asset
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_app_geodata_config_proto_init() }
func file_app_geodata_config_proto_init() {
if File_app_geodata_config_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_geodata_config_proto_rawDesc), len(file_app_geodata_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_geodata_config_proto_goTypes,
DependencyIndexes: file_app_geodata_config_proto_depIdxs,
MessageInfos: file_app_geodata_config_proto_msgTypes,
}.Build()
File_app_geodata_config_proto = out.File
file_app_geodata_config_proto_goTypes = nil
file_app_geodata_config_proto_depIdxs = nil
}
-21
View File
@@ -1,21 +0,0 @@
syntax = "proto3";
package xray.app.geodata;
option csharp_namespace = "Xray.App.Geodata";
option go_package = "github.com/xtls/xray-core/app/geodata";
option java_package = "com.xray.app.geodata";
option java_multiple_files = true;
message Asset {
string url = 1;
string file = 2;
}
message Config {
string cron = 1;
string outbound = 2;
repeated Asset assets = 3;
}
-304
View File
@@ -1,304 +0,0 @@
package geodata
import (
"context"
go_errors "errors"
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet/tagged"
)
const idleTimeout = 30 * time.Second
type stage struct {
target string
temp string
}
type downloader struct {
ctx context.Context
client *http.Client
}
type idleConn struct {
net.Conn
}
func (c *idleConn) Read(b []byte) (int, error) {
t := time.AfterFunc(idleTimeout, func() {
_ = c.Close()
})
n, err := c.Conn.Read(b)
if !t.Stop() {
_ = c.Close()
return n, errors.New("connection idle timeout")
}
return n, err
}
func (c *idleConn) Write(b []byte) (int, error) {
return c.Conn.Write(b)
}
func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader {
return &downloader{
ctx: ctx,
client: newClient(ctx, dispatcher, outbound),
}
}
func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string) *http.Client {
return &http.Client{
Transport: &http.Transport{
Proxy: nil,
DisableKeepAlives: true,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
var conn net.Conn
err := task.Run(ctx, func() error {
if tagged.Dialer == nil {
return errors.New("tagged dialer is not initialized")
}
dest, err := net.ParseDestination(network + ":" + address)
if err != nil {
return errors.New("cannot understand address").Base(err)
}
c, err := tagged.Dialer(baseCtx, dispatcher, dest, outbound)
if err != nil {
return errors.New("cannot dial remote address ", dest).Base(err)
}
conn = c
return nil
})
if err != nil {
return nil, errors.New("cannot finish connection").Base(err)
}
return &idleConn{
Conn: conn,
}, nil
},
TLSHandshakeTimeout: idleTimeout,
ResponseHeaderTimeout: idleTimeout,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
}
func (d *downloader) download(assets []*Asset) ([]stage, error) {
staged := make([]stage, 0, len(assets))
for _, asset := range assets {
stage, err := d.downloadOne(asset)
if err != nil {
clean(staged)
return nil, err
}
staged = append(staged, stage)
}
return staged, nil
}
func (d *downloader) downloadOne(asset *Asset) (stage, error) {
target, err := filesystem.ResolveAsset(asset.File)
if err != nil {
return stage{}, err
}
errors.LogInfo(d.ctx, "downloading geodata asset from ", asset.Url, " to ", target)
temp, err := tempFile(target, ".tmp")
if err != nil {
return stage{}, err
}
tempName := temp.Name()
keepTemp := false
defer func() {
if !keepTemp {
os.Remove(tempName)
}
}()
if err := d.fetch(asset.Url, temp); err != nil {
temp.Close()
return stage{}, err
}
if err := temp.Chmod(0o644); err != nil {
temp.Close()
return stage{}, err
}
if err := temp.Close(); err != nil {
return stage{}, err
}
keepTemp = true
return stage{
target: target,
temp: tempName,
}, nil
}
func (d *downloader) fetch(rawURL string, writer io.Writer) error {
req, err := http.NewRequestWithContext(d.ctx, http.MethodGet, rawURL, nil)
if err != nil {
return err
}
utils.TryDefaultHeadersWith(req.Header, "nav")
resp, err := d.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
io.Copy(io.Discard, resp.Body)
return errors.New("unexpected status code: ", resp.StatusCode)
}
n, err := io.Copy(writer, resp.Body)
if err != nil {
return err
}
if n == 0 {
return errors.New("empty response body")
}
return nil
}
func clean(assets []stage) {
for _, asset := range assets {
if asset.temp != "" {
os.Remove(asset.temp)
}
}
}
type tx struct {
swaps []swap
}
type swap struct {
target string
backup string
hadOriginal bool
}
func swapAll(assets []stage) (*tx, error) {
t := &tx{}
for _, asset := range assets {
s, err := swapOne(asset)
if err != nil {
return nil, errors.Combine(err, t.rollback())
}
t.swaps = append(t.swaps, s)
}
return t, nil
}
func swapOne(asset stage) (swap, error) {
backup, err := backupFile(asset.target)
if err != nil {
return swap{}, err
}
s := swap{
target: asset.target,
backup: backup,
}
if err := os.Rename(asset.target, backup); err != nil {
if !go_errors.Is(err, os.ErrNotExist) {
return swap{}, err
}
if err := os.Remove(backup); err != nil && !go_errors.Is(err, os.ErrNotExist) {
return swap{}, err
}
} else {
s.hadOriginal = true
}
if err := os.Rename(asset.temp, asset.target); err != nil {
if s.hadOriginal {
if restoreErr := os.Rename(backup, asset.target); restoreErr != nil {
return swap{}, errors.Combine(err, restoreErr)
}
}
return swap{}, err
}
return s, nil
}
func (t *tx) rollback() error {
var errs []error
for i := len(t.swaps) - 1; i >= 0; i-- {
if err := t.swaps[i].rollback(); err != nil {
errs = append(errs, err)
}
}
return errors.Combine(errs...)
}
func (s swap) rollback() error {
var errs []error
if err := os.Remove(s.target); err != nil && !go_errors.Is(err, os.ErrNotExist) {
errs = append(errs, err)
}
if s.hadOriginal {
if err := os.Rename(s.backup, s.target); err != nil {
errs = append(errs, err)
}
} else if err := os.Remove(s.backup); err != nil && !go_errors.Is(err, os.ErrNotExist) {
errs = append(errs, err)
}
return errors.Combine(errs...)
}
func (t *tx) commit() error {
var errs []error
for _, swap := range t.swaps {
if err := os.Remove(swap.backup); err != nil && !go_errors.Is(err, os.ErrNotExist) {
errs = append(errs, err)
}
}
return errors.Combine(errs...)
}
func tempFile(target string, suffix string) (*os.File, error) {
dir := filepath.Dir(target)
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
return os.CreateTemp(dir, "."+filepath.Base(target)+".*"+suffix)
}
func backupFile(target string) (string, error) {
file, err := tempFile(target, ".bak")
if err != nil {
return "", err
}
name := file.Name()
if err := file.Close(); err != nil {
os.Remove(name)
return "", err
}
if err := os.Remove(name); err != nil {
return "", err
}
return name, nil
}
-134
View File
@@ -1,134 +0,0 @@
package geodata
import (
"context"
"sync"
"github.com/robfig/cron/v3"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
commongeodata "github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing"
)
type Instance struct {
assets []*Asset
downloader *downloader
tasker *cron.Cron
mu sync.Mutex
running bool
}
func New(ctx context.Context, config *Config) (*Instance, error) {
if config.Cron == "" {
return &Instance{}, nil
}
g := &Instance{
assets: config.Assets,
}
if len(g.assets) > 0 {
var dispatcher routing.Dispatcher
if err := core.RequireFeatures(ctx, func(d routing.Dispatcher) {
dispatcher = d
}); err != nil {
return nil, errors.New("failed to get dispatcher for geodata downloader").Base(err)
}
g.downloader = newDownloader(ctx, dispatcher, config.Outbound)
}
g.tasker = cron.New(
cron.WithChain(cron.SkipIfStillRunning(cron.DiscardLogger)),
cron.WithLogger(cron.DiscardLogger),
)
if _, err := g.tasker.AddFunc(config.Cron, g.execute); err != nil {
return nil, errors.New("invalid geodata cron").Base(err)
}
errors.LogInfo(ctx, "scheduled geodata reload with cron: ", config.Cron)
return g, nil
}
func (g *Instance) execute() {
var err error
if g.downloader != nil {
err = g.reloadWithUpdate()
} else {
err = reload()
}
if err != nil {
errors.LogErrorInner(context.Background(), err, "scheduled geodata reload failed")
}
}
func (g *Instance) reloadWithUpdate() error {
staged, err := g.downloader.download(g.assets)
if err != nil {
return err
}
defer clean(staged)
tx, err := swapAll(staged)
if err != nil {
return err
}
if err := reload(); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload geodata after downloading assets, rolling back")
rollbackErr := tx.rollback()
return errors.Combine(err, rollbackErr)
}
return tx.commit()
}
func reload() error {
return errors.Combine(commongeodata.IPReg.Reload(), commongeodata.DomainReg.Reload())
}
func (g *Instance) Type() interface{} {
return (*Instance)(nil)
}
func (g *Instance) Start() error {
g.mu.Lock()
defer g.mu.Unlock()
if g.running {
return nil
}
if g.tasker != nil {
g.tasker.Start()
}
g.running = true
return nil
}
func (g *Instance) Close() error {
g.mu.Lock()
defer g.mu.Unlock()
if !g.running {
return nil
}
if g.tasker != nil {
<-g.tasker.Stop().Done()
}
g.running = false
return nil
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) {
return New(ctx, cfg.(*Config))
}))
}
+7 -1
View File
@@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/xtls/xray-core/app/observatory" "github.com/xtls/xray-core/app/observatory"
"github.com/xtls/xray-core/app/stats"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
@@ -38,12 +39,16 @@ func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, er
c.ohm = om c.ohm = om
})) }))
expvar.Publish("stats", expvar.Func(func() interface{} { expvar.Publish("stats", expvar.Func(func() interface{} {
manager, ok := c.statsManager.(*stats.Manager)
if !ok {
return nil
}
resp := map[string]map[string]map[string]int64{ resp := map[string]map[string]map[string]int64{
"inbound": {}, "inbound": {},
"outbound": {}, "outbound": {},
"user": {}, "user": {},
} }
c.statsManager.VisitCounters(func(name string, counter feature_stats.Counter) bool { manager.VisitCounters(func(name string, counter feature_stats.Counter) bool {
nameSplit := strings.Split(name, ">>>") nameSplit := strings.Split(name, ">>>")
typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3] typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3]
if item, found := resp[typeName][tagOrUser]; found { if item, found := resp[typeName][tagOrUser]; found {
@@ -85,6 +90,7 @@ func (p *MetricsHandler) Type() interface{} {
} }
func (p *MetricsHandler) Start() error { func (p *MetricsHandler) Start() error {
// direct listen a port if listen is set // direct listen a port if listen is set
if p.listen != "" { if p.listen != "" {
TCPlistener, err := net.Listen("tcp", p.listen) TCPlistener, err := net.Listen("tcp", p.listen)
+2 -4
View File
@@ -2,6 +2,7 @@ package burst
import ( import (
"context" "context"
"sync" "sync"
"github.com/xtls/xray-core/app/observatory" "github.com/xtls/xray-core/app/observatory"
@@ -31,10 +32,6 @@ func (o *Observer) GetObservation(ctx context.Context) (proto.Message, error) {
return &observatory.ObservationResult{Status: o.createResult()}, nil return &observatory.ObservationResult{Status: o.createResult()}, nil
} }
func (o *Observer) Check(tag []string) {
o.hp.Check(tag)
}
func (o *Observer) createResult() []*observatory.OutboundStatus { func (o *Observer) createResult() []*observatory.OutboundStatus {
var result []*observatory.OutboundStatus var result []*observatory.OutboundStatus
o.hp.access.Lock() o.hp.access.Lock()
@@ -71,6 +68,7 @@ func (o *Observer) Start() error {
o.hp.StartScheduler(func() ([]string, error) { o.hp.StartScheduler(func() ([]string, error) {
hs, ok := o.ohm.(outbound.HandlerSelector) hs, ok := o.ohm.(outbound.HandlerSelector)
if !ok { if !ok {
return nil, errors.New("outbound.Manager is not a HandlerSelector") return nil, errors.New("outbound.Manager is not a HandlerSelector")
} }
+24 -42
View File
@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common/dice" "github.com/xtls/xray-core/common/dice"
@@ -25,12 +24,11 @@ type HealthPingSettings struct {
// HealthPing is the health checker for balancers // HealthPing is the health checker for balancers
type HealthPing struct { type HealthPing struct {
ctx context.Context ctx context.Context
cancelCtx context.CancelFunc dispatcher routing.Dispatcher
cancelPending atomic.Pointer[context.CancelFunc] access sync.Mutex
dispatcher routing.Dispatcher ticker *time.Ticker
access sync.Mutex tickerClose chan struct{}
ticker *time.Ticker
Settings *HealthPingSettings Settings *HealthPingSettings
Results map[string]*HealthPingRTTS Results map[string]*HealthPingRTTS
@@ -64,10 +62,10 @@ func NewHealthPing(ctx context.Context, dispatcher routing.Dispatcher, config *H
settings.Destination = "https://connectivitycheck.gstatic.com/generate_204" settings.Destination = "https://connectivitycheck.gstatic.com/generate_204"
} }
if settings.Interval == 0 { if settings.Interval == 0 {
settings.Interval = 1 * time.Minute settings.Interval = time.Duration(1) * time.Minute
} else if settings.Interval < 10*time.Second { } else if settings.Interval < 10 {
errors.LogWarning(ctx, "health check interval is too small, 10s is applied") errors.LogWarning(ctx, "health check interval is too small, 10s is applied")
settings.Interval = 10 * time.Second settings.Interval = time.Duration(10) * time.Second
} }
if settings.SamplingCount <= 0 { if settings.SamplingCount <= 0 {
settings.SamplingCount = 10 settings.SamplingCount = 10
@@ -75,12 +73,10 @@ func NewHealthPing(ctx context.Context, dispatcher routing.Dispatcher, config *H
if settings.Timeout <= 0 { if settings.Timeout <= 0 {
// results are saved after all health pings finish, // results are saved after all health pings finish,
// a larger timeout could possibly makes checks run longer // a larger timeout could possibly makes checks run longer
settings.Timeout = 5 * time.Second settings.Timeout = time.Duration(5) * time.Second
} }
ctx, cancel := context.WithCancel(ctx)
return &HealthPing{ return &HealthPing{
ctx: ctx, ctx: ctx,
cancelCtx: cancel,
dispatcher: dispatcher, dispatcher: dispatcher,
Settings: settings, Settings: settings,
Results: nil, Results: nil,
@@ -94,9 +90,9 @@ func (h *HealthPing) StartScheduler(selector func() ([]string, error)) {
} }
interval := h.Settings.Interval * time.Duration(h.Settings.SamplingCount) interval := h.Settings.Interval * time.Duration(h.Settings.SamplingCount)
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
tickerClose := make(chan struct{})
h.ticker = ticker h.ticker = ticker
h.tickerClose = tickerClose
// init run to get a fast check result
go func() { go func() {
tags, err := selector() tags, err := selector()
if err != nil { if err != nil {
@@ -114,20 +110,13 @@ func (h *HealthPing) StartScheduler(selector func() ([]string, error)) {
errors.LogWarning(h.ctx, "error select outbounds for scheduled health check: ", err) errors.LogWarning(h.ctx, "error select outbounds for scheduled health check: ", err)
return return
} }
subCtx, cancel := context.WithCancel(h.ctx) h.doCheck(tags, interval, h.Settings.SamplingCount)
old := h.cancelPending.Swap(&cancel)
if old != nil {
errors.LogDebug(h.ctx, "scheduled health check not finished before next round, canceling previous one")
(*old)()
}
h.doCheck(subCtx, tags, interval, h.Settings.SamplingCount)
h.cancelPending.CompareAndSwap(&cancel, nil)
h.Cleanup(tags) h.Cleanup(tags)
}() }()
select { select {
case <-ticker.C: case <-ticker.C:
continue continue
case <-h.ctx.Done(): case <-tickerClose:
return return
} }
} }
@@ -141,7 +130,8 @@ func (h *HealthPing) StopScheduler() {
} }
h.ticker.Stop() h.ticker.Stop()
h.ticker = nil h.ticker = nil
h.cancelCtx() close(h.tickerClose)
h.tickerClose = nil
} }
// Check implements the HealthChecker // Check implements the HealthChecker
@@ -150,7 +140,7 @@ func (h *HealthPing) Check(tags []string) error {
return nil return nil
} }
errors.LogInfo(h.ctx, "perform one-time health check for tags ", tags) errors.LogInfo(h.ctx, "perform one-time health check for tags ", tags)
h.doCheck(h.ctx, tags, 0, 1) h.doCheck(tags, 0, 1)
return nil return nil
} }
@@ -161,14 +151,13 @@ type rtt struct {
// doCheck performs the 'rounds' amount checks in given 'duration'. You should make // doCheck performs the 'rounds' amount checks in given 'duration'. You should make
// sure all tags are valid for current balancer // sure all tags are valid for current balancer
// cancel ctx will stop all pending checks func (h *HealthPing) doCheck(tags []string, duration time.Duration, rounds int) {
func (h *HealthPing) doCheck(ctx context.Context, tags []string, duration time.Duration, rounds int) {
count := len(tags) * rounds count := len(tags) * rounds
if count == 0 { if count == 0 {
return return
} }
ch := make(chan *rtt, count) ch := make(chan *rtt, count)
timers := make([]*time.Timer, 0, count)
for _, tag := range tags { for _, tag := range tags {
handler := tag handler := tag
client := newPingClient( client := newPingClient(
@@ -183,7 +172,7 @@ func (h *HealthPing) doCheck(ctx context.Context, tags []string, duration time.D
if duration > 0 { if duration > 0 {
delay = time.Duration(dice.RollInt63n(int64(duration))) delay = time.Duration(dice.RollInt63n(int64(duration)))
} }
timers = append(timers, time.AfterFunc(delay, func() { time.AfterFunc(delay, func() {
errors.LogDebug(h.ctx, "checking ", handler) errors.LogDebug(h.ctx, "checking ", handler)
delay, err := client.MeasureDelay(h.Settings.HttpMethod) delay, err := client.MeasureDelay(h.Settings.HttpMethod)
if err == nil { if err == nil {
@@ -211,21 +200,14 @@ func (h *HealthPing) doCheck(ctx context.Context, tags []string, duration time.D
handler: handler, handler: handler,
value: rttFailed, value: rttFailed,
} }
})) })
} }
} }
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
select { rtt := <-ch
case rtt := <-ch: if rtt.value > 0 {
if rtt.value > 0 { // should not put results when network is down
// should not put results when network is down h.PutResult(rtt.handler, rtt.value)
h.PutResult(rtt.handler, rtt.value)
}
case <-ctx.Done():
for _, timer := range timers {
timer.Stop()
}
return
} }
} }
} }
+2 -2
View File
@@ -59,7 +59,7 @@ func (h *HealthPingRTTS) Put(d time.Duration) {
if h.rtts == nil { if h.rtts == nil {
h.rtts = make([]*pingRTT, h.cap) h.rtts = make([]*pingRTT, h.cap)
for i := 0; i < h.cap; i++ { for i := 0; i < h.cap; i++ {
h.rtts[i] = &pingRTT{value: rttUntested} h.rtts[i] = &pingRTT{}
} }
h.idx = -1 h.idx = -1
} }
@@ -88,7 +88,7 @@ func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
validRTTs := make([]time.Duration, 0) validRTTs := make([]time.Duration, 0)
for _, rtt := range h.rtts { for _, rtt := range h.rtts {
switch { switch {
case rtt.value == rttUntested || time.Since(rtt.time) > h.validity: case rtt.value == 0 || time.Since(rtt.time) > h.validity:
continue continue
case rtt.value == rttFailed: case rtt.value == rttFailed:
stats.Fail++ stats.Fail++
-2
View File
@@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet/tagged" "github.com/xtls/xray-core/transport/internet/tagged"
) )
@@ -62,7 +61,6 @@ func (s *pingClient) MeasureDelay(httpMethod string) (time.Duration, error) {
if err != nil { if err != nil {
return rttFailed, err return rttFailed, err
} }
utils.TryDefaultHeadersWith(req.Header, "nav")
start := time.Now() start := time.Now()
resp, err := s.httpClient.Do(req) resp, err := s.httpClient.Do(req)
+6 -18
View File
@@ -5,7 +5,6 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"slices"
"sort" "sort"
"sync" "sync"
"time" "time"
@@ -16,7 +15,6 @@ import (
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal/done" "github.com/xtls/xray-core/common/signal/done"
"github.com/xtls/xray-core/common/task" "github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/extension" "github.com/xtls/xray-core/features/extension"
"github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/outbound"
@@ -71,7 +69,7 @@ func (o *Observer) background() {
outbounds := hs.Select(o.config.SubjectSelector) outbounds := hs.Select(o.config.SubjectSelector)
o.clearRemovedOutbounds(outbounds) o.updateStatus(outbounds)
sleepTime := time.Second * 10 sleepTime := time.Second * 10
if o.config.ProbeInterval != 0 { if o.config.ProbeInterval != 0 {
@@ -112,19 +110,11 @@ func (o *Observer) background() {
} }
} }
func (o *Observer) clearRemovedOutbounds(outbounds []string) { func (o *Observer) updateStatus(outbounds []string) {
o.statusLock.Lock() o.statusLock.Lock()
defer o.statusLock.Unlock() defer o.statusLock.Unlock()
if len(o.status) == 0 { // TODO should remove old inbound that is removed
return _ = outbounds
}
var pruned []*OutboundStatus
for _, status := range o.status {
if slices.Contains(outbounds, status.OutboundTag) {
pruned = append(pruned, status)
}
}
o.status = pruned
} }
func (o *Observer) probe(outbound string) ProbeResult { func (o *Observer) probe(outbound string) ProbeResult {
@@ -172,9 +162,7 @@ func (o *Observer) probe(outbound string) ProbeResult {
if o.config.ProbeUrl != "" { if o.config.ProbeUrl != "" {
probeURL = o.config.ProbeUrl probeURL = o.config.ProbeUrl
} }
req, _ := http.NewRequest(http.MethodGet, probeURL, nil) response, err := httpClient.Get(probeURL)
utils.TryDefaultHeadersWith(req.Header, "nav")
response, err := httpClient.Do(req)
if err != nil { if err != nil {
return errors.New("outbound failed to relay connection").Base(err) return errors.New("outbound failed to relay connection").Base(err)
} }
@@ -186,7 +174,7 @@ func (o *Observer) probe(outbound string) ProbeResult {
return nil return nil
}) })
if err != nil { if err != nil {
errorMessage := "the outbound " + outbound + " is dead: GET request failed:" + err.Error() + "with outbound handler report underlying connection failed" var errorMessage = "the outbound " + outbound + " is dead: GET request failed:" + err.Error() + "with outbound handler report underlying connection failed"
errors.LogInfoInner(o.ctx, errorCollectorForRequest.UnderlyingError(), errorMessage) errors.LogInfoInner(o.ctx, errorCollectorForRequest.UnderlyingError(), errorMessage)
return ProbeResult{Alive: false, LastErrorReason: errorMessage} return ProbeResult{Alive: false, LastErrorReason: errorMessage}
} }
-64
View File
@@ -1,64 +0,0 @@
package observatory
import "testing"
func TestObserverUpdateStatusPrunesStaleOutbounds(t *testing.T) {
observer := &Observer{
status: []*OutboundStatus{
{
OutboundTag: "keep",
Alive: true,
Delay: 42,
LastErrorReason: "",
LastSeenTime: 111,
LastTryTime: 222,
},
{
OutboundTag: "drop",
Alive: false,
Delay: 99999999,
LastErrorReason: "probe failed",
LastSeenTime: 333,
LastTryTime: 444,
},
},
}
observer.clearRemovedOutbounds([]string{"keep"})
if len(observer.status) != 1 {
t.Fatalf("expected 1 status after pruning, got %d", len(observer.status))
}
got := observer.status[0]
if got.OutboundTag != "keep" {
t.Fatalf("expected remaining status for keep, got %q", got.OutboundTag)
}
if !got.Alive {
t.Fatal("expected remaining status to preserve Alive field")
}
if got.Delay != 42 {
t.Fatalf("expected remaining status to preserve Delay, got %d", got.Delay)
}
if got.LastSeenTime != 111 {
t.Fatalf("expected remaining status to preserve LastSeenTime, got %d", got.LastSeenTime)
}
if got.LastTryTime != 222 {
t.Fatalf("expected remaining status to preserve LastTryTime, got %d", got.LastTryTime)
}
}
func TestObserverUpdateStatusClearsWhenNoOutboundsRemain(t *testing.T) {
observer := &Observer{
status: []*OutboundStatus{
{OutboundTag: "drop-1"},
{OutboundTag: "drop-2"},
},
}
observer.clearRemovedOutbounds(nil)
if len(observer.status) != 0 {
t.Fatalf("expected all statuses to be removed, got %d", len(observer.status))
}
}
+1 -1
View File
@@ -138,7 +138,7 @@ func (s *handlerServer) GetInboundUsers(ctx context.Context, request *GetInbound
if len(request.Email) > 0 { if len(request.Email) > 0 {
return &GetInboundUserResponse{Users: []*protocol.User{protocol.ToProtoUser(um.GetUser(ctx, request.Email))}}, nil return &GetInboundUserResponse{Users: []*protocol.User{protocol.ToProtoUser(um.GetUser(ctx, request.Email))}}, nil
} }
result := make([]*protocol.User, 0, 100) var result = make([]*protocol.User, 0, 100)
users := um.GetUsers(ctx) users := um.GetUsers(ctx)
for _, u := range users { for _, u := range users {
result = append(result, protocol.ToProtoUser(u)) result = append(result, protocol.ToProtoUser(u))
-33
View File
@@ -1,34 +1 @@
package proxyman package proxyman
import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/session"
)
func BuildSniffingRequest(config *SniffingConfig) (session.SniffingRequest, error) {
if config == nil {
return session.SniffingRequest{}, nil
}
request := session.SniffingRequest{
Enabled: config.Enabled,
OverrideDestinationForProtocol: config.DestinationOverride,
MetadataOnly: config.MetadataOnly,
RouteOnly: config.RouteOnly,
}
if len(config.DomainsExcluded) > 0 {
excludeForDomain, err := geodata.DomainReg.BuildDomainMatcher(config.DomainsExcluded)
if err != nil {
return session.SniffingRequest{}, err
}
request.ExcludeForDomain = excludeForDomain
}
if len(config.IpsExcluded) > 0 {
excludeForIP, err := geodata.IPReg.BuildIPMatcher(config.IpsExcluded)
if err != nil {
return session.SniffingRequest{}, err
}
request.ExcludeForIP = excludeForIP
}
return request, nil
}
+43 -47
View File
@@ -7,7 +7,6 @@
package proxyman package proxyman
import ( import (
geodata "github.com/xtls/xray-core/common/geodata"
net "github.com/xtls/xray-core/common/net" net "github.com/xtls/xray-core/common/net"
serial "github.com/xtls/xray-core/common/serial" serial "github.com/xtls/xray-core/common/serial"
internet "github.com/xtls/xray-core/transport/internet" internet "github.com/xtls/xray-core/transport/internet"
@@ -67,9 +66,8 @@ type SniffingConfig struct {
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
// Override target destination if sniff'ed protocol is in the given list. // Override target destination if sniff'ed protocol is in the given list.
// Supported values are "http", "tls", "fakedns". // Supported values are "http", "tls", "fakedns".
DestinationOverride []string `protobuf:"bytes,2,rep,name=destination_override,json=destinationOverride,proto3" json:"destination_override,omitempty"` DestinationOverride []string `protobuf:"bytes,2,rep,name=destination_override,json=destinationOverride,proto3" json:"destination_override,omitempty"`
DomainsExcluded []*geodata.DomainRule `protobuf:"bytes,3,rep,name=domains_excluded,json=domainsExcluded,proto3" json:"domains_excluded,omitempty"` DomainsExcluded []string `protobuf:"bytes,3,rep,name=domains_excluded,json=domainsExcluded,proto3" json:"domains_excluded,omitempty"`
IpsExcluded []*geodata.IPRule `protobuf:"bytes,6,rep,name=ips_excluded,json=ipsExcluded,proto3" json:"ips_excluded,omitempty"`
// Whether should only try to sniff metadata without waiting for client input. // Whether should only try to sniff metadata without waiting for client input.
// Can be used to support SMTP like protocol where server send the first // Can be used to support SMTP like protocol where server send the first
// message. // message.
@@ -123,20 +121,13 @@ func (x *SniffingConfig) GetDestinationOverride() []string {
return nil return nil
} }
func (x *SniffingConfig) GetDomainsExcluded() []*geodata.DomainRule { func (x *SniffingConfig) GetDomainsExcluded() []string {
if x != nil { if x != nil {
return x.DomainsExcluded return x.DomainsExcluded
} }
return nil return nil
} }
func (x *SniffingConfig) GetIpsExcluded() []*geodata.IPRule {
if x != nil {
return x.IpsExcluded
}
return nil
}
func (x *SniffingConfig) GetMetadataOnly() bool { func (x *SniffingConfig) GetMetadataOnly() bool {
if x != nil { if x != nil {
return x.MetadataOnly return x.MetadataOnly
@@ -420,8 +411,10 @@ type MultiplexingConfig struct {
XudpConcurrency int32 `protobuf:"varint,3,opt,name=xudpConcurrency,proto3" json:"xudpConcurrency,omitempty"` XudpConcurrency int32 `protobuf:"varint,3,opt,name=xudpConcurrency,proto3" json:"xudpConcurrency,omitempty"`
// "reject" (default), "allow" or "skip". // "reject" (default), "allow" or "skip".
XudpProxyUDP443 string `protobuf:"bytes,4,opt,name=xudpProxyUDP443,proto3" json:"xudpProxyUDP443,omitempty"` XudpProxyUDP443 string `protobuf:"bytes,4,opt,name=xudpProxyUDP443,proto3" json:"xudpProxyUDP443,omitempty"`
unknownFields protoimpl.UnknownFields // MaxReuseTimes for an connection
sizeCache protoimpl.SizeCache MaxReuseTimes int32 `protobuf:"varint,5,opt,name=maxReuseTimes,proto3" json:"maxReuseTimes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *MultiplexingConfig) Reset() { func (x *MultiplexingConfig) Reset() {
@@ -482,17 +475,23 @@ func (x *MultiplexingConfig) GetXudpProxyUDP443() string {
return "" return ""
} }
func (x *MultiplexingConfig) GetMaxReuseTimes() int32 {
if x != nil {
return x.MaxReuseTimes
}
return 0
}
var File_app_proxyman_config_proto protoreflect.FileDescriptor var File_app_proxyman_config_proto protoreflect.FileDescriptor
const file_app_proxyman_config_proto_rawDesc = "" + const file_app_proxyman_config_proto_rawDesc = "" +
"\n" + "\n" +
"\x19app/proxyman/config.proto\x12\x11xray.app.proxyman\x1a\x18common/net/address.proto\x1a\x15common/net/port.proto\x1a\x1ftransport/internet/config.proto\x1a!common/serial/typed_message.proto\x1a\x1bcommon/geodata/geodat.proto\"\x0f\n" + "\x19app/proxyman/config.proto\x12\x11xray.app.proxyman\x1a\x18common/net/address.proto\x1a\x15common/net/port.proto\x1a\x1ftransport/internet/config.proto\x1a!common/serial/typed_message.proto\"\x0f\n" +
"\rInboundConfig\"\xad\x02\n" + "\rInboundConfig\"\xcc\x01\n" +
"\x0eSniffingConfig\x12\x18\n" + "\x0eSniffingConfig\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\x121\n" + "\aenabled\x18\x01 \x01(\bR\aenabled\x121\n" +
"\x14destination_override\x18\x02 \x03(\tR\x13destinationOverride\x12J\n" + "\x14destination_override\x18\x02 \x03(\tR\x13destinationOverride\x12)\n" +
"\x10domains_excluded\x18\x03 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x0fdomainsExcluded\x12>\n" + "\x10domains_excluded\x18\x03 \x03(\tR\x0fdomainsExcluded\x12#\n" +
"\fips_excluded\x18\x06 \x03(\v2\x1b.xray.common.geodata.IPRuleR\vipsExcluded\x12#\n" +
"\rmetadata_only\x18\x04 \x01(\bR\fmetadataOnly\x12\x1d\n" + "\rmetadata_only\x18\x04 \x01(\bR\fmetadataOnly\x12\x1d\n" +
"\n" + "\n" +
"route_only\x18\x05 \x01(\bR\trouteOnly\"\xe5\x02\n" + "route_only\x18\x05 \x01(\bR\trouteOnly\"\xe5\x02\n" +
@@ -513,12 +512,13 @@ const file_app_proxyman_config_proto_rawDesc = "" +
"\x0eproxy_settings\x18\x03 \x01(\v2$.xray.transport.internet.ProxyConfigR\rproxySettings\x12T\n" + "\x0eproxy_settings\x18\x03 \x01(\v2$.xray.transport.internet.ProxyConfigR\rproxySettings\x12T\n" +
"\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" + "\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" +
"\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" + "\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" +
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategy\"\xa4\x01\n" + "\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategy\"\xca\x01\n" +
"\x12MultiplexingConfig\x12\x18\n" + "\x12MultiplexingConfig\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" + "\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" +
"\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" + "\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" +
"\x0fxudpConcurrency\x18\x03 \x01(\x05R\x0fxudpConcurrency\x12(\n" + "\x0fxudpConcurrency\x18\x03 \x01(\x05R\x0fxudpConcurrency\x12(\n" +
"\x0fxudpProxyUDP443\x18\x04 \x01(\tR\x0fxudpProxyUDP443BU\n" + "\x0fxudpProxyUDP443\x18\x04 \x01(\tR\x0fxudpProxyUDP443\x12$\n" +
"\rmaxReuseTimes\x18\x05 \x01(\x05R\rmaxReuseTimesBU\n" +
"\x15com.xray.app.proxymanP\x01Z&github.com/xtls/xray-core/app/proxyman\xaa\x02\x11Xray.App.Proxymanb\x06proto3" "\x15com.xray.app.proxymanP\x01Z&github.com/xtls/xray-core/app/proxyman\xaa\x02\x11Xray.App.Proxymanb\x06proto3"
var ( var (
@@ -542,34 +542,30 @@ var file_app_proxyman_config_proto_goTypes = []any{
(*OutboundConfig)(nil), // 4: xray.app.proxyman.OutboundConfig (*OutboundConfig)(nil), // 4: xray.app.proxyman.OutboundConfig
(*SenderConfig)(nil), // 5: xray.app.proxyman.SenderConfig (*SenderConfig)(nil), // 5: xray.app.proxyman.SenderConfig
(*MultiplexingConfig)(nil), // 6: xray.app.proxyman.MultiplexingConfig (*MultiplexingConfig)(nil), // 6: xray.app.proxyman.MultiplexingConfig
(*geodata.DomainRule)(nil), // 7: xray.common.geodata.DomainRule (*net.PortList)(nil), // 7: xray.common.net.PortList
(*geodata.IPRule)(nil), // 8: xray.common.geodata.IPRule (*net.IPOrDomain)(nil), // 8: xray.common.net.IPOrDomain
(*net.PortList)(nil), // 9: xray.common.net.PortList (*internet.StreamConfig)(nil), // 9: xray.transport.internet.StreamConfig
(*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain (*serial.TypedMessage)(nil), // 10: xray.common.serial.TypedMessage
(*internet.StreamConfig)(nil), // 11: xray.transport.internet.StreamConfig (*internet.ProxyConfig)(nil), // 11: xray.transport.internet.ProxyConfig
(*serial.TypedMessage)(nil), // 12: xray.common.serial.TypedMessage (internet.DomainStrategy)(0), // 12: xray.transport.internet.DomainStrategy
(*internet.ProxyConfig)(nil), // 13: xray.transport.internet.ProxyConfig
(internet.DomainStrategy)(0), // 14: xray.transport.internet.DomainStrategy
} }
var file_app_proxyman_config_proto_depIdxs = []int32{ var file_app_proxyman_config_proto_depIdxs = []int32{
7, // 0: xray.app.proxyman.SniffingConfig.domains_excluded:type_name -> xray.common.geodata.DomainRule 7, // 0: xray.app.proxyman.ReceiverConfig.port_list:type_name -> xray.common.net.PortList
8, // 1: xray.app.proxyman.SniffingConfig.ips_excluded:type_name -> xray.common.geodata.IPRule 8, // 1: xray.app.proxyman.ReceiverConfig.listen:type_name -> xray.common.net.IPOrDomain
9, // 2: xray.app.proxyman.ReceiverConfig.port_list:type_name -> xray.common.net.PortList 9, // 2: xray.app.proxyman.ReceiverConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig
10, // 3: xray.app.proxyman.ReceiverConfig.listen:type_name -> xray.common.net.IPOrDomain 1, // 3: xray.app.proxyman.ReceiverConfig.sniffing_settings:type_name -> xray.app.proxyman.SniffingConfig
11, // 4: xray.app.proxyman.ReceiverConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig 10, // 4: xray.app.proxyman.InboundHandlerConfig.receiver_settings:type_name -> xray.common.serial.TypedMessage
1, // 5: xray.app.proxyman.ReceiverConfig.sniffing_settings:type_name -> xray.app.proxyman.SniffingConfig 10, // 5: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage
12, // 6: xray.app.proxyman.InboundHandlerConfig.receiver_settings:type_name -> xray.common.serial.TypedMessage 8, // 6: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain
12, // 7: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage 9, // 7: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig
10, // 8: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain 11, // 8: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig
11, // 9: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig 6, // 9: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
13, // 10: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig 12, // 10: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
6, // 11: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig 11, // [11:11] is the sub-list for method output_type
14, // 12: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy 11, // [11:11] is the sub-list for method input_type
13, // [13:13] is the sub-list for method output_type 11, // [11:11] is the sub-list for extension type_name
13, // [13:13] is the sub-list for method input_type 11, // [11:11] is the sub-list for extension extendee
13, // [13:13] is the sub-list for extension type_name 0, // [0:11] is the sub-list for field type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
} }
func init() { file_app_proxyman_config_proto_init() } func init() { file_app_proxyman_config_proto_init() }
+3 -5
View File
@@ -10,7 +10,6 @@ import "common/net/address.proto";
import "common/net/port.proto"; import "common/net/port.proto";
import "transport/internet/config.proto"; import "transport/internet/config.proto";
import "common/serial/typed_message.proto"; import "common/serial/typed_message.proto";
import "common/geodata/geodat.proto";
message InboundConfig {} message InboundConfig {}
@@ -21,10 +20,7 @@ message SniffingConfig {
// Override target destination if sniff'ed protocol is in the given list. // Override target destination if sniff'ed protocol is in the given list.
// Supported values are "http", "tls", "fakedns". // Supported values are "http", "tls", "fakedns".
repeated string destination_override = 2; repeated string destination_override = 2;
repeated string domains_excluded = 3;
repeated xray.common.geodata.DomainRule domains_excluded = 3;
repeated xray.common.geodata.IPRule ips_excluded = 6;
// Whether should only try to sniff metadata without waiting for client input. // Whether should only try to sniff metadata without waiting for client input.
// Can be used to support SMTP like protocol where server send the first // Can be used to support SMTP like protocol where server send the first
@@ -72,4 +68,6 @@ message MultiplexingConfig {
int32 xudpConcurrency = 3; int32 xudpConcurrency = 3;
// "reject" (default), "allow" or "skip". // "reject" (default), "allow" or "skip".
string xudpProxyUDP443 = 4; string xudpProxyUDP443 = 4;
// MaxReuseTimes for an connection
int32 maxReuseTimes = 5;
} }
+10 -16
View File
@@ -53,17 +53,18 @@ type AlwaysOnInboundHandler struct {
} }
func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *proxyman.ReceiverConfig, proxyConfig interface{}) (*AlwaysOnInboundHandler, error) { func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *proxyman.ReceiverConfig, proxyConfig interface{}) (*AlwaysOnInboundHandler, error) {
sniffingRequest, err := proxyman.BuildSniffingRequest(receiverConfig.SniffingSettings)
if err != nil {
return nil, err
}
// Set tag and sniffing config in context before creating proxy // Set tag and sniffing config in context before creating proxy
// This allows proxies like TUN to access these settings // This allows proxies like TUN to access these settings
ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: tag}) ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: tag})
if receiverConfig.SniffingSettings != nil { if receiverConfig.SniffingSettings != nil {
ctx = session.ContextWithContent(ctx, &session.Content{ ctx = session.ContextWithContent(ctx, &session.Content{
SniffingRequest: sniffingRequest, SniffingRequest: session.SniffingRequest{
Enabled: receiverConfig.SniffingSettings.Enabled,
OverrideDestinationForProtocol: receiverConfig.SniffingSettings.DestinationOverride,
ExcludeForDomain: receiverConfig.SniffingSettings.DomainsExcluded,
MetadataOnly: receiverConfig.SniffingSettings.MetadataOnly,
RouteOnly: receiverConfig.SniffingSettings.RouteOnly,
},
}) })
} }
rawProxy, err := common.CreateObject(ctx, proxyConfig) rawProxy, err := common.CreateObject(ctx, proxyConfig)
@@ -116,7 +117,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
stream: mss, stream: mss,
tag: tag, tag: tag,
dispatcher: h.mux, dispatcher: h.mux,
sniffingRequest: sniffingRequest, sniffingConfig: receiverConfig.SniffingSettings,
uplinkCounter: uplinkCounter, uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter, downlinkCounter: downlinkCounter,
ctx: ctx, ctx: ctx,
@@ -138,7 +139,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
recvOrigDest: receiverConfig.ReceiveOriginalDestination, recvOrigDest: receiverConfig.ReceiveOriginalDestination,
tag: tag, tag: tag,
dispatcher: h.mux, dispatcher: h.mux,
sniffingRequest: sniffingRequest, sniffingConfig: receiverConfig.SniffingSettings,
uplinkCounter: uplinkCounter, uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter, downlinkCounter: downlinkCounter,
ctx: ctx, ctx: ctx,
@@ -153,7 +154,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
address: address, address: address,
port: net.Port(port), port: net.Port(port),
dispatcher: h.mux, dispatcher: h.mux,
sniffingRequest: sniffingRequest, sniffingConfig: receiverConfig.SniffingSettings,
uplinkCounter: uplinkCounter, uplinkCounter: uplinkCounter,
downlinkCounter: downlinkCounter, downlinkCounter: downlinkCounter,
stream: mss, stream: mss,
@@ -170,12 +171,6 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
// Start implements common.Runnable. // Start implements common.Runnable.
func (h *AlwaysOnInboundHandler) Start() error { func (h *AlwaysOnInboundHandler) Start() error {
// for inbound without worker (TUN)
if run, ok := h.proxy.(common.Runnable); ok {
if err := run.Start(); err != nil {
return errors.New("failed to start proxy").Base(err)
}
}
for _, worker := range h.workers { for _, worker := range h.workers {
if err := worker.Start(); err != nil { if err := worker.Start(); err != nil {
return err return err
@@ -191,7 +186,6 @@ func (h *AlwaysOnInboundHandler) Close() error {
errs = append(errs, worker.Close()) errs = append(errs, worker.Close())
} }
errs = append(errs, h.mux.Close()) errs = append(errs, h.mux.Close())
errs = append(errs, common.Close(h.proxy))
if err := errors.Combine(errs...); err != nil { if err := errors.Combine(errs...); err != nil {
return errors.New("failed to close all resources").Base(err) return errors.New("failed to close all resources").Base(err)
} }
+3 -3
View File
@@ -16,10 +16,10 @@ import (
// Manager manages all inbound handlers. // Manager manages all inbound handlers.
type Manager struct { type Manager struct {
access sync.RWMutex access sync.RWMutex
untaggedHandlers []inbound.Handler untaggedHandlers []inbound.Handler
taggedHandlers map[string]inbound.Handler taggedHandlers map[string]inbound.Handler
running bool running bool
} }
// New returns a new Manager for inbound handlers. // New returns a new Manager for inbound handlers.
+25 -13
View File
@@ -6,6 +6,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/xtls/xray-core/app/proxyman"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
c "github.com/xtls/xray-core/common/ctx" c "github.com/xtls/xray-core/common/ctx"
@@ -18,9 +19,7 @@ import (
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats" "github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/proxy" "github.com/xtls/xray-core/proxy"
hysteria_proxy "github.com/xtls/xray-core/proxy/hysteria"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria"
"github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tcp" "github.com/xtls/xray-core/transport/internet/tcp"
"github.com/xtls/xray-core/transport/internet/udp" "github.com/xtls/xray-core/transport/internet/udp"
@@ -42,7 +41,7 @@ type tcpWorker struct {
recvOrigDest bool recvOrigDest bool
tag string tag string
dispatcher routing.Dispatcher dispatcher routing.Dispatcher
sniffingRequest session.SniffingRequest sniffingConfig *proxyman.SniffingConfig
uplinkCounter stats.Counter uplinkCounter stats.Counter
downlinkCounter stats.Counter downlinkCounter stats.Counter
@@ -117,7 +116,13 @@ func (w *tcpWorker) callback(conn stat.Connection) {
}) })
content := new(session.Content) content := new(session.Content)
content.SniffingRequest = w.sniffingRequest if w.sniffingConfig != nil {
content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded
content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly
}
ctx = session.ContextWithContent(ctx, content) ctx = session.ContextWithContent(ctx, content)
if err := w.proxy.Process(ctx, net.Network_TCP, conn, w.dispatcher); err != nil { if err := w.proxy.Process(ctx, net.Network_TCP, conn, w.dispatcher); err != nil {
@@ -133,11 +138,6 @@ func (w *tcpWorker) Proxy() proxy.Inbound {
func (w *tcpWorker) Start() error { func (w *tcpWorker) Start() error {
ctx := context.Background() ctx := context.Background()
if v, ok := w.proxy.(*hysteria_proxy.Server); ok {
ctx = hysteria.ContextWithValidator(ctx, v.HysteriaInboundValidator())
}
hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) { hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) {
go w.callback(conn) go w.callback(conn)
}) })
@@ -266,7 +266,7 @@ type udpWorker struct {
tag string tag string
stream *internet.MemoryStreamConfig stream *internet.MemoryStreamConfig
dispatcher routing.Dispatcher dispatcher routing.Dispatcher
sniffingRequest session.SniffingRequest sniffingConfig *proxyman.SniffingConfig
uplinkCounter stats.Counter uplinkCounter stats.Counter
downlinkCounter stats.Counter downlinkCounter stats.Counter
@@ -356,7 +356,13 @@ func (w *udpWorker) callback(b *buf.Buffer, source net.Destination, originalDest
Tag: w.tag, Tag: w.tag,
}) })
content := new(session.Content) content := new(session.Content)
content.SniffingRequest = w.sniffingRequest if w.sniffingConfig != nil {
content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded
content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly
}
ctx = session.ContextWithContent(ctx, content) ctx = session.ContextWithContent(ctx, content)
if err := w.proxy.Process(ctx, net.Network_UDP, conn, w.dispatcher); err != nil { if err := w.proxy.Process(ctx, net.Network_UDP, conn, w.dispatcher); err != nil {
errors.LogInfoInner(ctx, err, "connection ends") errors.LogInfoInner(ctx, err, "connection ends")
@@ -472,7 +478,7 @@ type dsWorker struct {
stream *internet.MemoryStreamConfig stream *internet.MemoryStreamConfig
tag string tag string
dispatcher routing.Dispatcher dispatcher routing.Dispatcher
sniffingRequest session.SniffingRequest sniffingConfig *proxyman.SniffingConfig
uplinkCounter stats.Counter uplinkCounter stats.Counter
downlinkCounter stats.Counter downlinkCounter stats.Counter
@@ -502,7 +508,13 @@ func (w *dsWorker) callback(conn stat.Connection) {
}) })
content := new(session.Content) content := new(session.Content)
content.SniffingRequest = w.sniffingRequest if w.sniffingConfig != nil {
content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded
content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly
}
ctx = session.ContextWithContent(ctx, content) ctx = session.ContextWithContent(ctx, content)
if err := w.proxy.Process(ctx, net.Network_UNIX, conn, w.dispatcher); err != nil { if err := w.proxy.Process(ctx, net.Network_UNIX, conn, w.dispatcher); err != nil {
+10 -3
View File
@@ -121,6 +121,12 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
if h.senderSettings != nil && h.senderSettings.MultiplexSettings != nil { if h.senderSettings != nil && h.senderSettings.MultiplexSettings != nil {
if config := h.senderSettings.MultiplexSettings; config.Enabled { if config := h.senderSettings.MultiplexSettings; config.Enabled {
// MaxReuseTimes use 60000 as default, and it also means the upper limit of MaxReuseTimes
// In mux cool spec, connection ID is 2 bytes, so physical limit is 65535, bu we reserve some IDs for future use
MaxReuseTimes := uint32(60000)
if config.MaxReuseTimes != 0 && config.MaxReuseTimes < 60000 {
MaxReuseTimes = uint32(config.MaxReuseTimes)
}
if config.Concurrency < 0 { if config.Concurrency < 0 {
h.mux = &mux.ClientManager{Enabled: false} h.mux = &mux.ClientManager{Enabled: false}
} }
@@ -136,7 +142,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
Dialer: h, Dialer: h,
Strategy: mux.ClientStrategy{ Strategy: mux.ClientStrategy{
MaxConcurrency: uint32(config.Concurrency), MaxConcurrency: uint32(config.Concurrency),
MaxConnection: 128, MaxReuseTimes: MaxReuseTimes,
}, },
}, },
}, },
@@ -157,7 +163,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
Dialer: h, Dialer: h,
Strategy: mux.ClientStrategy{ Strategy: mux.ClientStrategy{
MaxConcurrency: uint32(config.XudpConcurrency), MaxConcurrency: uint32(config.XudpConcurrency),
MaxConnection: 128, MaxReuseTimes: 128,
}, },
}, },
}, },
@@ -348,7 +354,7 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound)
errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String()) errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String())
} }
} }
// case addr.Family().IsDomain(): //case addr.Family().IsDomain():
default: default:
ob.Gateway = addr ob.Gateway = addr
@@ -396,6 +402,7 @@ func (h *Handler) ProxySettings() *serial.TypedMessage {
} }
func ParseRandomIP(addr net.Address, prefix string) net.Address { func ParseRandomIP(addr net.Address, prefix string) net.Address {
_, ipnet, _ := net.ParseCIDR(addr.IP().String() + "/" + prefix) _, ipnet, _ := net.ParseCIDR(addr.IP().String() + "/" + prefix)
ones, bits := ipnet.Mask.Size() ones, bits := ipnet.Mask.Size()
+8 -7
View File
@@ -22,8 +22,8 @@ import (
) )
func TestInterfaces(t *testing.T) { func TestInterfaces(t *testing.T) {
_ = outbound.Handler(new(Handler)) _ = (outbound.Handler)(new(Handler))
_ = outbound.Manager(new(Manager)) _ = (outbound.Manager)(new(Manager))
} }
const xrayKey core.XrayKey = 1 const xrayKey core.XrayKey = 1
@@ -43,12 +43,12 @@ func TestOutboundWithoutStatCounter(t *testing.T) {
} }
v, _ := core.New(config) v, _ := core.New(config)
v.AddFeature(outbound.Manager(new(Manager))) v.AddFeature((outbound.Manager)(new(Manager)))
ctx := context.WithValue(context.Background(), xrayKey, v) ctx := context.WithValue(context.Background(), xrayKey, v)
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}}) ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{ h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
Tag: "tag", Tag: "tag",
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}), ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
}) })
conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146)) conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146))
_, ok := conn.(*stat.CounterConnection) _, ok := conn.(*stat.CounterConnection)
@@ -73,12 +73,12 @@ func TestOutboundWithStatCounter(t *testing.T) {
} }
v, _ := core.New(config) v, _ := core.New(config)
v.AddFeature(outbound.Manager(new(Manager))) v.AddFeature((outbound.Manager)(new(Manager)))
ctx := context.WithValue(context.Background(), xrayKey, v) ctx := context.WithValue(context.Background(), xrayKey, v)
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}}) ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{ h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
Tag: "tag", Tag: "tag",
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}), ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
}) })
conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146)) conn, _ := h.(*Handler).Dial(ctx, net.TCPDestination(net.DomainAddress("localhost"), 13146))
_, ok := conn.(*stat.CounterConnection) _, ok := conn.(*stat.CounterConnection)
@@ -88,6 +88,7 @@ func TestOutboundWithStatCounter(t *testing.T) {
} }
func TestTagsCache(t *testing.T) { func TestTagsCache(t *testing.T) {
test_duration := 10 * time.Second test_duration := 10 * time.Second
threads_num := 50 threads_num := 50
delay := 10 * time.Millisecond delay := 10 * time.Millisecond
@@ -117,7 +118,7 @@ func TestTagsCache(t *testing.T) {
tag := fmt.Sprintf("%s%d", tags_prefix, idx) tag := fmt.Sprintf("%s%d", tags_prefix, idx)
cfg := &core.OutboundHandlerConfig{ cfg := &core.OutboundHandlerConfig{
Tag: tag, Tag: tag,
ProxySettings: serial.ToTypedMessage(&freedom.Config{FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}}}), ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
} }
if h, err := NewHandler(ctx, cfg); err == nil { if h, err := NewHandler(ctx, cfg); err == nil {
if err := ohm.AddHandler(ctx, h); err == nil { if err := ohm.AddHandler(ctx, h); err == nil {
+1
View File
@@ -162,6 +162,7 @@ func (m *Manager) ListHandlers(ctx context.Context) []outbound.Handler {
// Select implements outbound.HandlerSelector. // Select implements outbound.HandlerSelector.
func (m *Manager) Select(selectors []string) []string { func (m *Manager) Select(selectors []string) []string {
key := strings.Join(selectors, ",") key := strings.Join(selectors, ",")
if cache, ok := m.tagsCache.Load(key); ok { if cache, ok := m.tagsCache.Load(key); ok {
return cache.([]string) return cache.([]string)
+1
View File
@@ -58,6 +58,7 @@ func (s *routingServer) AddRule(ctx context.Context, request *AddRuleRequest) (*
return &AddRuleResponse{}, bo.AddRule(request.Config, request.ShouldAppend) return &AddRuleResponse{}, bo.AddRule(request.Config, request.ShouldAppend)
} }
return nil, errors.New("unsupported router implementation") return nil, errors.New("unsupported router implementation")
} }
func (s *routingServer) RemoveRule(ctx context.Context, request *RemoveRuleRequest) (*RemoveRuleResponse, error) { func (s *routingServer) RemoveRule(ctx context.Context, request *RemoveRuleRequest) (*RemoveRuleResponse, error) {
+3 -4
View File
@@ -12,7 +12,6 @@ import (
. "github.com/xtls/xray-core/app/router/command" . "github.com/xtls/xray-core/app/router/command"
"github.com/xtls/xray-core/app/stats" "github.com/xtls/xray-core/app/stats"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/testing/mocks" "github.com/xtls/xray-core/testing/mocks"
@@ -304,12 +303,12 @@ func TestServiceTestRoute(t *testing.T) {
TargetTag: &router.RoutingRule_Tag{Tag: "out"}, TargetTag: &router.RoutingRule_Tag{Tag: "out"},
}, },
{ {
Domain: []*geodata.DomainRule{{Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "com"}}}}, Domain: []*router.Domain{{Type: router.Domain_Domain, Value: "com"}},
TargetTag: &router.RoutingRule_Tag{Tag: "out"}, TargetTag: &router.RoutingRule_Tag{Tag: "out"},
}, },
{ {
SourceIp: []*geodata.IPRule{{Value: &geodata.IPRule_Custom{Custom: &geodata.CIDRRule{Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}}}, SourceGeoip: []*router.GeoIP{{CountryCode: "private", Cidr: []*router.CIDR{{Ip: []byte{127, 0, 0, 0}, Prefix: 8}}}},
TargetTag: &router.RoutingRule_Tag{Tag: "out"}, TargetTag: &router.RoutingRule_Tag{Tag: "out"},
}, },
{ {
UserEmail: []string{"example@example.com"}, UserEmail: []string{"example@example.com"},
+66 -29
View File
@@ -2,6 +2,7 @@ package router
import ( import (
"context" "context"
"io"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -9,10 +10,9 @@ import (
"strings" "strings"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/routing/dns"
) )
type Condition interface { type Condition interface {
@@ -45,18 +45,67 @@ func (v *ConditionChan) Len() int {
return len(*v) return len(*v)
} }
type DomainMatcher struct{ geodata.DomainMatcher } var matcherTypeMap = map[Domain_Type]strmatcher.Type{
Domain_Plain: strmatcher.Substr,
Domain_Regex: strmatcher.Regex,
Domain_Domain: strmatcher.Domain,
Domain_Full: strmatcher.Full,
}
func NewDomainMatcher(rules []*geodata.DomainRule) (*DomainMatcher, error) { type DomainMatcher struct {
m, err := geodata.DomainReg.BuildDomainMatcher(rules) Matchers strmatcher.IndexMatcher
}
func SerializeDomainMatcher(domains []*Domain, w io.Writer) error {
g := strmatcher.NewMphMatcherGroup()
for _, d := range domains {
matcherType, f := matcherTypeMap[d.Type]
if !f {
continue
}
_, err := g.AddPattern(d.Value, matcherType)
if err != nil {
return err
}
}
g.Build()
// serialize
return g.Serialize(w)
}
func NewDomainMatcherFromBuffer(data []byte) (*strmatcher.MphMatcherGroup, error) {
matcher, err := strmatcher.NewMphMatcherGroupFromBuffer(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &DomainMatcher{DomainMatcher: m}, nil return matcher, nil
}
func NewMphMatcherGroup(domains []*Domain) (*DomainMatcher, error) {
g := strmatcher.NewMphMatcherGroup()
for i, d := range domains {
domains[i] = nil
matcherType, f := matcherTypeMap[d.Type]
if !f {
errors.LogError(context.Background(), "ignore unsupported domain type ", d.Type, " of rule ", d.Value)
continue
}
_, err := g.AddPattern(d.Value, matcherType)
if err != nil {
errors.LogErrorInner(context.Background(), err, "ignore domain rule ", d.Type, " ", d.Value)
continue
}
}
g.Build()
return &DomainMatcher{
Matchers: g,
}, nil
} }
func (m *DomainMatcher) ApplyDomain(domain string) bool { func (m *DomainMatcher) ApplyDomain(domain string) bool {
return m.DomainMatcher.MatchAny(strings.ToLower(domain)) return len(m.Matchers.Match(strings.ToLower(domain))) > 0
} }
// Apply implements Condition. // Apply implements Condition.
@@ -65,7 +114,7 @@ func (m *DomainMatcher) Apply(ctx routing.Context) bool {
if len(domain) == 0 { if len(domain) == 0 {
return false return false
} }
return m.DomainMatcher.MatchAny(strings.ToLower(domain)) return m.ApplyDomain(domain)
} }
type MatcherAsType byte type MatcherAsType byte
@@ -78,16 +127,16 @@ const (
) )
type IPMatcher struct { type IPMatcher struct {
matcher geodata.IPMatcher matcher GeoIPMatcher
asType MatcherAsType asType MatcherAsType
} }
func NewIPMatcher(rules []*geodata.IPRule, asType MatcherAsType) (*IPMatcher, error) { func NewIPMatcher(geoips []*GeoIP, asType MatcherAsType) (*IPMatcher, error) {
m, err := geodata.IPReg.BuildIPMatcher(rules) matcher, err := BuildOptimizedGeoIPMatcher(geoips...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &IPMatcher{matcher: m, asType: asType}, nil return &IPMatcher{matcher: matcher, asType: asType}, nil
} }
// Apply implements Condition. // Apply implements Condition.
@@ -341,10 +390,8 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
if len(ctx.GetSourceIPs()) == 0 { if len(ctx.GetSourceIPs()) == 0 {
return false return false
} }
srcPort := ctx.GetSourcePort().String()
srcPort := uint16(ctx.GetSourcePort())
srcIP := ctx.GetSourceIPs()[0].String() srcIP := ctx.GetSourceIPs()[0].String()
var network string var network string
switch ctx.GetNetwork() { switch ctx.GetNetwork() {
case net.Network_TCP: case net.Network_TCP:
@@ -354,21 +401,11 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
default: default:
return false return false
} }
src, err := net.ParseDestination(strings.Join([]string{network, srcIP, srcPort}, ":"))
var dstIP string if err != nil {
var dstPort uint16 = 0 return false
// do not use resolved IP because Android process lookup needs original dst ip
resolvableContext, ok := ctx.(*dns.ResolvableContext)
if ok && len(resolvableContext.Context.GetTargetIPs()) > 0 {
dstIP = resolvableContext.Context.GetTargetIPs()[0].String()
dstPort = uint16(resolvableContext.Context.GetTargetPort())
} else if len(ctx.GetTargetIPs()) > 0 {
dstIP = ctx.GetTargetIPs()[0].String()
dstPort = uint16(ctx.GetTargetPort())
} }
pid, name, absPath, err := net.FindProcess(src)
pid, name, absPath, err := net.FindProcess(network, srcIP, uint16(srcPort), dstIP, uint16(dstPort))
if err != nil { if err != nil {
if err != net.ErrNotLocal { if err != net.ErrNotLocal {
errors.LogError(context.Background(), "Unables to find local process name: ", err) errors.LogError(context.Background(), "Unables to find local process name: ", err)
@@ -1,22 +1,19 @@
package geodata package router
import ( import (
"context" "context"
"net/netip" "net/netip"
"runtime"
"slices"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
"go4.org/netipx" "go4.org/netipx"
) )
type IPMatcher interface { type GeoIPMatcher interface {
// TODO: (PERF) all net.IP -> netipx.Addr // TODO: (PERF) all net.IP -> netipx.Addr
// Invalid IP always return false. // Invalid IP always return false.
@@ -36,13 +33,13 @@ type IPMatcher interface {
SetReverse(reverse bool) SetReverse(reverse bool)
} }
type IPSet struct { type GeoIPSet struct {
ipv4, ipv6 *netipx.IPSet ipv4, ipv6 *netipx.IPSet
max4, max6 uint8 max4, max6 uint8
} }
type HeuristicIPMatcher struct { type HeuristicGeoIPMatcher struct {
ipset *IPSet ipset *GeoIPSet
reverse bool reverse bool
} }
@@ -51,8 +48,8 @@ type ipBucket struct {
ips []net.IP ips []net.IP
} }
// Match implements IPMatcher. // Match implements GeoIPMatcher.
func (m *HeuristicIPMatcher) Match(ip net.IP) bool { func (m *HeuristicGeoIPMatcher) Match(ip net.IP) bool {
ipx, ok := netipx.FromStdIP(ip) ipx, ok := netipx.FromStdIP(ip)
if !ok { if !ok {
return false return false
@@ -60,24 +57,18 @@ func (m *HeuristicIPMatcher) Match(ip net.IP) bool {
return m.matchAddr(ipx) return m.matchAddr(ipx)
} }
func (m *HeuristicIPMatcher) matchAddr(ipx netip.Addr) bool { func (m *HeuristicGeoIPMatcher) matchAddr(ipx netip.Addr) bool {
if ipx.Is4() { if ipx.Is4() {
if m.ipset.max4 == 0xff {
return false
}
return m.ipset.ipv4.Contains(ipx) != m.reverse return m.ipset.ipv4.Contains(ipx) != m.reverse
} }
if ipx.Is6() { if ipx.Is6() {
if m.ipset.max6 == 0xff {
return false
}
return m.ipset.ipv6.Contains(ipx) != m.reverse return m.ipset.ipv6.Contains(ipx) != m.reverse
} }
return false return false
} }
// AnyMatch implements IPMatcher. // AnyMatch implements GeoIPMatcher.
func (m *HeuristicIPMatcher) AnyMatch(ips []net.IP) bool { func (m *HeuristicGeoIPMatcher) AnyMatch(ips []net.IP) bool {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return false return false
@@ -126,8 +117,8 @@ func (m *HeuristicIPMatcher) AnyMatch(ips []net.IP) bool {
return false return false
} }
// Matches implements IPMatcher. // Matches implements GeoIPMatcher.
func (m *HeuristicIPMatcher) Matches(ips []net.IP) bool { func (m *HeuristicGeoIPMatcher) Matches(ips []net.IP) bool {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return false return false
@@ -214,8 +205,8 @@ func prefixKeyFromIP(ip net.IP) (key [9]byte, ok bool) {
return key, false // illegal return key, false // illegal
} }
// FilterIPs implements IPMatcher. // FilterIPs implements GeoIPMatcher.
func (m *HeuristicIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { func (m *HeuristicGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return []net.IP{}, []net.IP{} return []net.IP{}, []net.IP{}
@@ -304,22 +295,22 @@ func (m *HeuristicIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatche
return return
} }
// ToggleReverse implements IPMatcher. // ToggleReverse implements GeoIPMatcher.
func (m *HeuristicIPMatcher) ToggleReverse() { func (m *HeuristicGeoIPMatcher) ToggleReverse() {
m.reverse = !m.reverse m.reverse = !m.reverse
} }
// SetReverse implements IPMatcher. // SetReverse implements GeoIPMatcher.
func (m *HeuristicIPMatcher) SetReverse(reverse bool) { func (m *HeuristicGeoIPMatcher) SetReverse(reverse bool) {
m.reverse = reverse m.reverse = reverse
} }
type GeneralMultiIPMatcher struct { type GeneralMultiGeoIPMatcher struct {
matchers []IPMatcher matchers []GeoIPMatcher
} }
// Match implements IPMatcher. // Match implements GeoIPMatcher.
func (mm *GeneralMultiIPMatcher) Match(ip net.IP) bool { func (mm *GeneralMultiGeoIPMatcher) Match(ip net.IP) bool {
for _, m := range mm.matchers { for _, m := range mm.matchers {
if m.Match(ip) { if m.Match(ip) {
return true return true
@@ -328,8 +319,8 @@ func (mm *GeneralMultiIPMatcher) Match(ip net.IP) bool {
return false return false
} }
// AnyMatch implements IPMatcher. // AnyMatch implements GeoIPMatcher.
func (mm *GeneralMultiIPMatcher) AnyMatch(ips []net.IP) bool { func (mm *GeneralMultiGeoIPMatcher) AnyMatch(ips []net.IP) bool {
for _, m := range mm.matchers { for _, m := range mm.matchers {
if m.AnyMatch(ips) { if m.AnyMatch(ips) {
return true return true
@@ -338,8 +329,8 @@ func (mm *GeneralMultiIPMatcher) AnyMatch(ips []net.IP) bool {
return false return false
} }
// Matches implements IPMatcher. // Matches implements GeoIPMatcher.
func (mm *GeneralMultiIPMatcher) Matches(ips []net.IP) bool { func (mm *GeneralMultiGeoIPMatcher) Matches(ips []net.IP) bool {
for _, m := range mm.matchers { for _, m := range mm.matchers {
if m.Matches(ips) { if m.Matches(ips) {
return true return true
@@ -348,8 +339,8 @@ func (mm *GeneralMultiIPMatcher) Matches(ips []net.IP) bool {
return false return false
} }
// FilterIPs implements IPMatcher. // FilterIPs implements GeoIPMatcher.
func (mm *GeneralMultiIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { func (mm *GeneralMultiGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) {
matched = make([]net.IP, 0, len(ips)) matched = make([]net.IP, 0, len(ips))
unmatched = ips unmatched = ips
for _, m := range mm.matchers { for _, m := range mm.matchers {
@@ -365,26 +356,26 @@ func (mm *GeneralMultiIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unma
return return
} }
// ToggleReverse implements IPMatcher. // ToggleReverse implements GeoIPMatcher.
func (mm *GeneralMultiIPMatcher) ToggleReverse() { func (mm *GeneralMultiGeoIPMatcher) ToggleReverse() {
for _, m := range mm.matchers { for _, m := range mm.matchers {
m.ToggleReverse() m.ToggleReverse()
} }
} }
// SetReverse implements IPMatcher. // SetReverse implements GeoIPMatcher.
func (mm *GeneralMultiIPMatcher) SetReverse(reverse bool) { func (mm *GeneralMultiGeoIPMatcher) SetReverse(reverse bool) {
for _, m := range mm.matchers { for _, m := range mm.matchers {
m.SetReverse(reverse) m.SetReverse(reverse)
} }
} }
type HeuristicMultiIPMatcher struct { type HeuristicMultiGeoIPMatcher struct {
matchers []*HeuristicIPMatcher matchers []*HeuristicGeoIPMatcher
} }
// Match implements IPMatcher. // Match implements GeoIPMatcher.
func (mm *HeuristicMultiIPMatcher) Match(ip net.IP) bool { func (mm *HeuristicMultiGeoIPMatcher) Match(ip net.IP) bool {
ipx, ok := netipx.FromStdIP(ip) ipx, ok := netipx.FromStdIP(ip)
if !ok { if !ok {
return false return false
@@ -398,8 +389,8 @@ func (mm *HeuristicMultiIPMatcher) Match(ip net.IP) bool {
return false return false
} }
// AnyMatch implements IPMatcher. // AnyMatch implements GeoIPMatcher.
func (mm *HeuristicMultiIPMatcher) AnyMatch(ips []net.IP) bool { func (mm *HeuristicMultiGeoIPMatcher) AnyMatch(ips []net.IP) bool {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return false return false
@@ -448,8 +439,8 @@ func (mm *HeuristicMultiIPMatcher) AnyMatch(ips []net.IP) bool {
return false return false
} }
// Matches implements IPMatcher. // Matches implements GeoIPMatcher.
func (mm *HeuristicMultiIPMatcher) Matches(ips []net.IP) bool { func (mm *HeuristicMultiGeoIPMatcher) Matches(ips []net.IP) bool {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return false return false
@@ -512,7 +503,7 @@ type ipViews struct {
precise4, precise6 []netip.Addr precise4, precise6 []netip.Addr
} }
func (v *ipViews) ensureForMatcher(m *HeuristicIPMatcher, ips []net.IP) bool { func (v *ipViews) ensureForMatcher(m *HeuristicGeoIPMatcher, ips []net.IP) bool {
needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil
needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil
needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil
@@ -590,8 +581,8 @@ func (v *ipViews) ensureForMatcher(m *HeuristicIPMatcher, ips []net.IP) bool {
return true return true
} }
// FilterIPs implements IPMatcher. // FilterIPs implements GeoIPMatcher.
func (mm *HeuristicMultiIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) { func (mm *HeuristicMultiGeoIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) {
n := len(ips) n := len(ips)
if n == 0 { if n == 0 {
return []net.IP{}, []net.IP{} return []net.IP{}, []net.IP{}
@@ -703,7 +694,7 @@ type ipBucketViews struct {
precise4, precise6 map[netip.Addr]net.IP precise4, precise6 map[netip.Addr]net.IP
} }
func (v *ipBucketViews) ensureForMatcher(m *HeuristicIPMatcher, ips []net.IP) { func (v *ipBucketViews) ensureForMatcher(m *HeuristicGeoIPMatcher, ips []net.IP) {
needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil needHeur4 := m.ipset.max4 <= 24 && v.buckets4 == nil
needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil needHeur6 := m.ipset.max6 <= 64 && v.buckets6 == nil
needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil needPrec4 := m.ipset.max4 > 24 && v.precise4 == nil
@@ -791,124 +782,71 @@ func (v *ipBucketViews) ensureForMatcher(m *HeuristicIPMatcher, ips []net.IP) {
} }
} }
// ToggleReverse implements IPMatcher. // ToggleReverse implements GeoIPMatcher.
func (mm *HeuristicMultiIPMatcher) ToggleReverse() { func (mm *HeuristicMultiGeoIPMatcher) ToggleReverse() {
for _, m := range mm.matchers { for _, m := range mm.matchers {
m.ToggleReverse() m.ToggleReverse()
} }
} }
// SetReverse implements IPMatcher. // SetReverse implements GeoIPMatcher.
func (mm *HeuristicMultiIPMatcher) SetReverse(reverse bool) { func (mm *HeuristicMultiGeoIPMatcher) SetReverse(reverse bool) {
for _, m := range mm.matchers { for _, m := range mm.matchers {
m.SetReverse(reverse) m.SetReverse(reverse)
} }
} }
type IPSetFactory struct { type GeoIPSetFactory struct {
sync.Mutex sync.Mutex
shared *utils.WeakCacheMap[string, IPSet] shared map[string]*GeoIPSet // TODO: cleanup
} }
func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, error) { var ipsetFactory = GeoIPSetFactory{shared: make(map[string]*GeoIPSet)}
key := buildGeoIPRulesKey(rules)
func (f *GeoIPSetFactory) GetOrCreate(key string, cidrGroups [][]*CIDR) (*GeoIPSet, error) {
f.Lock() f.Lock()
defer f.Unlock() defer f.Unlock()
if ipset, ok := f.shared.Load(key); ok { if ipset := f.shared[key]; ipset != nil {
errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key)
return ipset, nil return ipset, nil
} }
errors.LogDebug(context.Background(), "geodata geoip matcher cache MISS ", key)
ipset, err := f.createFrom(func(add func(*CIDR)) error { ipset, err := f.Create(cidrGroups...)
for _, r := range rules {
cidrs, err := loadIP(r.File, r.Code)
if err != nil {
return err
}
for i, c := range cidrs {
add(c)
cidrs[i] = nil // peak mem
}
}
return nil
})
if err == nil { if err == nil {
f.shared.Store(key, ipset) f.shared[key] = ipset
} }
return ipset, err return ipset, err
} }
func buildGeoIPRulesKey(rules []*GeoIPRule) string { func (f *GeoIPSetFactory) Create(cidrGroups ...[]*CIDR) (*GeoIPSet, error) {
rules = slices.Clone(rules)
sort.Slice(rules, func(i, j int) bool {
ri, rj := rules[i], rules[j]
if ri.File != rj.File {
return ri.File < rj.File
}
return ri.Code < rj.Code
})
var sb strings.Builder
sb.Grow(len(rules) * 20) // geoip.dat:xx,
var last *GeoIPRule
for i, r := range rules {
if i == 0 || (r.File != last.File || r.Code != last.Code) {
last = r
sb.WriteString(r.File)
sb.WriteString(":")
sb.WriteString(r.Code)
sb.WriteString(",")
}
}
return sb.String()
}
func (f *IPSetFactory) CreateFromCIDRs(cidrs []*CIDR) (*IPSet, error) {
return f.createFrom(func(add func(*CIDR)) error {
for _, c := range cidrs {
add(c)
}
return nil
})
}
func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error) {
var ipv4Builder, ipv6Builder netipx.IPSetBuilder var ipv4Builder, ipv6Builder netipx.IPSetBuilder
err := yield(func(c *CIDR) { for _, cidrGroup := range cidrGroups {
ipBytes := c.GetIp() for i, cidrEntry := range cidrGroup {
prefixLen := int(c.GetPrefix()) cidrGroup[i] = nil
ipBytes := cidrEntry.GetIp()
prefixLen := int(cidrEntry.GetPrefix())
addr, ok := netip.AddrFromSlice(ipBytes) addr, ok := netip.AddrFromSlice(ipBytes)
if !ok { if !ok {
errors.LogError(context.Background(), "ignore invalid IP byte slice: ", ipBytes) errors.LogError(context.Background(), "ignore invalid IP byte slice: ", ipBytes)
return continue
} }
prefix := netip.PrefixFrom(addr, prefixLen) prefix := netip.PrefixFrom(addr, prefixLen)
if !prefix.IsValid() { if !prefix.IsValid() {
errors.LogError(context.Background(), "ignore created invalid prefix from addr ", addr, " and length ", prefixLen) errors.LogError(context.Background(), "ignore created invalid prefix from addr ", addr, " and length ", prefixLen)
return continue
} }
if addr.Is4() { if addr.Is4() {
ipv4Builder.AddPrefix(prefix) ipv4Builder.AddPrefix(prefix)
} else if addr.Is6() { } else if addr.Is6() {
ipv6Builder.AddPrefix(prefix) ipv6Builder.AddPrefix(prefix)
}
} }
})
if err != nil {
return nil, err
} }
// peak mem
runtime.GC()
defer runtime.GC()
ipv4, err := ipv4Builder.IPSet() ipv4, err := ipv4Builder.IPSet()
if err != nil { if err != nil {
return nil, errors.New("failed to build IPv4 set").Base(err) return nil, errors.New("failed to build IPv4 set").Base(err)
@@ -918,106 +856,107 @@ func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error)
return nil, errors.New("failed to build IPv6 set").Base(err) return nil, errors.New("failed to build IPv6 set").Base(err)
} }
var has4, has6 bool
var max4, max6 int var max4, max6 int
for _, p := range ipv4.Prefixes() { for _, p := range ipv4.Prefixes() {
has4 = true
if b := p.Bits(); b > max4 { if b := p.Bits(); b > max4 {
max4 = b max4 = b
} }
} }
for _, p := range ipv6.Prefixes() { for _, p := range ipv6.Prefixes() {
has6 = true
if b := p.Bits(); b > max6 { if b := p.Bits(); b > max6 {
max6 = b max6 = b
} }
} }
if !has4 { if max4 == 0 {
max4 = 0xff max4 = 0xff
} else if max4 == 0 {
max4 = 0xfe
} }
if !has6 { if max6 == 0 {
max6 = 0xff max6 = 0xff
} else if max6 == 0 {
max6 = 0xfe
} }
return &IPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil return &GeoIPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil
} }
func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error) { func BuildOptimizedGeoIPMatcher(geoips ...*GeoIP) (GeoIPMatcher, error) {
n := len(rules) n := len(geoips)
posCustom := make([]*CIDR, 0, n) if n == 0 {
negCustom := make([]*CIDR, 0, n) return nil, errors.New("no geoip configs provided")
posGeoip := make([]*GeoIPRule, 0, n) }
negGeoip := make([]*GeoIPRule, 0, n)
for _, r := range rules { var subs []*HeuristicGeoIPMatcher
switch v := r.Value.(type) { pos := make([]*GeoIP, 0, n)
case *IPRule_Custom: neg := make([]*GeoIP, 0, n/2)
if !v.Custom.ReverseMatch {
posCustom = append(posCustom, v.Custom.Cidr) for _, geoip := range geoips {
} else { if geoip == nil {
negCustom = append(negCustom, v.Custom.Cidr) return nil, errors.New("geoip entry is nil")
}
if geoip.CountryCode == "" {
ipset, err := ipsetFactory.Create(geoip.Cidr)
if err != nil {
return nil, err
} }
case *IPRule_Geoip: subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: geoip.ReverseMatch})
if !v.Geoip.ReverseMatch { continue
posGeoip = append(posGeoip, v.Geoip) }
} else { if !geoip.ReverseMatch {
negGeoip = append(negGeoip, v.Geoip) pos = append(pos, geoip)
} else {
neg = append(neg, geoip)
}
}
buildIPSet := func(mergeables []*GeoIP) (*GeoIPSet, error) {
n := len(mergeables)
if n == 0 {
return nil, nil
}
sort.Slice(mergeables, func(i, j int) bool {
gi, gj := mergeables[i], mergeables[j]
return gi.CountryCode < gj.CountryCode
})
var sb strings.Builder
sb.Grow(n * 3) // xx,
cidrGroups := make([][]*CIDR, 0, n)
var last *GeoIP
for i, geoip := range mergeables {
if i == 0 || (geoip.CountryCode != last.CountryCode) {
last = geoip
sb.WriteString(geoip.CountryCode)
sb.WriteString(",")
cidrGroups = append(cidrGroups, geoip.Cidr)
} }
default:
panic("unknown ip rule type")
} }
return ipsetFactory.GetOrCreate(sb.String(), cidrGroups)
} }
subs := make([]*HeuristicIPMatcher, 0, 4) ipset, err := buildIPSet(pos)
if err != nil {
if len(posCustom) > 0 { return nil, err
ipset, err := f.CreateFromCIDRs(posCustom) }
if err != nil { if ipset != nil {
return nil, err subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: false})
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
} }
if len(negCustom) > 0 { ipset, err = buildIPSet(neg)
ipset, err := f.CreateFromCIDRs(negCustom) if err != nil {
if err != nil { return nil, err
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: true})
} }
if ipset != nil {
if len(posGeoip) > 0 { subs = append(subs, &HeuristicGeoIPMatcher{ipset: ipset, reverse: true})
ipset, err := f.GetOrCreateFromGeoIPRules(posGeoip)
if err != nil {
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: false})
}
if len(negGeoip) > 0 {
ipset, err := f.GetOrCreateFromGeoIPRules(negGeoip)
if err != nil {
return nil, err
}
subs = append(subs, &HeuristicIPMatcher{ipset: ipset, reverse: true})
} }
switch len(subs) { switch len(subs) {
case 0: case 0:
return nil, errors.New("no valid ip matcher") return nil, errors.New("no valid geoip matcher")
case 1: case 1:
return subs[0], nil return subs[0], nil
default: default:
return &HeuristicMultiIPMatcher{matchers: subs}, nil return &HeuristicMultiGeoIPMatcher{matchers: subs}, nil
} }
} }
func newIPSetFactory() *IPSetFactory {
return &IPSetFactory{shared: utils.NewWeakCacheMap[string, IPSet]()}
}
+266
View File
@@ -0,0 +1,266 @@
package router_test
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/platform/filesystem"
"google.golang.org/protobuf/proto"
)
func getAssetPath(file string) (string, error) {
path := platform.GetAssetLocation(file)
_, err := os.Stat(path)
if os.IsNotExist(err) {
path := filepath.Join("..", "..", "resources", file)
_, err := os.Stat(path)
if os.IsNotExist(err) {
return "", fmt.Errorf("can't find %s in standard asset locations or {project_root}/resources", file)
}
if err != nil {
return "", fmt.Errorf("can't stat %s: %v", path, err)
}
return path, nil
}
if err != nil {
return "", fmt.Errorf("can't stat %s: %v", path, err)
}
return path, nil
}
func TestGeoIPMatcher(t *testing.T) {
cidrList := []*router.CIDR{
{Ip: []byte{0, 0, 0, 0}, Prefix: 8},
{Ip: []byte{10, 0, 0, 0}, Prefix: 8},
{Ip: []byte{100, 64, 0, 0}, Prefix: 10},
{Ip: []byte{127, 0, 0, 0}, Prefix: 8},
{Ip: []byte{169, 254, 0, 0}, Prefix: 16},
{Ip: []byte{172, 16, 0, 0}, Prefix: 12},
{Ip: []byte{192, 0, 0, 0}, Prefix: 24},
{Ip: []byte{192, 0, 2, 0}, Prefix: 24},
{Ip: []byte{192, 168, 0, 0}, Prefix: 16},
{Ip: []byte{192, 18, 0, 0}, Prefix: 15},
{Ip: []byte{198, 51, 100, 0}, Prefix: 24},
{Ip: []byte{203, 0, 113, 0}, Prefix: 24},
{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
{Ip: []byte{91, 108, 4, 0}, Prefix: 16},
}
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: cidrList,
})
common.Must(err)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: true,
},
{
Input: "192.0.0.0",
Output: true,
},
{
Input: "192.0.1.0",
Output: false,
},
{
Input: "0.1.0.0",
Output: true,
},
{
Input: "1.0.0.1",
Output: false,
},
{
Input: "8.8.8.7",
Output: false,
},
{
Input: "8.8.8.8",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
{
Input: "91.108.255.254",
Output: true,
},
}
for _, testCase := range testCases {
ip := net.ParseAddress(testCase.Input).IP()
actual := matcher.Match(ip)
if actual != testCase.Output {
t.Error("expect input", testCase.Input, "to be", testCase.Output, ", but actually", actual)
}
}
}
func TestGeoIPMatcherRegression(t *testing.T) {
cidrList := []*router.CIDR{
{Ip: []byte{98, 108, 20, 0}, Prefix: 22},
{Ip: []byte{98, 108, 20, 0}, Prefix: 23},
}
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: cidrList,
})
common.Must(err)
testCases := []struct {
Input string
Output bool
}{
{
Input: "98.108.22.11",
Output: true,
},
{
Input: "98.108.25.0",
Output: false,
},
}
for _, testCase := range testCases {
ip := net.ParseAddress(testCase.Input).IP()
actual := matcher.Match(ip)
if actual != testCase.Output {
t.Error("expect input", testCase.Input, "to be", testCase.Output, ", but actually", actual)
}
}
}
func TestGeoIPReverseMatcher(t *testing.T) {
cidrList := []*router.CIDR{
{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
{Ip: []byte{91, 108, 4, 0}, Prefix: 16},
}
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: cidrList,
})
common.Must(err)
matcher.SetReverse(true) // Reverse match
testCases := []struct {
Input string
Output bool
}{
{
Input: "8.8.8.8",
Output: false,
},
{
Input: "2001:cdba::3257:9652",
Output: true,
},
{
Input: "91.108.255.254",
Output: false,
},
}
for _, testCase := range testCases {
ip := net.ParseAddress(testCase.Input).IP()
actual := matcher.Match(ip)
if actual != testCase.Output {
t.Error("expect input", testCase.Input, "to be", testCase.Output, ", but actually", actual)
}
}
}
func TestGeoIPMatcher4CN(t *testing.T) {
ips, err := loadGeoIP("CN")
common.Must(err)
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: ips,
})
common.Must(err)
if matcher.Match([]byte{8, 8, 8, 8}) {
t.Error("expect CN geoip doesn't contain 8.8.8.8, but actually does")
}
}
func TestGeoIPMatcher6US(t *testing.T) {
ips, err := loadGeoIP("US")
common.Must(err)
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: ips,
})
common.Must(err)
if !matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP()) {
t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not")
}
}
func loadGeoIP(country string) ([]*router.CIDR, error) {
path, err := getAssetPath("geoip.dat")
if err != nil {
return nil, err
}
geoipBytes, err := filesystem.ReadFile(path)
if err != nil {
return nil, err
}
var geoipList router.GeoIPList
if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil {
return nil, err
}
for _, geoip := range geoipList.Entry {
if geoip.CountryCode == country {
return geoip.Cidr, nil
}
}
panic("country not found: " + country)
}
func BenchmarkGeoIPMatcher4CN(b *testing.B) {
ips, err := loadGeoIP("CN")
common.Must(err)
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: ips,
})
common.Must(err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = matcher.Match([]byte{8, 8, 8, 8})
}
}
func BenchmarkGeoIPMatcher6US(b *testing.B) {
ips, err := loadGeoIP("US")
common.Must(err)
matcher, err := router.BuildOptimizedGeoIPMatcher(&router.GeoIP{
Cidr: ips,
})
common.Must(err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = matcher.Match(net.ParseAddress("2001:4860:4860::8888").IP())
}
}
+167
View File
@@ -0,0 +1,167 @@
package router_test
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common/platform/filesystem"
)
func TestDomainMatcherSerialization(t *testing.T) {
domains := []*router.Domain{
{Type: router.Domain_Domain, Value: "google.com"},
{Type: router.Domain_Domain, Value: "v2ray.com"},
{Type: router.Domain_Full, Value: "full.example.com"},
}
var buf bytes.Buffer
if err := router.SerializeDomainMatcher(domains, &buf); err != nil {
t.Fatalf("Serialize failed: %v", err)
}
matcher, err := router.NewDomainMatcherFromBuffer(buf.Bytes())
if err != nil {
t.Fatalf("Deserialize failed: %v", err)
}
dMatcher := &router.DomainMatcher{
Matchers: matcher,
}
testCases := []struct {
Input string
Match bool
}{
{"google.com", true},
{"maps.google.com", true},
{"v2ray.com", true},
{"full.example.com", true},
{"example.com", false},
}
for _, tc := range testCases {
if res := dMatcher.ApplyDomain(tc.Input); res != tc.Match {
t.Errorf("Match(%s) = %v, want %v", tc.Input, res, tc.Match)
}
}
}
func TestGeoSiteSerialization(t *testing.T) {
sites := []*router.GeoSite{
{
CountryCode: "CN",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "baidu.cn"},
{Type: router.Domain_Domain, Value: "qq.com"},
},
},
{
CountryCode: "US",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "google.com"},
{Type: router.Domain_Domain, Value: "facebook.com"},
},
},
}
var buf bytes.Buffer
if err := router.SerializeGeoSiteList(sites, nil, nil, &buf); err != nil {
t.Fatalf("SerializeGeoSiteList failed: %v", err)
}
tmp := t.TempDir()
path := filepath.Join(tmp, "matcher.cache")
f, err := os.Create(path)
require.NoError(t, err)
_, err = f.Write(buf.Bytes())
require.NoError(t, err)
f.Close()
f, err = os.Open(path)
require.NoError(t, err)
defer f.Close()
require.NoError(t, err)
data, _ := filesystem.ReadFile(path)
// cn
gp, err := router.LoadGeoSiteMatcher(bytes.NewReader(data), "CN")
if err != nil {
t.Fatalf("LoadGeoSiteMatcher(CN) failed: %v", err)
}
cnMatcher := &router.DomainMatcher{
Matchers: gp,
}
if !cnMatcher.ApplyDomain("baidu.cn") {
t.Error("CN matcher should match baidu.cn")
}
if cnMatcher.ApplyDomain("google.com") {
t.Error("CN matcher should NOT match google.com")
}
// us
gp, err = router.LoadGeoSiteMatcher(bytes.NewReader(data), "US")
if err != nil {
t.Fatalf("LoadGeoSiteMatcher(US) failed: %v", err)
}
usMatcher := &router.DomainMatcher{
Matchers: gp,
}
if !usMatcher.ApplyDomain("google.com") {
t.Error("US matcher should match google.com")
}
if usMatcher.ApplyDomain("baidu.cn") {
t.Error("US matcher should NOT match baidu.cn")
}
// unknown
_, err = router.LoadGeoSiteMatcher(bytes.NewReader(data), "unknown")
if err == nil {
t.Error("LoadGeoSiteMatcher(unknown) should fail")
}
}
func TestGeoSiteSerializationWithDeps(t *testing.T) {
sites := []*router.GeoSite{
{
CountryCode: "geosite:cn",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "baidu.cn"},
},
},
{
CountryCode: "geosite:google@cn",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "google.cn"},
},
},
{
CountryCode: "rule-1",
Domain: []*router.Domain{
{Type: router.Domain_Domain, Value: "google.com"},
},
},
}
deps := map[string][]string{
"rule-1": {"geosite:cn", "geosite:google@cn"},
}
var buf bytes.Buffer
err := router.SerializeGeoSiteList(sites, deps, nil, &buf)
require.NoError(t, err)
matcher, err := router.LoadGeoSiteMatcher(bytes.NewReader(buf.Bytes()), "rule-1")
require.NoError(t, err)
require.True(t, matcher.Match("google.com") != nil)
require.True(t, matcher.Match("baidu.cn") != nil)
require.True(t, matcher.Match("google.cn") != nil)
}
+94 -38
View File
@@ -1,19 +1,20 @@
package router_test package router_test
import ( import (
"path/filepath"
"strconv" "strconv"
"testing" "testing"
. "github.com/xtls/xray-core/app/router" . "github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/geodata" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/protocol/http" "github.com/xtls/xray-core/common/protocol/http"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
routing_session "github.com/xtls/xray-core/features/routing/session" routing_session "github.com/xtls/xray-core/features/routing/session"
"google.golang.org/protobuf/proto"
) )
func withBackground() routing.Context { func withBackground() routing.Context {
@@ -44,15 +45,18 @@ func TestRoutingRule(t *testing.T) {
}{ }{
{ {
rule: &RoutingRule{ rule: &RoutingRule{
Domain: []*geodata.DomainRule{ Domain: []*Domain{
{ {
Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Substr, Value: "example.com"}}, Value: "example.com",
Type: Domain_Plain,
}, },
{ {
Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Domain, Value: "google.com"}}, Value: "google.com",
Type: Domain_Domain,
}, },
{ {
Value: &geodata.DomainRule_Custom{Custom: &geodata.Domain{Type: geodata.Domain_Regex, Value: "^facebook\\.com$"}}, Value: "^facebook\\.com$",
Type: Domain_Regex,
}, },
}, },
}, },
@@ -89,25 +93,20 @@ func TestRoutingRule(t *testing.T) {
}, },
{ {
rule: &RoutingRule{ rule: &RoutingRule{
Ip: []*geodata.IPRule{ Geoip: []*GeoIP{
{ {
Value: &geodata.IPRule_Custom{ Cidr: []*CIDR{
Custom: &geodata.CIDRRule{ {
Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32}, Ip: []byte{8, 8, 8, 8},
Prefix: 32,
}, },
}, {
}, Ip: []byte{8, 8, 8, 8},
{ Prefix: 32,
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: []byte{8, 8, 8, 8}, Prefix: 32},
}, },
}, {
}, Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(),
{ Prefix: 128,
Value: &geodata.IPRule_Custom{
Custom: &geodata.CIDRRule{
Cidr: &geodata.CIDR{Ip: net.ParseAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334").IP(), Prefix: 128},
}, },
}, },
}, },
@@ -134,11 +133,12 @@ func TestRoutingRule(t *testing.T) {
}, },
{ {
rule: &RoutingRule{ rule: &RoutingRule{
SourceIp: []*geodata.IPRule{ SourceGeoip: []*GeoIP{
{ {
Value: &geodata.IPRule_Custom{ Cidr: []*CIDR{
Custom: &geodata.CIDRRule{ {
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16}, Ip: []byte{192, 168, 0, 0},
Prefix: 16,
}, },
}, },
}, },
@@ -300,12 +300,35 @@ func TestRoutingRule(t *testing.T) {
} }
} }
func loadGeoSite(country string) ([]*Domain, error) {
path, err := getAssetPath("geosite.dat")
if err != nil {
return nil, err
}
geositeBytes, err := filesystem.ReadFile(path)
if err != nil {
return nil, err
}
var geositeList GeoSiteList
if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil {
return nil, err
}
for _, site := range geositeList.Entry {
if site.CountryCode == country {
return site.Domain, nil
}
}
return nil, errors.New("country not found: " + country)
}
func TestChinaSites(t *testing.T) { func TestChinaSites(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) domains, err := loadGeoSite("CN")
rules, err := geodata.ParseDomainRules([]string{"geosite:cn"}, geodata.Domain_Substr)
common.Must(err) common.Must(err)
matcher, err := NewDomainMatcher(rules) acMatcher, err := NewMphMatcherGroup(domains)
common.Must(err) common.Must(err)
type TestCase struct { type TestCase struct {
@@ -336,19 +359,18 @@ func TestChinaSites(t *testing.T) {
} }
for _, testCase := range testCases { for _, testCase := range testCases {
r := matcher.ApplyDomain(testCase.Domain) r := acMatcher.ApplyDomain(testCase.Domain)
if r != testCase.Output { if r != testCase.Output {
t.Error("DomainMatcher expected output ", testCase.Output, " for domain ", testCase.Domain, " but got ", r) t.Error("ACDomainMatcher expected output ", testCase.Output, " for domain ", testCase.Domain, " but got ", r)
} }
} }
} }
func BenchmarkMphDomainMatcher(b *testing.B) { func BenchmarkMphDomainMatcher(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) domains, err := loadGeoSite("CN")
rules, err := geodata.ParseDomainRules([]string{"geosite:cn"}, geodata.Domain_Substr)
common.Must(err) common.Must(err)
matcher, err := NewDomainMatcher(rules) matcher, err := NewMphMatcherGroup(domains)
common.Must(err) common.Must(err)
type TestCase struct { type TestCase struct {
@@ -387,11 +409,45 @@ func BenchmarkMphDomainMatcher(b *testing.B) {
} }
func BenchmarkMultiGeoIPMatcher(b *testing.B) { func BenchmarkMultiGeoIPMatcher(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) var geoips []*GeoIP
rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:jp", "geoip:ca", "geoip:us"})
common.Must(err)
matcher, err := NewIPMatcher(rules, MatcherAsType_Target) {
ips, err := loadGeoIP("CN")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "CN",
Cidr: ips,
})
}
{
ips, err := loadGeoIP("JP")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "JP",
Cidr: ips,
})
}
{
ips, err := loadGeoIP("CA")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "CA",
Cidr: ips,
})
}
{
ips, err := loadGeoIP("US")
common.Must(err)
geoips = append(geoips, &GeoIP{
CountryCode: "US",
Cidr: ips,
})
}
matcher, err := NewIPMatcher(geoips, MatcherAsType_Target)
common.Must(err) common.Must(err)
ctx := withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)}) ctx := withOutbound(&session.Outbound{Target: net.TCPDestination(net.ParseAddress("8.8.8.8"), 80)})
+53 -11
View File
@@ -3,9 +3,12 @@ package router
import ( import (
"context" "context"
"regexp" "regexp"
"runtime"
"strings" "strings"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/features/outbound" "github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
) )
@@ -15,7 +18,6 @@ type Rule struct {
RuleTag string RuleTag string
Balancer *Balancer Balancer *Balancer
Condition Condition Condition Condition
Webhook *WebhookNotifier
} }
func (r *Rule) GetTag() (string, error) { func (r *Rule) GetTag() (string, error) {
@@ -73,37 +75,60 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) {
conds.Add(&AttributeMatcher{configuredKeys}) conds.Add(&AttributeMatcher{configuredKeys})
} }
if len(rr.Ip) > 0 { if len(rr.Geoip) > 0 {
cond, err := NewIPMatcher(rr.Ip, MatcherAsType_Target) cond, err := NewIPMatcher(rr.Geoip, MatcherAsType_Target)
if err != nil { if err != nil {
return nil, err return nil, err
} }
conds.Add(cond) conds.Add(cond)
rr.Geoip = nil
runtime.GC()
} }
if len(rr.SourceIp) > 0 { if len(rr.SourceGeoip) > 0 {
cond, err := NewIPMatcher(rr.SourceIp, MatcherAsType_Source) cond, err := NewIPMatcher(rr.SourceGeoip, MatcherAsType_Source)
if err != nil { if err != nil {
return nil, err return nil, err
} }
conds.Add(cond) conds.Add(cond)
rr.SourceGeoip = nil
runtime.GC()
} }
if len(rr.LocalIp) > 0 { if len(rr.LocalGeoip) > 0 {
cond, err := NewIPMatcher(rr.LocalIp, MatcherAsType_Local) cond, err := NewIPMatcher(rr.LocalGeoip, MatcherAsType_Local)
if err != nil { if err != nil {
return nil, err return nil, err
} }
conds.Add(cond) conds.Add(cond)
errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces") errors.LogWarning(context.Background(), "Due to some limitations, in UDP connections, localIP is always equal to listen interface IP, so \"localIP\" rule condition does not work properly on UDP inbound connections that listen on all interfaces")
rr.LocalGeoip = nil
runtime.GC()
} }
if len(rr.Domain) > 0 { if len(rr.Domain) > 0 {
cond, err := NewDomainMatcher(rr.Domain) var matcher *DomainMatcher
if err != nil { var err error
return nil, err // Check if domain matcher cache is provided via environment
domainMatcherPath := platform.NewEnvFlag(platform.MphCachePath).GetValue(func() string { return "" })
if domainMatcherPath != "" {
matcher, err = GetDomainMatcherWithRuleTag(domainMatcherPath, rr.RuleTag)
if err != nil {
return nil, errors.New("failed to build domain condition from cached MphDomainMatcher").Base(err)
}
errors.LogDebug(context.Background(), "MphDomainMatcher loaded from cache for ", rr.RuleTag, " rule tag)")
} else {
matcher, err = NewMphMatcherGroup(rr.Domain)
if err != nil {
return nil, errors.New("failed to build domain condition with MphDomainMatcher").Base(err)
}
errors.LogDebug(context.Background(), "MphDomainMatcher is enabled for ", len(rr.Domain), " domain rule(s)")
} }
conds.Add(cond) conds.Add(matcher)
rr.Domain = nil
runtime.GC()
} }
if len(rr.Process) > 0 { if len(rr.Process) > 0 {
@@ -163,3 +188,20 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch
return nil, errors.New("unrecognized balancer type") return nil, errors.New("unrecognized balancer type")
} }
} }
func GetDomainMatcherWithRuleTag(domainMatcherPath string, ruleTag string) (*DomainMatcher, error) {
f, err := filesystem.NewFileReader(domainMatcherPath)
if err != nil {
return nil, errors.New("failed to load file: ", domainMatcherPath).Base(err)
}
defer f.Close()
g, err := LoadGeoSiteMatcher(f, ruleTag)
if err != nil {
return nil, errors.New("failed to load file:", domainMatcherPath).Base(err)
}
return &DomainMatcher{
Matchers: g,
}, nil
}
+603 -169
View File
File diff suppressed because it is too large Load Diff
+75 -22
View File
@@ -9,7 +9,67 @@ option java_multiple_files = true;
import "common/serial/typed_message.proto"; import "common/serial/typed_message.proto";
import "common/net/port.proto"; import "common/net/port.proto";
import "common/net/network.proto"; import "common/net/network.proto";
import "common/geodata/geodat.proto";
// Domain for routing decision.
message Domain {
// Type of domain value.
enum Type {
// The value is used as is.
Plain = 0;
// The value is used as a regular expression.
Regex = 1;
// The value is a root domain.
Domain = 2;
// The value is a domain.
Full = 3;
}
// Domain matching type.
Type type = 1;
// Domain value.
string value = 2;
message Attribute {
string key = 1;
oneof typed_value {
bool bool_value = 2;
int64 int_value = 3;
}
}
// Attributes of this domain. May be used for filtering.
repeated Attribute attribute = 3;
}
// IP for routing decision, in CIDR form.
message CIDR {
// IP address, should be either 4 or 16 bytes.
bytes ip = 1;
// Number of leading ones in the network mask.
uint32 prefix = 2;
}
message GeoIP {
string country_code = 1;
repeated CIDR cidr = 2;
bool reverse_match = 3;
}
message GeoIPList {
repeated GeoIP entry = 1;
}
message GeoSite {
string country_code = 1;
repeated Domain domain = 2;
}
message GeoSiteList {
repeated GeoSite entry = 1;
}
message RoutingRule { message RoutingRule {
oneof target_tag { oneof target_tag {
@@ -19,23 +79,26 @@ message RoutingRule {
// Tag of routing balancer. // Tag of routing balancer.
string balancing_tag = 12; string balancing_tag = 12;
} }
string rule_tag = 19;
string rule_tag = 19;
// List of domains for target domain matching. // List of domains for target domain matching.
repeated xray.common.geodata.DomainRule domain = 2; repeated Domain domain = 2;
// List of IPs for target IP address matching. // List of GeoIPs for target IP address matching. If this entry exists, the
repeated xray.common.geodata.IPRule ip = 10; // cidr above will have no effect. GeoIP fields with the same country code are
// supposed to contain exactly same content. They will be merged during
// runtime. For customized GeoIPs, please leave country code empty.
repeated GeoIP geoip = 10;
// List of ports for target port matching. // List of ports.
xray.common.net.PortList port_list = 14; xray.common.net.PortList port_list = 14;
// List of networks for matching. // List of networks for matching.
repeated xray.common.net.Network networks = 13; repeated xray.common.net.Network networks = 13;
// List of IPs for source IP address matching. // List of GeoIPs for source IP address matching. If this entry exists, the
repeated xray.common.geodata.IPRule source_ip = 11; // source_cidr above will have no effect.
repeated GeoIP source_geoip = 11;
// List of ports for source port matching. // List of ports for source port matching.
xray.common.net.PortList source_port_list = 16; xray.common.net.PortList source_port_list = 16;
@@ -46,22 +109,11 @@ message RoutingRule {
map<string, string> attributes = 15; map<string, string> attributes = 15;
// List of IPs for local IP address matching. repeated GeoIP local_geoip = 17;
repeated xray.common.geodata.IPRule local_ip = 17;
// List of ports for local port matching.
xray.common.net.PortList local_port_list = 18; xray.common.net.PortList local_port_list = 18;
xray.common.net.PortList vless_route_list = 20; xray.common.net.PortList vless_route_list = 20;
repeated string process = 21; repeated string process = 21;
WebhookConfig webhook = 22;
}
message WebhookConfig {
string url = 1;
uint32 deduplication = 2;
map<string, string> headers = 3;
} }
message BalancingRule { message BalancingRule {
@@ -96,7 +148,8 @@ message Config {
// Use domain as is. // Use domain as is.
AsIs = 0; AsIs = 0;
reserved 1; // [Deprecated] Always resolve IP for domains.
// UseIp = 1;
// Resolve to IP if the domain doesn't match any rules. // Resolve to IP if the domain doesn't match any rules.
IpIfNonMatch = 2; IpIfNonMatch = 2;
+100
View File
@@ -0,0 +1,100 @@
package router
import (
"encoding/gob"
"errors"
"io"
"runtime"
"github.com/xtls/xray-core/common/strmatcher"
)
type geoSiteListGob struct {
Sites map[string][]byte
Deps map[string][]string
Hosts map[string][]string
}
func SerializeGeoSiteList(sites []*GeoSite, deps map[string][]string, hosts map[string][]string, w io.Writer) error {
data := geoSiteListGob{
Sites: make(map[string][]byte),
Deps: deps,
Hosts: hosts,
}
for _, site := range sites {
if site == nil {
continue
}
var buf bytesWriter
if err := SerializeDomainMatcher(site.Domain, &buf); err != nil {
return err
}
data.Sites[site.CountryCode] = buf.Bytes()
}
return gob.NewEncoder(w).Encode(data)
}
type bytesWriter struct {
data []byte
}
func (w *bytesWriter) Write(p []byte) (n int, err error) {
w.data = append(w.data, p...)
return len(p), nil
}
func (w *bytesWriter) Bytes() []byte {
return w.data
}
func LoadGeoSiteMatcher(r io.Reader, countryCode string) (strmatcher.IndexMatcher, error) {
var data geoSiteListGob
if err := gob.NewDecoder(r).Decode(&data); err != nil {
return nil, err
}
return loadWithDeps(&data, countryCode, make(map[string]bool))
}
func loadWithDeps(data *geoSiteListGob, code string, visited map[string]bool) (strmatcher.IndexMatcher, error) {
if visited[code] {
return nil, errors.New("cyclic dependency")
}
visited[code] = true
var matchers []strmatcher.IndexMatcher
if siteData, ok := data.Sites[code]; ok {
m, err := NewDomainMatcherFromBuffer(siteData)
if err == nil {
matchers = append(matchers, m)
}
}
if deps, ok := data.Deps[code]; ok {
for _, dep := range deps {
m, err := loadWithDeps(data, dep, visited)
if err == nil {
matchers = append(matchers, m)
}
}
}
if len(matchers) == 0 {
return nil, errors.New("matcher not found for: " + code)
}
if len(matchers) == 1 {
return matchers[0], nil
}
runtime.GC()
return &strmatcher.IndexMatcherGroup{Matchers: matchers}, nil
}
func LoadGeoSiteHosts(r io.Reader) (map[string][]string, error) {
var data geoSiteListGob
if err := gob.NewDecoder(r).Decode(&data); err != nil {
return nil, err
}
return data.Hosts, nil
}
+2 -60
View File
@@ -57,7 +57,6 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
for _, rule := range config.Rule { for _, rule := range config.Rule {
cond, err := rule.BuildCondition() cond, err := rule.BuildCondition()
if err != nil { if err != nil {
r.closeWebhooks()
return err return err
} }
rr := &Rule{ rr := &Rule{
@@ -65,22 +64,10 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
Tag: rule.GetTag(), Tag: rule.GetTag(),
RuleTag: rule.GetRuleTag(), RuleTag: rule.GetRuleTag(),
} }
if wh := rule.GetWebhook(); wh != nil {
notifier, err := NewWebhookNotifier(wh)
if err != nil {
r.closeWebhooks()
return err
}
rr.Webhook = notifier
}
btag := rule.GetBalancingTag() btag := rule.GetBalancingTag()
if len(btag) > 0 { if len(btag) > 0 {
brule, found := r.balancers[btag] brule, found := r.balancers[btag]
if !found { if !found {
if rr.Webhook != nil {
rr.Webhook.Close()
}
r.closeWebhooks()
return errors.New("balancer ", btag, " not found") return errors.New("balancer ", btag, " not found")
} }
rr.Balancer = brule rr.Balancer = brule
@@ -93,7 +80,6 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
// PickRoute implements routing.Router. // PickRoute implements routing.Router.
func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) { func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) {
originalCtx := ctx
rule, ctx, err := r.pickRouteInternal(ctx) rule, ctx, err := r.pickRouteInternal(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -102,14 +88,12 @@ func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if rule.Webhook != nil {
rule.Webhook.Fire(originalCtx, tag)
}
return &Route{Context: ctx, outboundTag: tag, ruleTag: rule.RuleTag}, nil return &Route{Context: ctx, outboundTag: tag, ruleTag: rule.RuleTag}, nil
} }
// AddRule implements routing.Router. // AddRule implements routing.Router.
func (r *Router) AddRule(config *serial.TypedMessage, shouldAppend bool) error { func (r *Router) AddRule(config *serial.TypedMessage, shouldAppend bool) error {
inst, err := config.GetInstance() inst, err := config.GetInstance()
if err != nil { if err != nil {
return err return err
@@ -125,11 +109,6 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
defer r.mu.Unlock() defer r.mu.Unlock()
if !shouldAppend { if !shouldAppend {
for _, rule := range r.rules {
if rule.Webhook != nil {
rule.Webhook.Close()
}
}
r.balancers = make(map[string]*Balancer, len(config.BalancingRule)) r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
r.rules = make([]*Rule, 0, len(config.Rule)) r.rules = make([]*Rule, 0, len(config.Rule))
} }
@@ -146,24 +125,12 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
r.balancers[rule.Tag] = balancer r.balancers[rule.Tag] = balancer
} }
startIdx := len(r.rules)
closeNewWebhooks := func() {
for i := startIdx; i < len(r.rules); i++ {
if r.rules[i].Webhook != nil {
r.rules[i].Webhook.Close()
}
}
r.rules = r.rules[:startIdx]
}
for _, rule := range config.Rule { for _, rule := range config.Rule {
if r.RuleExists(rule.GetRuleTag()) { if r.RuleExists(rule.GetRuleTag()) {
closeNewWebhooks()
return errors.New("duplicate ruleTag ", rule.GetRuleTag()) return errors.New("duplicate ruleTag ", rule.GetRuleTag())
} }
cond, err := rule.BuildCondition() cond, err := rule.BuildCondition()
if err != nil { if err != nil {
closeNewWebhooks()
return err return err
} }
rr := &Rule{ rr := &Rule{
@@ -171,22 +138,10 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
Tag: rule.GetTag(), Tag: rule.GetTag(),
RuleTag: rule.GetRuleTag(), RuleTag: rule.GetRuleTag(),
} }
if wh := rule.GetWebhook(); wh != nil {
notifier, err := NewWebhookNotifier(wh)
if err != nil {
closeNewWebhooks()
return err
}
rr.Webhook = notifier
}
btag := rule.GetBalancingTag() btag := rule.GetBalancingTag()
if len(btag) > 0 { if len(btag) > 0 {
brule, found := r.balancers[btag] brule, found := r.balancers[btag]
if !found { if !found {
if rr.Webhook != nil {
rr.Webhook.Close()
}
closeNewWebhooks()
return errors.New("balancer ", btag, " not found") return errors.New("balancer ", btag, " not found")
} }
rr.Balancer = brule rr.Balancer = brule
@@ -218,14 +173,13 @@ func (r *Router) RemoveRule(tag string) error {
for _, rule := range r.rules { for _, rule := range r.rules {
if rule.RuleTag != tag { if rule.RuleTag != tag {
newRules = append(newRules, rule) newRules = append(newRules, rule)
} else if rule.Webhook != nil {
rule.Webhook.Close()
} }
} }
r.rules = newRules r.rules = newRules
return nil return nil
} }
return errors.New("empty tag name!") return errors.New("empty tag name!")
} }
// ListRule implements routing.Router // ListRule implements routing.Router
@@ -279,20 +233,8 @@ func (r *Router) Start() error {
return nil return nil
} }
// closeWebhooks closes all webhook notifiers in the current rule set.
func (r *Router) closeWebhooks() {
for _, rule := range r.rules {
if rule.Webhook != nil {
rule.Webhook.Close()
}
}
}
// Close implements common.Closable. // Close implements common.Closable.
func (r *Router) Close() error { func (r *Router) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
r.closeWebhooks()
return nil return nil
} }
+15 -13
View File
@@ -7,7 +7,6 @@ import (
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
. "github.com/xtls/xray-core/app/router" . "github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
@@ -156,11 +155,12 @@ func TestIPOnDemand(t *testing.T) {
TargetTag: &RoutingRule_Tag{ TargetTag: &RoutingRule_Tag{
Tag: "test", Tag: "test",
}, },
Ip: []*geodata.IPRule{ Geoip: []*GeoIP{
{ {
Value: &geodata.IPRule_Custom{ Cidr: []*CIDR{
Custom: &geodata.CIDRRule{ {
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16}, Ip: []byte{192, 168, 0, 0},
Prefix: 16,
}, },
}, },
}, },
@@ -200,11 +200,12 @@ func TestIPIfNonMatchDomain(t *testing.T) {
TargetTag: &RoutingRule_Tag{ TargetTag: &RoutingRule_Tag{
Tag: "test", Tag: "test",
}, },
Ip: []*geodata.IPRule{ Geoip: []*GeoIP{
{ {
Value: &geodata.IPRule_Custom{ Cidr: []*CIDR{
Custom: &geodata.CIDRRule{ {
Cidr: &geodata.CIDR{Ip: []byte{192, 168, 0, 0}, Prefix: 16}, Ip: []byte{192, 168, 0, 0},
Prefix: 16,
}, },
}, },
}, },
@@ -244,11 +245,12 @@ func TestIPIfNonMatchIP(t *testing.T) {
TargetTag: &RoutingRule_Tag{ TargetTag: &RoutingRule_Tag{
Tag: "test", Tag: "test",
}, },
Ip: []*geodata.IPRule{ Geoip: []*GeoIP{
{ {
Value: &geodata.IPRule_Custom{ Cidr: []*CIDR{
Custom: &geodata.CIDRRule{ {
Cidr: &geodata.CIDR{Ip: []byte{127, 0, 0, 0}, Prefix: 8}, Ip: []byte{127, 0, 0, 0},
Prefix: 8,
}, },
}, },
}, },
+6 -22
View File
@@ -3,7 +3,6 @@ package router
import ( import (
"context" "context"
"math" "math"
"slices"
"sort" "sort"
"time" "time"
@@ -78,7 +77,7 @@ func (s *LeastLoadStrategy) PickOutbound(candidates []string) string {
} }
func (s *LeastLoadStrategy) pickOutbounds(candidates []string) []*node { func (s *LeastLoadStrategy) pickOutbounds(candidates []string) []*node {
qualified := s.getNodes(candidates) qualified := s.getNodes(candidates, time.Duration(s.settings.MaxRTT))
selects := s.selectLeastLoad(qualified) selects := s.selectLeastLoad(qualified)
return selects return selects
} }
@@ -139,7 +138,7 @@ func (s *LeastLoadStrategy) selectLeastLoad(nodes []*node) []*node {
return nodes[:count] return nodes[:count]
} }
func (s *LeastLoadStrategy) getNodes(candidates []string) []*node { func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration) []*node {
if s.observer == nil { if s.observer == nil {
errors.LogError(s.ctx, "observer is nil") errors.LogError(s.ctx, "observer is nil")
return make([]*node, 0) return make([]*node, 0)
@@ -152,10 +151,12 @@ func (s *LeastLoadStrategy) getNodes(candidates []string) []*node {
results := observeResult.(*observatory.ObservationResult) results := observeResult.(*observatory.ObservationResult)
outboundlist := outboundList(candidates)
var ret []*node var ret []*node
for _, v := range results.Status { for _, v := range results.Status {
if s.shouldSelectNode(v, candidates) { if v.Alive && (v.Delay < maxRTT.Milliseconds() || maxRTT == 0) && outboundlist.contains(v.OutboundTag) {
record := &node{ record := &node{
Tag: v.OutboundTag, Tag: v.OutboundTag,
CountAll: 1, CountAll: 1,
@@ -171,8 +172,8 @@ func (s *LeastLoadStrategy) getNodes(candidates []string) []*node {
record.RTTDeviationCost = time.Duration(s.costs.Apply(v.OutboundTag, float64(v.HealthPing.Deviation))) record.RTTDeviationCost = time.Duration(s.costs.Apply(v.OutboundTag, float64(v.HealthPing.Deviation)))
record.CountAll = int(v.HealthPing.All) record.CountAll = int(v.HealthPing.All)
record.CountFail = int(v.HealthPing.Fail) record.CountFail = int(v.HealthPing.Fail)
}
}
ret = append(ret, record) ret = append(ret, record)
} }
} }
@@ -181,23 +182,6 @@ func (s *LeastLoadStrategy) getNodes(candidates []string) []*node {
return ret return ret
} }
func (s *LeastLoadStrategy) shouldSelectNode(v *observatory.OutboundStatus, candidates []string) bool {
maxRTT := time.Duration(s.settings.MaxRTT)
if !v.Alive {
return false
}
if maxRTT != 0 && v.Delay >= maxRTT.Milliseconds() {
return false
}
if !slices.Contains(candidates, v.OutboundTag) {
return false
}
if v.HealthPing != nil && v.HealthPing.All > 0 && s.settings.Tolerance > 0 && float64(v.HealthPing.Fail)/float64(v.HealthPing.All) > float64(s.settings.Tolerance) {
return false
}
return true
}
func leastloadSort(nodes []*node) { func leastloadSort(nodes []*node) {
sort.Slice(nodes, func(i, j int) bool { sort.Slice(nodes, func(i, j int) bool {
left := nodes[i] left := nodes[i]
-5
View File
@@ -85,7 +85,6 @@ func TestSelectLeastExpected(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns)) t.Errorf("expected: %v, actual: %v", expected, len(ns))
} }
} }
func TestSelectLeastExpected2(t *testing.T) { func TestSelectLeastExpected2(t *testing.T) {
strategy := &LeastLoadStrategy{ strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{ settings: &StrategyLeastLoadConfig{
@@ -103,7 +102,6 @@ func TestSelectLeastExpected2(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns)) t.Errorf("expected: %v, actual: %v", expected, len(ns))
} }
} }
func TestSelectLeastExpectedAndBaselines(t *testing.T) { func TestSelectLeastExpectedAndBaselines(t *testing.T) {
strategy := &LeastLoadStrategy{ strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{ settings: &StrategyLeastLoadConfig{
@@ -124,7 +122,6 @@ func TestSelectLeastExpectedAndBaselines(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns)) t.Errorf("expected: %v, actual: %v", expected, len(ns))
} }
} }
func TestSelectLeastExpectedAndBaselines2(t *testing.T) { func TestSelectLeastExpectedAndBaselines2(t *testing.T) {
strategy := &LeastLoadStrategy{ strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{ settings: &StrategyLeastLoadConfig{
@@ -145,7 +142,6 @@ func TestSelectLeastExpectedAndBaselines2(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns)) t.Errorf("expected: %v, actual: %v", expected, len(ns))
} }
} }
func TestSelectLeastLoadBaselines(t *testing.T) { func TestSelectLeastLoadBaselines(t *testing.T) {
strategy := &LeastLoadStrategy{ strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{ settings: &StrategyLeastLoadConfig{
@@ -164,7 +160,6 @@ func TestSelectLeastLoadBaselines(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns)) t.Errorf("expected: %v, actual: %v", expected, len(ns))
} }
} }
func TestSelectLeastLoadBaselinesNoQualified(t *testing.T) { func TestSelectLeastLoadBaselinesNoQualified(t *testing.T) {
strategy := &LeastLoadStrategy{ strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{ settings: &StrategyLeastLoadConfig{
-241
View File
@@ -1,241 +0,0 @@
package router
import (
"bytes"
"context"
"encoding/json"
"io"
"net"
"net/http"
"sync"
"time"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/routing"
routing_session "github.com/xtls/xray-core/features/routing/session"
)
func ptr[T any](v T) *T { return &v }
type event struct {
Email *string `json:"email"`
Level *uint32 `json:"level"`
Protocol *string `json:"protocol"`
Network *string `json:"network"`
Source *string `json:"source"`
Destination *string `json:"destination"`
OriginalTarget *string `json:"originalTarget"`
RouteTarget *string `json:"routeTarget"`
InboundTag *string `json:"inboundTag"`
InboundName *string `json:"inboundName"`
InboundLocal *string `json:"inboundLocal"`
OutboundTag *string `json:"outboundTag"`
Timestamp int64 `json:"ts"`
}
type WebhookNotifier struct {
url string
headers map[string]string
deduplication uint32
client *http.Client
seen sync.Map
done chan struct{}
wg sync.WaitGroup
closeOnce sync.Once
}
func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
if cfg == nil || cfg.Url == "" {
return nil, nil
}
httpURL, socketPath := utils.SplitHTTPUnixURL(cfg.Url)
h := &WebhookNotifier{
url: httpURL,
deduplication: cfg.Deduplication,
client: &http.Client{
Timeout: 5 * time.Second,
},
done: make(chan struct{}),
}
if socketPath != "" {
dialAddr := utils.ResolveSocketPath(socketPath)
h.client.Transport = &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", dialAddr)
},
}
}
if len(cfg.Headers) > 0 {
h.headers = make(map[string]string, len(cfg.Headers))
for k, v := range cfg.Headers {
h.headers[k] = v
}
}
if h.deduplication > 0 {
h.wg.Add(1)
go h.cleanupLoop()
}
return h, nil
}
func (h *WebhookNotifier) Fire(ctx routing.Context, outboundTag string) {
ev := buildEvent(ctx, outboundTag)
email := ""
if ev.Email != nil {
email = *ev.Email
}
if h.isDuplicate(email) {
return
}
h.wg.Add(1)
select {
case <-h.done:
h.wg.Done()
return
default:
}
go func() {
defer h.wg.Done()
h.post(ev)
}()
}
func buildEvent(ctx routing.Context, outboundTag string) *event {
ev := &event{
Timestamp: time.Now().Unix(),
OutboundTag: ptr(outboundTag),
InboundTag: ptr(ctx.GetInboundTag()),
Protocol: ptr(ctx.GetProtocol()),
Network: ptr(ctx.GetNetwork().SystemString()),
}
if user := ctx.GetUser(); user != "" {
ev.Email = ptr(user)
}
if srcIPs := ctx.GetSourceIPs(); len(srcIPs) > 0 {
srcPort := ctx.GetSourcePort()
ev.Source = ptr(net.JoinHostPort(srcIPs[0].String(), srcPort.String()))
}
targetPort := ctx.GetTargetPort()
if domain := ctx.GetTargetDomain(); domain != "" {
ev.Destination = ptr(net.JoinHostPort(domain, targetPort.String()))
} else if targetIPs := ctx.GetTargetIPs(); len(targetIPs) > 0 {
ev.Destination = ptr(net.JoinHostPort(targetIPs[0].String(), targetPort.String()))
}
if localIPs := ctx.GetLocalIPs(); len(localIPs) > 0 {
localPort := ctx.GetLocalPort()
ev.InboundLocal = ptr(net.JoinHostPort(localIPs[0].String(), localPort.String()))
}
if sctx, ok := ctx.(*routing_session.Context); ok {
enrichFromSession(ev, sctx)
}
return ev
}
func enrichFromSession(ev *event, sctx *routing_session.Context) {
if sctx.Inbound != nil {
ev.InboundName = ptr(sctx.Inbound.Name)
if sctx.Inbound.User != nil {
ev.Level = ptr(sctx.Inbound.User.Level)
}
}
if sctx.Outbound != nil {
if sctx.Outbound.OriginalTarget.Address != nil {
ev.OriginalTarget = ptr(sctx.Outbound.OriginalTarget.String())
}
if sctx.Outbound.RouteTarget.Address != nil {
ev.RouteTarget = ptr(sctx.Outbound.RouteTarget.String())
}
}
}
func (h *WebhookNotifier) post(ev *event) {
body, err := json.Marshal(ev)
if err != nil {
errors.LogWarning(context.Background(), "webhook: marshal failed: ", err)
return
}
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, h.url, bytes.NewReader(body))
if err != nil {
errors.LogWarning(context.Background(), "webhook: request build failed: ", err)
return
}
req.Header.Set("Content-Type", "application/json")
for k, v := range h.headers {
req.Header.Set(k, v)
}
resp, err := h.client.Do(req)
if err != nil {
errors.LogInfo(context.Background(), "webhook: POST failed: ", err)
return
}
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
if resp.StatusCode >= 400 {
errors.LogWarning(context.Background(), "webhook: POST returned status ", resp.StatusCode)
}
}
func (h *WebhookNotifier) isDuplicate(email string) bool {
if h.deduplication == 0 || email == "" {
return false
}
ttl := time.Duration(h.deduplication) * time.Second
now := time.Now()
if v, loaded := h.seen.LoadOrStore(email, now); loaded {
if now.Sub(v.(time.Time)) < ttl {
return true
}
h.seen.Store(email, now)
}
return false
}
func (h *WebhookNotifier) cleanupLoop() {
defer h.wg.Done()
ttl := time.Duration(h.deduplication) * time.Second
ticker := time.NewTicker(ttl)
defer ticker.Stop()
for {
select {
case <-h.done:
return
case <-ticker.C:
now := time.Now()
h.seen.Range(func(key, value any) bool {
if now.Sub(value.(time.Time)) >= ttl {
h.seen.Delete(key)
}
return true
})
}
}
}
func (h *WebhookNotifier) Close() error {
h.closeOnce.Do(func() {
close(h.done)
})
h.wg.Wait()
h.client.CloseIdleConnections()
return nil
}
+18 -83
View File
@@ -3,10 +3,12 @@ package command
import ( import (
"context" "context"
"runtime" "runtime"
"strings"
"time" "time"
"github.com/xtls/xray-core/app/stats"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
feature_stats "github.com/xtls/xray-core/features/stats" feature_stats "github.com/xtls/xray-core/features/stats"
grpc "google.golang.org/grpc" grpc "google.golang.org/grpc"
@@ -68,10 +70,9 @@ func (s *statsServer) GetStatsOnlineIpList(ctx context.Context, request *GetStat
} }
ips := make(map[string]int64) ips := make(map[string]int64)
c.ForEach(func(ip string, lastSeen int64) bool { for ip, t := range c.IpTimeMap() {
ips[ip] = lastSeen ips[ip] = t.Unix()
return true }
})
return &GetStatsOnlineIpListResponse{ return &GetStatsOnlineIpListResponse{
Name: request.Name, Name: request.Name,
@@ -85,87 +86,21 @@ func (s *statsServer) GetAllOnlineUsers(ctx context.Context, request *GetAllOnli
}, nil }, nil
} }
func (s *statsServer) GetUsersStats(ctx context.Context, request *GetUsersStatsRequest) (*GetUsersStatsResponse, error) {
userMap := make(map[string]*UserStat)
s.stats.VisitOnlineMaps(func(name string, om feature_stats.OnlineMap) bool {
if om.Count() == 0 {
return true
}
_, rest, _ := strings.Cut(name, ">>>")
email, _, _ := strings.Cut(rest, ">>>")
user := &UserStat{Email: email}
om.ForEach(func(ip string, lastSeen int64) bool {
user.Ips = append(user.Ips, &OnlineIPEntry{
Ip: ip,
LastSeen: lastSeen,
})
return true
})
if len(user.Ips) > 0 {
userMap[email] = user
}
return true
})
if request.IncludeTraffic {
for _, u := range userMap {
u.Traffic = &TrafficUserStat{}
}
const (
prefixUser = "user>>>"
suffixUplink = ">>>traffic>>>uplink"
suffixDownlink = ">>>traffic>>>downlink"
)
s.stats.VisitCounters(func(name string, c feature_stats.Counter) bool {
var email string
var isUplink bool
if strings.HasSuffix(name, suffixUplink) {
email = name[len(prefixUser) : len(name)-len(suffixUplink)]
isUplink = true
} else if strings.HasSuffix(name, suffixDownlink) {
email = name[len(prefixUser) : len(name)-len(suffixDownlink)]
} else {
return true
}
u, ok := userMap[email]
if !ok {
return true
}
var value int64
if request.Reset_ {
value = c.Set(0)
} else {
value = c.Value()
}
if isUplink {
u.Traffic.Uplink = value
} else {
u.Traffic.Downlink = value
}
return true
})
}
resp := &GetUsersStatsResponse{}
for _, u := range userMap {
resp.Users = append(resp.Users, u)
}
return resp, nil
}
func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) { func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) {
matcher, err := strmatcher.Substr.New(request.Pattern)
if err != nil {
return nil, err
}
response := &QueryStatsResponse{} response := &QueryStatsResponse{}
s.stats.VisitCounters(func(name string, c feature_stats.Counter) bool { manager, ok := s.stats.(*stats.Manager)
if strings.Contains(name, request.Pattern) { if !ok {
return nil, errors.New("QueryStats only works its own stats.Manager.")
}
manager.VisitCounters(func(name string, c feature_stats.Counter) bool {
if matcher.Match(name) {
var value int64 var value int64
if request.Reset_ { if request.Reset_ {
value = c.Set(0) value = c.Set(0)
+28 -314
View File
@@ -551,266 +551,6 @@ func (x *GetAllOnlineUsersResponse) GetUsers() []string {
return nil return nil
} }
type OnlineIPEntry struct {
state protoimpl.MessageState `protogen:"open.v1"`
Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
LastSeen int64 `protobuf:"varint,2,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *OnlineIPEntry) Reset() {
*x = OnlineIPEntry{}
mi := &file_app_stats_command_command_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *OnlineIPEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*OnlineIPEntry) ProtoMessage() {}
func (x *OnlineIPEntry) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use OnlineIPEntry.ProtoReflect.Descriptor instead.
func (*OnlineIPEntry) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{10}
}
func (x *OnlineIPEntry) GetIp() string {
if x != nil {
return x.Ip
}
return ""
}
func (x *OnlineIPEntry) GetLastSeen() int64 {
if x != nil {
return x.LastSeen
}
return 0
}
type TrafficUserStat struct {
state protoimpl.MessageState `protogen:"open.v1"`
Uplink int64 `protobuf:"varint,1,opt,name=uplink,proto3" json:"uplink,omitempty"`
Downlink int64 `protobuf:"varint,2,opt,name=downlink,proto3" json:"downlink,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *TrafficUserStat) Reset() {
*x = TrafficUserStat{}
mi := &file_app_stats_command_command_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *TrafficUserStat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TrafficUserStat) ProtoMessage() {}
func (x *TrafficUserStat) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TrafficUserStat.ProtoReflect.Descriptor instead.
func (*TrafficUserStat) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{11}
}
func (x *TrafficUserStat) GetUplink() int64 {
if x != nil {
return x.Uplink
}
return 0
}
func (x *TrafficUserStat) GetDownlink() int64 {
if x != nil {
return x.Downlink
}
return 0
}
type UserStat struct {
state protoimpl.MessageState `protogen:"open.v1"`
Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"`
Ips []*OnlineIPEntry `protobuf:"bytes,2,rep,name=ips,proto3" json:"ips,omitempty"`
Traffic *TrafficUserStat `protobuf:"bytes,3,opt,name=traffic,proto3" json:"traffic,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UserStat) Reset() {
*x = UserStat{}
mi := &file_app_stats_command_command_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UserStat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UserStat) ProtoMessage() {}
func (x *UserStat) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UserStat.ProtoReflect.Descriptor instead.
func (*UserStat) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{12}
}
func (x *UserStat) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
func (x *UserStat) GetIps() []*OnlineIPEntry {
if x != nil {
return x.Ips
}
return nil
}
func (x *UserStat) GetTraffic() *TrafficUserStat {
if x != nil {
return x.Traffic
}
return nil
}
type GetUsersStatsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
IncludeTraffic bool `protobuf:"varint,1,opt,name=include_traffic,json=includeTraffic,proto3" json:"include_traffic,omitempty"`
Reset_ bool `protobuf:"varint,2,opt,name=reset,proto3" json:"reset,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetUsersStatsRequest) Reset() {
*x = GetUsersStatsRequest{}
mi := &file_app_stats_command_command_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetUsersStatsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUsersStatsRequest) ProtoMessage() {}
func (x *GetUsersStatsRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetUsersStatsRequest.ProtoReflect.Descriptor instead.
func (*GetUsersStatsRequest) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{13}
}
func (x *GetUsersStatsRequest) GetIncludeTraffic() bool {
if x != nil {
return x.IncludeTraffic
}
return false
}
func (x *GetUsersStatsRequest) GetReset_() bool {
if x != nil {
return x.Reset_
}
return false
}
type GetUsersStatsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Users []*UserStat `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetUsersStatsResponse) Reset() {
*x = GetUsersStatsResponse{}
mi := &file_app_stats_command_command_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetUsersStatsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUsersStatsResponse) ProtoMessage() {}
func (x *GetUsersStatsResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetUsersStatsResponse.ProtoReflect.Descriptor instead.
func (*GetUsersStatsResponse) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{14}
}
func (x *GetUsersStatsResponse) GetUsers() []*UserStat {
if x != nil {
return x.Users
}
return nil
}
type Config struct { type Config struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@@ -819,7 +559,7 @@ type Config struct {
func (x *Config) Reset() { func (x *Config) Reset() {
*x = Config{} *x = Config{}
mi := &file_app_stats_command_command_proto_msgTypes[15] mi := &file_app_stats_command_command_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -831,7 +571,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {} func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message { func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[15] mi := &file_app_stats_command_command_proto_msgTypes[10]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -844,7 +584,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead. // Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) { func (*Config) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{15} return file_app_stats_command_command_proto_rawDescGZIP(), []int{10}
} }
var File_app_stats_command_command_proto protoreflect.FileDescriptor var File_app_stats_command_command_proto protoreflect.FileDescriptor
@@ -888,23 +628,8 @@ const file_app_stats_command_command_proto_rawDesc = "" +
"\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\"\x1a\n" + "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\"\x1a\n" +
"\x18GetAllOnlineUsersRequest\"1\n" + "\x18GetAllOnlineUsersRequest\"1\n" +
"\x19GetAllOnlineUsersResponse\x12\x14\n" + "\x19GetAllOnlineUsersResponse\x12\x14\n" +
"\x05users\x18\x01 \x03(\tR\x05users\"<\n" + "\x05users\x18\x01 \x03(\tR\x05users\"\b\n" +
"\rOnlineIPEntry\x12\x0e\n" + "\x06Config2\x96\x05\n" +
"\x02ip\x18\x01 \x01(\tR\x02ip\x12\x1b\n" +
"\tlast_seen\x18\x02 \x01(\x03R\blastSeen\"E\n" +
"\x0fTrafficUserStat\x12\x16\n" +
"\x06uplink\x18\x01 \x01(\x03R\x06uplink\x12\x1a\n" +
"\bdownlink\x18\x02 \x01(\x03R\bdownlink\"\x9c\x01\n" +
"\bUserStat\x12\x14\n" +
"\x05email\x18\x01 \x01(\tR\x05email\x127\n" +
"\x03ips\x18\x02 \x03(\v2%.xray.app.stats.command.OnlineIPEntryR\x03ips\x12A\n" +
"\atraffic\x18\x03 \x01(\v2'.xray.app.stats.command.TrafficUserStatR\atraffic\"U\n" +
"\x14GetUsersStatsRequest\x12'\n" +
"\x0finclude_traffic\x18\x01 \x01(\bR\x0eincludeTraffic\x12\x14\n" +
"\x05reset\x18\x02 \x01(\bR\x05reset\"O\n" +
"\x15GetUsersStatsResponse\x126\n" +
"\x05users\x18\x01 \x03(\v2 .xray.app.stats.command.UserStatR\x05users\"\b\n" +
"\x06Config2\x86\x06\n" +
"\fStatsService\x12_\n" + "\fStatsService\x12_\n" +
"\bGetStats\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" + "\bGetStats\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" +
"\x0eGetStatsOnline\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" + "\x0eGetStatsOnline\x12'.xray.app.stats.command.GetStatsRequest\x1a(.xray.app.stats.command.GetStatsResponse\"\x00\x12e\n" +
@@ -912,8 +637,7 @@ const file_app_stats_command_command_proto_rawDesc = "" +
"QueryStats\x12).xray.app.stats.command.QueryStatsRequest\x1a*.xray.app.stats.command.QueryStatsResponse\"\x00\x12b\n" + "QueryStats\x12).xray.app.stats.command.QueryStatsRequest\x1a*.xray.app.stats.command.QueryStatsResponse\"\x00\x12b\n" +
"\vGetSysStats\x12'.xray.app.stats.command.SysStatsRequest\x1a(.xray.app.stats.command.SysStatsResponse\"\x00\x12w\n" + "\vGetSysStats\x12'.xray.app.stats.command.SysStatsRequest\x1a(.xray.app.stats.command.SysStatsResponse\"\x00\x12w\n" +
"\x14GetStatsOnlineIpList\x12'.xray.app.stats.command.GetStatsRequest\x1a4.xray.app.stats.command.GetStatsOnlineIpListResponse\"\x00\x12z\n" + "\x14GetStatsOnlineIpList\x12'.xray.app.stats.command.GetStatsRequest\x1a4.xray.app.stats.command.GetStatsOnlineIpListResponse\"\x00\x12z\n" +
"\x11GetAllOnlineUsers\x120.xray.app.stats.command.GetAllOnlineUsersRequest\x1a1.xray.app.stats.command.GetAllOnlineUsersResponse\"\x00\x12n\n" + "\x11GetAllOnlineUsers\x120.xray.app.stats.command.GetAllOnlineUsersRequest\x1a1.xray.app.stats.command.GetAllOnlineUsersResponse\"\x00Bd\n" +
"\rGetUsersStats\x12,.xray.app.stats.command.GetUsersStatsRequest\x1a-.xray.app.stats.command.GetUsersStatsResponse\"\x00Bd\n" +
"\x1acom.xray.app.stats.commandP\x01Z+github.com/xtls/xray-core/app/stats/command\xaa\x02\x16Xray.App.Stats.Commandb\x06proto3" "\x1acom.xray.app.stats.commandP\x01Z+github.com/xtls/xray-core/app/stats/command\xaa\x02\x16Xray.App.Stats.Commandb\x06proto3"
var ( var (
@@ -928,7 +652,7 @@ func file_app_stats_command_command_proto_rawDescGZIP() []byte {
return file_app_stats_command_command_proto_rawDescData return file_app_stats_command_command_proto_rawDescData
} }
var file_app_stats_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 17) var file_app_stats_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_app_stats_command_command_proto_goTypes = []any{ var file_app_stats_command_command_proto_goTypes = []any{
(*GetStatsRequest)(nil), // 0: xray.app.stats.command.GetStatsRequest (*GetStatsRequest)(nil), // 0: xray.app.stats.command.GetStatsRequest
(*Stat)(nil), // 1: xray.app.stats.command.Stat (*Stat)(nil), // 1: xray.app.stats.command.Stat
@@ -940,40 +664,30 @@ var file_app_stats_command_command_proto_goTypes = []any{
(*GetStatsOnlineIpListResponse)(nil), // 7: xray.app.stats.command.GetStatsOnlineIpListResponse (*GetStatsOnlineIpListResponse)(nil), // 7: xray.app.stats.command.GetStatsOnlineIpListResponse
(*GetAllOnlineUsersRequest)(nil), // 8: xray.app.stats.command.GetAllOnlineUsersRequest (*GetAllOnlineUsersRequest)(nil), // 8: xray.app.stats.command.GetAllOnlineUsersRequest
(*GetAllOnlineUsersResponse)(nil), // 9: xray.app.stats.command.GetAllOnlineUsersResponse (*GetAllOnlineUsersResponse)(nil), // 9: xray.app.stats.command.GetAllOnlineUsersResponse
(*OnlineIPEntry)(nil), // 10: xray.app.stats.command.OnlineIPEntry (*Config)(nil), // 10: xray.app.stats.command.Config
(*TrafficUserStat)(nil), // 11: xray.app.stats.command.TrafficUserStat nil, // 11: xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry
(*UserStat)(nil), // 12: xray.app.stats.command.UserStat
(*GetUsersStatsRequest)(nil), // 13: xray.app.stats.command.GetUsersStatsRequest
(*GetUsersStatsResponse)(nil), // 14: xray.app.stats.command.GetUsersStatsResponse
(*Config)(nil), // 15: xray.app.stats.command.Config
nil, // 16: xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry
} }
var file_app_stats_command_command_proto_depIdxs = []int32{ var file_app_stats_command_command_proto_depIdxs = []int32{
1, // 0: xray.app.stats.command.GetStatsResponse.stat:type_name -> xray.app.stats.command.Stat 1, // 0: xray.app.stats.command.GetStatsResponse.stat:type_name -> xray.app.stats.command.Stat
1, // 1: xray.app.stats.command.QueryStatsResponse.stat:type_name -> xray.app.stats.command.Stat 1, // 1: xray.app.stats.command.QueryStatsResponse.stat:type_name -> xray.app.stats.command.Stat
16, // 2: xray.app.stats.command.GetStatsOnlineIpListResponse.ips:type_name -> xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry 11, // 2: xray.app.stats.command.GetStatsOnlineIpListResponse.ips:type_name -> xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry
10, // 3: xray.app.stats.command.UserStat.ips:type_name -> xray.app.stats.command.OnlineIPEntry 0, // 3: xray.app.stats.command.StatsService.GetStats:input_type -> xray.app.stats.command.GetStatsRequest
11, // 4: xray.app.stats.command.UserStat.traffic:type_name -> xray.app.stats.command.TrafficUserStat 0, // 4: xray.app.stats.command.StatsService.GetStatsOnline:input_type -> xray.app.stats.command.GetStatsRequest
12, // 5: xray.app.stats.command.GetUsersStatsResponse.users:type_name -> xray.app.stats.command.UserStat 3, // 5: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest
0, // 6: xray.app.stats.command.StatsService.GetStats:input_type -> xray.app.stats.command.GetStatsRequest 5, // 6: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest
0, // 7: xray.app.stats.command.StatsService.GetStatsOnline:input_type -> xray.app.stats.command.GetStatsRequest 0, // 7: xray.app.stats.command.StatsService.GetStatsOnlineIpList:input_type -> xray.app.stats.command.GetStatsRequest
3, // 8: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest 8, // 8: xray.app.stats.command.StatsService.GetAllOnlineUsers:input_type -> xray.app.stats.command.GetAllOnlineUsersRequest
5, // 9: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest 2, // 9: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse
0, // 10: xray.app.stats.command.StatsService.GetStatsOnlineIpList:input_type -> xray.app.stats.command.GetStatsRequest 2, // 10: xray.app.stats.command.StatsService.GetStatsOnline:output_type -> xray.app.stats.command.GetStatsResponse
8, // 11: xray.app.stats.command.StatsService.GetAllOnlineUsers:input_type -> xray.app.stats.command.GetAllOnlineUsersRequest 4, // 11: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse
13, // 12: xray.app.stats.command.StatsService.GetUsersStats:input_type -> xray.app.stats.command.GetUsersStatsRequest 6, // 12: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse
2, // 13: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse 7, // 13: xray.app.stats.command.StatsService.GetStatsOnlineIpList:output_type -> xray.app.stats.command.GetStatsOnlineIpListResponse
2, // 14: xray.app.stats.command.StatsService.GetStatsOnline:output_type -> xray.app.stats.command.GetStatsResponse 9, // 14: xray.app.stats.command.StatsService.GetAllOnlineUsers:output_type -> xray.app.stats.command.GetAllOnlineUsersResponse
4, // 15: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse 9, // [9:15] is the sub-list for method output_type
6, // 16: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse 3, // [3:9] is the sub-list for method input_type
7, // 17: xray.app.stats.command.StatsService.GetStatsOnlineIpList:output_type -> xray.app.stats.command.GetStatsOnlineIpListResponse 3, // [3:3] is the sub-list for extension type_name
9, // 18: xray.app.stats.command.StatsService.GetAllOnlineUsers:output_type -> xray.app.stats.command.GetAllOnlineUsersResponse 3, // [3:3] is the sub-list for extension extendee
14, // 19: xray.app.stats.command.StatsService.GetUsersStats:output_type -> xray.app.stats.command.GetUsersStatsResponse 0, // [0:3] is the sub-list for field type_name
13, // [13:20] is the sub-list for method output_type
6, // [6:13] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
} }
func init() { file_app_stats_command_command_proto_init() } func init() { file_app_stats_command_command_proto_init() }
@@ -987,7 +701,7 @@ func file_app_stats_command_command_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_stats_command_command_proto_rawDesc), len(file_app_stats_command_command_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_stats_command_command_proto_rawDesc), len(file_app_stats_command_command_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 17, NumMessages: 12,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
-26
View File
@@ -57,31 +57,6 @@ message GetAllOnlineUsersResponse {
repeated string users = 1; repeated string users = 1;
} }
message OnlineIPEntry {
string ip = 1;
int64 last_seen = 2;
}
message TrafficUserStat {
int64 uplink = 1;
int64 downlink = 2;
}
message UserStat {
string email = 1;
repeated OnlineIPEntry ips = 2;
TrafficUserStat traffic = 3;
}
message GetUsersStatsRequest {
bool include_traffic = 1;
bool reset = 2;
}
message GetUsersStatsResponse {
repeated UserStat users = 1;
}
service StatsService { service StatsService {
rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {} rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {}
rpc GetStatsOnline(GetStatsRequest) returns (GetStatsResponse) {} rpc GetStatsOnline(GetStatsRequest) returns (GetStatsResponse) {}
@@ -89,7 +64,6 @@ service StatsService {
rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {} rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {}
rpc GetStatsOnlineIpList(GetStatsRequest) returns (GetStatsOnlineIpListResponse) {} rpc GetStatsOnlineIpList(GetStatsRequest) returns (GetStatsOnlineIpListResponse) {}
rpc GetAllOnlineUsers(GetAllOnlineUsersRequest) returns (GetAllOnlineUsersResponse) {} rpc GetAllOnlineUsers(GetAllOnlineUsersRequest) returns (GetAllOnlineUsersResponse) {}
rpc GetUsersStats(GetUsersStatsRequest) returns (GetUsersStatsResponse) {}
} }
message Config {} message Config {}
-38
View File
@@ -25,7 +25,6 @@ const (
StatsService_GetSysStats_FullMethodName = "/xray.app.stats.command.StatsService/GetSysStats" StatsService_GetSysStats_FullMethodName = "/xray.app.stats.command.StatsService/GetSysStats"
StatsService_GetStatsOnlineIpList_FullMethodName = "/xray.app.stats.command.StatsService/GetStatsOnlineIpList" StatsService_GetStatsOnlineIpList_FullMethodName = "/xray.app.stats.command.StatsService/GetStatsOnlineIpList"
StatsService_GetAllOnlineUsers_FullMethodName = "/xray.app.stats.command.StatsService/GetAllOnlineUsers" StatsService_GetAllOnlineUsers_FullMethodName = "/xray.app.stats.command.StatsService/GetAllOnlineUsers"
StatsService_GetUsersStats_FullMethodName = "/xray.app.stats.command.StatsService/GetUsersStats"
) )
// StatsServiceClient is the client API for StatsService service. // StatsServiceClient is the client API for StatsService service.
@@ -38,7 +37,6 @@ type StatsServiceClient interface {
GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error) GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error)
GetStatsOnlineIpList(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsOnlineIpListResponse, error) GetStatsOnlineIpList(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsOnlineIpListResponse, error)
GetAllOnlineUsers(ctx context.Context, in *GetAllOnlineUsersRequest, opts ...grpc.CallOption) (*GetAllOnlineUsersResponse, error) GetAllOnlineUsers(ctx context.Context, in *GetAllOnlineUsersRequest, opts ...grpc.CallOption) (*GetAllOnlineUsersResponse, error)
GetUsersStats(ctx context.Context, in *GetUsersStatsRequest, opts ...grpc.CallOption) (*GetUsersStatsResponse, error)
} }
type statsServiceClient struct { type statsServiceClient struct {
@@ -109,16 +107,6 @@ func (c *statsServiceClient) GetAllOnlineUsers(ctx context.Context, in *GetAllOn
return out, nil return out, nil
} }
func (c *statsServiceClient) GetUsersStats(ctx context.Context, in *GetUsersStatsRequest, opts ...grpc.CallOption) (*GetUsersStatsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetUsersStatsResponse)
err := c.cc.Invoke(ctx, StatsService_GetUsersStats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// StatsServiceServer is the server API for StatsService service. // StatsServiceServer is the server API for StatsService service.
// All implementations must embed UnimplementedStatsServiceServer // All implementations must embed UnimplementedStatsServiceServer
// for forward compatibility. // for forward compatibility.
@@ -129,7 +117,6 @@ type StatsServiceServer interface {
GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error)
GetStatsOnlineIpList(context.Context, *GetStatsRequest) (*GetStatsOnlineIpListResponse, error) GetStatsOnlineIpList(context.Context, *GetStatsRequest) (*GetStatsOnlineIpListResponse, error)
GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error)
GetUsersStats(context.Context, *GetUsersStatsRequest) (*GetUsersStatsResponse, error)
mustEmbedUnimplementedStatsServiceServer() mustEmbedUnimplementedStatsServiceServer()
} }
@@ -158,9 +145,6 @@ func (UnimplementedStatsServiceServer) GetStatsOnlineIpList(context.Context, *Ge
func (UnimplementedStatsServiceServer) GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) { func (UnimplementedStatsServiceServer) GetAllOnlineUsers(context.Context, *GetAllOnlineUsersRequest) (*GetAllOnlineUsersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAllOnlineUsers not implemented") return nil, status.Error(codes.Unimplemented, "method GetAllOnlineUsers not implemented")
} }
func (UnimplementedStatsServiceServer) GetUsersStats(context.Context, *GetUsersStatsRequest) (*GetUsersStatsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetUsersStats not implemented")
}
func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {} func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {}
func (UnimplementedStatsServiceServer) testEmbeddedByValue() {} func (UnimplementedStatsServiceServer) testEmbeddedByValue() {}
@@ -290,24 +274,6 @@ func _StatsService_GetAllOnlineUsers_Handler(srv interface{}, ctx context.Contex
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _StatsService_GetUsersStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUsersStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StatsServiceServer).GetUsersStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StatsService_GetUsersStats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StatsServiceServer).GetUsersStats(ctx, req.(*GetUsersStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
// StatsService_ServiceDesc is the grpc.ServiceDesc for StatsService service. // StatsService_ServiceDesc is the grpc.ServiceDesc for StatsService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -339,10 +305,6 @@ var StatsService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetAllOnlineUsers", MethodName: "GetAllOnlineUsers",
Handler: _StatsService_GetAllOnlineUsers_Handler, Handler: _StatsService_GetAllOnlineUsers_Handler,
}, },
{
MethodName: "GetUsersStats",
Handler: _StatsService_GetUsersStats_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "app/stats/command/command.proto", Metadata: "app/stats/command/command.proto",
+64 -66
View File
@@ -2,86 +2,84 @@ package stats
import ( import (
"sync" "sync"
"sync/atomic"
"time" "time"
) )
const ( // OnlineMap is an implementation of stats.OnlineMap.
localhostIPv4 = "127.0.0.1"
localhostIPv6 = "[::1]"
)
type ipEntry struct {
refCount int
lastSeen int64
}
// OnlineMap is a refcount-based implementation of stats.OnlineMap.
// IPs are tracked by reference counting: AddIP increments, RemoveIP decrements.
// An IP is removed from the map when its reference count reaches zero.
type OnlineMap struct { type OnlineMap struct {
entries map[string]ipEntry ipList map[string]time.Time
access sync.Mutex access sync.RWMutex
count atomic.Int64 lastCleanup time.Time
cleanupPeriod time.Duration
} }
// NewOnlineMap creates a new OnlineMap instance. // NewOnlineMap creates a new instance of OnlineMap.
func NewOnlineMap() *OnlineMap { func NewOnlineMap() *OnlineMap {
return &OnlineMap{ return &OnlineMap{
entries: make(map[string]ipEntry), ipList: make(map[string]time.Time),
} lastCleanup: time.Now(),
} cleanupPeriod: 10 * time.Second,
// AddIP implements stats.OnlineMap.
func (om *OnlineMap) AddIP(ip string) {
if ip == localhostIPv4 || ip == localhostIPv6 {
return
}
now := time.Now().Unix()
om.access.Lock()
defer om.access.Unlock()
if e, ok := om.entries[ip]; ok {
e.refCount++
e.lastSeen = now
om.entries[ip] = e
} else {
om.entries[ip] = ipEntry{
refCount: 1,
lastSeen: now,
}
om.count.Add(1)
}
}
// RemoveIP implements stats.OnlineMap.
func (om *OnlineMap) RemoveIP(ip string) {
om.access.Lock()
defer om.access.Unlock()
e, ok := om.entries[ip]
if !ok {
return
}
e.refCount--
if e.refCount <= 0 {
delete(om.entries, ip)
om.count.Add(-1)
} else {
om.entries[ip] = e
} }
} }
// Count implements stats.OnlineMap. // Count implements stats.OnlineMap.
func (om *OnlineMap) Count() int { func (c *OnlineMap) Count() int {
return int(om.count.Load()) c.access.RLock()
defer c.access.RUnlock()
return len(c.ipList)
} }
// ForEach calls fn for each online IP. If fn returns false, iteration stops. // List implements stats.OnlineMap.
func (om *OnlineMap) ForEach(fn func(string, int64) bool) { func (c *OnlineMap) List() []string {
om.access.Lock() return c.GetKeys()
defer om.access.Unlock() }
for ip, e := range om.entries {
if !fn(ip, e.lastSeen) { // AddIP implements stats.OnlineMap.
break func (c *OnlineMap) AddIP(ip string) {
if ip == "127.0.0.1" {
return
}
c.access.Lock()
c.ipList[ip] = time.Now()
c.access.Unlock()
if time.Since(c.lastCleanup) > c.cleanupPeriod {
c.RemoveExpiredIPs()
c.lastCleanup = time.Now()
}
}
func (c *OnlineMap) GetKeys() []string {
c.access.RLock()
defer c.access.RUnlock()
keys := []string{}
for k := range c.ipList {
keys = append(keys, k)
}
return keys
}
func (c *OnlineMap) RemoveExpiredIPs() {
c.access.Lock()
defer c.access.Unlock()
now := time.Now()
for k, t := range c.ipList {
diff := now.Sub(t)
if diff.Seconds() > 20 {
delete(c.ipList, k)
} }
} }
} }
func (c *OnlineMap) IpTimeMap() map[string]time.Time {
if time.Since(c.lastCleanup) > c.cleanupPeriod {
c.RemoveExpiredIPs()
c.lastCleanup = time.Now()
}
return c.ipList
}
+21 -37
View File
@@ -11,19 +11,19 @@ import (
// Manager is an implementation of stats.Manager. // Manager is an implementation of stats.Manager.
type Manager struct { type Manager struct {
access sync.RWMutex access sync.RWMutex
counters map[string]*Counter counters map[string]*Counter
onlineMaps map[string]*OnlineMap onlineMap map[string]*OnlineMap
channels map[string]*Channel channels map[string]*Channel
running bool running bool
} }
// NewManager creates an instance of Statistics Manager. // NewManager creates an instance of Statistics Manager.
func NewManager(ctx context.Context, config *Config) (*Manager, error) { func NewManager(ctx context.Context, config *Config) (*Manager, error) {
m := &Manager{ m := &Manager{
counters: make(map[string]*Counter), counters: make(map[string]*Counter),
onlineMaps: make(map[string]*OnlineMap), onlineMap: make(map[string]*OnlineMap),
channels: make(map[string]*Channel), channels: make(map[string]*Channel),
} }
return m, nil return m, nil
@@ -88,12 +88,12 @@ func (m *Manager) RegisterOnlineMap(name string) (stats.OnlineMap, error) {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
if _, found := m.onlineMaps[name]; found { if _, found := m.onlineMap[name]; found {
return nil, errors.New("OnlineMap ", name, " already registered.") return nil, errors.New("onlineMap ", name, " already registered.")
} }
errors.LogDebug(context.Background(), "create new OnlineMap ", name) errors.LogDebug(context.Background(), "create new onlineMap ", name)
om := NewOnlineMap() om := NewOnlineMap()
m.onlineMaps[name] = om m.onlineMap[name] = om
return om, nil return om, nil
} }
@@ -102,9 +102,9 @@ func (m *Manager) UnregisterOnlineMap(name string) error {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
if _, found := m.onlineMaps[name]; found { if _, found := m.onlineMap[name]; found {
errors.LogDebug(context.Background(), "remove OnlineMap ", name) errors.LogDebug(context.Background(), "remove onlineMap ", name)
delete(m.onlineMaps, name) delete(m.onlineMap, name)
} }
return nil return nil
} }
@@ -114,24 +114,12 @@ func (m *Manager) GetOnlineMap(name string) stats.OnlineMap {
m.access.RLock() m.access.RLock()
defer m.access.RUnlock() defer m.access.RUnlock()
if om, found := m.onlineMaps[name]; found { if om, found := m.onlineMap[name]; found {
return om return om
} }
return nil return nil
} }
// VisitOnlineMaps calls visitor function on all managed online maps.
// The visitor runs under a read lock; it must not call RegisterOnlineMap or UnregisterOnlineMap (would deadlock).
func (m *Manager) VisitOnlineMaps(visitor func(string, stats.OnlineMap) bool) {
m.access.RLock()
defer m.access.RUnlock()
for name, om := range m.onlineMaps {
if !visitor(name, om) {
break
}
}
}
// RegisterChannel implements stats.Manager. // RegisterChannel implements stats.Manager.
func (m *Manager) RegisterChannel(name string) (stats.Channel, error) { func (m *Manager) RegisterChannel(name string) (stats.Channel, error) {
m.access.Lock() m.access.Lock()
@@ -175,12 +163,12 @@ func (m *Manager) GetChannel(name string) stats.Channel {
// GetAllOnlineUsers implements stats.Manager. // GetAllOnlineUsers implements stats.Manager.
func (m *Manager) GetAllOnlineUsers() []string { func (m *Manager) GetAllOnlineUsers() []string {
m.access.RLock() m.access.Lock()
defer m.access.RUnlock() defer m.access.Unlock()
usersOnline := make([]string, 0, len(m.onlineMaps)) usersOnline := make([]string, 0, len(m.onlineMap))
for user, om := range m.onlineMaps { for user, onlineMap := range m.onlineMap {
if om.Count() > 0 { if len(onlineMap.IpTimeMap()) > 0 {
usersOnline = append(usersOnline, user) usersOnline = append(usersOnline, user)
} }
} }
@@ -210,10 +198,6 @@ func (m *Manager) Close() error {
m.access.Lock() m.access.Lock()
defer m.access.Unlock() defer m.access.Unlock()
m.running = false m.running = false
for name := range m.onlineMaps {
errors.LogDebug(context.Background(), "remove OnlineMap ", name)
delete(m.onlineMaps, name)
}
errs := []error{} errs := []error{}
for name, channel := range m.channels { for name, channel := range m.channels {
errors.LogDebug(context.Background(), "remove channel ", name) errors.LogDebug(context.Background(), "remove channel ", name)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
) )
func TestInterface(t *testing.T) { func TestInterface(t *testing.T) {
_ = stats.Manager(new(Manager)) _ = (stats.Manager)(new(Manager))
} }
func TestStatsChannelRunnable(t *testing.T) { func TestStatsChannelRunnable(t *testing.T) {
+2 -3
View File
@@ -2,11 +2,10 @@ package version
import ( import (
"context" "context"
"strconv"
"strings"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"strconv"
"strings"
) )
type Version struct { type Version struct {
+8
View File
@@ -56,6 +56,10 @@ type readError struct {
error error
} }
func NewReadError(err error) error {
return readError{err}
}
func (e readError) Error() string { func (e readError) Error() string {
return e.error.Error() return e.error.Error()
} }
@@ -74,6 +78,10 @@ type writeError struct {
error error
} }
func NewWriteError(err error) error {
return writeError{err}
}
func (e writeError) Error() string { func (e writeError) Error() string {
return e.error.Error() return e.error.Error()
} }
+2 -2
View File
@@ -38,8 +38,8 @@ func MergeMulti(dest MultiBuffer, src MultiBuffer) (MultiBuffer, MultiBuffer) {
// MergeBytes merges the given bytes into MultiBuffer and return the new address of the merged MultiBuffer. // MergeBytes merges the given bytes into MultiBuffer and return the new address of the merged MultiBuffer.
func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer { func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer {
n := len(dest) n := len(dest)
if n > 0 && !dest[n-1].IsFull() { if n > 0 && !(dest)[n-1].IsFull() {
nBytes, _ := dest[n-1].Write(src) nBytes, _ := (dest)[n-1].Write(src)
src = src[nBytes:] src = src[nBytes:]
} }
+6 -6
View File
@@ -121,11 +121,11 @@ func TestPacketReader_ReadMultiBuffer(t *testing.T) {
} }
func TestReaderInterface(t *testing.T) { func TestReaderInterface(t *testing.T) {
_ = io.Reader(new(ReadVReader)) _ = (io.Reader)(new(ReadVReader))
_ = Reader(new(ReadVReader)) _ = (Reader)(new(ReadVReader))
_ = Reader(new(BufferedReader)) _ = (Reader)(new(BufferedReader))
_ = io.Reader(new(BufferedReader)) _ = (io.Reader)(new(BufferedReader))
_ = io.ByteReader(new(BufferedReader)) _ = (io.ByteReader)(new(BufferedReader))
_ = io.WriterTo(new(BufferedReader)) _ = (io.WriterTo)(new(BufferedReader))
} }
+3 -3
View File
@@ -1,5 +1,5 @@
//go:build !windows && !wasm && !illumos && !openbsd //go:build !windows && !wasm && !illumos
// +build !windows,!wasm,!illumos,!openbsd // +build !windows,!wasm,!illumos
package buf package buf
@@ -19,7 +19,7 @@ func (r *posixReader) Init(bs []*Buffer) {
} }
for idx, b := range bs { for idx, b := range bs {
iovecs = append(iovecs, syscall.Iovec{ iovecs = append(iovecs, syscall.Iovec{
Base: &b.v[0], Base: &(b.v[0]),
}) })
iovecs[idx].SetLen(int(Size)) iovecs[idx].SetLen(int(Size))
} }
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build !wasm && !openbsd //go:build !wasm
// +build !wasm,!openbsd // +build !wasm
package buf package buf
-17
View File
@@ -1,17 +0,0 @@
//go:build wasm || openbsd
// +build wasm openbsd
package buf
import (
"io"
"syscall"
"github.com/xtls/xray-core/features/stats"
)
const useReadv = false
func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader {
panic("not implemented")
}
+15
View File
@@ -0,0 +1,15 @@
//go:build wasm
// +build wasm
package buf
import (
"io"
"syscall"
)
const useReadv = false
func NewReadVReader(reader io.Reader, rawConn syscall.RawConn) Reader {
panic("not implemented")
}
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build !wasm && !openbsd //go:build !wasm
// +build !wasm,!openbsd // +build !wasm
package buf_test package buf_test
-20
View File
@@ -4,11 +4,8 @@ package crypto // import "github.com/xtls/xray-core/common/crypto"
import ( import (
"crypto/rand" "crypto/rand"
"math/big" "math/big"
"github.com/xtls/xray-core/common"
) )
// [,)
func RandBetween(from int64, to int64) int64 { func RandBetween(from int64, to int64) int64 {
if from == to { if from == to {
return from return from
@@ -19,20 +16,3 @@ func RandBetween(from int64, to int64) int64 {
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from)) bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
return from + bigInt.Int64() return from + bigInt.Int64()
} }
// [,]
func RandBytesBetween(b []byte, from, to byte) {
common.Must2(rand.Read(b))
if from > to {
from, to = to, from
}
if to-from == 255 {
return
}
for i := range b {
b[i] = from + b[i]%(to-from+1)
}
}
-238
View File
@@ -1,238 +0,0 @@
package geodata
import (
"context"
"runtime"
"strings"
"sync"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata/strmatcher"
"github.com/xtls/xray-core/common/utils"
)
type DomainMatcher interface {
// Match returns the indices of all rules that match the input domain.
// The returned slice is owned by the caller and may be safely modified.
// Note: the slice may contain duplicates and the order is unspecified.
Match(input string) []uint32
MatchAny(input string) bool
}
type DomainMatcherFactory interface {
BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
}
type MphDomainMatcherFactory struct {
sync.Mutex
shared *utils.WeakCacheMap[string, strmatcher.MphValueMatcher]
}
func buildDomainRulesKey(rules []*DomainRule) string {
var sb strings.Builder
cache := false
for _, r := range rules {
switch v := r.Value.(type) {
case *DomainRule_Custom:
sb.WriteString(v.Custom.Type.String())
sb.WriteString(":")
sb.WriteString(v.Custom.Value)
sb.WriteString(",")
case *DomainRule_Geosite:
cache = true
sb.WriteString(v.Geosite.File)
sb.WriteString(":")
sb.WriteString(v.Geosite.Code)
sb.WriteString("@")
sb.WriteString(v.Geosite.Attrs)
sb.WriteString(",")
default:
panic("unknown domain rule type")
}
}
if !cache {
return ""
}
return sb.String()
}
// BuildMatcher implements DomainMatcherFactory.
func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
key := buildDomainRulesKey(rules)
if key != "" {
f.Lock()
defer f.Unlock()
if g, ok := f.shared.Load(key); ok {
errors.LogDebug(context.Background(), "geodata mph domain matcher cache HIT for ", len(rules), " rules")
return g, nil
}
errors.LogDebug(context.Background(), "geodata mph domain matcher cache MISS for ", len(rules), " rules")
}
g := strmatcher.NewMphValueMatcher()
for i, r := range rules {
switch v := r.Value.(type) {
case *DomainRule_Custom:
m, err := parseDomain(v.Custom)
if err != nil {
return nil, err
}
g.Add(m, uint32(i))
case *DomainRule_Geosite:
domains, err := loadSiteWithAttrs(v.Geosite.File, v.Geosite.Code, v.Geosite.Attrs)
if err != nil {
return nil, err
}
for j, d := range domains {
domains[j] = nil // peak mem
m, err := parseDomain(d)
if err != nil {
errors.LogError(context.Background(), "ignore invalid geosite entry in ", v.Geosite.File, ":", v.Geosite.Code, " at index ", j, ", ", err)
continue
}
g.Add(m, uint32(i))
}
default:
panic("unknown domain rule type")
}
}
if err := g.Build(); err != nil {
return nil, err
}
if key != "" {
f.shared.Store(key, g)
}
return g, nil
}
type CompactDomainMatcherFactory struct {
sync.Mutex
shared *utils.WeakCacheMap[string, strmatcher.LinearAnyMatcher]
}
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
key := rule.File + ":" + rule.Code + "@" + rule.Attrs
f.Lock()
defer f.Unlock()
if s, ok := f.shared.Load(key); ok {
errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key)
return s, nil
}
errors.LogDebug(context.Background(), "geodata geosite matcher cache MISS ", key)
s := strmatcher.NewLinearAnyMatcher()
domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
if err != nil {
return nil, err
}
for i, d := range domains {
domains[i] = nil // peak mem
m, err := parseDomain(d)
if err != nil {
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
continue
}
s.Add(m)
}
f.shared.Store(key, s)
return s, err
}
// BuildMatcher implements DomainMatcherFactory.
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
compact := &CompactDomainMatcher{
matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
values: make([]uint32, 0, len(rules)),
}
for i, r := range rules {
switch v := r.Value.(type) {
case *DomainRule_Custom:
m, err := parseDomain(v.Custom)
if err != nil {
return nil, err
}
if compact.custom == nil {
compact.custom = strmatcher.NewLinearValueMatcher()
}
compact.custom.Add(m, uint32(i))
case *DomainRule_Geosite:
m, err := f.getOrCreateFrom(v.Geosite)
if err != nil {
return nil, err
}
compact.matchers = append(compact.matchers, m)
compact.values = append(compact.values, uint32(i))
default:
panic("unknown domain rule type")
}
}
return compact, nil
}
type CompactDomainMatcher struct {
custom strmatcher.ValueMatcher
matchers []strmatcher.MatcherSet
values []uint32
}
// Match implements DomainMatcher.
func (c *CompactDomainMatcher) Match(input string) []uint32 {
var result []uint32
if c.custom != nil {
result = append(result, c.custom.Match(input)...)
}
for i, m := range c.matchers {
if m.MatchAny(input) {
result = append(result, c.values[i])
}
}
return result
}
// MatchAny implements DomainMatcher.
func (c *CompactDomainMatcher) MatchAny(input string) bool {
if c.custom != nil && c.custom.MatchAny(input) {
return true
}
for _, m := range c.matchers {
if m.MatchAny(input) {
return true
}
}
return false
}
func parseDomain(d *Domain) (strmatcher.Matcher, error) {
if d == nil {
return nil, errors.New("domain must not be nil")
}
switch d.Type {
case Domain_Substr:
return strmatcher.Substr.New(strings.ToLower(d.Value))
case Domain_Regex:
return strmatcher.Regex.New(d.Value)
case Domain_Domain:
return strmatcher.Domain.New(d.Value)
case Domain_Full:
return strmatcher.Full.New(strings.ToLower(d.Value))
default:
return nil, errors.New("unknown domain type: ", d.Type)
}
}
func newDomainMatcherFactory() DomainMatcherFactory {
switch runtime.GOOS {
case "ios", "android":
return &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
default:
return &MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}
}
}
-74
View File
@@ -1,74 +0,0 @@
package geodata
import (
"path/filepath"
"reflect"
"slices"
"testing"
"github.com/xtls/xray-core/common/geodata/strmatcher"
"github.com/xtls/xray-core/common/utils"
)
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
})
if err != nil {
t.Fatalf("BuildMatcher() failed: %v", err)
}
got := matcher.Match("example.com")
slices.Sort(got)
want := []uint32{0, 1}
if !reflect.DeepEqual(got, want) {
t.Fatalf("Match() = %v, want %v", got, want)
}
}
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
})
if err != nil {
t.Fatalf("BuildMatcher() failed: %v", err)
}
got := matcher.Match("163.com")
slices.Sort(got)
want := []uint32{0, 1}
if !reflect.DeepEqual(got, want) {
t.Fatalf("Match() = %v, want %v", got, want)
}
}
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
matcher, err := (&MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}).
BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
})
if err != nil {
t.Fatalf("BuildMatcher() failed: %v", err)
}
got := matcher.Match("example.com")
if !reflect.DeepEqual(got, []uint32{0, 1}) {
t.Fatalf("Match() = %v, want %v", got, []uint32{0, 1})
}
got[0] = 1
gotAgain := matcher.Match("example.com")
if !reflect.DeepEqual(gotAgain, []uint32{0, 1}) {
t.Fatalf("Match() after caller mutation = %v, want %v", gotAgain, []uint32{0, 1})
}
}
-94
View File
@@ -1,94 +0,0 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
)
type DomainRegistry struct {
mu sync.Mutex
factory DomainMatcherFactory
matchers []*DynamicDomainMatcher
}
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
r.mu.Lock()
defer r.mu.Unlock()
m, err := r.factory.BuildMatcher(rules)
if err != nil {
return nil, err
}
d := NewDynamicDomainMatcher(rules, m)
r.matchers = append(r.matchers, d)
return d, nil
}
func (r *DomainRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(r.matchers), " domain matcher(s)")
factory := newDomainMatcherFactory()
type reloadEntry struct {
dynamic *DynamicDomainMatcher
matcher DomainMatcher
}
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
m, err := factory.BuildMatcher(d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i)
return err
}
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
}
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.factory = factory
errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(r.matchers), " domain matcher(s)")
return nil
}
func newDomainRegistry() *DomainRegistry {
return &DomainRegistry{
factory: newDomainMatcherFactory(),
}
}
var DomainReg = newDomainRegistry()
type domainMatcherState struct {
matcher DomainMatcher
}
type DynamicDomainMatcher struct {
rules []*DomainRule
state atomic.Pointer[domainMatcherState]
}
// Match implements DomainMatcher.
func (d *DynamicDomainMatcher) Match(input string) []uint32 {
return d.state.Load().matcher.Match(input)
}
// MatchAny implements DomainMatcher.
func (d *DynamicDomainMatcher) MatchAny(input string) bool {
return d.state.Load().matcher.MatchAny(input)
}
func (d *DynamicDomainMatcher) Reload(newMatcher DomainMatcher) {
d.state.Store(&domainMatcherState{matcher: newMatcher})
}
func NewDynamicDomainMatcher(rules []*DomainRule, matcher DomainMatcher) *DynamicDomainMatcher {
d := &DynamicDomainMatcher{rules: rules}
d.Reload(matcher)
return d
}
-965
View File
@@ -1,965 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.5
// source: common/geodata/geodat.proto
package geodata
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// Type of domain value.
type Domain_Type int32
const (
// The value is used as a sub string.
Domain_Substr Domain_Type = 0
// The value is used as a regular expression.
Domain_Regex Domain_Type = 1
// The value is a domain.
Domain_Domain Domain_Type = 2
// The value is a full domain.
Domain_Full Domain_Type = 3
)
// Enum value maps for Domain_Type.
var (
Domain_Type_name = map[int32]string{
0: "Substr",
1: "Regex",
2: "Domain",
3: "Full",
}
Domain_Type_value = map[string]int32{
"Substr": 0,
"Regex": 1,
"Domain": 2,
"Full": 3,
}
)
func (x Domain_Type) Enum() *Domain_Type {
p := new(Domain_Type)
*p = x
return p
}
func (x Domain_Type) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Domain_Type) Descriptor() protoreflect.EnumDescriptor {
return file_common_geodata_geodat_proto_enumTypes[0].Descriptor()
}
func (Domain_Type) Type() protoreflect.EnumType {
return &file_common_geodata_geodat_proto_enumTypes[0]
}
func (x Domain_Type) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Domain_Type.Descriptor instead.
func (Domain_Type) EnumDescriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{0, 0}
}
type Domain struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Domain matching type.
Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=xray.common.geodata.Domain_Type" json:"type,omitempty"`
// Domain value.
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
// Attributes of this domain. May be used for filtering.
Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Domain) Reset() {
*x = Domain{}
mi := &file_common_geodata_geodat_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Domain) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain) ProtoMessage() {}
func (x *Domain) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Domain.ProtoReflect.Descriptor instead.
func (*Domain) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{0}
}
func (x *Domain) GetType() Domain_Type {
if x != nil {
return x.Type
}
return Domain_Substr
}
func (x *Domain) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *Domain) GetAttribute() []*Domain_Attribute {
if x != nil {
return x.Attribute
}
return nil
}
type GeoSite struct {
state protoimpl.MessageState `protogen:"open.v1"`
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoSite) Reset() {
*x = GeoSite{}
mi := &file_common_geodata_geodat_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoSite) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSite) ProtoMessage() {}
func (x *GeoSite) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoSite.ProtoReflect.Descriptor instead.
func (*GeoSite) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{1}
}
func (x *GeoSite) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *GeoSite) GetDomain() []*Domain {
if x != nil {
return x.Domain
}
return nil
}
type GeoSiteList struct {
state protoimpl.MessageState `protogen:"open.v1"`
Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoSiteList) Reset() {
*x = GeoSiteList{}
mi := &file_common_geodata_geodat_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoSiteList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSiteList) ProtoMessage() {}
func (x *GeoSiteList) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoSiteList.ProtoReflect.Descriptor instead.
func (*GeoSiteList) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{2}
}
func (x *GeoSiteList) GetEntry() []*GeoSite {
if x != nil {
return x.Entry
}
return nil
}
type GeoSiteRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
File string `protobuf:"bytes,1,opt,name=file,proto3" json:"file,omitempty"`
Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"`
Attrs string `protobuf:"bytes,3,opt,name=attrs,proto3" json:"attrs,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoSiteRule) Reset() {
*x = GeoSiteRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoSiteRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSiteRule) ProtoMessage() {}
func (x *GeoSiteRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoSiteRule.ProtoReflect.Descriptor instead.
func (*GeoSiteRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{3}
}
func (x *GeoSiteRule) GetFile() string {
if x != nil {
return x.File
}
return ""
}
func (x *GeoSiteRule) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *GeoSiteRule) GetAttrs() string {
if x != nil {
return x.Attrs
}
return ""
}
type DomainRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Value:
//
// *DomainRule_Geosite
// *DomainRule_Custom
Value isDomainRule_Value `protobuf_oneof:"value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DomainRule) Reset() {
*x = DomainRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DomainRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DomainRule) ProtoMessage() {}
func (x *DomainRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DomainRule.ProtoReflect.Descriptor instead.
func (*DomainRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{4}
}
func (x *DomainRule) GetValue() isDomainRule_Value {
if x != nil {
return x.Value
}
return nil
}
func (x *DomainRule) GetGeosite() *GeoSiteRule {
if x != nil {
if x, ok := x.Value.(*DomainRule_Geosite); ok {
return x.Geosite
}
}
return nil
}
func (x *DomainRule) GetCustom() *Domain {
if x != nil {
if x, ok := x.Value.(*DomainRule_Custom); ok {
return x.Custom
}
}
return nil
}
type isDomainRule_Value interface {
isDomainRule_Value()
}
type DomainRule_Geosite struct {
Geosite *GeoSiteRule `protobuf:"bytes,1,opt,name=geosite,proto3,oneof"`
}
type DomainRule_Custom struct {
Custom *Domain `protobuf:"bytes,2,opt,name=custom,proto3,oneof"`
}
func (*DomainRule_Geosite) isDomainRule_Value() {}
func (*DomainRule_Custom) isDomainRule_Value() {}
type CIDR struct {
state protoimpl.MessageState `protogen:"open.v1"`
// IP address, should be either 4 or 16 bytes.
Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
// Number of leading ones in the network mask.
Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CIDR) Reset() {
*x = CIDR{}
mi := &file_common_geodata_geodat_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CIDR) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CIDR) ProtoMessage() {}
func (x *CIDR) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CIDR.ProtoReflect.Descriptor instead.
func (*CIDR) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{5}
}
func (x *CIDR) GetIp() []byte {
if x != nil {
return x.Ip
}
return nil
}
func (x *CIDR) GetPrefix() uint32 {
if x != nil {
return x.Prefix
}
return 0
}
type CIDRRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
Cidr *CIDR `protobuf:"bytes,1,opt,name=cidr,proto3" json:"cidr,omitempty"`
ReverseMatch bool `protobuf:"varint,2,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CIDRRule) Reset() {
*x = CIDRRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CIDRRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CIDRRule) ProtoMessage() {}
func (x *CIDRRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CIDRRule.ProtoReflect.Descriptor instead.
func (*CIDRRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{6}
}
func (x *CIDRRule) GetCidr() *CIDR {
if x != nil {
return x.Cidr
}
return nil
}
func (x *CIDRRule) GetReverseMatch() bool {
if x != nil {
return x.ReverseMatch
}
return false
}
type GeoIP struct {
state protoimpl.MessageState `protogen:"open.v1"`
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"`
ReverseMatch bool `protobuf:"varint,3,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoIP) Reset() {
*x = GeoIP{}
mi := &file_common_geodata_geodat_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoIP) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIP) ProtoMessage() {}
func (x *GeoIP) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
func (*GeoIP) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{7}
}
func (x *GeoIP) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *GeoIP) GetCidr() []*CIDR {
if x != nil {
return x.Cidr
}
return nil
}
func (x *GeoIP) GetReverseMatch() bool {
if x != nil {
return x.ReverseMatch
}
return false
}
type GeoIPList struct {
state protoimpl.MessageState `protogen:"open.v1"`
Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoIPList) Reset() {
*x = GeoIPList{}
mi := &file_common_geodata_geodat_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoIPList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIPList) ProtoMessage() {}
func (x *GeoIPList) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
func (*GeoIPList) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{8}
}
func (x *GeoIPList) GetEntry() []*GeoIP {
if x != nil {
return x.Entry
}
return nil
}
type GeoIPRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
File string `protobuf:"bytes,1,opt,name=file,proto3" json:"file,omitempty"`
Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"`
ReverseMatch bool `protobuf:"varint,3,opt,name=reverse_match,json=reverseMatch,proto3" json:"reverse_match,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GeoIPRule) Reset() {
*x = GeoIPRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GeoIPRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIPRule) ProtoMessage() {}
func (x *GeoIPRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoIPRule.ProtoReflect.Descriptor instead.
func (*GeoIPRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{9}
}
func (x *GeoIPRule) GetFile() string {
if x != nil {
return x.File
}
return ""
}
func (x *GeoIPRule) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *GeoIPRule) GetReverseMatch() bool {
if x != nil {
return x.ReverseMatch
}
return false
}
type IPRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Value:
//
// *IPRule_Geoip
// *IPRule_Custom
Value isIPRule_Value `protobuf_oneof:"value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *IPRule) Reset() {
*x = IPRule{}
mi := &file_common_geodata_geodat_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *IPRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IPRule) ProtoMessage() {}
func (x *IPRule) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use IPRule.ProtoReflect.Descriptor instead.
func (*IPRule) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{10}
}
func (x *IPRule) GetValue() isIPRule_Value {
if x != nil {
return x.Value
}
return nil
}
func (x *IPRule) GetGeoip() *GeoIPRule {
if x != nil {
if x, ok := x.Value.(*IPRule_Geoip); ok {
return x.Geoip
}
}
return nil
}
func (x *IPRule) GetCustom() *CIDRRule {
if x != nil {
if x, ok := x.Value.(*IPRule_Custom); ok {
return x.Custom
}
}
return nil
}
type isIPRule_Value interface {
isIPRule_Value()
}
type IPRule_Geoip struct {
Geoip *GeoIPRule `protobuf:"bytes,1,opt,name=geoip,proto3,oneof"`
}
type IPRule_Custom struct {
Custom *CIDRRule `protobuf:"bytes,2,opt,name=custom,proto3,oneof"`
}
func (*IPRule_Geoip) isIPRule_Value() {}
func (*IPRule_Custom) isIPRule_Value() {}
type Domain_Attribute struct {
state protoimpl.MessageState `protogen:"open.v1"`
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
// Types that are valid to be assigned to TypedValue:
//
// *Domain_Attribute_BoolValue
// *Domain_Attribute_IntValue
TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Domain_Attribute) Reset() {
*x = Domain_Attribute{}
mi := &file_common_geodata_geodat_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Domain_Attribute) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain_Attribute) ProtoMessage() {}
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
mi := &file_common_geodata_geodat_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Domain_Attribute.ProtoReflect.Descriptor instead.
func (*Domain_Attribute) Descriptor() ([]byte, []int) {
return file_common_geodata_geodat_proto_rawDescGZIP(), []int{0, 0}
}
func (x *Domain_Attribute) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue {
if x != nil {
return x.TypedValue
}
return nil
}
func (x *Domain_Attribute) GetBoolValue() bool {
if x != nil {
if x, ok := x.TypedValue.(*Domain_Attribute_BoolValue); ok {
return x.BoolValue
}
}
return false
}
func (x *Domain_Attribute) GetIntValue() int64 {
if x != nil {
if x, ok := x.TypedValue.(*Domain_Attribute_IntValue); ok {
return x.IntValue
}
}
return 0
}
type isDomain_Attribute_TypedValue interface {
isDomain_Attribute_TypedValue()
}
type Domain_Attribute_BoolValue struct {
BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"`
}
type Domain_Attribute_IntValue struct {
IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"`
}
func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {}
func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {}
var File_common_geodata_geodat_proto protoreflect.FileDescriptor
const file_common_geodata_geodat_proto_rawDesc = "" +
"\n" +
"\x1bcommon/geodata/geodat.proto\x12\x13xray.common.geodata\"\xbc\x02\n" +
"\x06Domain\x124\n" +
"\x04type\x18\x01 \x01(\x0e2 .xray.common.geodata.Domain.TypeR\x04type\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value\x12C\n" +
"\tattribute\x18\x03 \x03(\v2%.xray.common.geodata.Domain.AttributeR\tattribute\x1al\n" +
"\tAttribute\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x1f\n" +
"\n" +
"bool_value\x18\x02 \x01(\bH\x00R\tboolValue\x12\x1d\n" +
"\tint_value\x18\x03 \x01(\x03H\x00R\bintValueB\r\n" +
"\vtyped_value\"3\n" +
"\x04Type\x12\n" +
"\n" +
"\x06Substr\x10\x00\x12\t\n" +
"\x05Regex\x10\x01\x12\n" +
"\n" +
"\x06Domain\x10\x02\x12\b\n" +
"\x04Full\x10\x03\"R\n" +
"\aGeoSite\x12\x12\n" +
"\x04code\x18\x01 \x01(\tR\x04code\x123\n" +
"\x06domain\x18\x02 \x03(\v2\x1b.xray.common.geodata.DomainR\x06domain\"A\n" +
"\vGeoSiteList\x122\n" +
"\x05entry\x18\x01 \x03(\v2\x1c.xray.common.geodata.GeoSiteR\x05entry\"K\n" +
"\vGeoSiteRule\x12\x12\n" +
"\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" +
"\x04code\x18\x02 \x01(\tR\x04code\x12\x14\n" +
"\x05attrs\x18\x03 \x01(\tR\x05attrs\"\x8a\x01\n" +
"\n" +
"DomainRule\x12<\n" +
"\ageosite\x18\x01 \x01(\v2 .xray.common.geodata.GeoSiteRuleH\x00R\ageosite\x125\n" +
"\x06custom\x18\x02 \x01(\v2\x1b.xray.common.geodata.DomainH\x00R\x06customB\a\n" +
"\x05value\".\n" +
"\x04CIDR\x12\x0e\n" +
"\x02ip\x18\x01 \x01(\fR\x02ip\x12\x16\n" +
"\x06prefix\x18\x02 \x01(\rR\x06prefix\"^\n" +
"\bCIDRRule\x12-\n" +
"\x04cidr\x18\x01 \x01(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
"\rreverse_match\x18\x02 \x01(\bR\freverseMatch\"o\n" +
"\x05GeoIP\x12\x12\n" +
"\x04code\x18\x01 \x01(\tR\x04code\x12-\n" +
"\x04cidr\x18\x02 \x03(\v2\x19.xray.common.geodata.CIDRR\x04cidr\x12#\n" +
"\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"=\n" +
"\tGeoIPList\x120\n" +
"\x05entry\x18\x01 \x03(\v2\x1a.xray.common.geodata.GeoIPR\x05entry\"X\n" +
"\tGeoIPRule\x12\x12\n" +
"\x04file\x18\x01 \x01(\tR\x04file\x12\x12\n" +
"\x04code\x18\x02 \x01(\tR\x04code\x12#\n" +
"\rreverse_match\x18\x03 \x01(\bR\freverseMatch\"\x82\x01\n" +
"\x06IPRule\x126\n" +
"\x05geoip\x18\x01 \x01(\v2\x1e.xray.common.geodata.GeoIPRuleH\x00R\x05geoip\x127\n" +
"\x06custom\x18\x02 \x01(\v2\x1d.xray.common.geodata.CIDRRuleH\x00R\x06customB\a\n" +
"\x05valueB[\n" +
"\x17com.xray.common.geodataP\x01Z(github.com/xtls/xray-core/common/geodata\xaa\x02\x13Xray.Common.Geodatab\x06proto3"
var (
file_common_geodata_geodat_proto_rawDescOnce sync.Once
file_common_geodata_geodat_proto_rawDescData []byte
)
func file_common_geodata_geodat_proto_rawDescGZIP() []byte {
file_common_geodata_geodat_proto_rawDescOnce.Do(func() {
file_common_geodata_geodat_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_common_geodata_geodat_proto_rawDesc), len(file_common_geodata_geodat_proto_rawDesc)))
})
return file_common_geodata_geodat_proto_rawDescData
}
var file_common_geodata_geodat_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_common_geodata_geodat_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_common_geodata_geodat_proto_goTypes = []any{
(Domain_Type)(0), // 0: xray.common.geodata.Domain.Type
(*Domain)(nil), // 1: xray.common.geodata.Domain
(*GeoSite)(nil), // 2: xray.common.geodata.GeoSite
(*GeoSiteList)(nil), // 3: xray.common.geodata.GeoSiteList
(*GeoSiteRule)(nil), // 4: xray.common.geodata.GeoSiteRule
(*DomainRule)(nil), // 5: xray.common.geodata.DomainRule
(*CIDR)(nil), // 6: xray.common.geodata.CIDR
(*CIDRRule)(nil), // 7: xray.common.geodata.CIDRRule
(*GeoIP)(nil), // 8: xray.common.geodata.GeoIP
(*GeoIPList)(nil), // 9: xray.common.geodata.GeoIPList
(*GeoIPRule)(nil), // 10: xray.common.geodata.GeoIPRule
(*IPRule)(nil), // 11: xray.common.geodata.IPRule
(*Domain_Attribute)(nil), // 12: xray.common.geodata.Domain.Attribute
}
var file_common_geodata_geodat_proto_depIdxs = []int32{
0, // 0: xray.common.geodata.Domain.type:type_name -> xray.common.geodata.Domain.Type
12, // 1: xray.common.geodata.Domain.attribute:type_name -> xray.common.geodata.Domain.Attribute
1, // 2: xray.common.geodata.GeoSite.domain:type_name -> xray.common.geodata.Domain
2, // 3: xray.common.geodata.GeoSiteList.entry:type_name -> xray.common.geodata.GeoSite
4, // 4: xray.common.geodata.DomainRule.geosite:type_name -> xray.common.geodata.GeoSiteRule
1, // 5: xray.common.geodata.DomainRule.custom:type_name -> xray.common.geodata.Domain
6, // 6: xray.common.geodata.CIDRRule.cidr:type_name -> xray.common.geodata.CIDR
6, // 7: xray.common.geodata.GeoIP.cidr:type_name -> xray.common.geodata.CIDR
8, // 8: xray.common.geodata.GeoIPList.entry:type_name -> xray.common.geodata.GeoIP
10, // 9: xray.common.geodata.IPRule.geoip:type_name -> xray.common.geodata.GeoIPRule
7, // 10: xray.common.geodata.IPRule.custom:type_name -> xray.common.geodata.CIDRRule
11, // [11:11] is the sub-list for method output_type
11, // [11:11] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
}
func init() { file_common_geodata_geodat_proto_init() }
func file_common_geodata_geodat_proto_init() {
if File_common_geodata_geodat_proto != nil {
return
}
file_common_geodata_geodat_proto_msgTypes[4].OneofWrappers = []any{
(*DomainRule_Geosite)(nil),
(*DomainRule_Custom)(nil),
}
file_common_geodata_geodat_proto_msgTypes[10].OneofWrappers = []any{
(*IPRule_Geoip)(nil),
(*IPRule_Custom)(nil),
}
file_common_geodata_geodat_proto_msgTypes[11].OneofWrappers = []any{
(*Domain_Attribute_BoolValue)(nil),
(*Domain_Attribute_IntValue)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_geodata_geodat_proto_rawDesc), len(file_common_geodata_geodat_proto_rawDesc)),
NumEnums: 1,
NumMessages: 12,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_common_geodata_geodat_proto_goTypes,
DependencyIndexes: file_common_geodata_geodat_proto_depIdxs,
EnumInfos: file_common_geodata_geodat_proto_enumTypes,
MessageInfos: file_common_geodata_geodat_proto_msgTypes,
}.Build()
File_common_geodata_geodat_proto = out.File
file_common_geodata_geodat_proto_goTypes = nil
file_common_geodata_geodat_proto_depIdxs = nil
}
-95
View File
@@ -1,95 +0,0 @@
syntax = "proto3";
package xray.common.geodata;
option csharp_namespace = "Xray.Common.Geodata";
option go_package = "github.com/xtls/xray-core/common/geodata";
option java_package = "com.xray.common.geodata";
option java_multiple_files = true;
message Domain {
// Type of domain value.
enum Type {
// The value is used as a sub string.
Substr = 0;
// The value is used as a regular expression.
Regex = 1;
// The value is a domain.
Domain = 2;
// The value is a full domain.
Full = 3;
}
// Domain matching type.
Type type = 1;
// Domain value.
string value = 2;
message Attribute {
string key = 1;
oneof typed_value {
bool bool_value = 2;
int64 int_value = 3;
}
}
// Attributes of this domain. May be used for filtering.
repeated Attribute attribute = 3;
}
message GeoSite {
string code = 1;
repeated Domain domain = 2;
}
message GeoSiteList {
repeated GeoSite entry = 1;
}
message GeoSiteRule {
string file = 1;
string code = 2;
string attrs = 3;
}
message DomainRule {
oneof value {
GeoSiteRule geosite = 1;
Domain custom = 2;
}
}
message CIDR {
// IP address, should be either 4 or 16 bytes.
bytes ip = 1;
// Number of leading ones in the network mask.
uint32 prefix = 2;
}
message CIDRRule {
CIDR cidr = 1;
bool reverse_match = 2;
}
message GeoIP {
string code = 1;
repeated CIDR cidr = 2;
bool reverse_match = 3;
}
message GeoIPList {
repeated GeoIP entry = 1;
}
message GeoIPRule {
string file = 1;
string code = 2;
bool reverse_match = 3;
}
message IPRule {
oneof value {
GeoIPRule geoip = 1;
CIDRRule custom = 2;
}
}
-207
View File
@@ -1,207 +0,0 @@
package geodata
import (
"bufio"
"bytes"
"io"
"runtime"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/platform/filesystem"
"google.golang.org/protobuf/proto"
)
func checkFile(file, code string) error {
r, err := filesystem.OpenAsset(file)
if err != nil {
return errors.New("failed to open ", file).Base(err)
}
defer r.Close()
if _, err := find(r, []byte(code), false); err != nil {
return errors.New("failed to check code ", code, " from ", file).Base(err)
}
return nil
}
func loadFile(file, code string) ([]byte, error) {
runtime.GC() // peak mem
r, err := filesystem.OpenAsset(file)
if err != nil {
return nil, errors.New("failed to open ", file).Base(err)
}
defer r.Close()
bs, err := find(r, []byte(code), true)
if err != nil {
return nil, errors.New("failed to load code ", code, " from ", file).Base(err)
}
return bs, nil
}
func loadIP(file, code string) ([]*CIDR, error) {
bs, err := loadFile(file, code)
if err != nil {
return nil, err
}
defer runtime.GC() // peak mem
var geoip GeoIP
if err := proto.Unmarshal(bs, &geoip); err != nil {
return nil, errors.New("error unmarshal IP in ", file, ":", code).Base(err)
}
return geoip.Cidr, nil
}
func loadSite(file, code string) ([]*Domain, error) {
bs, err := loadFile(file, code)
if err != nil {
return nil, err
}
defer runtime.GC() // peak mem
var geosite GeoSite
if err := proto.Unmarshal(bs, &geosite); err != nil {
return nil, errors.New("error unmarshal Site in ", file, ":", code).Base(err)
}
return geosite.Domain, nil
}
func decodeVarint(br *bufio.Reader) (uint64, error) {
var x uint64
for shift := uint(0); shift < 64; shift += 7 {
b, err := br.ReadByte()
if err != nil {
return 0, err
}
x |= (uint64(b) & 0x7F) << shift
if (b & 0x80) == 0 {
return x, nil
}
}
// The number is too large to represent in a 64-bit value.
return 0, errors.New("varint overflow")
}
func find(r io.Reader, code []byte, readBody bool) ([]byte, error) {
codeL := len(code)
if codeL == 0 {
return nil, errors.New("empty code")
}
br := bufio.NewReaderSize(r, 64*1024)
need := 2 + codeL // TODO: if code too long
prefixBuf := make([]byte, need)
for {
if _, err := br.ReadByte(); err != nil {
return nil, err
}
x, err := decodeVarint(br)
if err != nil {
return nil, err
}
bodyL := int(x)
if bodyL <= 0 {
return nil, errors.New("invalid body length: ", bodyL)
}
prefixL := bodyL
if prefixL > need {
prefixL = need
}
prefix := prefixBuf[:prefixL]
if _, err := io.ReadFull(br, prefix); err != nil {
return nil, err
}
match := false
if bodyL >= need {
if int(prefix[1]) == codeL && bytes.Equal(prefix[2:need], code) {
if !readBody {
return nil, nil
}
match = true
}
}
remain := bodyL - prefixL
if match {
out := make([]byte, bodyL)
copy(out, prefix)
if remain > 0 {
if _, err := io.ReadFull(br, out[prefixL:]); err != nil {
return nil, err
}
}
return out, nil
}
if remain > 0 {
if _, err := br.Discard(remain); err != nil {
return nil, err
}
}
}
}
type AttributeMatcher interface {
Match(*Domain) bool
}
type HasAttrMatcher string
// Match reports whether this matcher matches any attribute on the domain.
func (m HasAttrMatcher) Match(domain *Domain) bool {
for _, attr := range domain.Attribute {
if attr.Key == string(m) {
return true
}
}
return false
}
type AllAttrsMatcher struct {
matchers []AttributeMatcher
}
// Match reports whether the domain matches every matcher in the list.
func (m *AllAttrsMatcher) Match(domain *Domain) bool {
for _, matcher := range m.matchers {
if !matcher.Match(domain) {
return false
}
}
return true
}
func NewAllAttrsMatcher(attrs string) AttributeMatcher {
if attrs == "" {
return nil
}
m := new(AllAttrsMatcher)
for _, attr := range strings.Split(attrs, "@") {
m.matchers = append(m.matchers, HasAttrMatcher(attr))
}
return m
}
func loadSiteWithAttrs(file, code, attrs string) ([]*Domain, error) {
domains, err := loadSite(file, code)
if err != nil {
return nil, err
}
matcher := NewAllAttrsMatcher(attrs)
if matcher == nil {
return domains, nil
}
filtered := make([]*Domain, 0, len(domains))
for _, d := range domains {
if matcher.Match(d) {
filtered = append(filtered, d)
}
}
return filtered, nil
}
-437
View File
@@ -1,437 +0,0 @@
package geodata
import (
"net"
"path/filepath"
"reflect"
"slices"
"testing"
"github.com/xtls/xray-core/common"
xnet "github.com/xtls/xray-core/common/net"
)
func buildIPMatcher(rawRules ...string) IPMatcher {
rules, err := ParseIPRules(rawRules)
common.Must(err)
matcher, err := newIPRegistry().BuildIPMatcher(rules)
common.Must(err)
return matcher
}
func sortIPStrings(ips []net.IP) []string {
output := make([]string, 0, len(ips))
for _, ip := range ips {
output = append(output, ip.String())
}
slices.Sort(output)
return output
}
func TestIPMatcher(t *testing.T) {
matcher := buildIPMatcher(
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.168.0.0/16",
"192.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"8.8.8.8/32",
"91.108.4.0/16",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: true,
},
{
Input: "192.0.0.0",
Output: true,
},
{
Input: "192.0.1.0",
Output: false,
},
{
Input: "0.1.0.0",
Output: true,
},
{
Input: "1.0.0.1",
Output: false,
},
{
Input: "8.8.8.7",
Output: false,
},
{
Input: "8.8.8.8",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
{
Input: "91.108.255.254",
Output: true,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherFullCIDR4(t *testing.T) {
matcher := buildIPMatcher(
"0.0.0.0/0",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: true,
},
{
Input: "0.0.0.0",
Output: true,
},
{
Input: "255.255.255.255",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
{
Input: "::0",
Output: false,
},
{
Input: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
Output: false,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherFullCIDR6(t *testing.T) {
matcher := buildIPMatcher(
"::0/0",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: false,
},
{
Input: "0.0.0.0",
Output: false,
},
{
Input: "255.255.255.255",
Output: false,
},
{
Input: "2001:cdba::3257:9652",
Output: true,
},
{
Input: "::0",
Output: true,
},
{
Input: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
Output: true,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherRegression(t *testing.T) {
matcher := buildIPMatcher(
"98.108.20.0/22",
"98.108.20.0/23",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "98.108.22.11",
Output: true,
},
{
Input: "98.108.25.0",
Output: false,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPReverseMatcher(t *testing.T) {
matcher := buildIPMatcher(
"8.8.8.8/32",
"91.108.4.0/16",
)
matcher.SetReverse(true)
testCases := []struct {
Input string
Output bool
}{
{
Input: "8.8.8.8",
Output: false,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
{
Input: "91.108.255.254",
Output: false,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPReverseMatcher2(t *testing.T) {
matcher := buildIPMatcher(
"8.8.8.8/32",
"91.108.4.0/16",
"fe80::", // Keep IPv6 family non-empty so reverse matching can evaluate IPv6 input.
)
matcher.SetReverse(true)
testCases := []struct {
Input string
Output bool
}{
{
Input: "8.8.8.8",
Output: false,
},
{
Input: "2001:cdba::3257:9652",
Output: true,
},
{
Input: "91.108.255.254",
Output: false,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPCustomReverseMatcher(t *testing.T) {
matcher := buildIPMatcher("!8.8.8.8/32")
testCases := []struct {
Input string
Output bool
}{
{
Input: "8.8.8.8",
Output: false,
},
{
Input: "1.1.1.1",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
matcher := buildIPMatcher(
"8.8.8.8/32",
"2001:4860:4860::8888/128",
)
ip := func(raw string) net.IP {
return xnet.ParseAddress(raw).IP()
}
if matcher.AnyMatch(nil) {
t.Fatal("expect AnyMatch(nil) to be false")
}
if !matcher.AnyMatch([]net.IP{
{},
ip("1.1.1.1"),
ip("8.8.8.8"),
}) {
t.Fatal("expect AnyMatch to ignore invalid IPs and return true when one valid IP matches")
}
if matcher.AnyMatch([]net.IP{
ip("1.1.1.1"),
ip("2001:db8::1"),
}) {
t.Fatal("expect AnyMatch to be false when no valid IP matches")
}
if !matcher.Matches([]net.IP{
ip("8.8.8.8"),
ip("2001:4860:4860::8888"),
}) {
t.Fatal("expect Matches to be true when all valid IPs match")
}
if matcher.Matches([]net.IP{
ip("8.8.8.8"),
ip("1.1.1.1"),
}) {
t.Fatal("expect Matches to be false when one valid IP does not match")
}
if matcher.Matches([]net.IP{
ip("8.8.8.8"),
{},
}) {
t.Fatal("expect Matches to be false when any IP is invalid")
}
}
func TestIPMatcherFilterIPs(t *testing.T) {
matcher := buildIPMatcher(
"8.8.8.8/32",
"91.108.4.0/16",
"2001:4860:4860::8888/128",
)
ip := func(raw string) net.IP {
return xnet.ParseAddress(raw).IP()
}
matched, unmatched := matcher.FilterIPs([]net.IP{
{},
ip("8.8.8.8"),
ip("91.108.255.254"),
ip("1.1.1.1"),
ip("2001:4860:4860::8888"),
ip("2001:db8::1"),
})
wantMatched := []string{
"2001:4860:4860::8888",
"8.8.8.8",
"91.108.255.254",
}
slices.Sort(wantMatched)
if v := sortIPStrings(matched); !reflect.DeepEqual(v, wantMatched) {
t.Error("unexpected output: ", v, " want ", wantMatched)
}
wantUnmatched := []string{
"1.1.1.1",
"2001:db8::1",
}
slices.Sort(wantUnmatched)
if v := sortIPStrings(unmatched); !reflect.DeepEqual(v, wantUnmatched) {
t.Error("unexpected output: ", v, " want ", wantUnmatched)
}
}
func TestIPMatcher4CN(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:cn")
if matcher.Match([]byte{8, 8, 8, 8}) {
t.Error("expect CN geoip doesn't contain 8.8.8.8, but actually does")
}
}
func TestIPMatcher6US(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:us")
if !matcher.Match(xnet.ParseAddress("2001:4860:4860::8888").IP()) {
t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not")
}
}
func BenchmarkIPMatcher4CN(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:cn")
ip := net.IP{8, 8, 8, 8}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = matcher.Match(ip)
}
}
func BenchmarkIPMatcher6US(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:us")
ip := xnet.ParseAddress("2001:4860:4860::8888").IP()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = matcher.Match(ip)
}
}
-135
View File
@@ -1,135 +0,0 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
type IPRegistry struct {
mu sync.Mutex
ipsetFactory *IPSetFactory
matchers []*DynamicIPMatcher
}
func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) {
r.mu.Lock()
defer r.mu.Unlock()
m, err := buildOptimizedIPMatcher(r.ipsetFactory, rules)
if err != nil {
return nil, err
}
d := NewDynamicIPMatcher(rules, m)
r.matchers = append(r.matchers, d)
return d, nil
}
func (r *IPRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(r.matchers), " IP matcher(s)")
factory := newIPSetFactory()
type reloadEntry struct {
dynamic *DynamicIPMatcher
matcher IPMatcher
}
reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range r.matchers {
m, err := buildOptimizedIPMatcher(factory, d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i)
return err
}
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
}
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.ipsetFactory = factory
errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(r.matchers), " IP matcher(s)")
return nil
}
func newIPRegistry() *IPRegistry {
return &IPRegistry{
ipsetFactory: newIPSetFactory(),
}
}
var IPReg = newIPRegistry()
type ipMatcherState struct {
matcher IPMatcher
}
type DynamicIPMatcher struct {
rules []*IPRule
state atomic.Pointer[ipMatcherState]
mu sync.Mutex
reverse bool
reverseSet bool
}
// Match implements IPMatcher.
func (d *DynamicIPMatcher) Match(ip net.IP) bool {
return d.state.Load().matcher.Match(ip)
}
// AnyMatch implements IPMatcher.
func (d *DynamicIPMatcher) AnyMatch(ips []net.IP) bool {
return d.state.Load().matcher.AnyMatch(ips)
}
// Matches implements IPMatcher.
func (d *DynamicIPMatcher) Matches(ips []net.IP) bool {
return d.state.Load().matcher.Matches(ips)
}
// FilterIPs implements IPMatcher.
func (d *DynamicIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) {
return d.state.Load().matcher.FilterIPs(ips)
}
// ToggleReverse implements IPMatcher.
func (d *DynamicIPMatcher) ToggleReverse() {
d.mu.Lock()
defer d.mu.Unlock()
d.reverse = !d.reverse
d.state.Load().matcher.ToggleReverse()
}
// SetReverse implements IPMatcher.
func (d *DynamicIPMatcher) SetReverse(reverse bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.reverse = reverse
d.reverseSet = true
d.state.Load().matcher.SetReverse(reverse)
}
func (d *DynamicIPMatcher) Reload(newMatcher IPMatcher) {
d.mu.Lock()
defer d.mu.Unlock()
if d.reverseSet {
newMatcher.SetReverse(d.reverse)
} else if d.reverse {
newMatcher.ToggleReverse()
}
d.state.Store(&ipMatcherState{matcher: newMatcher})
}
func NewDynamicIPMatcher(rules []*IPRule, matcher IPMatcher) *DynamicIPMatcher {
d := &DynamicIPMatcher{rules: rules}
d.Reload(matcher)
return d
}
-265
View File
@@ -1,265 +0,0 @@
package geodata
import (
"strconv"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
const (
DefaultGeoIPDat = "geoip.dat"
DefaultGeoSiteDat = "geosite.dat"
)
func ParseIPRules(rules []string) ([]*IPRule, error) {
var ipRules []*IPRule
for i, r := range rules {
r, reverse := cutReversePrefix(r)
if strings.HasPrefix(r, "geoip:") {
r = "ext:" + DefaultGeoIPDat + ":" + r[len("geoip:"):]
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-ip:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
}
}
var rule isIPRule_Value
var err error
if prefix > 0 {
rule, err = parseGeoIPRule(r[prefix:], reverse)
} else {
rule, err = parseCustomIPRule(r, reverse)
}
if err != nil {
return nil, errors.New("illegal ip rule: ", rules[i]).Base(err)
}
ipRules = append(ipRules, &IPRule{Value: rule})
}
return ipRules, nil
}
func cutReversePrefix(s string) (string, bool) {
reverse := false
for strings.HasPrefix(s, "!") {
s = s[1:]
reverse = !reverse
}
return s, reverse
}
func parseGeoIPRule(rule string, reverse bool) (*IPRule_Geoip, error) {
file, code, ok := strings.Cut(rule, ":")
if !ok {
return nil, errors.New("syntax error")
}
if file == "" {
return nil, errors.New("empty file")
}
code, codeReverse := cutReversePrefix(code)
reverse = reverse != codeReverse
if code == "" {
return nil, errors.New("empty code")
}
code = strings.ToUpper(code)
if err := checkFile(file, code); err != nil {
return nil, err
}
return &IPRule_Geoip{
Geoip: &GeoIPRule{
File: file,
Code: code,
ReverseMatch: reverse,
},
}, nil
}
func parseCustomIPRule(rule string, reverse bool) (*IPRule_Custom, error) {
cidr, err := parseCIDR(rule)
if err != nil {
return nil, err
}
return &IPRule_Custom{
Custom: &CIDRRule{
Cidr: cidr,
ReverseMatch: reverse,
},
}, nil
}
func parseCIDR(s string) (*CIDR, error) {
ipStr, prefixStr, _ := strings.Cut(s, "/")
ipAddr := net.ParseAddress(ipStr)
var maxPrefix uint32
switch ipAddr.Family() {
case net.AddressFamilyIPv4:
maxPrefix = 32
case net.AddressFamilyIPv6:
maxPrefix = 128
default:
return nil, errors.New("unsupported address family")
}
prefixBits := maxPrefix
if prefixStr != "" {
parsedPrefix, err := strconv.ParseUint(prefixStr, 10, 32)
if err != nil {
return nil, errors.New("invalid CIDR prefix length: ", prefixStr).Base(err)
}
prefixBits = uint32(parsedPrefix)
}
if prefixBits > maxPrefix {
return nil, errors.New("CIDR prefix length ", prefixBits, " exceeds max ", maxPrefix)
}
return &CIDR{
Ip: []byte(ipAddr.IP()),
Prefix: prefixBits,
}, nil
}
func ParseDomainRule(r string, defaultType Domain_Type) (*DomainRule, error) {
if strings.HasPrefix(r, "geosite:") {
r = "ext:" + DefaultGeoSiteDat + ":" + r[len("geosite:"):]
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
}
}
var rule isDomainRule_Value
var err error
if prefix > 0 {
rule, err = parseGeoSiteRule(r[prefix:])
} else {
rule, err = parseCustomDomainRule(r, defaultType)
}
if err != nil {
return nil, errors.New("illegal domain rule: ", r).Base(err)
}
return &DomainRule{Value: rule}, nil
}
func ParseDomainRules(rules []string, defaultType Domain_Type) ([]*DomainRule, error) {
var domainRules []*DomainRule
for i, r := range rules {
if strings.HasPrefix(r, "geosite:") {
r = "ext:" + DefaultGeoSiteDat + ":" + r[len("geosite:"):]
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
}
}
var rule isDomainRule_Value
var err error
if prefix > 0 {
rule, err = parseGeoSiteRule(r[prefix:])
} else {
rule, err = parseCustomDomainRule(r, defaultType)
}
if err != nil {
return nil, errors.New("illegal domain rule: ", rules[i]).Base(err)
}
domainRules = append(domainRules, &DomainRule{Value: rule})
}
return domainRules, nil
}
func parseGeoSiteRule(rule string) (*DomainRule_Geosite, error) {
file, codeWithAttrs, ok := strings.Cut(rule, ":")
if !ok {
return nil, errors.New("syntax error")
}
if file == "" {
return nil, errors.New("empty file")
}
if strings.HasSuffix(codeWithAttrs, "@") || strings.Contains(codeWithAttrs, "@@") {
return nil, errors.New("empty attr")
}
code, attrs, _ := strings.Cut(codeWithAttrs, "@")
if code == "" {
return nil, errors.New("empty code")
}
code = strings.ToUpper(code)
if err := checkFile(file, code); err != nil {
return nil, err
}
return &DomainRule_Geosite{
Geosite: &GeoSiteRule{
File: file,
Code: code,
Attrs: strings.ToLower(attrs),
},
}, nil
}
func parseCustomDomainRule(rule string, defaultType Domain_Type) (*DomainRule_Custom, error) {
domain := new(Domain)
switch {
case strings.HasPrefix(rule, "regexp:"):
domain.Type = Domain_Regex
domain.Value = rule[7:]
case strings.HasPrefix(rule, "domain:"):
domain.Type = Domain_Domain
domain.Value = rule[7:]
case strings.HasPrefix(rule, "full:"):
domain.Type = Domain_Full
domain.Value = rule[5:]
case strings.HasPrefix(rule, "keyword:"):
domain.Type = Domain_Substr
domain.Value = rule[8:]
case strings.HasPrefix(rule, "dotless:"):
domain.Type = Domain_Regex
switch substr := rule[8:]; {
case substr == "":
domain.Value = "^[^.]*$"
case !strings.Contains(substr, "."):
domain.Value = "^[^.]*" + substr + "[^.]*$"
default:
return nil, errors.New("substr in dotless rule should not contain a dot")
}
default:
domain.Type = defaultType
domain.Value = rule
}
return &DomainRule_Custom{
Custom: domain,
}, nil
}
-106
View File
@@ -1,106 +0,0 @@
package geodata_test
import (
"path/filepath"
"testing"
"github.com/xtls/xray-core/common/geodata"
)
func TestParseIPRules(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules := []string{
"geoip:us",
"geoip:cn",
"!geoip:cn",
"!!geoip:cn",
"geoip:!cn",
"geoip:!!cn",
"!geoip:!cn",
"ext:geoip.dat:!cn",
"ext:geoip.dat:!!cn",
"ext:geoip.dat:ca",
"ext-ip:geoip.dat:!cn",
"ext-ip:geoip.dat:!ca",
"192.168.0.0/24",
"!192.168.0.0/24",
"!!192.168.0.0/24",
"!!!192.168.0.0/24",
"192.168.0.1",
"fe80::/64",
"fe80::",
}
_, err := geodata.ParseIPRules(rules)
if err != nil {
t.Fatalf("Failed to parse ip rules, got %s", err)
}
}
func TestParseIPRuleReverse(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
for _, tt := range []struct {
rule string
reverse bool
}{
{rule: "!192.168.0.0/24", reverse: true},
{rule: "!!192.168.0.0/24", reverse: false},
{rule: "!!!192.168.0.0/24", reverse: true},
{rule: "!!!!192.168.0.0/24", reverse: false},
{rule: "geoip:cn", reverse: false},
{rule: "!geoip:cn", reverse: true},
{rule: "!!geoip:cn", reverse: false},
{rule: "geoip:!cn", reverse: true},
{rule: "geoip:!!cn", reverse: false},
{rule: "!geoip:!cn", reverse: false},
{rule: "!!geoip:!cn", reverse: true},
{rule: "!geoip:!!cn", reverse: true},
{rule: "ext:geoip.dat:!!!cn", reverse: true},
} {
t.Run(tt.rule, func(t *testing.T) {
rules, err := geodata.ParseIPRules([]string{tt.rule})
if err != nil {
t.Fatalf("Failed to parse ip rules, got %s", err)
}
if len(rules) != 1 {
t.Fatalf("Expected 1 rule, got %d", len(rules))
}
switch rule := rules[0]; {
case rule.GetGeoip() != nil:
if rule.GetGeoip().GetReverseMatch() != tt.reverse {
t.Fatalf("Expected geoip reverse match to be %t", tt.reverse)
}
case rule.GetCustom() != nil:
if rule.GetCustom().GetReverseMatch() != tt.reverse {
t.Fatalf("Expected custom reverse match to be %t", tt.reverse)
}
default:
t.Fatal("Expected ip rule")
}
})
}
}
func TestParseDomainRules(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules := []string{
"geosite:cn",
"geosite:geolocation-!cn",
"geosite:cn@!cn",
"ext:geosite.dat:geolocation-!cn",
"ext:geosite.dat:cn@!cn",
"ext-site:geosite.dat:geolocation-!cn",
"ext-site:geosite.dat:cn@!cn",
"domain:google.com",
}
_, err := geodata.ParseDomainRules(rules, geodata.Domain_Domain)
if err != nil {
t.Fatalf("Failed to parse domain rules, got %s", err)
}
}
@@ -1,53 +0,0 @@
package strmatcher
// LinearAnyMatcher is an implementation of AnyMatcher.
type LinearAnyMatcher struct {
full *FullMatcherSet
domain *DomainMatcherSet
substr *SubstrMatcherSet
regex *SimpleMatcherSet
}
func NewLinearAnyMatcher() *LinearAnyMatcher {
return new(LinearAnyMatcher)
}
// Add implements AnyMatcher.Add.
func (s *LinearAnyMatcher) Add(matcher Matcher) {
switch matcher := matcher.(type) {
case FullMatcher:
if s.full == nil {
s.full = NewFullMatcherSet()
}
s.full.AddFullMatcher(matcher)
case DomainMatcher:
if s.domain == nil {
s.domain = NewDomainMatcherSet()
}
s.domain.AddDomainMatcher(matcher)
case SubstrMatcher:
if s.substr == nil {
s.substr = new(SubstrMatcherSet)
}
s.substr.AddSubstrMatcher(matcher)
default:
if s.regex == nil {
s.regex = new(SimpleMatcherSet)
}
s.regex.AddMatcher(matcher)
}
}
// MatchAny implements AnyMatcher.MatchAny.
func (s *LinearAnyMatcher) MatchAny(input string) bool {
if s.full != nil && s.full.MatchAny(input) {
return true
}
if s.domain != nil && s.domain.MatchAny(input) {
return true
}
if s.substr != nil && s.substr.MatchAny(input) {
return true
}
return s.regex != nil && s.regex.MatchAny(input)
}
@@ -1,58 +0,0 @@
package strmatcher_test
import (
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func BenchmarkLinearIndexMatcher(b *testing.B) {
benchmarkIndexMatcher(b, func() IndexMatcher {
return NewLinearIndexMatcher()
})
}
func BenchmarkMphIndexMatcher(b *testing.B) {
benchmarkIndexMatcher(b, func() IndexMatcher {
return NewMphIndexMatcher()
})
}
func benchmarkIndexMatcher(b *testing.B, ctor func() IndexMatcher) {
b.Run("Match", func(b *testing.B) {
b.Run("Domain------------", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true})
})
b.Run("Domain+Full-------", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true})
})
b.Run("Domain+Full+Substr", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true, Substr: true})
})
b.Run("All-Fail----------", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: false, Full: false, Substr: false})
})
})
b.Run("Match/Dotless", func(b *testing.B) { // Dotless domain matcher automatically inserted in DNS app when "localhost" DNS is used.
b.Run("All-Succ", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: true, Full: true, Substr: true, Regex: true})
})
b.Run("All-Fail", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{Domain: false, Full: false, Substr: false, Regex: false})
})
})
b.Run("MatchAny", func(b *testing.B) {
b.Run("First-Full--", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: true, Domain: true, Substr: true})
})
b.Run("First-Domain", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: true, Substr: true})
})
b.Run("First-Substr", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: false, Substr: true})
})
b.Run("All-Fail----", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{Full: false, Domain: false, Substr: false})
})
})
}
@@ -1,149 +0,0 @@
package strmatcher_test
import (
"strconv"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func BenchmarkFullMatcher(b *testing.B) {
b.Run("SimpleMatcherGroup------", func(b *testing.B) {
benchmarkMatcherType(b, Full, func() MatcherGroup {
return new(SimpleMatcherGroup)
})
})
b.Run("FullMatcherGroup--------", func(b *testing.B) {
benchmarkMatcherType(b, Full, func() MatcherGroup {
return NewFullMatcherGroup()
})
})
b.Run("ACAutomationMatcherGroup", func(b *testing.B) {
benchmarkMatcherType(b, Full, func() MatcherGroup {
return NewACAutomatonMatcherGroup()
})
})
b.Run("MphMatcherGroup---------", func(b *testing.B) {
benchmarkMatcherType(b, Full, func() MatcherGroup {
return NewMphMatcherGroup()
})
})
}
func BenchmarkDomainMatcher(b *testing.B) {
b.Run("SimpleMatcherGroup------", func(b *testing.B) {
benchmarkMatcherType(b, Domain, func() MatcherGroup {
return new(SimpleMatcherGroup)
})
})
b.Run("DomainMatcherGroup------", func(b *testing.B) {
benchmarkMatcherType(b, Domain, func() MatcherGroup {
return NewDomainMatcherGroup()
})
})
b.Run("ACAutomationMatcherGroup", func(b *testing.B) {
benchmarkMatcherType(b, Domain, func() MatcherGroup {
return NewACAutomatonMatcherGroup()
})
})
b.Run("MphMatcherGroup---------", func(b *testing.B) {
benchmarkMatcherType(b, Domain, func() MatcherGroup {
return NewMphMatcherGroup()
})
})
}
func BenchmarkSubstrMatcher(b *testing.B) {
b.Run("SimpleMatcherGroup------", func(b *testing.B) {
benchmarkMatcherType(b, Substr, func() MatcherGroup {
return new(SimpleMatcherGroup)
})
})
b.Run("SubstrMatcherGroup------", func(b *testing.B) {
benchmarkMatcherType(b, Substr, func() MatcherGroup {
return new(SubstrMatcherGroup)
})
})
b.Run("ACAutomationMatcherGroup", func(b *testing.B) {
benchmarkMatcherType(b, Substr, func() MatcherGroup {
return NewACAutomatonMatcherGroup()
})
})
}
// Utility functions for benchmark
func benchmarkMatcherType(b *testing.B, t Type, ctor func() MatcherGroup) {
b.Run("Match", func(b *testing.B) {
b.Run("Succ", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{t: true})
})
b.Run("Fail", func(b *testing.B) {
benchmarkMatch(b, ctor(), map[Type]bool{t: false})
})
})
b.Run("MatchAny", func(b *testing.B) {
b.Run("Succ", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{t: true})
})
b.Run("Fail", func(b *testing.B) {
benchmarkMatchAny(b, ctor(), map[Type]bool{t: false})
})
})
}
func benchmarkMatch(b *testing.B, g MatcherGroup, enabledTypes map[Type]bool) {
prepareMatchers(g, enabledTypes)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = g.Match("0.example.com")
}
}
func benchmarkMatchAny(b *testing.B, g MatcherGroup, enabledTypes map[Type]bool) {
prepareMatchers(g, enabledTypes)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = g.MatchAny("0.example.com")
}
}
func prepareMatchers(g MatcherGroup, enabledTypes map[Type]bool) {
for matcherType, hasMatch := range enabledTypes {
switch matcherType {
case Domain:
if hasMatch {
AddMatcherToGroup(g, DomainMatcher("example.com"), 0)
}
for i := 1; i < 1024; i++ {
AddMatcherToGroup(g, DomainMatcher(strconv.Itoa(i)+".example.com"), uint32(i))
}
case Full:
if hasMatch {
AddMatcherToGroup(g, FullMatcher("0.example.com"), 0)
}
for i := 1; i < 64; i++ {
AddMatcherToGroup(g, FullMatcher(strconv.Itoa(i)+".example.com"), uint32(i))
}
case Substr:
if hasMatch {
AddMatcherToGroup(g, SubstrMatcher("example.com"), 0)
}
for i := 1; i < 4; i++ {
AddMatcherToGroup(g, SubstrMatcher(strconv.Itoa(i)+".example.com"), uint32(i))
}
case Regex:
matcher, err := Regex.New("^[^.]*$") // Dotless domain matcher automatically inserted in DNS app when "localhost" DNS is used.
common.Must(err)
AddMatcherToGroup(g, matcher, 0)
}
}
if g, ok := g.(buildable); ok {
common.Must(g.Build())
}
}
type buildable interface {
Build() error
}
@@ -1,96 +0,0 @@
package strmatcher
// LinearIndexMatcher is an implementation of IndexMatcher.
type LinearIndexMatcher struct {
count uint32
full *FullMatcherGroup
domain *DomainMatcherGroup
substr *SubstrMatcherGroup
regex *SimpleMatcherGroup
}
func NewLinearIndexMatcher() *LinearIndexMatcher {
return new(LinearIndexMatcher)
}
// Add implements IndexMatcher.Add.
func (g *LinearIndexMatcher) Add(matcher Matcher) uint32 {
g.count++
index := g.count
switch matcher := matcher.(type) {
case FullMatcher:
if g.full == nil {
g.full = NewFullMatcherGroup()
}
g.full.AddFullMatcher(matcher, index)
case DomainMatcher:
if g.domain == nil {
g.domain = NewDomainMatcherGroup()
}
g.domain.AddDomainMatcher(matcher, index)
case SubstrMatcher:
if g.substr == nil {
g.substr = new(SubstrMatcherGroup)
}
g.substr.AddSubstrMatcher(matcher, index)
default:
if g.regex == nil {
g.regex = new(SimpleMatcherGroup)
}
g.regex.AddMatcher(matcher, index)
}
return index
}
// Build implements IndexMatcher.Build.
func (*LinearIndexMatcher) Build() error {
return nil
}
// Match implements IndexMatcher.Match.
func (g *LinearIndexMatcher) Match(input string) []uint32 {
// Allocate capacity to prevent matches escaping to heap
result := make([][]uint32, 0, 5)
if g.full != nil {
if matches := g.full.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.domain != nil {
if matches := g.domain.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.substr != nil {
if matches := g.substr.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.regex != nil {
if matches := g.regex.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
return CompositeMatches(result)
}
// MatchAny implements IndexMatcher.MatchAny.
func (g *LinearIndexMatcher) MatchAny(input string) bool {
if g.full != nil && g.full.MatchAny(input) {
return true
}
if g.domain != nil && g.domain.MatchAny(input) {
return true
}
if g.substr != nil && g.substr.MatchAny(input) {
return true
}
return g.regex != nil && g.regex.MatchAny(input)
}
// Size implements IndexMatcher.Size.
func (g *LinearIndexMatcher) Size() uint32 {
return g.count
}
@@ -1,95 +0,0 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
// See https://github.com/v2fly/v2ray-core/issues/92#issuecomment-673238489
func TestLinearIndexMatcher(t *testing.T) {
rules := []struct {
Type Type
Domain string
}{
{
Type: Regex,
Domain: "apis\\.us$",
},
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Domain,
Domain: "com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Full,
Domain: "fonts.googleapis.com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Domain,
Domain: "example.com",
},
}
cases := []struct {
Input string
Output []uint32
}{
{
Input: "www.baidu.com",
Output: []uint32{5, 9, 4},
},
{
Input: "fonts.googleapis.com",
Output: []uint32{8, 3, 7, 4, 2, 6},
},
{
Input: "example.googleapis.com",
Output: []uint32{3, 7, 4, 2, 6},
},
{
Input: "testapis.us",
Output: []uint32{2, 6, 1},
},
{
Input: "example.com",
Output: []uint32{10, 4},
},
}
matcherGroup := NewLinearIndexMatcher()
for _, rule := range rules {
matcher, err := rule.Type.New(rule.Domain)
common.Must(err)
matcherGroup.Add(matcher)
}
matcherGroup.Build()
for _, test := range cases {
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}
@@ -1,100 +0,0 @@
package strmatcher
import "runtime"
// A MphIndexMatcher is divided into three parts:
// 1. `full` and `domain` patterns are matched by Rabin-Karp algorithm and minimal perfect hash table;
// 2. `substr` patterns are matched by ac automaton;
// 3. `regex` patterns are matched with the regex library.
type MphIndexMatcher struct {
count uint32
mph *MphMatcherGroup
ac *ACAutomatonMatcherGroup
regex *SimpleMatcherGroup
}
func NewMphIndexMatcher() *MphIndexMatcher {
return new(MphIndexMatcher)
}
// Add implements IndexMatcher.Add.
func (g *MphIndexMatcher) Add(matcher Matcher) uint32 {
g.count++
index := g.count
switch matcher := matcher.(type) {
case FullMatcher:
if g.mph == nil {
g.mph = NewMphMatcherGroup()
}
g.mph.AddFullMatcher(matcher, index)
case DomainMatcher:
if g.mph == nil {
g.mph = NewMphMatcherGroup()
}
g.mph.AddDomainMatcher(matcher, index)
case SubstrMatcher:
if g.ac == nil {
g.ac = NewACAutomatonMatcherGroup()
}
g.ac.AddSubstrMatcher(matcher, index)
case *RegexMatcher:
if g.regex == nil {
g.regex = &SimpleMatcherGroup{}
}
g.regex.AddMatcher(matcher, index)
}
return index
}
// Build implements IndexMatcher.Build.
func (g *MphIndexMatcher) Build() error {
if g.mph != nil {
runtime.GC() // peak mem
g.mph.Build()
}
runtime.GC() // peak mem
if g.ac != nil {
g.ac.Build()
runtime.GC() // peak mem
}
return nil
}
// Match implements IndexMatcher.Match.
func (g *MphIndexMatcher) Match(input string) []uint32 {
result := make([][]uint32, 0, 5)
if g.mph != nil {
if matches := g.mph.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.ac != nil {
if matches := g.ac.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
if g.regex != nil {
if matches := g.regex.Match(input); len(matches) > 0 {
result = append(result, matches)
}
}
return CompositeMatches(result)
}
// MatchAny implements IndexMatcher.MatchAny.
func (g *MphIndexMatcher) MatchAny(input string) bool {
if g.mph != nil && g.mph.MatchAny(input) {
return true
}
if g.ac != nil && g.ac.MatchAny(input) {
return true
}
return g.regex != nil && g.regex.MatchAny(input)
}
// Size implements IndexMatcher.Size.
func (g *MphIndexMatcher) Size() uint32 {
return g.count
}
@@ -1,94 +0,0 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestMphIndexMatcher(t *testing.T) {
rules := []struct {
Type Type
Domain string
}{
{
Type: Regex,
Domain: "apis\\.us$",
},
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Domain,
Domain: "com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Substr,
Domain: "apis",
},
{
Type: Domain,
Domain: "googleapis.com",
},
{
Type: Full,
Domain: "fonts.googleapis.com",
},
{
Type: Full,
Domain: "www.baidu.com",
},
{
Type: Domain,
Domain: "example.com",
},
}
cases := []struct {
Input string
Output []uint32
}{
{
Input: "www.baidu.com",
Output: []uint32{5, 9, 4},
},
{
Input: "fonts.googleapis.com",
Output: []uint32{8, 3, 7, 4, 2, 6},
},
{
Input: "example.googleapis.com",
Output: []uint32{3, 7, 4, 2, 6},
},
{
Input: "testapis.us",
Output: []uint32{2, 6, 1},
},
{
Input: "example.com",
Output: []uint32{10, 4},
},
}
matcherGroup := NewMphIndexMatcher()
for _, rule := range rules {
matcher, err := rule.Type.New(rule.Domain)
common.Must(err)
matcherGroup.Add(matcher)
}
matcherGroup.Build()
for _, test := range cases {
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
t.Error("unexpected output: ", m, " for test case ", test)
}
}
}

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