Compare commits

..
Author SHA1 Message Date
Fangliding 2f16233259 upd reality 2026-05-01 17:50:53 +08:00
386 changed files with 10515 additions and 15537 deletions
-1
View File
@@ -1 +0,0 @@
powershell.exe -ExecutionPolicy Bypass -File ".\xray_no_window.ps1"
-1
View File
@@ -1 +0,0 @@
Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden
-1
View File
@@ -1 +0,0 @@
CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0
+3 -6
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 \
+3 -6
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 \
+1 -1
View File
@@ -65,7 +65,7 @@ jobs:
echo "LATEST=$LATEST" >>${GITHUB_ENV} echo "LATEST=$LATEST" >>${GITHUB_ENV}
- name: Checkout code - name: Checkout code
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v4
+8 -12
View File
@@ -11,16 +11,15 @@ 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@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-geodat- key: xray-geodat-
- name: Restore Wintun Cache - name: Restore Wintun Cache
uses: actions/cache/restore@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-wintun- key: xray-wintun-
@@ -76,14 +75,13 @@ 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 }}
CGO_ENABLED: 0 CGO_ENABLED: 0
steps: steps:
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: Show workflow information - name: Show workflow information
run: | run: |
@@ -119,13 +117,13 @@ jobs:
# 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
- name: Restore Geodat Cache - name: Restore Geodat Cache
uses: actions/cache/restore@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-geodat- key: xray-geodat-
- name: Restore Wintun Cache - name: Restore Wintun Cache
uses: actions/cache/restore@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-wintun- key: xray-wintun-
@@ -134,17 +132,15 @@ jobs:
run: | run: |
mv -f resources/geo* build_assets/ mv -f resources/geo* build_assets/
if [[ ${GOOS} == 'windows' ]]; then if [[ ${GOOS} == 'windows' ]]; then
cp .github/build/windows/* build_assets/ echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs
fi echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1
if [[ ${GOOS} == 'windows' ]]; then
echo 'Adding Wintun into packages'
if [[ ${GOARCH} == 'amd64' ]]; then if [[ ${GOARCH} == 'amd64' ]]; then
mv resources/wintun/bin/amd64/wintun.dll build_assets/ mv resources/wintun/bin/amd64/wintun.dll build_assets/
fi fi
if [[ ${GOARCH} == '386' ]]; then if [[ ${GOARCH} == '386' ]]; then
mv resources/wintun/bin/x86/wintun.dll build_assets/ mv resources/wintun/bin/x86/wintun.dll build_assets/
fi fi
mv resources/wintun/LICENSE.txt build_assets/LICENSE-Wintun mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt
fi fi
- name: Copy README.md & LICENSE - name: Copy README.md & LICENSE
+8 -15
View File
@@ -11,16 +11,15 @@ 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@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-geodat- key: xray-geodat-
- name: Restore Wintun Cache - name: Restore Wintun Cache
uses: actions/cache/restore@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-wintun- key: xray-wintun-
@@ -162,7 +161,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 }}
@@ -170,7 +168,7 @@ jobs:
CGO_ENABLED: 0 CGO_ENABLED: 0
steps: steps:
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: Set up NDK - name: Set up NDK
if: matrix.goos == 'android' if: matrix.goos == 'android'
@@ -210,9 +208,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
@@ -225,14 +220,14 @@ jobs:
fi fi
- name: Restore Geodat Cache - name: Restore Geodat Cache
uses: actions/cache/restore@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-geodat- key: xray-geodat-
- name: Restore Wintun Cache - name: Restore Wintun Cache
if: matrix.goos == 'windows' if: matrix.goos == 'windows'
uses: actions/cache/restore@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-wintun- key: xray-wintun-
@@ -241,10 +236,8 @@ jobs:
run: | run: |
mv -f resources/geo* build_assets/ mv -f resources/geo* build_assets/
if [[ ${GOOS} == 'windows' ]]; then if [[ ${GOOS} == 'windows' ]]; then
cp .github/build/windows/* build_assets/ echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs
fi echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1
if [[ ${GOOS} == 'windows' ]]; then
echo 'Adding Wintun into packages'
if [[ ${GOARCH} == 'amd64' ]]; then if [[ ${GOARCH} == 'amd64' ]]; then
mv resources/wintun/bin/amd64/wintun.dll build_assets/ mv resources/wintun/bin/amd64/wintun.dll build_assets/
fi fi
@@ -254,7 +247,7 @@ 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
mv resources/wintun/LICENSE.txt build_assets/LICENSE-Wintun mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt
fi fi
- name: Copy README.md & LICENSE - name: Copy README.md & LICENSE
+9 -16
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Restore Geodat Cache - name: Restore Geodat Cache
uses: actions/cache/restore@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-geodat- key: xray-geodat-
@@ -59,7 +59,7 @@ jobs:
done done
- name: Save Geodat Cache - name: Save Geodat Cache
uses: actions/cache/save@v6 uses: actions/cache/save@v5
if: ${{ steps.update.outputs.unhit }} if: ${{ steps.update.outputs.unhit }}
with: with:
path: resources path: resources
@@ -68,12 +68,9 @@ jobs:
wintun: wintun:
if: github.event.schedule == '30 22 * * *' || github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' if: github.event.schedule == '30 22 * * *' || github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
ASSETVER: 0.14.1
ASSETHASH: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
steps: steps:
- name: Restore Wintun Cache - name: Restore Wintun Cache
uses: actions/cache/restore@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-wintun- key: xray-wintun-
@@ -99,6 +96,7 @@ jobs:
echo -e "Checking if wintun.dll for ${ARCHITECTURE} exists..." echo -e "Checking if wintun.dll for ${ARCHITECTURE} exists..."
if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then
echo -e "wintun.dll for ${ARCHITECTURE} exists" echo -e "wintun.dll for ${ARCHITECTURE} exists"
continue
else else
echo -e "wintun.dll for ${ARCHITECTURE} is missing" echo -e "wintun.dll for ${ARCHITECTURE} is missing"
missing=true missing=true
@@ -115,21 +113,16 @@ jobs:
fi fi
if [[ "$missing" == true ]]; then if [[ "$missing" == true ]]; then
FILENAME=wintun.zip FILENAME=wintun.zip
DOWNLOAD_FILE=wintun-${ASSETVER}.zip DOWNLOAD_FILE=wintun-0.14.1.zip
echo -e "Downloading https://www.wintun.net/builds/${DOWNLOAD_FILE}..." echo -e "Downloading https://www.wintun.net/builds/${DOWNLOAD_FILE}..."
curl -L "https://www.wintun.net/builds/${DOWNLOAD_FILE}" -o "${FILENAME}" curl -L "https://www.wintun.net/builds/${DOWNLOAD_FILE}" -o "${FILENAME}"
if [[ "$(sha256sum "./${FILENAME}" | awk -F ' ' '{print $1}')" == "${ASSETHASH}" ]]; then echo -e "Unpacking wintun..."
echo -e "Unpacking wintun..." unzip -u ${FILENAME} -d resources/
unzip -u ${FILENAME} -d resources/ echo "unhit=true" >> $GITHUB_OUTPUT
echo "unhit=true" >> $GITHUB_OUTPUT
else
echo -e "Digest of ${FILENAME} mismatch."
exit 1
fi
fi fi
- name: Save Wintun Cache - name: Save Wintun Cache
uses: actions/cache/save@v6 uses: actions/cache/save@v5
if: ${{ steps.update.outputs.unhit }} if: ${{ steps.update.outputs.unhit }}
with: with:
path: resources path: resources
+5 -27
View File
@@ -1,4 +1,4 @@
name: Tests and Checkings name: Test
on: on:
push: push:
@@ -8,10 +8,9 @@ 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@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-geodat- key: xray-geodat-
@@ -37,10 +36,9 @@ jobs:
check-proto: check-proto:
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: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: Check Proto Version Header - name: Check Proto Version Header
run: | run: |
head -n 4 core/config.pb.go > ref.txt head -n 4 core/config.pb.go > ref.txt
@@ -52,28 +50,8 @@ jobs:
fi fi
done 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@v7
- 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 }}
@@ -83,14 +61,14 @@ jobs:
os: [windows-latest, ubuntu-latest, macos-latest] os: [windows-latest, ubuntu-latest, macos-latest]
steps: steps:
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v7 uses: actions/checkout@v6
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- name: Restore Geodat Cache - name: Restore Geodat Cache
uses: actions/cache/restore@v6 uses: actions/cache/restore@v5
with: with:
path: resources path: resources
key: xray-geodat- key: xray-geodat-
+3 -38
View File
@@ -73,7 +73,8 @@
- [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
- [Magic_V2Ray](https://github.com/vincentng295/Magic_V2Ray) - [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-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`
@@ -110,8 +111,6 @@
- [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) - [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)
@@ -119,8 +118,7 @@
- [SimpleXray](https://github.com/lhear/SimpleXray) - [SimpleXray](https://github.com/lhear/SimpleXray)
- [XrayFA](https://github.com/Q7DF1/XrayFA) - [XrayFA](https://github.com/Q7DF1/XrayFA)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal) - [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [OneXray](https://github.com/OneXray/OneXray) - [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk)
- [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)
@@ -145,14 +143,10 @@
- [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) - [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)
@@ -165,7 +159,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)
@@ -187,27 +180,6 @@
- [Xray-core v1.0.0](https://github.com/XTLS/Xray-core/releases/tag/v1.0.0) was forked from [v2fly-core 9a03cc5](https://github.com/v2fly/v2ray-core/commit/9a03cc5c98d04cc28320fcee26dbc236b3291256), and we have made & accumulated a huge number of enhancements over time, check [the release notes for each version](https://github.com/XTLS/Xray-core/releases). - [Xray-core v1.0.0](https://github.com/XTLS/Xray-core/releases/tag/v1.0.0) was forked from [v2fly-core 9a03cc5](https://github.com/v2fly/v2ray-core/commit/9a03cc5c98d04cc28320fcee26dbc236b3291256), and we have made & accumulated a huge number of enhancements over time, check [the release notes for each version](https://github.com/XTLS/Xray-core/releases).
- For third-party projects used in [Xray-core](https://github.com/XTLS/Xray-core), check your local or [the latest go.mod](https://github.com/XTLS/Xray-core/blob/main/go.mod). - For third-party projects used in [Xray-core](https://github.com/XTLS/Xray-core), check your local or [the latest go.mod](https://github.com/XTLS/Xray-core/blob/main/go.mod).
### Bundled Third-Party Components Redistribution
**Certain optional features dynamically load third-party components. These optional components are separate works distributed under their own licenses, and are bundled into the ZIP package for ease of use. Users may replace these components under the licenses from these components.**
These components include:
#### Wintun
This distribution contains unmodified official precompiled and pre-signed Wintun binaries.
- Project: Wintun
- Copyright: Copyright (C) 2018-2021 WireGuard LLC. All Rights Reserved.
- Redistribution License: Prebuilt Binaries License (PBL) bundled with official precompiled and pre-signed binaries from wintun.net
- Component(s): wintun.dll
- Source: https://www.wintun.net/
- Included in:
- Windows x86 (windows-32, win7-32)
- Windows x86-64 (windows-64, win7-64)
- Windows AArch64 (windows-arm64)
- Notes: Wintun is an optional runtime-loaded component only used for TUN inbound functionality on supported Windows platforms.
## One-line Compilation ## One-line Compilation
### Windows (PowerShell) ### Windows (PowerShell)
@@ -231,13 +203,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
} }
+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 {
+70 -4
View File
@@ -5,16 +5,18 @@ 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/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/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/common/utils"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
) )
@@ -86,7 +88,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
return nil, errors.New("failed to create hosts").Base(err) return nil, errors.New("failed to create hosts").Base(err)
} }
defaultTag := config.Tag var defaultTag = config.Tag
if len(config.Tag) == 0 { if len(config.Tag) == 0 {
defaultTag = generateRandomTag() defaultTag = generateRandomTag()
} }
@@ -139,7 +141,7 @@ 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
} }
@@ -221,7 +223,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 {
@@ -537,3 +539,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
}
+8 -8
View File
@@ -148,7 +148,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}}, IpsBlocked: &freedom.IPRules{},
}), }),
}, },
}, },
@@ -210,7 +210,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}}, IpsBlocked: &freedom.IPRules{},
}), }),
}, },
}, },
@@ -350,7 +350,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}}, IpsBlocked: &freedom.IPRules{},
}), }),
}, },
}, },
@@ -421,7 +421,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}}, IpsBlocked: &freedom.IPRules{},
}), }),
}, },
}, },
@@ -490,7 +490,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}}, IpsBlocked: &freedom.IPRules{},
}), }),
}, },
}, },
@@ -577,7 +577,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}}, IpsBlocked: &freedom.IPRules{},
}), }),
}, },
}, },
@@ -696,7 +696,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}}, IpsBlocked: &freedom.IPRules{},
}), }),
}, },
}, },
@@ -953,7 +953,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}}, IpsBlocked: &freedom.IPRules{},
}), }),
}, },
}, },
+6 -16
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
@@ -198,14 +193,9 @@ func parseResponse(payload []byte) (*IPRecord, error) {
ipRecord := &IPRecord{ ipRecord := &IPRecord{
ReqID: h.ID, ReqID: h.ID,
RCode: h.RCode, RCode: h.RCode,
Expire: now.Add(time.Second * dns_feature.DefaultTTL),
RawHeader: &h, RawHeader: &h,
} }
defer func() {
// set to default TTL if no valid TTL is found
if ipRecord.Expire.IsZero() {
ipRecord.Expire = now.Add(time.Second * dns_feature.DefaultTTL)
}
}()
L: L:
for { for {
@@ -222,7 +212,7 @@ L:
ttl = 1 ttl = 1
} }
expire := now.Add(time.Duration(ttl) * time.Second) expire := now.Add(time.Duration(ttl) * time.Second)
if ipRecord.Expire.IsZero() || ipRecord.Expire.After(expire) { if ipRecord.Expire.After(expire) {
ipRecord.Expire = expire ipRecord.Expire = expire
} }
+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)
+2 -3
View File
@@ -10,7 +10,6 @@ import (
"github.com/xtls/xray-core/common/geodata" "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/common/utils"
"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"
@@ -135,7 +134,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
} }
@@ -167,7 +166,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 -20
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 {
+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)
} }
} }
+41 -83
View File
@@ -2,7 +2,6 @@ package geodata
import ( import (
"context" "context"
"crypto/tls"
go_errors "errors" go_errors "errors"
"io" "io"
"net/http" "net/http"
@@ -10,7 +9,6 @@ import (
"path/filepath" "path/filepath"
"time" "time"
utls "github.com/refraction-networking/utls"
"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/platform/filesystem" "github.com/xtls/xray-core/common/platform/filesystem"
@@ -18,7 +16,6 @@ import (
"github.com/xtls/xray-core/common/utils" "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"
"golang.org/x/net/http2"
) )
const idleTimeout = 30 * time.Second const idleTimeout = 30 * time.Second
@@ -29,9 +26,8 @@ type stage struct {
} }
type downloader struct { type downloader struct {
ctx context.Context ctx context.Context
httpClient *http.Client client *http.Client
httpsClient *http.Client
} }
type idleConn struct { type idleConn struct {
@@ -57,84 +53,52 @@ func (c *idleConn) Write(b []byte) (int, error) {
func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader { func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader {
return &downloader{ return &downloader{
ctx: ctx, ctx: ctx,
httpClient: newClient(ctx, dispatcher, outbound, false), client: newClient(ctx, dispatcher, outbound),
httpsClient: newClient(ctx, dispatcher, outbound, true),
} }
} }
func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string, isHTTPS bool) *http.Client { func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string) *http.Client {
dial := func(ctx context.Context, network, address string) (net.Conn, error) { return &http.Client{
var conn net.Conn Transport: &http.Transport{
err := task.Run(ctx, func() error { Proxy: nil,
if tagged.Dialer == nil { DisableKeepAlives: true,
return errors.New("tagged dialer is not initialized") DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
} var conn net.Conn
dest, err := net.ParseDestination(network + ":" + address) err := task.Run(ctx, func() error {
if err != nil { if tagged.Dialer == nil {
return errors.New("cannot understand address").Base(err) return errors.New("tagged dialer is not initialized")
} }
c, err := tagged.Dialer(baseCtx, dispatcher, dest, outbound) dest, err := net.ParseDestination(network + ":" + address)
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
}
if isHTTPS {
return &http.Client{
Transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, network string, address string, cfg *tls.Config) (net.Conn, error) {
conn, err := dial(ctx, network, address)
if err != nil { if err != nil {
return nil, err return errors.New("cannot understand address").Base(err)
} }
host, _, _ := net.SplitHostPort(address) c, err := tagged.Dialer(baseCtx, dispatcher, dest, outbound)
tlsConn := utls.UClient(conn, &utls.Config{ServerName: host}, utls.HelloChrome_Auto) if err != nil {
handshakeCtx, cancel := context.WithTimeout(ctx, idleTimeout) return errors.New("cannot dial remote address ", dest).Base(err)
defer cancel()
if err := tlsConn.HandshakeContext(handshakeCtx); err != nil {
conn.Close()
return nil, err
} }
return tlsConn, nil conn = c
}, return nil
}, })
CheckRedirect: func(req *http.Request, via []*http.Request) error { if err != nil {
if req.URL.Scheme != "https" { return nil, errors.New("cannot finish connection").Base(err)
return errors.New("redirected to non-https URL: ", req.URL.String())
} }
if len(via) >= 10 { return &idleConn{
return errors.New("stopped after 10 redirects") Conn: conn,
} }, nil
return nil
}, },
} TLSHandshakeTimeout: idleTimeout,
} else { ResponseHeaderTimeout: idleTimeout,
return &http.Client{ },
Transport: &http.Transport{ CheckRedirect: func(req *http.Request, via []*http.Request) error {
Proxy: nil, if req.URL.Scheme != "https" {
DisableKeepAlives: true, return errors.New("redirected to non-https URL: ", req.URL.String())
DialContext: dial, }
ResponseHeaderTimeout: idleTimeout, if len(via) >= 10 {
}, return errors.New("stopped after 10 redirects")
CheckRedirect: func(req *http.Request, via []*http.Request) error { }
if req.URL.Scheme != "https" { return nil
return errors.New("redirected to non-https URL: ", req.URL.String()) },
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
} }
} }
@@ -196,13 +160,7 @@ func (d *downloader) fetch(rawURL string, writer io.Writer) error {
} }
utils.TryDefaultHeadersWith(req.Header, "nav") utils.TryDefaultHeadersWith(req.Header, "nav")
var client *http.Client resp, err := d.client.Do(req)
if req.URL.Scheme == "https" {
client = d.httpsClient
} else {
client = d.httpClient
}
resp, err := client.Do(req)
if err != nil { if err != nil {
return err return err
} }
+59 -150
View File
@@ -2,18 +2,15 @@ package metrics
import ( import (
"context" "context"
"encoding/json"
stderrors "errors"
"expvar" "expvar"
stdnet "net"
"net/http" "net/http"
"net/http/pprof" _ "net/http/pprof"
"strings" "strings"
"github.com/xtls/xray-core/app/observatory" "github.com/xtls/xray-core/app/observatory"
"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"
xnet "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/signal/done" "github.com/xtls/xray-core/common/signal/done"
"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"
@@ -24,17 +21,15 @@ import (
type MetricsHandler struct { type MetricsHandler struct {
ohm outbound.Manager ohm outbound.Manager
statsManager feature_stats.Manager statsManager feature_stats.Manager
ctx context.Context observatory extension.Observatory
tag string tag string
listen string listen string
tcpListener xnet.Listener tcpListener net.Listener
listener *OutboundListener
} }
// NewMetricsHandler creates a new MetricsHandler based on the given config. // NewMetricsHandler creates a new MetricsHandler based on the given config.
func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, error) { func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, error) {
c := &MetricsHandler{ c := &MetricsHandler{
ctx: ctx,
tag: config.Tag, tag: config.Tag,
listen: config.Listen, listen: config.Listen,
} }
@@ -42,6 +37,46 @@ func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, er
c.statsManager = sm c.statsManager = sm
c.ohm = om c.ohm = om
})) }))
expvar.Publish("stats", expvar.Func(func() interface{} {
resp := map[string]map[string]map[string]int64{
"inbound": {},
"outbound": {},
"user": {},
}
c.statsManager.VisitCounters(func(name string, counter feature_stats.Counter) bool {
nameSplit := strings.Split(name, ">>>")
typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3]
if item, found := resp[typeName][tagOrUser]; found {
item[direction] = counter.Value()
} else {
resp[typeName][tagOrUser] = map[string]int64{
direction: counter.Value(),
}
}
return true
})
return resp
}))
expvar.Publish("observatory", expvar.Func(func() interface{} {
if c.observatory == nil {
common.Must(core.RequireFeatures(ctx, func(observatory extension.Observatory) error {
c.observatory = observatory
return nil
}))
if c.observatory == nil {
return nil
}
}
resp := map[string]*observatory.OutboundStatus{}
if o, err := c.observatory.GetObservation(context.Background()); err != nil {
return err
} else {
for _, x := range o.(*observatory.ObservationResult).GetStatus() {
resp[x.OutboundTag] = x
}
}
return resp
}))
return c, nil return c, nil
} }
@@ -50,172 +85,46 @@ func (p *MetricsHandler) Type() interface{} {
} }
func (p *MetricsHandler) Start() error { func (p *MetricsHandler) Start() error {
handler := p.httpHandler()
// direct listen a port if listen is set // direct listen a port if listen is set
if p.listen != "" { if p.listen != "" {
TCPlistener, err := xnet.Listen("tcp", p.listen) TCPlistener, err := net.Listen("tcp", p.listen)
if err != nil { if err != nil {
return err return err
} }
p.tcpListener = TCPlistener p.tcpListener = TCPlistener
errors.LogInfo(context.Background(), "Metrics server listening on ", p.listen) errors.LogInfo(context.Background(), "Metrics server listening on ", p.listen)
go p.serve(TCPlistener, handler) go func() {
} if err := http.Serve(TCPlistener, http.DefaultServeMux); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
if p.tag == "" { }
if p.tcpListener == nil { }()
return errors.New("metrics must have a tag or listen address")
}
return nil
} }
listener := &OutboundListener{ listener := &OutboundListener{
buffer: make(chan xnet.Conn, 4), buffer: make(chan net.Conn, 4),
done: done.New(), done: done.New(),
} }
p.listener = listener
go p.serve(listener, handler) go func() {
if err := http.Serve(listener, http.DefaultServeMux); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}()
if err := p.ohm.RemoveHandler(context.Background(), p.tag); err != nil { if err := p.ohm.RemoveHandler(context.Background(), p.tag); err != nil {
errors.LogInfo(context.Background(), "failed to remove existing handler") errors.LogInfo(context.Background(), "failed to remove existing handler")
} }
if err := p.ohm.AddHandler(context.Background(), &Outbound{ return p.ohm.AddHandler(context.Background(), &Outbound{
tag: p.tag, tag: p.tag,
listener: listener, listener: listener,
}); err != nil { })
if closeErr := p.Close(); closeErr != nil {
errors.LogErrorInner(context.Background(), closeErr, "failed to close metrics server after start failure")
}
return err
}
return nil
} }
func (p *MetricsHandler) Close() error { func (p *MetricsHandler) Close() error {
var errs []error return nil
if p.tcpListener != nil {
errs = append(errs, p.tcpListener.Close())
p.tcpListener = nil
}
if p.listener != nil {
errs = append(errs, p.listener.Close())
p.listener = nil
}
if p.ohm != nil && p.tag != "" {
if err := p.ohm.RemoveHandler(context.Background(), p.tag); err != nil {
errors.LogInfo(context.Background(), "failed to remove metrics handler")
}
}
return errors.Combine(errs...)
}
func (p *MetricsHandler) serve(listener xnet.Listener, handler http.Handler) {
if err := http.Serve(listener, handler); err != nil && !isClosedListenerError(err) {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}
func isClosedListenerError(err error) bool {
if err == nil {
return true
}
if stderrors.Is(err, stdnet.ErrClosed) || stderrors.Is(err, http.ErrServerClosed) {
return true
}
errText := err.Error()
return strings.Contains(errText, "listen closed") ||
strings.Contains(errText, "use of closed network connection")
}
func (p *MetricsHandler) httpHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/debug/vars", p.handleDebugVars)
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
return mux
}
func (p *MetricsHandler) handleDebugVars(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
vars := map[string]json.RawMessage{}
expvar.Do(func(kv expvar.KeyValue) {
value := json.RawMessage(kv.Value.String())
if !json.Valid(value) {
value = json.RawMessage("null")
}
vars[kv.Key] = value
})
vars["stats"] = marshalJSON(p.stats())
vars["observatory"] = marshalJSON(p.observatoryStatus())
payload, err := json.Marshal(vars)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(payload)
}
func marshalJSON(value interface{}) json.RawMessage {
data, err := json.Marshal(value)
if err != nil {
return json.RawMessage("null")
}
return data
}
func (p *MetricsHandler) stats() map[string]map[string]map[string]int64 {
resp := map[string]map[string]map[string]int64{
"inbound": {},
"outbound": {},
"user": {},
}
p.statsManager.VisitCounters(func(name string, counter feature_stats.Counter) bool {
nameSplit := strings.Split(name, ">>>")
if len(nameSplit) < 4 {
return true
}
typeName, tagOrUser, direction := nameSplit[0], nameSplit[1], nameSplit[3]
items, found := resp[typeName]
if !found {
items = map[string]map[string]int64{}
resp[typeName] = items
}
if item, found := items[tagOrUser]; found {
item[direction] = counter.Value()
} else {
items[tagOrUser] = map[string]int64{
direction: counter.Value(),
}
}
return true
})
return resp
}
func (p *MetricsHandler) observatoryStatus() interface{} {
feature := core.MustFromContext(p.ctx).GetFeature(extension.ObservatoryType())
if feature == nil {
return nil
}
observatoryFeature := feature.(extension.Observatory)
resp := map[string]*observatory.OutboundStatus{}
if o, err := observatoryFeature.GetObservation(context.Background()); err != nil {
return err
} else {
for _, x := range o.(*observatory.ObservationResult).GetStatus() {
resp[x.OutboundTag] = x
}
}
return resp
} }
func init() { func init() {
-161
View File
@@ -1,161 +0,0 @@
package metrics
import (
"context"
"encoding/json"
stdnet "net"
"net/http"
"net/http/httptest"
"testing"
"github.com/xtls/xray-core/app/dispatcher"
"github.com/xtls/xray-core/app/proxyman"
_ "github.com/xtls/xray-core/app/proxyman/inbound"
_ "github.com/xtls/xray-core/app/proxyman/outbound"
appstats "github.com/xtls/xray-core/app/stats"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/core"
feature_outbound "github.com/xtls/xray-core/features/outbound"
)
func TestMetricsCanRestartInSameProcess(t *testing.T) {
for i := 0; i < 2; i++ {
server := startMetricsTestServer(t)
readMetricsVars(t, server)
readMetricsPprof(t, server)
if err := server.Close(); err != nil {
t.Fatalf("failed to close metrics server: %v", err)
}
}
}
func TestMetricsCanRunMultipleInstancesInSameProcess(t *testing.T) {
server1 := startMetricsTestServer(t)
t.Cleanup(func() {
_ = server1.Close()
})
server2 := startMetricsTestServer(t)
t.Cleanup(func() {
_ = server2.Close()
})
readMetricsVars(t, server1)
readMetricsVars(t, server2)
}
func TestMetricsListenOnlyWithoutTagDoesNotRegisterOutbound(t *testing.T) {
listen := pickMetricsListenAddress(t)
server := startMetricsTestServerWithMetricsConfig(t, &Config{
Listen: listen,
})
t.Cleanup(func() {
_ = server.Close()
})
response, err := http.Get("http://" + listen + "/debug/vars")
if err != nil {
t.Fatalf("failed to read listen-only metrics: %v", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("unexpected listen-only metrics status: %d", response.StatusCode)
}
outboundManager := server.GetFeature(feature_outbound.ManagerType()).(feature_outbound.Manager)
if handlers := outboundManager.ListHandlers(context.Background()); len(handlers) != 0 {
t.Fatalf("listen-only metrics registered outbound handlers: got %d, want 0", len(handlers))
}
}
func startMetricsTestServer(t *testing.T) *core.Instance {
return startMetricsTestServerWithMetricsConfig(t, &Config{
Tag: "metrics_out",
})
}
func startMetricsTestServerWithMetricsConfig(t *testing.T, metricsConfig *Config) *core.Instance {
t.Helper()
server, err := core.New(metricsTestConfig(metricsConfig))
if err != nil {
t.Fatalf("failed to create metrics server: %v", err)
}
if err := server.Start(); err != nil {
_ = server.Close()
t.Fatalf("failed to start metrics server: %v", err)
}
return server
}
func metricsTestConfig(metricsConfig *Config) *core.Config {
return &core.Config{
App: []*serial.TypedMessage{
serial.ToTypedMessage(&dispatcher.Config{}),
serial.ToTypedMessage(&proxyman.InboundConfig{}),
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
serial.ToTypedMessage(&appstats.Config{}),
serial.ToTypedMessage(metricsConfig),
},
}
}
func pickMetricsListenAddress(t *testing.T) string {
t.Helper()
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to pick metrics listen address: %v", err)
}
defer listener.Close()
return listener.Addr().String()
}
func readMetricsVars(t *testing.T, server *core.Instance) {
t.Helper()
recorder := httptest.NewRecorder()
metricsHandler(t, server).httpHandler().ServeHTTP(
recorder,
httptest.NewRequest(http.MethodGet, "/debug/vars", nil),
)
if recorder.Code != http.StatusOK {
t.Fatalf("unexpected metrics vars status: %d", recorder.Code)
}
var payload map[string]interface{}
if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil {
t.Fatalf("failed to decode metrics vars: %v", err)
}
if _, found := payload["stats"]; !found {
t.Fatal("metrics vars missing stats")
}
if _, found := payload["observatory"]; !found {
t.Fatal("metrics vars missing observatory")
}
}
func readMetricsPprof(t *testing.T, server *core.Instance) {
t.Helper()
recorder := httptest.NewRecorder()
metricsHandler(t, server).httpHandler().ServeHTTP(
recorder,
httptest.NewRequest(http.MethodGet, "/debug/pprof/goroutine?debug=1", nil),
)
if recorder.Code != http.StatusOK {
t.Fatalf("unexpected metrics pprof status: %d", recorder.Code)
}
}
func metricsHandler(t *testing.T, server *core.Instance) *MetricsHandler {
t.Helper()
feature := server.GetFeature((*MetricsHandler)(nil))
handler, ok := feature.(*MetricsHandler)
if !ok || handler == nil {
t.Fatal("metrics handler not registered")
}
return handler
}
+2
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"
@@ -71,6 +72,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++
+1 -1
View File
@@ -186,7 +186,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}
} }
+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))
+14 -22
View File
@@ -57,23 +57,16 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
if err != nil { if err != nil {
return nil, err return nil, err
} }
src := net.TCPDestination(net.AnyIP, 0)
if receiverConfig.Listen != nil {
src.Address = receiverConfig.Listen.AsAddress()
}
if receiverConfig.PortList != nil && len(receiverConfig.PortList.Range) > 0 {
src.Port = net.Port(receiverConfig.PortList.Range[0].From)
}
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
if err != nil {
return nil, errors.New("failed to parse stream config").Base(err).AtWarning()
}
newCtx := session.ContextWithInbound(ctx, &session.Inbound{Tag: tag, Source: src}) // Set tag and sniffing config in context before creating proxy
newCtx = session.ContextWithContent(newCtx, &session.Content{SniffingRequest: sniffingRequest}) // This allows proxies like TUN to access these settings
newCtx = session.ContextWithStreamSettings(newCtx, mss) ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: tag})
if receiverConfig.SniffingSettings != nil {
rawProxy, err := common.CreateObject(newCtx, proxyConfig) ctx = session.ContextWithContent(ctx, &session.Content{
SniffingRequest: sniffingRequest,
})
}
rawProxy, err := common.CreateObject(ctx, proxyConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -99,6 +92,11 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
address = net.AnyIP address = net.AnyIP
} }
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
if err != nil {
return nil, errors.New("failed to parse stream config").Base(err).AtWarning()
}
if receiverConfig.ReceiveOriginalDestination { if receiverConfig.ReceiveOriginalDestination {
if mss.SocketSettings == nil { if mss.SocketSettings == nil {
mss.SocketSettings = &internet.SocketConfig{} mss.SocketSettings = &internet.SocketConfig{}
@@ -172,12 +170,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
+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.
+6 -4
View File
@@ -18,9 +18,9 @@ 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/proxy/hysteria/account"
hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx"
"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"
@@ -134,8 +134,10 @@ 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 { type HysteriaInboundValidator interface{ HysteriaInboundValidator() *account.Validator }
ctx = hysteria.ContextWithValidator(ctx, v.HysteriaInboundValidator()) if v, ok := w.proxy.(HysteriaInboundValidator); ok {
ctx = hyCtx.ContextWithRequireDatagram(ctx, true)
ctx = hyCtx.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) {
+9 -4
View File
@@ -6,6 +6,7 @@ import (
goerrors "errors" goerrors "errors"
"io" "io"
"math/big" "math/big"
"os"
"github.com/xtls/xray-core/common/dice" "github.com/xtls/xray-core/common/dice"
@@ -108,9 +109,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
ctx = session.ContextWithFullHandler(ctx, h) ctx = session.ContextWithFullHandler(ctx, h)
newCtx := session.ContextWithStreamSettings(ctx, h.streamSettings) rawProxyHandler, err := common.CreateObject(ctx, proxyConfig)
rawProxyHandler, err := common.CreateObject(newCtx, proxyConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -307,6 +306,11 @@ func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connecti
ob := outbounds[len(outbounds)-1] ob := outbounds[len(outbounds)-1]
h.SetOutboundGateway(ctx, ob) h.SetOutboundGateway(ctx, ob)
} }
}
if conn, err := h.getUoTConnection(ctx, dest); err != os.ErrInvalid {
return conn, err
} }
conn, err := internet.Dial(ctx, dest, h.streamSettings) conn, err := internet.Dial(ctx, dest, h.streamSettings)
@@ -344,7 +348,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
@@ -392,6 +396,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)
+35
View File
@@ -0,0 +1,35 @@
package outbound
import (
"context"
"os"
"github.com/sagernet/sing/common/uot"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
)
func (h *Handler) getUoTConnection(ctx context.Context, dest net.Destination) (stat.Connection, error) {
if dest.Address == nil {
return nil, errors.New("nil destination address")
}
if !dest.Address.Family().IsDomain() {
return nil, os.ErrInvalid
}
var uotVersion int
if dest.Address.Domain() == uot.MagicAddress {
uotVersion = uot.Version
} else if dest.Address.Domain() == uot.LegacyMagicAddress {
uotVersion = uot.LegacyVersion
} else {
return nil, os.ErrInvalid
}
packetConn, err := internet.ListenSystemPacket(ctx, &net.UDPAddr{IP: net.AnyIP.IP(), Port: 0}, h.streamSettings.SocketSettings)
if err != nil {
return nil, errors.New("unable to listen socket").Base(err)
}
conn := uot.NewServerConn(packetConn, uotVersion)
return h.getStatCouterConnection(conn), nil
}
+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) {
+2
View File
@@ -110,6 +110,7 @@ func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) {
// 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
@@ -226,6 +227,7 @@ func (r *Router) RemoveRule(tag string) error {
return nil return nil
} }
return errors.New("empty tag name!") return errors.New("empty tag name!")
} }
// ListRule implements routing.Router // ListRule implements routing.Router
+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{
+49 -3
View File
@@ -7,15 +7,61 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"path/filepath"
"runtime"
"strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"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"
) )
// parseURL splits a webhook URL into an HTTP URL and an optional Unix socket
// path. For regular http/https URLs the input is returned unchanged with an
// empty socketPath. For Unix sockets the format is:
//
// /path/to/socket.sock:/http/path
// @abstract:/http/path
// @@padded:/http/path
//
// The :/ separator after the socket path delimits the HTTP request path.
// If omitted, "/" is used.
func parseURL(raw string) (httpURL, socketPath string) {
if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') {
return raw, ""
}
if idx := strings.Index(raw, ":/"); idx >= 0 {
return "http://localhost" + raw[idx+1:], raw[:idx]
}
return "http://localhost/", raw
}
// resolveSocketPath applies platform-specific transformations to a Unix
// socket path, matching the behaviour of the listen side in
// transport/internet/system_listener.go.
//
// For abstract sockets (prefix @) on Linux/Android:
// - single @ — used as-is (lock-free abstract socket)
// - double @@ — stripped to single @ and padded to
// syscall.RawSockaddrUnix{}.Path length (HAProxy compat)
func resolveSocketPath(path string) string {
if len(path) == 0 || path[0] != '@' {
return path
}
if runtime.GOOS != "linux" && runtime.GOOS != "android" {
return path
}
if len(path) > 1 && path[1] == '@' {
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path))
copy(fullAddr, path[1:])
return string(fullAddr)
}
return path
}
func ptr[T any](v T) *T { return &v } func ptr[T any](v T) *T { return &v }
type event struct { type event struct {
@@ -50,7 +96,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
return nil, nil return nil, nil
} }
httpURL, socketPath := utils.SplitHTTPUnixURL(cfg.Url) httpURL, socketPath := parseURL(cfg.Url)
h := &WebhookNotifier{ h := &WebhookNotifier{
url: httpURL, url: httpURL,
deduplication: cfg.Deduplication, deduplication: cfg.Deduplication,
@@ -61,7 +107,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
} }
if socketPath != "" { if socketPath != "" {
dialAddr := utils.ResolveSocketPath(socketPath) dialAddr := resolveSocketPath(socketPath)
h.client.Transport = &http.Transport{ h.client.Transport = &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer var d net.Dialer
+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 {
+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))
} }
+1 -1
View File
@@ -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))
} }
-49
View File
@@ -1,49 +0,0 @@
package geodata
import (
"sync"
"github.com/xtls/xray-core/common"
)
var privateIPMatcher = sync.OnceValue(func() IPMatcher {
return common.Must2(IPReg.BuildIPMatcher(common.Must2(ParseIPRules([]string{
"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.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/3",
"::/127",
"fc00::/7",
"fe80::/10",
"ff00::/8",
}))))
})
func GetPrivateIPMatcher() IPMatcher { return privateIPMatcher() }
var privateDomainMatcher = sync.OnceValue(func() DomainMatcher {
return common.Must2(DomainReg.BuildDomainMatcher(common.Must2(ParseDomainRules([]string{
"lan",
"localdomain",
"example",
"invalid",
"localhost",
"test",
"local",
"home.arpa",
"internal",
"regexp:^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$", // Dotless domains
}, Domain_Domain))))
})
func GetPrivateDomainMatcher() DomainMatcher { return privateDomainMatcher() }
+9 -10
View File
@@ -8,7 +8,6 @@ import (
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata/strmatcher" "github.com/xtls/xray-core/common/geodata/strmatcher"
"github.com/xtls/xray-core/common/utils"
) )
type DomainMatcher interface { type DomainMatcher interface {
@@ -26,7 +25,7 @@ type DomainMatcherFactory interface {
type MphDomainMatcherFactory struct { type MphDomainMatcherFactory struct {
sync.Mutex sync.Mutex
shared *utils.WeakCacheMap[string, strmatcher.MphValueMatcher] shared map[string]strmatcher.MatcherGroup // TODO: cleanup
} }
func buildDomainRulesKey(rules []*DomainRule) string { func buildDomainRulesKey(rules []*DomainRule) string {
@@ -66,7 +65,7 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
if key != "" { if key != "" {
f.Lock() f.Lock()
defer f.Unlock() defer f.Unlock()
if g, ok := f.shared.Load(key); ok { if g := f.shared[key]; g != nil {
errors.LogDebug(context.Background(), "geodata mph domain matcher cache HIT for ", len(rules), " rules") errors.LogDebug(context.Background(), "geodata mph domain matcher cache HIT for ", len(rules), " rules")
return g, nil return g, nil
} }
@@ -103,14 +102,14 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
return nil, err return nil, err
} }
if key != "" { if key != "" {
f.shared.Store(key, g) f.shared[key] = g
} }
return g, nil return g, nil
} }
type CompactDomainMatcherFactory struct { type CompactDomainMatcherFactory struct {
sync.Mutex sync.Mutex
shared *utils.WeakCacheMap[string, strmatcher.LinearAnyMatcher] shared map[string]strmatcher.MatcherSet // TODO: cleanup
} }
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) { func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
@@ -119,7 +118,7 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat
f.Lock() f.Lock()
defer f.Unlock() defer f.Unlock()
if s, ok := f.shared.Load(key); ok { if s := f.shared[key]; s != nil {
errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key) errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key)
return s, nil return s, nil
} }
@@ -139,7 +138,7 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat
} }
s.Add(m) s.Add(m)
} }
f.shared.Store(key, s) f.shared[key] = s
return s, err return s, err
} }
@@ -220,7 +219,7 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
case Domain_Regex: case Domain_Regex:
return strmatcher.Regex.New(d.Value) return strmatcher.Regex.New(d.Value)
case Domain_Domain: case Domain_Domain:
return strmatcher.Domain.New(strings.ToLower(d.Value)) return strmatcher.Domain.New(d.Value)
case Domain_Full: case Domain_Full:
return strmatcher.Full.New(strings.ToLower(d.Value)) return strmatcher.Full.New(strings.ToLower(d.Value))
default: default:
@@ -231,8 +230,8 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
func newDomainMatcherFactory() DomainMatcherFactory { func newDomainMatcherFactory() DomainMatcherFactory {
switch runtime.GOOS { switch runtime.GOOS {
case "ios", "android": case "ios", "android":
return &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()} return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
default: default:
return &MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()} return &MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
} }
} }
+6 -8
View File
@@ -7,11 +7,10 @@ import (
"testing" "testing"
"github.com/xtls/xray-core/common/geodata/strmatcher" "github.com/xtls/xray-core/common/geodata/strmatcher"
"github.com/xtls/xray-core/common/utils"
) )
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) { func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()} factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
matcher, err := factory.BuildMatcher([]*DomainRule{ matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}}, {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}}, {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
@@ -32,7 +31,7 @@ func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) { func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()} factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherSet)}
matcher, err := factory.BuildMatcher([]*DomainRule{ matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}}, {Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}}, {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
@@ -51,11 +50,10 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
} }
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) { func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
matcher, err := (&MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}). matcher, err := (&MphDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}).BuildMatcher([]*DomainRule{
BuildMatcher([]*DomainRule{ {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}}, {Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}}, })
})
if err != nil { if err != nil {
t.Fatalf("BuildMatcher() failed: %v", err) t.Fatalf("BuildMatcher() failed: %v", err)
} }
+7 -15
View File
@@ -6,14 +6,12 @@ import (
"sync/atomic" "sync/atomic"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/common/uuid"
) )
type DomainRegistry struct { type DomainRegistry struct {
mu sync.Mutex mu sync.Mutex
factory DomainMatcherFactory factory DomainMatcherFactory
matchers *utils.WeakCacheMap[uuid.UUID, DynamicDomainMatcher] matchers []*DynamicDomainMatcher
} }
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) { func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
@@ -26,7 +24,7 @@ func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher,
} }
d := NewDynamicDomainMatcher(rules, m) d := NewDynamicDomainMatcher(rules, m)
r.matchers.Store(uuid.New(), d) r.matchers = append(r.matchers, d)
return d, nil return d, nil
} }
@@ -34,20 +32,15 @@ func (r *DomainRegistry) Reload() error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
var matchers []*DynamicDomainMatcher errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(r.matchers), " domain matcher(s)")
r.matchers.Range(func(_ uuid.UUID, matcher *DynamicDomainMatcher) bool {
matchers = append(matchers, matcher)
return true
})
errors.LogInfo(context.Background(), "reloading GeoSite data for ", len(matchers), " domain matcher(s)")
factory := newDomainMatcherFactory() factory := newDomainMatcherFactory()
type reloadEntry struct { type reloadEntry struct {
dynamic *DynamicDomainMatcher dynamic *DynamicDomainMatcher
matcher DomainMatcher matcher DomainMatcher
} }
reloaded := make([]reloadEntry, len(matchers)) reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range matchers { for i, d := range r.matchers {
m, err := factory.BuildMatcher(d.rules) m, err := factory.BuildMatcher(d.rules)
if err != nil { if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i) errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i)
@@ -59,14 +52,13 @@ func (r *DomainRegistry) Reload() error {
entry.dynamic.Reload(entry.matcher) entry.dynamic.Reload(entry.matcher)
} }
r.factory = factory r.factory = factory
errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(matchers), " domain matcher(s)") errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(r.matchers), " domain matcher(s)")
return nil return nil
} }
func newDomainRegistry() *DomainRegistry { func newDomainRegistry() *DomainRegistry {
return &DomainRegistry{ return &DomainRegistry{
factory: newDomainMatcherFactory(), factory: newDomainMatcherFactory(),
matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicDomainMatcher](),
} }
} }
+4 -5
View File
@@ -11,7 +11,6 @@ import (
"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"
) )
@@ -807,7 +806,7 @@ func (mm *HeuristicMultiIPMatcher) SetReverse(reverse bool) {
type IPSetFactory struct { type IPSetFactory struct {
sync.Mutex sync.Mutex
shared *utils.WeakCacheMap[string, IPSet] shared map[string]*IPSet // TODO: cleanup
} }
func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, error) { func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, error) {
@@ -816,7 +815,7 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
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) errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key)
return ipset, nil return ipset, nil
} }
@@ -836,7 +835,7 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
return nil return nil
}) })
if err == nil { if err == nil {
f.shared.Store(key, ipset) f.shared[key] = ipset
} }
return ipset, err return ipset, err
} }
@@ -1019,5 +1018,5 @@ func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error
} }
func newIPSetFactory() *IPSetFactory { func newIPSetFactory() *IPSetFactory {
return &IPSetFactory{shared: utils.NewWeakCacheMap[string, IPSet]()} return &IPSetFactory{shared: make(map[string]*IPSet)}
} }
+3 -3
View File
@@ -315,7 +315,7 @@ func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
} }
if !matcher.AnyMatch([]net.IP{ if !matcher.AnyMatch([]net.IP{
{}, net.IP{},
ip("1.1.1.1"), ip("1.1.1.1"),
ip("8.8.8.8"), ip("8.8.8.8"),
}) { }) {
@@ -345,7 +345,7 @@ func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
if matcher.Matches([]net.IP{ if matcher.Matches([]net.IP{
ip("8.8.8.8"), ip("8.8.8.8"),
{}, net.IP{},
}) { }) {
t.Fatal("expect Matches to be false when any IP is invalid") t.Fatal("expect Matches to be false when any IP is invalid")
} }
@@ -362,7 +362,7 @@ func TestIPMatcherFilterIPs(t *testing.T) {
} }
matched, unmatched := matcher.FilterIPs([]net.IP{ matched, unmatched := matcher.FilterIPs([]net.IP{
{}, net.IP{},
ip("8.8.8.8"), ip("8.8.8.8"),
ip("91.108.255.254"), ip("91.108.255.254"),
ip("1.1.1.1"), ip("1.1.1.1"),
+11 -19
View File
@@ -7,27 +7,25 @@ import (
"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"
"github.com/xtls/xray-core/common/uuid"
) )
type IPRegistry struct { type IPRegistry struct {
mu sync.Mutex mu sync.Mutex
factory *IPSetFactory ipsetFactory *IPSetFactory
matchers *utils.WeakCacheMap[uuid.UUID, DynamicIPMatcher] matchers []*DynamicIPMatcher
} }
func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) { func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
m, err := buildOptimizedIPMatcher(r.factory, rules) m, err := buildOptimizedIPMatcher(r.ipsetFactory, rules)
if err != nil { if err != nil {
return nil, err return nil, err
} }
d := NewDynamicIPMatcher(rules, m) d := NewDynamicIPMatcher(rules, m)
r.matchers.Store(uuid.New(), d) r.matchers = append(r.matchers, d)
return d, nil return d, nil
} }
@@ -35,20 +33,15 @@ func (r *IPRegistry) Reload() error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
var matchers []*DynamicIPMatcher errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(r.matchers), " IP matcher(s)")
r.matchers.Range(func(_ uuid.UUID, matcher *DynamicIPMatcher) bool {
matchers = append(matchers, matcher)
return true
})
errors.LogInfo(context.Background(), "reloading GeoIP data for ", len(matchers), " IP matcher(s)")
factory := newIPSetFactory() factory := newIPSetFactory()
type reloadEntry struct { type reloadEntry struct {
dynamic *DynamicIPMatcher dynamic *DynamicIPMatcher
matcher IPMatcher matcher IPMatcher
} }
reloaded := make([]reloadEntry, len(matchers)) reloaded := make([]reloadEntry, len(r.matchers))
for i, d := range matchers { for i, d := range r.matchers {
m, err := buildOptimizedIPMatcher(factory, d.rules) m, err := buildOptimizedIPMatcher(factory, d.rules)
if err != nil { if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i) errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i)
@@ -59,15 +52,14 @@ func (r *IPRegistry) Reload() error {
for _, entry := range reloaded { for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher) entry.dynamic.Reload(entry.matcher)
} }
r.factory = factory r.ipsetFactory = factory
errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(matchers), " IP matcher(s)") errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(r.matchers), " IP matcher(s)")
return nil return nil
} }
func newIPRegistry() *IPRegistry { func newIPRegistry() *IPRegistry {
return &IPRegistry{ return &IPRegistry{
factory: newIPSetFactory(), ipsetFactory: newIPSetFactory(),
matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicIPMatcher](),
} }
} }
+2 -2
View File
@@ -138,7 +138,7 @@ func ParseDomainRule(r string, defaultType Domain_Type) (*DomainRule, error) {
} }
prefix := 0 prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} { for _, ext := range [...]string{"ext:", "ext-domain:"} {
if strings.HasPrefix(r, ext) { if strings.HasPrefix(r, ext) {
prefix = len(ext) prefix = len(ext)
break break
@@ -167,7 +167,7 @@ func ParseDomainRules(rules []string, defaultType Domain_Type) ([]*DomainRule, e
} }
prefix := 0 prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} { for _, ext := range [...]string{"ext:", "ext-domain:"} {
if strings.HasPrefix(r, ext) { if strings.HasPrefix(r, ext) {
prefix = len(ext) prefix = len(ext)
break break
+1
View File
@@ -98,6 +98,7 @@ func (l *generalLogger) run() {
} }
func (l *generalLogger) Handle(msg Message) { func (l *generalLogger) Handle(msg Message) {
select { select {
case l.buffer <- msg: case l.buffer <- msg:
default: default:
-348
View File
@@ -1,348 +0,0 @@
//go:build darwin
package net
import (
"bytes"
"net"
"net/netip"
"path/filepath"
"strings"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
"github.com/xtls/xray-core/common/errors"
)
const (
darwinProcPIDListFDs = 1
darwinProcPIDFDSocketInfo = 3
darwinProcFDTypeSocket = 2
darwinProcFDInfoSize = 8
darwinSocketFDInfoSize = 792
darwinSocketFDInfoPSIOff = 24
darwinSocketInfoProtoOff = darwinSocketFDInfoPSIOff + 156
darwinSocketInfoFamilyOff = darwinSocketFDInfoPSIOff + 160
darwinSocketInfoKindOff = darwinSocketFDInfoPSIOff + 232
darwinSocketInfoInSockOff = darwinSocketFDInfoPSIOff + 240
darwinInSockInfoFPortOff = darwinSocketInfoInSockOff
darwinInSockInfoLPortOff = darwinSocketInfoInSockOff + 4
darwinInSockInfoVFlagOff = darwinSocketInfoInSockOff + 24
darwinInSockInfoFAddrOff = darwinSocketInfoInSockOff + 32
darwinInSockInfoLAddrOff = darwinSocketInfoInSockOff + 48
darwinInSockInfoSize = 80
darwinInSockInfoIPv4 = 0x1
darwinInSockInfoIPv6 = 0x2
darwinSockInfoIN = 1
darwinSockInfoTCP = 2
)
type darwinSocketMatchLevel int
const (
darwinSocketNoMatch darwinSocketMatchLevel = iota
darwinSocketPortMatch
darwinSocketRemoteMatch
darwinSocketLocalMatch
darwinSocketExactMatch
)
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (PID int, Name string, AbsolutePath string, err error) {
isLocal, err := IsLocal(net.ParseIP(srcIP))
if err != nil {
return 0, "", "", errors.New("failed to determine if address is local: ", err)
}
if !isLocal {
return 0, "", "", ErrNotLocal
}
if network != "tcp" && network != "udp" {
panic("Unsupported network type for process lookup.")
}
srcAddr, err := netip.ParseAddr(srcIP)
if err != nil {
return 0, "", "", errors.New("invalid source IP address: ", srcIP)
}
srcAddr = srcAddr.Unmap()
var dstAddr netip.Addr
hasDstAddr := false
if destIP != "" && destPort != 0 {
dstAddr, err = netip.ParseAddr(destIP)
if err != nil {
return 0, "", "", errors.New("invalid destination IP address: ", destIP)
}
dstAddr = dstAddr.Unmap()
hasDstAddr = true
}
processes, err := unix.SysctlKinfoProcSlice("kern.proc.all")
if err != nil {
return 0, "", "", errors.New("failed to list processes").Base(err)
}
var bestPID int32
bestLevel := darwinSocketNoMatch
ambiguousBest := false
for _, process := range processes {
pid := process.Proc.P_pid
if pid <= 0 {
continue
}
matchLevel, err := darwinProcessSocketMatchLevel(pid, network, srcAddr, srcPort, dstAddr, destPort, hasDstAddr)
if err != nil || matchLevel == darwinSocketNoMatch {
continue
}
if matchLevel == darwinSocketExactMatch {
bestPID = pid
bestLevel = matchLevel
ambiguousBest = false
break
}
if matchLevel > bestLevel {
bestPID = pid
bestLevel = matchLevel
ambiguousBest = false
continue
}
if matchLevel == bestLevel {
ambiguousBest = true
}
}
if bestLevel == darwinSocketNoMatch {
return 0, "", "", errors.New("process not found for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
}
if ambiguousBest {
return 0, "", "", errors.New("ambiguous process match for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
}
absPath, err := darwinProcessPath(bestPID)
if err != nil {
return 0, "", "", errors.New("could not get process path for PID ", bestPID, ": ", err)
}
absPath = filepath.ToSlash(absPath)
return int(bestPID), filepath.Base(absPath), absPath, nil
}
func darwinProcessSocketMatchLevel(pid int32, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) (darwinSocketMatchLevel, error) {
fds, err := darwinProcessFDs(pid)
if err != nil {
return darwinSocketNoMatch, err
}
bestLevel := darwinSocketNoMatch
info := make([]byte, darwinSocketFDInfoSize)
for fd := 0; fd+darwinProcFDInfoSize <= len(fds); fd += darwinProcFDInfoSize {
fdNumber := int32(darwinReadNativeUint32(fds[fd : fd+4]))
fdType := darwinReadNativeUint32(fds[fd+4 : fd+8])
if fdType != darwinProcFDTypeSocket {
continue
}
n, err := darwinProcPIDFDInfo(pid, fdNumber, darwinProcPIDFDSocketInfo, info)
if err != nil || n < darwinSocketInfoInSockOff+darwinInSockInfoSize {
continue
}
level := darwinSocketInfoMatchLevel(info[:n], network, srcAddr, srcPort, dstAddr, dstPort, hasDstAddr)
if level == darwinSocketExactMatch {
return level, nil
}
if level > bestLevel {
bestLevel = level
}
}
return bestLevel, nil
}
func darwinProcessFDs(pid int32) ([]byte, error) {
n, err := darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, nil)
if err != nil {
return nil, err
}
if n <= 0 {
return nil, nil
}
buf := make([]byte, n)
n, err = darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, buf)
if err != nil {
return nil, err
}
return buf[:n], nil
}
func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) darwinSocketMatchLevel {
protocol := int(darwinReadNativeUint32(info[darwinSocketInfoProtoOff : darwinSocketInfoProtoOff+4]))
family := int(darwinReadNativeUint32(info[darwinSocketInfoFamilyOff : darwinSocketInfoFamilyOff+4]))
kind := int(darwinReadNativeUint32(info[darwinSocketInfoKindOff : darwinSocketInfoKindOff+4]))
switch network {
case "tcp":
if protocol != unix.IPPROTO_TCP || kind != darwinSockInfoTCP {
return darwinSocketNoMatch
}
case "udp":
if protocol != unix.IPPROTO_UDP || kind != darwinSockInfoIN {
return darwinSocketNoMatch
}
default:
return darwinSocketNoMatch
}
vflag := info[darwinInSockInfoVFlagOff]
if srcAddr.Is4() {
if family != unix.AF_INET || vflag&darwinInSockInfoIPv4 == 0 {
return darwinSocketNoMatch
}
} else {
if family != unix.AF_INET6 || vflag&darwinInSockInfoIPv6 == 0 {
return darwinSocketNoMatch
}
}
localPort := int32(darwinReadNativeUint32(info[darwinInSockInfoLPortOff : darwinInSockInfoLPortOff+4]))
if !darwinPortMatches(localPort, srcPort) {
return darwinSocketNoMatch
}
localAddrMatches := darwinAddrMatchesOrUnspecified(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], srcAddr)
foreignAddrRaw := info[darwinInSockInfoFAddrOff : darwinInSockInfoFAddrOff+16]
foreignPort := int32(darwinReadNativeUint32(info[darwinInSockInfoFPortOff : darwinInSockInfoFPortOff+4]))
if !hasDstAddr {
if localAddrMatches {
return darwinSocketExactMatch
}
return darwinSocketNoMatch
}
remoteMatches := darwinPortMatches(foreignPort, dstPort) && darwinAddrMatches(foreignAddrRaw, dstAddr)
if network == "udp" && darwinEndpointIsZero(foreignAddrRaw, foreignPort) && localAddrMatches {
return darwinSocketExactMatch
}
switch {
case localAddrMatches && remoteMatches:
return darwinSocketExactMatch
case localAddrMatches:
return darwinSocketLocalMatch
case remoteMatches:
return darwinSocketRemoteMatch
default:
return darwinSocketPortMatch
}
}
func darwinPortMatches(value int32, port uint16) bool {
raw := uint16(value)
return raw == port || darwinNtohs(raw) == port
}
func darwinNtohs(value uint16) uint16 {
return value<<8 | value>>8
}
func darwinAddrMatches(raw []byte, addr netip.Addr) bool {
if addr.Is4() {
ip := addr.As4()
return bytes.Equal(raw[12:16], ip[:])
}
ip := addr.As16()
return bytes.Equal(raw, ip[:])
}
func darwinAddrMatchesOrUnspecified(raw []byte, addr netip.Addr) bool {
if darwinAddrMatches(raw, addr) {
return true
}
if addr.Is4() {
return darwinBytesAreZero(raw[12:16])
}
return darwinBytesAreZero(raw)
}
func darwinEndpointIsZero(rawAddr []byte, port int32) bool {
return uint32(port) == 0 && darwinBytesAreZero(rawAddr)
}
func darwinBytesAreZero(raw []byte) bool {
for _, value := range raw {
if value != 0 {
return false
}
}
return true
}
func darwinReadNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}
func darwinProcessPath(pid int32) (string, error) {
buf := make([]byte, unix.PathMax)
n, err := darwinProcPIDPath(pid, buf)
if err != nil {
return "", err
}
if n <= 0 {
return "", errors.New("empty process path")
}
return strings.TrimRight(string(buf[:n]), "\x00"), nil
}
func darwinProcPIDInfo(pid int32, flavor int, arg uint64, buf []byte) (int, error) {
var ptr unsafe.Pointer
if len(buf) > 0 {
ptr = unsafe.Pointer(&buf[0])
}
r0, _, errno := syscall_syscall6(libc_proc_pidinfo_trampoline_addr, uintptr(pid), uintptr(flavor), uintptr(arg), uintptr(ptr), uintptr(len(buf)), 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
func darwinProcPIDFDInfo(pid int32, fd int32, flavor int, buf []byte) (int, error) {
var ptr unsafe.Pointer
if len(buf) > 0 {
ptr = unsafe.Pointer(&buf[0])
}
r0, _, errno := syscall_syscall6(libc_proc_pidfdinfo_trampoline_addr, uintptr(pid), uintptr(fd), uintptr(flavor), uintptr(ptr), uintptr(len(buf)), 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
func darwinProcPIDPath(pid int32, buf []byte) (int, error) {
r0, _, errno := syscall_syscall6(libc_proc_pidpath_trampoline_addr, uintptr(pid), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0, 0, 0)
if errno != 0 {
return 0, errno
}
return int(r0), nil
}
var libc_proc_pidinfo_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidinfo proc_pidinfo "/usr/lib/libproc.dylib"
var libc_proc_pidfdinfo_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidfdinfo proc_pidfdinfo "/usr/lib/libproc.dylib"
var libc_proc_pidpath_trampoline_addr uintptr
//go:cgo_import_dynamic libc_proc_pidpath proc_pidpath "/usr/lib/libproc.dylib"
// Implemented in the runtime package (runtime/sys_darwin.go).
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
//go:linkname syscall_syscall6 syscall.syscall6
-18
View File
@@ -1,18 +0,0 @@
//go:build darwin
#include "textflag.h"
TEXT libc_proc_pidinfo_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidinfo(SB)
GLOBL ·libc_proc_pidinfo_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidinfo_trampoline_addr(SB)/8, $libc_proc_pidinfo_trampoline<>(SB)
TEXT libc_proc_pidfdinfo_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidfdinfo(SB)
GLOBL ·libc_proc_pidfdinfo_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidfdinfo_trampoline_addr(SB)/8, $libc_proc_pidfdinfo_trampoline<>(SB)
TEXT libc_proc_pidpath_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_proc_pidpath(SB)
GLOBL ·libc_proc_pidpath_trampoline_addr(SB), RODATA, $8
DATA ·libc_proc_pidpath_trampoline_addr(SB)/8, $libc_proc_pidpath_trampoline<>(SB)
-293
View File
@@ -1,293 +0,0 @@
//go:build darwin
package net
import (
stdnet "net"
"net/netip"
"os"
"testing"
"time"
"unsafe"
"golang.org/x/sys/unix"
)
func TestFindProcessDarwinTCP(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.Dial("tcp", listener.Addr().String())
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
remote := conn.RemoteAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), remote.IP.String(), uint16(remote.Port))
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinUDP(t *testing.T) {
conn, err := stdnet.ListenUDP("udp", &stdnet.UDPAddr{IP: stdnet.ParseIP("127.0.0.1")})
if err != nil {
t.Fatal(err)
}
defer conn.Close()
local := conn.LocalAddr().(*stdnet.UDPAddr)
pid, name, path, err := FindProcess("udp", local.IP.String(), uint16(local.Port), "", 0)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinNonLocal(t *testing.T) {
_, _, _, err := FindProcess("tcp", "203.0.113.1", 80, "", 0)
if err != ErrNotLocal {
t.Fatalf("expected ErrNotLocal, got %v", err)
}
}
func TestFindProcessDarwinUnsupportedNetwork(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("expected panic")
}
}()
_, _, _, _ = FindProcess("icmp", "127.0.0.1", 0, "", 0)
}
func assertCurrentProcess(t *testing.T, pid int, name string, path string) {
t.Helper()
if pid != os.Getpid() {
t.Fatalf("expected pid %d, got %d (%s, %s)", os.Getpid(), pid, name, path)
}
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
if path == "" || name == "" {
t.Fatalf("expected process path and name, got name=%q path=%q", name, path)
}
if sameFile(executable, path) {
return
}
t.Fatalf("expected executable %q, got %q", executable, path)
}
func sameFile(left string, right string) bool {
leftInfo, leftErr := os.Stat(left)
rightInfo, rightErr := os.Stat(right)
if leftErr != nil || rightErr != nil {
return false
}
return os.SameFile(leftInfo, rightInfo)
}
func TestFindProcessDarwinTCPWithoutDestination(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "", 0)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestFindProcessDarwinTCPWithDifferentDestination(t *testing.T) {
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan stdnet.Conn, 1)
go func() {
conn, err := listener.Accept()
if err == nil {
accepted <- conn
return
}
close(accepted)
}()
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
serverConn := <-accepted
if serverConn == nil {
t.Fatal("server did not accept tcp connection")
}
defer serverConn.Close()
local := conn.LocalAddr().(*stdnet.TCPAddr)
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "203.0.113.10", 443)
if err != nil {
t.Fatal(err)
}
assertCurrentProcess(t, pid, name, path)
}
func TestDarwinSocketInfoMatchLevelFallbacks(t *testing.T) {
src := netip.MustParseAddr("198.18.0.2")
dst := netip.MustParseAddr("203.0.113.10")
otherLocal := netip.MustParseAddr("192.168.1.10")
otherRemote := netip.MustParseAddr("198.51.100.10")
unspecifiedLocal := netip.MustParseAddr("0.0.0.0")
tests := []struct {
name string
local netip.Addr
remote netip.Addr
hasDst bool
wantLevel darwinSocketMatchLevel
}{
{
name: "exact",
local: src,
remote: dst,
hasDst: true,
wantLevel: darwinSocketExactMatch,
},
{
name: "unspecified local with matching remote",
local: unspecifiedLocal,
remote: dst,
hasDst: true,
wantLevel: darwinSocketExactMatch,
},
{
name: "unspecified local without destination",
local: unspecifiedLocal,
remote: otherRemote,
hasDst: false,
wantLevel: darwinSocketExactMatch,
},
{
name: "local match with different remote",
local: src,
remote: otherRemote,
hasDst: true,
wantLevel: darwinSocketLocalMatch,
},
{
name: "remote match with different local",
local: otherLocal,
remote: dst,
hasDst: true,
wantLevel: darwinSocketRemoteMatch,
},
{
name: "port only with destination",
local: otherLocal,
remote: otherRemote,
hasDst: true,
wantLevel: darwinSocketPortMatch,
},
{
name: "different local without destination",
local: otherLocal,
remote: otherRemote,
hasDst: false,
wantLevel: darwinSocketNoMatch,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
info := newDarwinSocketInfo("tcp", test.local, 12345, test.remote, 443)
level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, test.hasDst)
if level != test.wantLevel {
t.Fatalf("unexpected match level: got %d, want %d", level, test.wantLevel)
}
})
}
}
func newDarwinSocketInfo(network string, local netip.Addr, localPort uint16, remote netip.Addr, remotePort uint16) []byte {
info := make([]byte, darwinSocketFDInfoSize)
switch network {
case "tcp":
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_TCP))
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoTCP))
case "udp":
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_UDP))
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoIN))
}
writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET))
info[darwinInSockInfoVFlagOff] = darwinInSockInfoIPv4
writeDarwinNativeUint32(info, darwinInSockInfoLPortOff, uint32(localPort))
writeDarwinNativeUint32(info, darwinInSockInfoFPortOff, uint32(remotePort))
copyDarwinIPv4(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], local)
copyDarwinIPv4(info[darwinInSockInfoFAddrOff:darwinInSockInfoFAddrOff+16], remote)
return info
}
func writeDarwinNativeUint32(b []byte, offset int, value uint32) {
*(*uint32)(unsafe.Pointer(&b[offset])) = value
}
func copyDarwinIPv4(dst []byte, addr netip.Addr) {
ip := addr.As4()
copy(dst[12:16], ip[:])
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !windows && !linux && !android && !darwin //go:build !windows && !linux && !android
package net package net
-1
View File
@@ -17,7 +17,6 @@ const (
UseFreedomSplice = "xray.buf.splice" UseFreedomSplice = "xray.buf.splice"
UseVmessPadding = "xray.vmess.padding" UseVmessPadding = "xray.vmess.padding"
UseCone = "xray.cone.disabled" UseCone = "xray.cone.disabled"
UseStrictJSON = "xray.json.strict"
BufferSize = "xray.ray.buffer.size" BufferSize = "xray.ray.buffer.size"
BrowserDialerAddress = "xray.browser.dialer" BrowserDialerAddress = "xray.browser.dialer"
+2 -3
View File
@@ -36,10 +36,9 @@ func TestNormalizeEnvName(t *testing.T) {
} }
func TestEnvFlag(t *testing.T) { func TestEnvFlag(t *testing.T) {
v := EnvFlag{ if v := (EnvFlag{
Name: "xxxxx.y", Name: "xxxxx.y",
}.GetValueAsInt(10) }.GetValueAsInt(10)); v != 10 {
if v != 10 {
t.Error("env value: ", v) t.Error("env value: ", v)
} }
} }
+10 -2
View File
@@ -28,6 +28,8 @@ const (
SecurityType_AUTO SecurityType = 2 SecurityType_AUTO SecurityType = 2
SecurityType_AES128_GCM SecurityType = 3 SecurityType_AES128_GCM SecurityType = 3
SecurityType_CHACHA20_POLY1305 SecurityType = 4 SecurityType_CHACHA20_POLY1305 SecurityType = 4
SecurityType_NONE SecurityType = 5 // [DEPRECATED 2023-06]
SecurityType_ZERO SecurityType = 6
) )
// Enum value maps for SecurityType. // Enum value maps for SecurityType.
@@ -37,12 +39,16 @@ var (
2: "AUTO", 2: "AUTO",
3: "AES128_GCM", 3: "AES128_GCM",
4: "CHACHA20_POLY1305", 4: "CHACHA20_POLY1305",
5: "NONE",
6: "ZERO",
} }
SecurityType_value = map[string]int32{ SecurityType_value = map[string]int32{
"UNKNOWN": 0, "UNKNOWN": 0,
"AUTO": 2, "AUTO": 2,
"AES128_GCM": 3, "AES128_GCM": 3,
"CHACHA20_POLY1305": 4, "CHACHA20_POLY1305": 4,
"NONE": 5,
"ZERO": 6,
} }
) )
@@ -123,13 +129,15 @@ const file_common_protocol_headers_proto_rawDesc = "" +
"\n" + "\n" +
"\x1dcommon/protocol/headers.proto\x12\x14xray.common.protocol\"H\n" + "\x1dcommon/protocol/headers.proto\x12\x14xray.common.protocol\"H\n" +
"\x0eSecurityConfig\x126\n" + "\x0eSecurityConfig\x126\n" +
"\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*L\n" + "\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*`\n" +
"\fSecurityType\x12\v\n" + "\fSecurityType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\b\n" + "\aUNKNOWN\x10\x00\x12\b\n" +
"\x04AUTO\x10\x02\x12\x0e\n" + "\x04AUTO\x10\x02\x12\x0e\n" +
"\n" + "\n" +
"AES128_GCM\x10\x03\x12\x15\n" + "AES128_GCM\x10\x03\x12\x15\n" +
"\x11CHACHA20_POLY1305\x10\x04B^\n" + "\x11CHACHA20_POLY1305\x10\x04\x12\b\n" +
"\x04NONE\x10\x05\x12\b\n" +
"\x04ZERO\x10\x06B^\n" +
"\x18com.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3" "\x18com.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3"
var ( var (
+2
View File
@@ -11,6 +11,8 @@ enum SecurityType {
AUTO = 2; AUTO = 2;
AES128_GCM = 3; AES128_GCM = 3;
CHACHA20_POLY1305 = 4; CHACHA20_POLY1305 = 4;
NONE = 5; // [DEPRECATED 2023-06]
ZERO = 6;
} }
message SecurityConfig { message SecurityConfig {
+10 -26
View File
@@ -1,41 +1,25 @@
package http package http
import ( import (
"context"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
) )
// ApplyTrustedXForwardedFor returns remoteAddr overridden by X-Forwarded-For only when a configured trusted header is present. // ParseXForwardedFor parses X-Forwarded-For header in http headers, and return the IP list in it.
func ApplyTrustedXForwardedFor(header http.Header, trusted []string, remoteAddr net.Addr) net.Addr { func ParseXForwardedFor(header http.Header) []net.Address {
value := header.Get("X-Forwarded-For") xff := header.Get("X-Forwarded-For")
if value == "" { if xff == "" {
return remoteAddr return nil
} }
for _, t := range trusted { list := strings.Split(xff, ",")
if len(header.Values(t)) > 0 { addrs := make([]net.Address, 0, len(list))
if idx := strings.IndexByte(value, ','); idx >= 0 { for _, proxy := range list {
value = value[:idx] addrs = append(addrs, net.ParseAddress(proxy))
}
if addr := net.ParseAddress(value); addr.Family().IsIP() {
return &net.TCPAddr{
IP: addr.IP(),
Port: 0,
}
}
return remoteAddr
}
} }
if len(trusted) == 0 { return addrs
errors.LogWarning(context.Background(), `received "X-Forwarded-For" from `, remoteAddr, ` but "sockopt.trustedXForwardedFor" is not configured; ignoring it and using the real remote address`)
} else {
errors.LogError(context.Background(), `ignored potentially forged "X-Forwarded-For" from `, remoteAddr, `: `, value)
}
return remoteAddr
} }
// RemoveHopByHopHeaders removes hop by hop headers in http header list. // RemoveHopByHopHeaders removes hop by hop headers in http header list.
+8 -33
View File
@@ -2,48 +2,23 @@ package http_test
import ( import (
"bufio" "bufio"
gonet "net"
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
"github.com/google/go-cmp/cmp"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
. "github.com/xtls/xray-core/common/protocol/http" . "github.com/xtls/xray-core/common/protocol/http"
) )
func TestApplyTrustedXForwardedFor(t *testing.T) { func TestParseXForwardedFor(t *testing.T) {
remoteAddr := &gonet.TCPAddr{IP: gonet.ParseIP("127.0.0.1"), Port: 12345} header := http.Header{}
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
t.Run("ignore X-Forwarded-For without trusted header", func(t *testing.T) { addrs := ParseXForwardedFor(header)
header := http.Header{} if r := cmp.Diff(addrs, []net.Address{net.ParseAddress("129.78.138.66"), net.ParseAddress("129.78.64.103")}); r != "" {
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103") t.Error(r)
}
if addr := ApplyTrustedXForwardedFor(header, nil, remoteAddr); addr != remoteAddr {
t.Fatalf("unexpected remote address: %v", addr)
}
})
t.Run("trust X-Forwarded-For", func(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
header.Add("X-Trusted-CDN", "")
addr := ApplyTrustedXForwardedFor(header, []string{"X-Trusted-CDN"}, remoteAddr)
if addr.String() != "129.78.138.66:0" {
t.Fatalf("unexpected remote address: %v", addr)
}
})
t.Run("ignore non-IP X-Forwarded-For", func(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "example.com")
header.Add("X-Trusted-CDN", "")
if addr := ApplyTrustedXForwardedFor(header, []string{"X-Trusted-CDN"}, remoteAddr); addr != remoteAddr {
t.Fatalf("unexpected remote address: %v", addr)
}
})
} }
func TestHopByHopHeadersRemoving(t *testing.T) { func TestHopByHopHeadersRemoving(t *testing.T) {
+1 -1
View File
@@ -19,7 +19,7 @@ type ID struct {
// Equals returns true if this ID equals to the other one. // Equals returns true if this ID equals to the other one.
func (id *ID) Equals(another *ID) bool { func (id *ID) Equals(another *ID) bool {
return id.uuid.Equals(&another.uuid) return id.uuid.Equals(&(another.uuid))
} }
func (id *ID) Bytes() []byte { func (id *ID) Bytes() []byte {
+1 -1
View File
@@ -229,7 +229,7 @@ func TestSniffQUICComplex(t *testing.T) {
t.Errorf("SniffQUIC() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("SniffQUIC() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if errors.Is(err, protocol.ErrProtoNeedMoreData) != tt.needsMoreData { if (errors.Is(err, protocol.ErrProtoNeedMoreData)) != tt.needsMoreData {
t.Errorf("SniffQUIC() error = %v, expectsNoClue %v", err, tt.needsMoreData) t.Errorf("SniffQUIC() error = %v, expectsNoClue %v", err, tt.needsMoreData)
return return
} }
+2 -2
View File
@@ -5,8 +5,8 @@ import (
) )
type ServerSpec struct { type ServerSpec struct {
Destination net.Destination Destination net.Destination
User *MemoryUser User *MemoryUser
} }
func NewServerSpec(dest net.Destination, user *MemoryUser) *ServerSpec { func NewServerSpec(dest net.Destination, user *MemoryUser) *ServerSpec {
+1
View File
@@ -228,6 +228,7 @@ func isValueKind(kind reflect.Kind) bool {
} }
func marshalInterface(v interface{}, ignoreNullValue bool, insertTypeInfo bool) interface{} { func marshalInterface(v interface{}, ignoreNullValue bool, insertTypeInfo bool) interface{} {
if r, ok := marshalKnownType(v, ignoreNullValue, insertTypeInfo); ok { if r, ok := marshalKnownType(v, ignoreNullValue, insertTypeInfo); ok {
return r return r
} }
+3 -1
View File
@@ -27,6 +27,7 @@ func TestMashalAccount(t *testing.T) {
j, ok := MarshalToJson(user, false) j, ok := MarshalToJson(user, false)
if !ok || strings.Contains(j, "_TypedMessage_") { if !ok || strings.Contains(j, "_TypedMessage_") {
t.Error("marshal account failed") t.Error("marshal account failed")
} }
@@ -78,12 +79,13 @@ func TestMashalStruct(t *testing.T) {
v := (*f2.Arr)[0]["foo"]["hello"] v := (*f2.Arr)[0]["foo"]["hello"]
if f1.N != f2.N || *f1.Np != *f2.Np || f1.S != f2.S || v != "world" { if f1.N != f2.N || *(f1.Np) != *(f2.Np) || f1.S != f2.S || v != "world" {
t.Error("f1 not equal to f2") t.Error("f1 not equal to f2")
} }
} }
func TestMarshalConfigJson(t *testing.T) { func TestMarshalConfigJson(t *testing.T) {
buf := bytes.NewBufferString(getConfig()) buf := bytes.NewBufferString(getConfig())
config, err := iserial.DecodeJSONConfig(buf) config, err := iserial.DecodeJSONConfig(buf)
if err != nil { if err != nil {
+29
View File
@@ -4,6 +4,7 @@ import (
"testing" "testing"
. "github.com/xtls/xray-core/common/serial" . "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
) )
func TestGetInstance(t *testing.T) { func TestGetInstance(t *testing.T) {
@@ -22,3 +23,31 @@ func TestConvertingNilMessage(t *testing.T) {
t.Error("expect nil, but actually not") t.Error("expect nil, but actually not")
} }
} }
func TestTypedMessageRoundTripPreservesFinalmaskCustomUDPMode(t *testing.T) {
msg := &custom.UDPConfig{
Mode: "standalone",
Client: []*custom.UDPItem{
{Rand: 12, Save: "txid"},
},
}
tm := ToTypedMessage(msg)
if tm == nil {
t.Fatal("expected typed message")
}
roundTrip, err := tm.GetInstance()
if err != nil {
t.Fatalf("GetInstance() failed: %v", err)
}
udp, ok := roundTrip.(*custom.UDPConfig)
if !ok {
t.Fatalf("unexpected round-trip type: %T", roundTrip)
}
if udp.GetMode() != "standalone" {
t.Fatalf("mode lost during typed message round-trip: got %q", udp.GetMode())
}
}
-10
View File
@@ -26,8 +26,6 @@ const (
fullHandlerKey ctx.SessionKey = 10 // outbound gets full handler fullHandlerKey ctx.SessionKey = 10 // outbound gets full handler
mitmAlpn11Key ctx.SessionKey = 11 // used by TLS dialer mitmAlpn11Key ctx.SessionKey = 11 // used by TLS dialer
mitmServerNameKey ctx.SessionKey = 12 // used by TLS dialer mitmServerNameKey ctx.SessionKey = 12 // used by TLS dialer
streamSettingsKey ctx.SessionKey = 13
) )
func ContextWithInbound(ctx context.Context, inbound *Inbound) context.Context { func ContextWithInbound(ctx context.Context, inbound *Inbound) context.Context {
@@ -194,11 +192,3 @@ func MitmServerNameFromContext(ctx context.Context) string {
} }
return "" return ""
} }
func ContextWithStreamSettings(ctx context.Context, streamSettings any) context.Context {
return context.WithValue(ctx, streamSettingsKey, streamSettings)
}
func StreamSettingsFromContext(ctx context.Context) any {
return ctx.Value(streamSettingsKey)
}
+4 -5
View File
@@ -3,7 +3,6 @@ package singbridge
import ( import (
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
) )
@@ -18,14 +17,14 @@ func ToNetwork(network string) net.Network {
} }
} }
func ToDestination(socksaddr M.Socksaddr, network net.Network) (net.Destination, error) { func ToDestination(socksaddr M.Socksaddr, network net.Network) net.Destination {
// IsFqdn() implicitly checks if the domain name is valid // IsFqdn() implicitly checks if the domain name is valid
if socksaddr.IsFqdn() { if socksaddr.IsFqdn() {
return net.Destination{ return net.Destination{
Network: network, Network: network,
Address: net.DomainAddress(socksaddr.Fqdn), Address: net.DomainAddress(socksaddr.Fqdn),
Port: net.Port(socksaddr.Port), Port: net.Port(socksaddr.Port),
}, nil }
} }
// IsIP() implicitly checks if the IP address is valid // IsIP() implicitly checks if the IP address is valid
@@ -34,10 +33,10 @@ func ToDestination(socksaddr M.Socksaddr, network net.Network) (net.Destination,
Network: network, Network: network,
Address: net.IPAddress(socksaddr.Addr.AsSlice()), Address: net.IPAddress(socksaddr.Addr.AsSlice()),
Port: net.Port(socksaddr.Port), Port: net.Port(socksaddr.Port),
}, nil }
} }
return net.Destination{}, errors.New("invalid socks address: ", socksaddr) return net.Destination{}
} }
func ToSocksaddr(destination net.Destination) M.Socksaddr { func ToSocksaddr(destination net.Destination) M.Socksaddr {
+2 -10
View File
@@ -26,11 +26,7 @@ func NewDialer(dialer internet.Dialer) *XrayDialer {
} }
func (d *XrayDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (d *XrayDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
dest, err := ToDestination(destination, ToNetwork(network)) return d.Dialer.Dial(ctx, ToDestination(destination, ToNetwork(network)))
if err != nil {
return nil, err
}
return d.Dialer.Dial(ctx, dest)
} }
func (d *XrayDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (d *XrayDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
@@ -47,17 +43,13 @@ func NewOutboundDialer(outbound proxy.Outbound, dialer internet.Dialer) *XrayOut
} }
func (d *XrayOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (d *XrayOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
dest, err := ToDestination(destination, ToNetwork(network))
if err != nil {
return nil, err
}
outbounds := session.OutboundsFromContext(ctx) outbounds := session.OutboundsFromContext(ctx)
if len(outbounds) == 0 { if len(outbounds) == 0 {
outbounds = []*session.Outbound{{}} outbounds = []*session.Outbound{{}}
ctx = session.ContextWithOutbounds(ctx, outbounds) ctx = session.ContextWithOutbounds(ctx, outbounds)
} }
ob := outbounds[len(outbounds)-1] ob := outbounds[len(outbounds)-1]
ob.Target = dest ob.Target = ToDestination(destination, ToNetwork(network))
opts := []pipe.Option{pipe.WithSizeLimit(64 * 1024)} opts := []pipe.Option{pipe.WithSizeLimit(64 * 1024)}
uplinkReader, uplinkWriter := pipe.New(opts...) uplinkReader, uplinkWriter := pipe.New(opts...)
+2 -10
View File
@@ -31,23 +31,15 @@ func NewDispatcher(dispatcher routing.Dispatcher, newErrorFunc func(values ...an
} }
func (d *Dispatcher) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { func (d *Dispatcher) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
dest, err := ToDestination(metadata.Destination, net.Network_TCP)
if err != nil {
return err
}
xConn := NewConn(conn) xConn := NewConn(conn)
return d.upstream.DispatchLink(ctx, dest, &transport.Link{ return d.upstream.DispatchLink(ctx, ToDestination(metadata.Destination, net.Network_TCP), &transport.Link{
Reader: xConn, Reader: xConn,
Writer: xConn, Writer: xConn,
}) })
} }
func (d *Dispatcher) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { func (d *Dispatcher) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
dest, err := ToDestination(metadata.Destination, net.Network_UDP) return d.upstream.DispatchLink(ctx, ToDestination(metadata.Destination, net.Network_UDP), &transport.Link{
if err != nil {
return err
}
return d.upstream.DispatchLink(ctx, dest, &transport.Link{
Reader: buf.NewPacketReader(conn.(io.Reader)), Reader: buf.NewPacketReader(conn.(io.Reader)),
Writer: buf.NewWriter(conn.(io.Writer)), Writer: buf.NewWriter(conn.(io.Writer)),
}) })
+30 -31
View File
@@ -10,21 +10,15 @@ import (
"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"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/signal"
"github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport"
) )
func CopyPacketConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, destination net.Destination, serverConn net.PacketConn) error { func CopyPacketConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, destination net.Destination, serverConn net.PacketConn) error {
cancel := func() {
common.Interrupt(link.Reader)
common.Interrupt(serverConn)
}
conn := &PacketConnWrapper{ conn := &PacketConnWrapper{
Reader: link.Reader, Reader: link.Reader,
Writer: link.Writer, Writer: link.Writer,
Dest: destination, Dest: destination,
Conn: inboundConn, Conn: inboundConn,
T: signal.CancelAfterInactivity(ctx, cancel, 300*time.Second),
} }
return ReturnError(bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(serverConn))) return ReturnError(bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(serverConn)))
} }
@@ -35,19 +29,11 @@ type PacketConnWrapper struct {
net.Conn net.Conn
Dest net.Destination Dest net.Destination
cached buf.MultiBuffer cached buf.MultiBuffer
// A simple patch to avoid goroutine leak since sing infra cannot awake read block by write err
T *signal.ActivityTimer
} }
func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (addr M.Socksaddr, err error) { // This ReadPacket implemented a timeout to avoid goroutine leak like PipeConnWrapper.Read()
w.T.Update() // as a temporarily solution
defer func() { func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (M.Socksaddr, error) {
if err != nil {
// uplinkonly
w.T.SetTimeout(2 * time.Second)
}
}()
if w.cached != nil { if w.cached != nil {
mb, bb := buf.SplitFirst(w.cached) mb, bb := buf.SplitFirst(w.cached)
if bb == nil { if bb == nil {
@@ -65,7 +51,30 @@ func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (addr M.Socksaddr, err
return ToSocksaddr(destination), nil return ToSocksaddr(destination), nil
} }
} }
mb, err := w.ReadMultiBuffer()
// timeout
type readResult struct {
mb buf.MultiBuffer
err error
}
c := make(chan readResult, 1)
go func() {
mb, err := w.ReadMultiBuffer()
c <- readResult{mb: mb, err: err}
}()
var mb buf.MultiBuffer
select {
case <-time.After(60 * time.Second):
common.Close(w.Reader)
common.Interrupt(w.Reader)
return M.Socksaddr{}, buf.ErrReadTimeout
case result := <-c:
if result.err != nil {
return M.Socksaddr{}, result.err
}
mb = result.mb
}
nb, bb := buf.SplitFirst(mb) nb, bb := buf.SplitFirst(mb)
if bb == nil { if bb == nil {
return M.Socksaddr{}, nil return M.Socksaddr{}, nil
@@ -83,22 +92,12 @@ func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (addr M.Socksaddr, err
} }
} }
func (w *PacketConnWrapper) WritePacket(buffer *B.Buffer, destination M.Socksaddr) (err error) { func (w *PacketConnWrapper) WritePacket(buffer *B.Buffer, destination M.Socksaddr) error {
w.T.Update()
defer func() {
if err != nil {
// downlinkonly
w.T.SetTimeout(5 * time.Second)
}
}()
endpoint, err := ToDestination(destination, net.Network_UDP)
if err != nil {
return err
}
vBuf := buf.New() vBuf := buf.New()
vBuf.Write(buffer.Bytes()) vBuf.Write(buffer.Bytes())
endpoint := ToDestination(destination, net.Network_UDP)
vBuf.UDP = &endpoint vBuf.UDP = &endpoint
return w.WriteMultiBuffer(buf.MultiBuffer{vBuf}) return w.Writer.WriteMultiBuffer(buf.MultiBuffer{vBuf})
} }
func (w *PacketConnWrapper) Close() error { func (w *PacketConnWrapper) Close() error {
+18 -18
View File
@@ -9,7 +9,6 @@ import (
"github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/bufio"
"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"
"github.com/xtls/xray-core/common/signal"
"github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport"
) )
@@ -23,11 +22,6 @@ func CopyConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, s
} else { } else {
conn.R = &buf.BufferedReader{Reader: link.Reader} conn.R = &buf.BufferedReader{Reader: link.Reader}
} }
cancel := func() {
common.Interrupt(link.Reader)
common.Interrupt(serverConn)
}
conn.T = signal.CancelAfterInactivity(ctx, cancel, 300*time.Second)
return ReturnError(bufio.CopyConn(ctx, conn, serverConn)) return ReturnError(bufio.CopyConn(ctx, conn, serverConn))
} }
@@ -35,27 +29,35 @@ type PipeConnWrapper struct {
R io.Reader R io.Reader
W buf.Writer W buf.Writer
net.Conn net.Conn
// A simple patch to avoid goroutine leak since sing infra cannot awake read block by write err
T *signal.ActivityTimer
} }
func (w *PipeConnWrapper) Close() error { func (w *PipeConnWrapper) Close() error {
return nil return nil
} }
// This Read implemented a timeout to avoid goroutine leak.
// as a temporarily solution
func (w *PipeConnWrapper) Read(b []byte) (n int, err error) { func (w *PipeConnWrapper) Read(b []byte) (n int, err error) {
w.T.Update() type readResult struct {
n, err = w.R.Read(b) n int
if err != nil { err error
// uplinkonly }
w.T.SetTimeout(2 * time.Second) c := make(chan readResult, 1)
go func() {
n, err := w.R.Read(b)
c <- readResult{n: n, err: err}
}()
select {
case result := <-c:
return result.n, result.err
case <-time.After(300 * time.Second):
common.Close(w.R)
common.Interrupt(w.R)
return 0, buf.ErrReadTimeout
} }
return
} }
func (w *PipeConnWrapper) Write(p []byte) (n int, err error) { func (w *PipeConnWrapper) Write(p []byte) (n int, err error) {
w.T.Update()
n = len(p) n = len(p)
var mb buf.MultiBuffer var mb buf.MultiBuffer
pLen := len(p) pLen := len(p)
@@ -74,8 +76,6 @@ func (w *PipeConnWrapper) Write(p []byte) (n int, err error) {
if err != nil { if err != nil {
n = 0 n = 0
buf.ReleaseMulti(mb) buf.ReleaseMulti(mb)
// downlinkonly
w.T.SetTimeout(5 * time.Second)
} }
return return
} }
-43
View File
@@ -1,43 +0,0 @@
package task
import (
"runtime"
"golang.org/x/sync/errgroup"
)
// ParallelForN runs fn(0..n-1) in parallel across runtime.GOMAXPROCS(0) worker
// goroutines. Indices are partitioned into contiguous chunks so the number of
// spawned goroutines stays bounded regardless of n.
//
// fn must be safe to call concurrently from different goroutines (each call
// receives its own unique index). Output collected by writing to indexed slots
// in a pre-allocated slice is a common safe pattern.
//
// Returns the first non-nil error reported by fn; other workers may still be
// finishing briefly afterwards.
func ParallelForN(n int, fn func(i int) error) error {
if n <= 0 {
return nil
}
workers := max(runtime.GOMAXPROCS(0), 1)
workers = min(workers, n)
chunk := (n + workers - 1) / workers
var eg errgroup.Group
for w := range workers {
start := w * chunk
end := min(start+chunk, n)
if start >= end {
break
}
eg.Go(func() error {
for i := start; i < end; i++ {
if err := fn(i); err != nil {
return err
}
}
return nil
})
}
return eg.Wait()
}
-50
View File
@@ -1,50 +0,0 @@
package task_test
import (
"errors"
"sync/atomic"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/task"
)
func TestParallelForN_Empty(t *testing.T) {
called := false
err := ParallelForN(0, func(i int) error {
called = true
return nil
})
common.Must(err)
if called {
t.Fatal("fn should not be called when n=0")
}
}
func TestParallelForN_AllIndicesCovered(t *testing.T) {
const N = 10000
var seen [N]int32
err := ParallelForN(N, func(i int) error {
atomic.AddInt32(&seen[i], 1)
return nil
})
common.Must(err)
for i := 0; i < N; i++ {
if seen[i] != 1 {
t.Fatalf("index %d called %d times, expected 1", i, seen[i])
}
}
}
func TestParallelForN_Error(t *testing.T) {
boom := errors.New("boom")
err := ParallelForN(1000, func(i int) error {
if i == 42 {
return boom
}
return nil
})
if err != boom {
t.Fatalf("expected %v, got %v", boom, err)
}
}
+7 -14
View File
@@ -10,44 +10,37 @@ func TestByteSizes(t *testing.T) {
size := units.ByteSize(0) size := units.ByteSize(0)
assertSizeString(t, size, "0") assertSizeString(t, size, "0")
size++ size++
assertSizeValue( assertSizeValue(t,
t,
assertSizeString(t, size, "1.00B"), assertSizeString(t, size, "1.00B"),
size, size,
) )
size <<= 10 size <<= 10
assertSizeValue( assertSizeValue(t,
t,
assertSizeString(t, size, "1.00KB"), assertSizeString(t, size, "1.00KB"),
size, size,
) )
size <<= 10 size <<= 10
assertSizeValue( assertSizeValue(t,
t,
assertSizeString(t, size, "1.00MB"), assertSizeString(t, size, "1.00MB"),
size, size,
) )
size <<= 10 size <<= 10
assertSizeValue( assertSizeValue(t,
t,
assertSizeString(t, size, "1.00GB"), assertSizeString(t, size, "1.00GB"),
size, size,
) )
size <<= 10 size <<= 10
assertSizeValue( assertSizeValue(t,
t,
assertSizeString(t, size, "1.00TB"), assertSizeString(t, size, "1.00TB"),
size, size,
) )
size <<= 10 size <<= 10
assertSizeValue( assertSizeValue(t,
t,
assertSizeString(t, size, "1.00PB"), assertSizeString(t, size, "1.00PB"),
size, size,
) )
size <<= 10 size <<= 10
assertSizeValue( assertSizeValue(t,
t,
assertSizeString(t, size, "1.00EB"), assertSizeString(t, size, "1.00EB"),
size, size,
) )
+43 -77
View File
@@ -4,10 +4,10 @@ import (
"hash/fnv" "hash/fnv"
"math" "math"
"math/rand" "math/rand"
"net/http"
"strconv" "strconv"
"strings"
"time" "time"
"net/http"
"strings"
"github.com/klauspost/cpuid/v2" "github.com/klauspost/cpuid/v2"
) )
@@ -18,7 +18,6 @@ func GetRandomizer() *rand.Rand {
fnvHash.Write([]byte(strconv.Itoa(cpuid.CPU.Family) + strconv.Itoa(cpuid.CPU.Model) + strconv.Itoa(cpuid.CPU.PhysicalCores) + strconv.Itoa(cpuid.CPU.LogicalCores) + strconv.Itoa(cpuid.CPU.CacheLine) + strconv.Itoa(cpuid.CPU.ThreadsPerCore))) fnvHash.Write([]byte(strconv.Itoa(cpuid.CPU.Family) + strconv.Itoa(cpuid.CPU.Model) + strconv.Itoa(cpuid.CPU.PhysicalCores) + strconv.Itoa(cpuid.CPU.LogicalCores) + strconv.Itoa(cpuid.CPU.CacheLine) + strconv.Itoa(cpuid.CPU.ThreadsPerCore)))
return rand.New(rand.NewSource(int64(fnvHash.Sum64()))) return rand.New(rand.NewSource(int64(fnvHash.Sum64())))
} }
var globalRng *rand.Rand = GetRandomizer() var globalRng *rand.Rand = GetRandomizer()
// The Chrome version generator will suffer from deviation of a normal distribution. // The Chrome version generator will suffer from deviation of a normal distribution.
@@ -27,114 +26,83 @@ func ChromeVersion() int {
var startVersion int = 144 var startVersion int = 144
var timeStart int64 = time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC).Unix() / 86400 var timeStart int64 = time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC).Unix() / 86400
var timeCurrent int64 = time.Now().Unix() / 86400 var timeCurrent int64 = time.Now().Unix() / 86400
var timeDiff int = int((timeCurrent - timeStart - 35)) - int(math.Floor(math.Pow(globalRng.Float64(), 2)*105)) var timeDiff int = int((timeCurrent - timeStart - 35)) - int(math.Floor(math.Pow(globalRng.Float64(), 2) * 105))
return startVersion + (timeDiff / 35) // It's 31.15 currently. return startVersion + (timeDiff / 35) // It's 31.15 currently.
} }
var safariMinorMap [25]int = [25]int{ var safariMinorMap [25]int = [25]int{0, 0, 0, 1, 1,
0, 0, 0, 1, 1,
1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4,
4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6}
}
// The following version generators use deterministic generators, but with the distribution scaled by a curve. // The following version generators use deterministic generators, but with the distribution scaled by a curve.
func CurlVersion() string { func CurlVersion() string {
// curl 8.0.0 was released on 20/03/2023. // curl 8.0.0 was released on 20/03/2023.
var timeCurrent int64 = time.Now().Unix() / 86400 var timeCurrent int64 = time.Now().Unix() / 86400
var timeStart int64 = time.Date(2023, 3, 20, 0, 0, 0, 0, time.UTC).Unix() / 86400 var timeStart int64 = time.Date(2023, 3, 20, 0, 0, 0, 0, time.UTC).Unix() / 86400
var timeDiff int = int((timeCurrent - timeStart - 60)) - int(math.Floor(math.Pow(globalRng.Float64(), 2)*165)) var timeDiff int = int((timeCurrent - timeStart - 60)) - int(math.Floor(math.Pow(globalRng.Float64(), 2) * 165))
var minorValue int = int(timeDiff / 57) // The release cadence is actually 56.67 days. var minorValue int = int(timeDiff / 57) // The release cadence is actually 56.67 days.
return "8." + strconv.Itoa(minorValue) + ".0" return "8." + strconv.Itoa(minorValue) + ".0"
} }
func FirefoxVersion() int { func FirefoxVersion() int {
// Firefox 128 ESR was released on 09/07/2023. // Firefox 128 ESR was released on 09/07/2023.
var timeCurrent int64 = time.Now().Unix() / 86400 var timeCurrent int64 = time.Now().Unix() / 86400
var timeStart int64 = time.Date(2024, 7, 29, 0, 0, 0, 0, time.UTC).Unix() / 86400 var timeStart int64 = time.Date(2024, 7, 29, 0, 0, 0, 0, time.UTC).Unix() / 86400
timeDiff := timeCurrent - timeStart - 25 - int64(math.Floor(math.Pow(globalRng.Float64(), 2)*50)) var timeDiff = timeCurrent - timeStart - 25 - int64(math.Floor(math.Pow(globalRng.Float64(), 2) * 50))
return int(timeDiff/30) + 128 return int(timeDiff / 30) + 128
} }
func SafariVersion() string { func SafariVersion() string {
var anchoredTime time.Time = time.Now() var anchoredTime time.Time = time.Now()
var releaseYear int = anchoredTime.Year() var releaseYear int = anchoredTime.Year()
var splitPoint time.Time = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC) var splitPoint time.Time = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC)
delayedDays := int(math.Floor(math.Pow(globalRng.Float64(), 3) * 75)) var delayedDays = int(math.Floor(math.Pow(globalRng.Float64(), 3) * 75))
splitPoint = splitPoint.AddDate(0, 0, delayedDays) splitPoint = splitPoint.AddDate(0, 0, delayedDays)
if anchoredTime.Compare(splitPoint) < 0 { if (anchoredTime.Compare(splitPoint) < 0) {
releaseYear-- releaseYear --
splitPoint = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC) splitPoint = time.Date(releaseYear, 9, 23, 0, 0, 0, 0, time.UTC)
splitPoint = splitPoint.AddDate(0, 0, delayedDays) splitPoint = splitPoint.AddDate(0, 0, delayedDays)
} }
minorVersion := safariMinorMap[(anchoredTime.Unix()-splitPoint.Unix())/1296000] var minorVersion = safariMinorMap[(anchoredTime.Unix() - splitPoint.Unix()) / 1296000]
return strconv.Itoa(releaseYear-1999) + "." + strconv.Itoa(minorVersion) return strconv.Itoa(releaseYear - 1999) + "." + strconv.Itoa(minorVersion)
} }
// The full Chromium brand GREASE implementation // The full Chromium brand GREASE implementation
var ( var clientHintGreaseNA = []string{" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"}
clientHintGreaseNA = []string{" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"} var clientHintVersionNA = []string{"8", "99", "24"}
clientHintVersionNA = []string{"8", "99", "24"} var clientHintShuffle3 = [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}}
clientHintShuffle3 = [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}} var clientHintShuffle4 = [][4]int{
clientHintShuffle4 = [][4]int{ {0, 1, 2, 3}, {0, 1, 3, 2}, {0, 2, 1, 3}, {0, 2, 3, 1}, {0, 3, 1, 2}, {0, 3, 2, 1},
{0, 1, 2, 3}, {1, 0, 2, 3}, {1, 0, 3, 2}, {1, 2, 0, 3}, {1, 2, 3, 0}, {1, 3, 0, 2}, {1, 3, 2, 0},
{0, 1, 3, 2}, {2, 0, 1, 3}, {2, 0, 3, 1}, {2, 1, 0, 3}, {2, 1, 3, 0}, {2, 3, 0, 1}, {2, 3, 1, 0},
{0, 2, 1, 3}, {3, 0, 1, 2}, {3, 0, 2, 1}, {3, 1, 0, 2}, {3, 1, 2, 0}, {3, 2, 0, 1}, {3, 2, 1, 0}}
{0, 2, 3, 1},
{0, 3, 1, 2},
{0, 3, 2, 1},
{1, 0, 2, 3},
{1, 0, 3, 2},
{1, 2, 0, 3},
{1, 2, 3, 0},
{1, 3, 0, 2},
{1, 3, 2, 0},
{2, 0, 1, 3},
{2, 0, 3, 1},
{2, 1, 0, 3},
{2, 1, 3, 0},
{2, 3, 0, 1},
{2, 3, 1, 0},
{3, 0, 1, 2},
{3, 0, 2, 1},
{3, 1, 0, 2},
{3, 1, 2, 0},
{3, 2, 0, 1},
{3, 2, 1, 0},
}
)
func getGreasedChInvalidBrand(seed int) string { func getGreasedChInvalidBrand(seed int) string {
return "\"Not" + clientHintGreaseNA[seed%len(clientHintGreaseNA)] + "A" + clientHintGreaseNA[(seed+1)%len(clientHintGreaseNA)] + "Brand\";v=\"" + clientHintVersionNA[seed%len(clientHintVersionNA)] + "\"" return "\"Not" + clientHintGreaseNA[seed % len(clientHintGreaseNA)] + "A" + clientHintGreaseNA[(seed + 1) % len(clientHintGreaseNA)] + "Brand\";v=\"" + clientHintVersionNA[seed % len(clientHintVersionNA)] + "\"";
} }
func getGreasedChOrder(brandLength int, seed int) []int { func getGreasedChOrder(brandLength int, seed int) []int {
switch brandLength { switch brandLength {
case 1: case 1:
return []int{0} return []int{0}
case 2: case 2:
return []int{seed % brandLength, (seed + 1) % brandLength} return []int{seed % brandLength, (seed + 1) % brandLength}
case 3: case 3:
return clientHintShuffle3[seed%len(clientHintShuffle3)][:] return clientHintShuffle3[seed % len(clientHintShuffle3)][:]
default: default:
return clientHintShuffle4[seed%len(clientHintShuffle4)][:] return clientHintShuffle4[seed % len(clientHintShuffle4)][:]
} }
//return []int{} //return []int{}
} }
func getUngreasedChUa(majorVersion int, forkName string) []string { func getUngreasedChUa(majorVersion int, forkName string) []string {
// Set the capacity to 4, the maximum allowed brand size, so Go will never allocate memory twice // Set the capacity to 4, the maximum allowed brand size, so Go will never allocate memory twice
baseChUa := make([]string, 0, 4) baseChUa := make([]string, 0, 4)
baseChUa = append(baseChUa, getGreasedChInvalidBrand(majorVersion), baseChUa = append(baseChUa, getGreasedChInvalidBrand(majorVersion),
"\"Chromium\";v=\""+strconv.Itoa(majorVersion)+"\"") "\"Chromium\";v=\"" + strconv.Itoa(majorVersion) + "\"")
switch forkName { switch forkName {
case "chrome": case "chrome":
baseChUa = append(baseChUa, "\"Google Chrome\";v=\""+strconv.Itoa(majorVersion)+"\"") baseChUa = append(baseChUa, "\"Google Chrome\";v=\"" + strconv.Itoa(majorVersion) + "\"")
case "edge": case "edge":
baseChUa = append(baseChUa, "\"Microsoft Edge\";v=\""+strconv.Itoa(majorVersion)+"\"") baseChUa = append(baseChUa, "\"Microsoft Edge\";v=\"" + strconv.Itoa(majorVersion) + "\"")
} }
return baseChUa return baseChUa
} }
func getGreasedChUa(majorVersion int, forkName string) string { func getGreasedChUa(majorVersion int, forkName string) string {
ungreasedCh := getUngreasedChUa(majorVersion, forkName) ungreasedCh := getUngreasedChUa(majorVersion, forkName)
shuffleMap := getGreasedChOrder(len(ungreasedCh), majorVersion) shuffleMap := getGreasedChOrder(len(ungreasedCh), majorVersion)
@@ -146,18 +114,16 @@ func getGreasedChUa(majorVersion int, forkName string) string {
} }
// The code below provides a coherent default browser user agent string based on a CPU-seeded PRNG. // The code below provides a coherent default browser user agent string based on a CPU-seeded PRNG.
var ( var CurlUA = "curl/" + CurlVersion()
CurlUA = "curl/" + CurlVersion() var AnchoredFirefoxVersion = strconv.Itoa(FirefoxVersion())
AnchoredFirefoxVersion = strconv.Itoa(FirefoxVersion()) var FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + AnchoredFirefoxVersion + ".0) Gecko/20100101 Firefox/" + AnchoredFirefoxVersion + ".0"
FirefoxUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" + AnchoredFirefoxVersion + ".0) Gecko/20100101 Firefox/" + AnchoredFirefoxVersion + ".0" var SafariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/" + SafariVersion() + " Safari/605.1.15"
SafariUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/" + SafariVersion() + " Safari/605.1.15" // Chromium browsers.
// Chromium browsers. var AnchoredChromeVersion = ChromeVersion()
AnchoredChromeVersion = ChromeVersion() var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36"
ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0 Safari/537.36" var ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome")
ChromeUACH = getGreasedChUa(AnchoredChromeVersion, "chrome") var MSEdgeUA = ChromeUA + "Edg/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0"
MSEdgeUA = ChromeUA + "Edg/" + strconv.Itoa(AnchoredChromeVersion) + ".0.0.0" var MSEdgeUACH = getGreasedChUa(AnchoredChromeVersion, "edge")
MSEdgeUACH = getGreasedChUa(AnchoredChromeVersion, "edge")
)
func applyMasqueradedHeaders(header http.Header, browser string, variant string) { func applyMasqueradedHeaders(header http.Header, browser string, variant string) {
// Browser-specific. // Browser-specific.
-73
View File
@@ -1,73 +0,0 @@
package utils
import (
"net"
"os"
"runtime"
"sync"
"time"
)
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
}
+1 -1
View File
@@ -109,4 +109,4 @@ func (m *TypedSyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) {
previous = anyPrevious.(V) previous = anyPrevious.(V)
} }
return previous, loaded return previous, loaded
} }
-54
View File
@@ -1,54 +0,0 @@
package utils
import (
"path/filepath"
"runtime"
"strings"
"syscall"
)
// ResolveSocketPath applies platform-specific transformations to a Unix
// socket path, matching the listen-side behaviour in
// transport/internet/system_listener.go.
//
// For abstract sockets (prefix @) on Linux/Android:
// - single @ — used as-is (lock-free abstract socket)
// - double @@ — stripped to single @ and padded to
// syscall.RawSockaddrUnix{}.Path length (HAProxy compat)
//
// Filesystem paths and abstract sockets on other platforms are returned
// unchanged.
func ResolveSocketPath(path string) string {
if len(path) == 0 || path[0] != '@' {
return path
}
if runtime.GOOS != "linux" && runtime.GOOS != "android" {
return path
}
if len(path) > 1 && path[1] == '@' {
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path))
copy(fullAddr, path[1:])
return string(fullAddr)
}
return path
}
// SplitHTTPUnixURL splits a target into an HTTP URL and an optional Unix
// socket path. For regular http(s) URLs the input is returned unchanged
// with an empty socketPath. For Unix sockets the format is:
//
// /path/to/socket.sock[:/http/path]
// @abstract[:/http/path]
// @@padded[:/http/path]
//
// The :/ separator delimits the socket path from the HTTP request path.
// If omitted, "/" is used.
func SplitHTTPUnixURL(raw string) (httpURL, socketPath string) {
if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') {
return raw, ""
}
if idx := strings.Index(raw, ":/"); idx >= 0 {
return "http://localhost" + raw[idx+1:], raw[:idx]
}
return "http://localhost/", raw
}
-59
View File
@@ -1,59 +0,0 @@
package utils
import (
"maps"
"runtime"
"sync"
"weak"
)
// WeakCacheMap is a map that holds weak references to values.
// Use for shared expensive objects and automatic cleanup when no longer used.
// This object can be GC and no goroutine is used for cleanup.
type WeakCacheMap[K comparable, V any] struct {
mu sync.Mutex
m map[K]weak.Pointer[V]
}
func NewWeakCacheMap[K comparable, V any]() *WeakCacheMap[K, V] {
return &WeakCacheMap[K, V]{
m: make(map[K]weak.Pointer[V]),
}
}
func (c *WeakCacheMap[K, V]) Load(key K) (value *V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
weakPtr := c.m[key].Value()
if weakPtr != nil {
return weakPtr, true
}
return nil, false
}
func (c *WeakCacheMap[K, V]) Store(key K, value *V) {
c.mu.Lock()
defer c.mu.Unlock()
weakPtr := weak.Make(value)
c.m[key] = weakPtr
runtime.AddCleanup(value, func(struct{}) {
c.mu.Lock()
defer c.mu.Unlock()
if c.m[key] == weakPtr {
delete(c.m, key)
}
}, struct{}{})
}
func (c *WeakCacheMap[K, V]) Range(f func(K, *V) bool) {
c.mu.Lock()
snapshot := maps.Clone(c.m)
c.mu.Unlock()
for k, v := range snapshot {
if value := v.Value(); value != nil {
if !f(k, value) {
break
}
}
}
}
+2 -2
View File
@@ -19,8 +19,8 @@ import (
var ( var (
Version_x byte = 26 Version_x byte = 26
Version_y byte = 6 Version_y byte = 4
Version_z byte = 27 Version_z byte = 25
) )
var ( var (
+3 -3
View File
@@ -53,7 +53,7 @@ func TestXrayDial(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}}, IpsBlocked: &freedom.IPRules{},
}), }),
}, },
}, },
@@ -105,7 +105,7 @@ func TestXrayDialUDPConn(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}}, IpsBlocked: &freedom.IPRules{},
}), }),
}, },
}, },
@@ -174,7 +174,7 @@ func TestXrayDialUDP(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}}, IpsBlocked: &freedom.IPRules{},
}), }),
}, },
}, },
+4 -4
View File
@@ -54,9 +54,9 @@ func TestXrayClose(t *testing.T) {
Listen: net.NewIPOrDomain(net.LocalHostIP), Listen: net.NewIPOrDomain(net.LocalHostIP),
}), }),
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
RewriteAddress: net.NewIPOrDomain(net.LocalHostIP), Address: net.NewIPOrDomain(net.LocalHostIP),
RewritePort: uint32(0), Port: uint32(0),
AllowedNetworks: []net.Network{net.Network_TCP}, Networks: []net.Network{net.Network_TCP},
}), }),
}, },
}, },
@@ -66,7 +66,7 @@ func TestXrayClose(t *testing.T) {
Receiver: &protocol.ServerEndpoint{ Receiver: &protocol.ServerEndpoint{
Address: net.NewIPOrDomain(net.LocalHostIP), Address: net.NewIPOrDomain(net.LocalHostIP),
Port: uint32(0), Port: uint32(0),
User: &protocol.User{ User: &protocol.User{
Account: serial.ToTypedMessage(&vmess.Account{ Account: serial.ToTypedMessage(&vmess.Account{
Id: userID.String(), Id: userID.String(),
}), }),
+1
View File
@@ -34,6 +34,7 @@ func (ctx *Context) GetSourceIPs() []net.IP {
} }
return nil return nil
} }
// GetSourcePort implements routing.Context. // GetSourcePort implements routing.Context.
+16 -22
View File
@@ -3,35 +3,33 @@ module github.com/xtls/xray-core
go 1.26 go 1.26
require ( require (
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6
github.com/cloudflare/circl v1.6.4 github.com/cloudflare/circl v1.6.3
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1 github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/klauspost/cpuid/v2 v2.4.0 github.com/klauspost/cpuid/v2 v2.3.0
github.com/miekg/dns v1.1.72 github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5 github.com/pelletier/go-toml v1.9.5
github.com/pion/stun/v3 v3.1.6 github.com/pires/go-proxyproto v0.12.0
github.com/pires/go-proxyproto v0.14.0
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.0
github.com/sagernet/sing v0.5.1 github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks v0.2.7
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f github.com/xtls/reality v0.0.0-20260501094811-4379845b089d
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.53.0 golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.56.0 golang.org/x/net v0.53.0
golang.org/x/sync v0.21.0 golang.org/x/sync v0.20.0
golang.org/x/sys v0.46.0 golang.org/x/sys v0.43.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/windows v1.0.1 golang.zx2c4.com/wireguard/windows v1.0.1
google.golang.org/grpc v1.82.0 google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
h12.io/socks v1.0.3 h12.io/socks v1.0.3
@@ -45,18 +43,14 @@ require (
github.com/juju/ratelimit v1.0.2 // indirect github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/compress v1.17.4 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/pion/dtls/v3 v3.1.4 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/mod v0.34.0 // indirect
golang.org/x/mod v0.36.0 // indirect golang.org/x/text v0.36.0 // indirect
golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.12.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.43.0 // indirect
golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+42 -50
View File
@@ -1,11 +1,11 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I= github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6 h1:cbF95uMsQwCwAzH2i8+2lNO2TReoELLuqeeMfyBjFbY=
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= github.com/apernet/quic-go v0.59.1-0.20260330051153-c402ee641eb6/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -33,8 +33,8 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -45,24 +45,16 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc= github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8=
github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pires/go-proxyproto v0.14.0 h1:2vIGIfVG8eVRsKF0xukEoeT5RWhDXxBU0uv6smLOKdI=
github.com/pires/go-proxyproto v0.14.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs= github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
@@ -75,45 +67,45 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8= github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/xtls/reality v0.0.0-20260501094811-4379845b089d h1:ca0n8upCDojatNr25id4npJBwUsMmLgtrvLYDj4J0Hg=
github.com/xtls/reality v0.0.0-20260501094811-4379845b089d/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -121,21 +113,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -147,10 +139,10 @@ golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs= golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+1 -2
View File
@@ -48,5 +48,4 @@ var configLoader = NewJSONConfigLoader(
"http": func() interface{} { return new(HTTPResponse) }, "http": func() interface{} { return new(HTTPResponse) },
}, },
"type", "type",
"", "")
)
+1 -2
View File
@@ -3,7 +3,6 @@ package conf
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math"
"strconv" "strconv"
"strings" "strings"
@@ -200,7 +199,7 @@ func (v *PortRange) UnmarshalJSON(data []byte) error {
if err == nil { if err == nil {
v.From = uint32(from) v.From = uint32(from)
v.To = uint32(to) v.To = uint32(to)
if v.From > v.To || v.To > math.MaxUint16 { if v.From > v.To {
return errors.New("invalid port range ", v.From, " -> ", v.To) return errors.New("invalid port range ", v.From, " -> ", v.To)
} }
return nil return nil
+33 -47
View File
@@ -12,9 +12,8 @@ import (
type DNSOutboundRuleConfig struct { type DNSOutboundRuleConfig struct {
Action string `json:"action"` Action string `json:"action"`
QType *PortList `json:"qType"` QType *PortList `json:"qtype"`
Domain *StringList `json:"domain"` Domain *StringList `json:"domain"`
RCode uint32 `json:"rCode"`
} }
func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) { func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) {
@@ -25,8 +24,8 @@ func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) {
rule.Action = dns.RuleAction_Direct rule.Action = dns.RuleAction_Direct
case "drop": case "drop":
rule.Action = dns.RuleAction_Drop rule.Action = dns.RuleAction_Drop
case "return": case "reject":
rule.Action = dns.RuleAction_Return rule.Action = dns.RuleAction_Reject
case "hijack": case "hijack":
rule.Action = dns.RuleAction_Hijack rule.Action = dns.RuleAction_Hijack
default: default:
@@ -35,8 +34,14 @@ func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) {
if c.QType != nil { if c.QType != nil {
for _, r := range c.QType.Range { for _, r := range c.QType.Range {
for qType := r.From; qType <= r.To; qType++ { if r.From > r.To {
rule.QType = append(rule.QType, int32(qType)) return nil, errors.New("invalid qtype range: ", r.String())
}
if r.To > 65535 {
return nil, errors.New("dns rule qtype out of range: ", r.String())
}
for qtype := r.From; qtype <= r.To; qtype++ {
rule.Qtype = append(rule.Qtype, int32(qtype))
} }
} }
} }
@@ -49,46 +54,29 @@ func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) {
rule.Domain = rules rule.Domain = rules
} }
if c.RCode > 65535 {
return nil, errors.New("rCode out of range: ", c.RCode)
}
rule.RCode = c.RCode
return rule, nil return rule, nil
} }
type DNSOutboundConfig struct { type DNSOutboundConfig struct {
RewriteNetwork Network `json:"rewriteNetwork"` Network Network `json:"network"`
RewriteAddress *Address `json:"rewriteAddress"` Address *Address `json:"address"`
RewritePort uint16 `json:"rewritePort"` Port uint16 `json:"port"`
Network Network `json:"network"` UserLevel uint32 `json:"userLevel"`
Address *Address `json:"address"` Rules []*DNSOutboundRuleConfig `json:"rules"`
Port uint16 `json:"port"` NonIPQuery *string `json:"nonIPQuery"` // todo: remove legacy
UserLevel uint32 `json:"userLevel"` BlockTypes *[]int32 `json:"blockTypes"` // todo: remove legacy
Rules []*DNSOutboundRuleConfig `json:"rules"`
NonIPQuery *string `json:"nonIPQuery"` // todo: remove legacy
BlockTypes *[]int32 `json:"blockTypes"` // todo: remove legacy
} }
func (c *DNSOutboundConfig) Build() (proto.Message, error) { func (c *DNSOutboundConfig) Build() (proto.Message, error) {
if len(c.Network) > 0 {
c.RewriteNetwork = c.Network
}
if c.Address != nil {
c.RewriteAddress = c.Address
}
if c.Port != 0 {
c.RewritePort = c.Port
}
config := &dns.Config{ config := &dns.Config{
RewriteServer: &net.Endpoint{ Server: &net.Endpoint{
Network: c.RewriteNetwork.Build(), Network: c.Network.Build(),
Port: uint32(c.RewritePort), Port: uint32(c.Port),
}, },
UserLevel: c.UserLevel, UserLevel: c.UserLevel,
} }
if c.RewriteAddress != nil { if c.Address != nil {
config.RewriteServer.Address = c.RewriteAddress.Build() config.Server.Address = c.Address.Build()
} }
// todo: remove legacy // todo: remove legacy
@@ -96,7 +84,7 @@ func (c *DNSOutboundConfig) Build() (proto.Message, error) {
if c.Rules != nil { if c.Rules != nil {
return nil, errors.New("legacy nonIPQuery and blockTypes cannot be mixed with rules") return nil, errors.New("legacy nonIPQuery and blockTypes cannot be mixed with rules")
} }
errors.PrintDeprecatedFeatureWarning(`"nonIPQuery" and "blockTypes"`, `"rules"`) errors.PrintDeprecatedFeatureWarning(`"nonIPQuery" and "blockTypes" in DNS outbound`, `"rules"`)
rules, err := c.buildLegacyDNSPolicy() rules, err := c.buildLegacyDNSPolicy()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -133,30 +121,28 @@ func (c *DNSOutboundConfig) buildLegacyDNSPolicy() ([]*dns.DNSRuleConfig, error)
if c.BlockTypes != nil && len(*c.BlockTypes) > 0 { if c.BlockTypes != nil && len(*c.BlockTypes) > 0 {
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Drop} rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Drop}
if mode == "reject" { if mode == "reject" {
rule.Action = dns.RuleAction_Return rule.Action = dns.RuleAction_Reject
rule.RCode = 5
} }
for _, qType := range *c.BlockTypes { for _, qtype := range *c.BlockTypes {
if qType < 0 || qType > 65535 { if qtype < 0 || qtype > 65535 {
return nil, errors.New("legacy blockTypes qType out of range: ", qType) return nil, errors.New("legacy blockTypes qtype out of range: ", qtype)
} }
rule.QType = append(rule.QType, qType) rule.Qtype = append(rule.Qtype, qtype)
} }
rules = append(rules, rule) rules = append(rules, rule)
} }
{ {
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Hijack} rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Hijack}
rule.QType = append(rule.QType, 1) rule.Qtype = append(rule.Qtype, 1)
rule.QType = append(rule.QType, 28) rule.Qtype = append(rule.Qtype, 28)
rules = append(rules, rule) rules = append(rules, rule)
} }
{ {
rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Return} rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Reject}
if mode == "reject" { if mode == "reject" {
rule.Action = dns.RuleAction_Return rule.Action = dns.RuleAction_Reject
rule.RCode = 5
} else if mode == "drop" { } else if mode == "drop" {
rule.Action = dns.RuleAction_Drop rule.Action = dns.RuleAction_Drop
} else if mode == "skip" { } else if mode == "skip" {
+27 -32
View File
@@ -24,7 +24,7 @@ func TestDnsProxyConfig(t *testing.T) {
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &dns.Config{ Output: &dns.Config{
RewriteServer: &net.Endpoint{ Server: &net.Endpoint{
Network: net.Network_TCP, Network: net.Network_TCP,
Address: net.NewIPOrDomain(net.IPAddress([]byte{8, 8, 8, 8})), Address: net.NewIPOrDomain(net.IPAddress([]byte{8, 8, 8, 8})),
Port: 53, Port: 53,
@@ -35,24 +35,24 @@ func TestDnsProxyConfig(t *testing.T) {
Input: `{ Input: `{
"rules": [{ "rules": [{
"action": "direct", "action": "direct",
"qType": "1,3,23-24" "qtype": "1,3,23-24"
}, { }, {
"action": "drop", "action": "drop",
"qType": 28, "qtype": 28,
"domain": ["domain:example.com", "full:example.com"] "domain": ["domain:example.com", "full:example.com"]
}] }]
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &dns.Config{ Output: &dns.Config{
RewriteServer: &net.Endpoint{}, Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{ Rule: []*dns.DNSRuleConfig{
{ {
Action: dns.RuleAction_Direct, Action: dns.RuleAction_Direct,
QType: []int32{1, 3, 23, 24}, Qtype: []int32{1, 3, 23, 24},
}, },
{ {
Action: dns.RuleAction_Drop, Action: dns.RuleAction_Drop,
QType: []int32{28}, Qtype: []int32{28},
Domain: []*geodata.DomainRule{ Domain: []*geodata.DomainRule{
{ {
Value: &geodata.DomainRule_Custom{ Value: &geodata.DomainRule_Custom{
@@ -78,18 +78,16 @@ func TestDnsProxyConfig(t *testing.T) {
{ {
Input: `{ Input: `{
"rules": [{ "rules": [{
"action": "return", "action": "reject",
"rCode": 5,
"domain": "keyword:example" "domain": "keyword:example"
}] }]
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &dns.Config{ Output: &dns.Config{
RewriteServer: &net.Endpoint{}, Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{ Rule: []*dns.DNSRuleConfig{
{ {
Action: dns.RuleAction_Return, Action: dns.RuleAction_Reject,
RCode: 5,
Domain: []*geodata.DomainRule{ Domain: []*geodata.DomainRule{
{ {
Value: &geodata.DomainRule_Custom{ Value: &geodata.DomainRule_Custom{
@@ -108,16 +106,16 @@ func TestDnsProxyConfig(t *testing.T) {
Input: `{ Input: `{
"rules": [{ "rules": [{
"action": "drop", "action": "drop",
"qType": 257 "qtype": 257
}] }]
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &dns.Config{ Output: &dns.Config{
RewriteServer: &net.Endpoint{}, Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{ Rule: []*dns.DNSRuleConfig{
{ {
Action: dns.RuleAction_Drop, Action: dns.RuleAction_Drop,
QType: []int32{257}, Qtype: []int32{257},
}, },
}, },
}, },
@@ -138,15 +136,14 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) {
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &dns.Config{ Output: &dns.Config{
RewriteServer: &net.Endpoint{}, Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{ Rule: []*dns.DNSRuleConfig{
{ {
Action: dns.RuleAction_Hijack, Action: dns.RuleAction_Hijack,
QType: []int32{1, 28}, Qtype: []int32{1, 28},
}, },
{ {
Action: dns.RuleAction_Return, Action: dns.RuleAction_Reject,
RCode: 5,
}, },
}, },
}, },
@@ -157,20 +154,18 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) {
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &dns.Config{ Output: &dns.Config{
RewriteServer: &net.Endpoint{}, Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{ Rule: []*dns.DNSRuleConfig{
{ {
Action: dns.RuleAction_Return, Action: dns.RuleAction_Reject,
QType: []int32{1, 65}, Qtype: []int32{1, 65},
RCode: 5,
}, },
{ {
Action: dns.RuleAction_Hijack, Action: dns.RuleAction_Hijack,
QType: []int32{1, 28}, Qtype: []int32{1, 28},
}, },
{ {
Action: dns.RuleAction_Return, Action: dns.RuleAction_Reject,
RCode: 5,
}, },
}, },
}, },
@@ -182,15 +177,15 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) {
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &dns.Config{ Output: &dns.Config{
RewriteServer: &net.Endpoint{}, Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{ Rule: []*dns.DNSRuleConfig{
{ {
Action: dns.RuleAction_Drop, Action: dns.RuleAction_Drop,
QType: []int32{1}, Qtype: []int32{1},
}, },
{ {
Action: dns.RuleAction_Hijack, Action: dns.RuleAction_Hijack,
QType: []int32{1, 28}, Qtype: []int32{1, 28},
}, },
{ {
Action: dns.RuleAction_Drop, Action: dns.RuleAction_Drop,
@@ -205,15 +200,15 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) {
}`, }`,
Parser: loadJSON(creator), Parser: loadJSON(creator),
Output: &dns.Config{ Output: &dns.Config{
RewriteServer: &net.Endpoint{}, Server: &net.Endpoint{},
Rule: []*dns.DNSRuleConfig{ Rule: []*dns.DNSRuleConfig{
{ {
Action: dns.RuleAction_Drop, Action: dns.RuleAction_Drop,
QType: []int32{65, 28}, Qtype: []int32{65, 28},
}, },
{ {
Action: dns.RuleAction_Hijack, Action: dns.RuleAction_Hijack,
QType: []int32{1, 28}, Qtype: []int32{1, 28},
}, },
{ {
Action: dns.RuleAction_Direct, Action: dns.RuleAction_Direct,
@@ -233,7 +228,7 @@ func TestDnsProxyConfigRejectsMixedLegacyAndNewFields(t *testing.T) {
_, err := loadJSON(creator)(`{ _, err := loadJSON(creator)(`{
"rules": [{ "rules": [{
"action": "direct", "action": "direct",
"qType": 65 "qtype": 65
}], }],
"blockTypes": [65] "blockTypes": [65]
}`) }`)
+5 -17
View File
@@ -8,39 +8,27 @@ import (
) )
type DokodemoConfig struct { type DokodemoConfig struct {
AllowedNetwork *NetworkList `json:"allowedNetwork"`
RewriteAddress *Address `json:"rewriteAddress"`
RewritePort uint16 `json:"rewritePort"`
Network *NetworkList `json:"network"`
Address *Address `json:"address"` Address *Address `json:"address"`
Port uint16 `json:"port"` Port uint16 `json:"port"`
PortMap map[string]string `json:"portMap"` PortMap map[string]string `json:"portMap"`
Network *NetworkList `json:"network"`
FollowRedirect bool `json:"followRedirect"` FollowRedirect bool `json:"followRedirect"`
UserLevel uint32 `json:"userLevel"` UserLevel uint32 `json:"userLevel"`
} }
func (v *DokodemoConfig) Build() (proto.Message, error) { func (v *DokodemoConfig) Build() (proto.Message, error) {
if v.Network != nil {
v.AllowedNetwork = v.Network
}
if v.Address != nil {
v.RewriteAddress = v.Address
}
if v.Port != 0 {
v.RewritePort = v.Port
}
config := new(dokodemo.Config) config := new(dokodemo.Config)
config.AllowedNetworks = v.AllowedNetwork.Build() if v.Address != nil {
if v.RewriteAddress != nil { config.Address = v.Address.Build()
config.RewriteAddress = v.RewriteAddress.Build()
} }
config.RewritePort = uint32(v.RewritePort) config.Port = uint32(v.Port)
config.PortMap = v.PortMap config.PortMap = v.PortMap
for _, v := range config.PortMap { for _, v := range config.PortMap {
if _, _, err := net.SplitHostPort(v); err != nil { if _, _, err := net.SplitHostPort(v); err != nil {
return nil, errors.New("invalid portMap: ", v).Base(err) return nil, errors.New("invalid portMap: ", v).Base(err)
} }
} }
config.Networks = v.Network.Build()
config.FollowRedirect = v.FollowRedirect config.FollowRedirect = v.FollowRedirect
config.UserLevel = v.UserLevel config.UserLevel = v.UserLevel
return config, nil return config, nil

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