Compare commits

..
1 Commits
Author SHA1 Message Date
Fangliding 4add189c24 little refine 2026-04-27 12:42:00 +08:00
433 changed files with 10825 additions and 20174 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
+5 -9
View File
@@ -32,13 +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/05_inbounds.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/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.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/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
RUN echo '{}' >/tmp/usr/local/etc/xray/07_transport.json
RUN echo '{}' >/tmp/usr/local/etc/xray/08_stats.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_reverse.json
# Create log files
RUN mkdir -p /tmp/var/log/xray && touch \
@@ -49,8 +45,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
FROM gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+5 -9
View File
@@ -32,13 +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/05_inbounds.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/08_fakedns.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.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/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
RUN echo '{}' >/tmp/usr/local/etc/xray/07_transport.json
RUN echo '{}' >/tmp/usr/local/etc/xray/08_stats.json
RUN echo '{}' >/tmp/usr/local/etc/xray/09_reverse.json
# Create log files
RUN mkdir -p /tmp/var/log/xray && touch \
@@ -58,8 +54,8 @@ RUN mkdir -p /tmp/var/log/xray && touch \
FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
+1 -20
View File
@@ -64,16 +64,8 @@ jobs:
echo "Latest: '$LATEST'."
echo "LATEST=$LATEST" >>${GITHUB_ENV}
NEWEST=false
if [[ "${{ github.event_name }}" == "release" ]]; then
NEWEST=true
fi
echo "Newest: '$NEWEST'."
echo "NEWEST=$NEWEST" >>${GITHUB_ENV}
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -132,13 +124,6 @@ jobs:
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
fi
if [[ "${{ env.NEWEST }}" == "true" ]]; then
echo "Adding 'pre-release' tag to manifest: '${{ env.FULL_IMAGE_NAME }}:pre-release'."
docker buildx imagetools create \
--tag ${{ env.FULL_IMAGE_NAME }}:pre-release \
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
fi
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
@@ -146,7 +131,3 @@ jobs:
if [[ "${{ env.LATEST }}" == "true" ]]; then
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:latest
fi
if [[ "${{ env.NEWEST }}" == "true" ]]; then
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:pre-release
fi
+8 -12
View File
@@ -11,16 +11,15 @@ on:
jobs:
check-assets:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-wintun-
@@ -76,14 +75,13 @@ jobs:
fail-fast: false
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:
GOOS: ${{ matrix.goos}}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0
steps:
- name: Checkout codebase
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Show workflow information
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
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-wintun-
@@ -134,17 +132,15 @@ jobs:
run: |
mv -f resources/geo* build_assets/
if [[ ${GOOS} == 'windows' ]]; then
cp .github/build/windows/* build_assets/
fi
if [[ ${GOOS} == 'windows' ]]; then
echo 'Adding Wintun into packages'
echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs
echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1
if [[ ${GOARCH} == 'amd64' ]]; then
mv resources/wintun/bin/amd64/wintun.dll build_assets/
fi
if [[ ${GOARCH} == '386' ]]; then
mv resources/wintun/bin/x86/wintun.dll build_assets/
fi
mv resources/wintun/LICENSE.txt build_assets/LICENSE-Wintun
mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt
fi
- name: Copy README.md & LICENSE
+8 -15
View File
@@ -11,16 +11,15 @@ on:
jobs:
check-assets:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-wintun-
@@ -162,7 +161,6 @@ jobs:
fail-fast: false
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:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
@@ -170,7 +168,7 @@ jobs:
CGO_ENABLED: 0
steps:
- name: Checkout codebase
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up NDK
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
# 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
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
echo 'Building Xray...'
if [[ ${GOARCH} == 'mips' || ${GOARCH} == 'mipsle' ]]; then
@@ -225,14 +220,14 @@ jobs:
fi
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
- name: Restore Wintun Cache
if: matrix.goos == 'windows'
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-wintun-
@@ -241,10 +236,8 @@ jobs:
run: |
mv -f resources/geo* build_assets/
if [[ ${GOOS} == 'windows' ]]; then
cp .github/build/windows/* build_assets/
fi
if [[ ${GOOS} == 'windows' ]]; then
echo 'Adding Wintun into packages'
echo 'CreateObject("Wscript.Shell").Run "xray.exe -config config.json",0' > build_assets/xray_no_window.vbs
echo 'Start-Process -FilePath ".\xray.exe" -ArgumentList "-config .\config.json" -WindowStyle Hidden' > build_assets/xray_no_window.ps1
if [[ ${GOARCH} == 'amd64' ]]; then
mv resources/wintun/bin/amd64/wintun.dll build_assets/
fi
@@ -254,7 +247,7 @@ jobs:
if [[ ${GOARCH} == 'arm64' ]]; then
mv resources/wintun/bin/arm64/wintun.dll build_assets/
fi
mv resources/wintun/LICENSE.txt build_assets/LICENSE-Wintun
mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt
fi
- name: Copy README.md & LICENSE
+9 -16
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
@@ -59,7 +59,7 @@ jobs:
done
- name: Save Geodat Cache
uses: actions/cache/save@v6
uses: actions/cache/save@v5
if: ${{ steps.update.outputs.unhit }}
with:
path: resources
@@ -68,12 +68,9 @@ jobs:
wintun:
if: github.event.schedule == '30 22 * * *' || github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
env:
ASSETVER: 0.14.1
ASSETHASH: 07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51
steps:
- name: Restore Wintun Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-wintun-
@@ -99,6 +96,7 @@ jobs:
echo -e "Checking if wintun.dll for ${ARCHITECTURE} exists..."
if [ -s "./resources/wintun/bin/${ARCHITECTURE}/wintun.dll" ]; then
echo -e "wintun.dll for ${ARCHITECTURE} exists"
continue
else
echo -e "wintun.dll for ${ARCHITECTURE} is missing"
missing=true
@@ -115,21 +113,16 @@ jobs:
fi
if [[ "$missing" == true ]]; then
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}..."
curl -L "https://www.wintun.net/builds/${DOWNLOAD_FILE}" -o "${FILENAME}"
if [[ "$(sha256sum "./${FILENAME}" | awk -F ' ' '{print $1}')" == "${ASSETHASH}" ]]; then
echo -e "Unpacking wintun..."
unzip -u ${FILENAME} -d resources/
echo "unhit=true" >> $GITHUB_OUTPUT
else
echo -e "Digest of ${FILENAME} mismatch."
exit 1
fi
echo -e "Unpacking wintun..."
unzip -u ${FILENAME} -d resources/
echo "unhit=true" >> $GITHUB_OUTPUT
fi
- name: Save Wintun Cache
uses: actions/cache/save@v6
uses: actions/cache/save@v5
if: ${{ steps.update.outputs.unhit }}
with:
path: resources
+5 -27
View File
@@ -1,4 +1,4 @@
name: Tests and Checkings
name: Test
on:
push:
@@ -8,10 +8,9 @@ on:
jobs:
check-assets:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
key: xray-geodat-
@@ -37,10 +36,9 @@ jobs:
check-proto:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
steps:
- name: Checkout codebase
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Check Proto Version Header
run: |
head -n 4 core/config.pb.go > ref.txt
@@ -52,28 +50,8 @@ jobs:
fi
done
check-format:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
permissions:
contents: read
steps:
- name: Checkout codebase
uses: actions/checkout@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:
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:
contents: read
runs-on: ${{ matrix.os }}
@@ -83,14 +61,14 @@ jobs:
os: [windows-latest, ubuntu-latest, macos-latest]
steps:
- name: Checkout codebase
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Restore Geodat Cache
uses: actions/cache/restore@v6
uses: actions/cache/restore@v5
with:
path: resources
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)
- [v2ray-agent](https://github.com/mack-a/v2ray-agent), [Xray_onekey](https://github.com/wulabing/Xray_onekey), [ProxySU](https://github.com/proxysu/ProxySU)
- 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)
- Homebrew
- `brew install xray`
@@ -110,8 +111,6 @@
- [Invisible Man - Xray](https://github.com/InvisibleManVPN/InvisibleMan-XRayClient)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [GenyConnect](https://github.com/genyleap/GenyConnect)
- [OneXray](https://github.com/OneXray/OneXray)
- [XrayUI-dev](https://github.com/PhoenixNil/XrayUI-dev)
- Android
- [v2rayNG](https://github.com/2dust/v2rayNG)
- [X-flutter](https://github.com/XTLS/X-flutter)
@@ -119,8 +118,7 @@
- [SimpleXray](https://github.com/lhear/SimpleXray)
- [XrayFA](https://github.com/Q7DF1/XrayFA)
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [OneXray](https://github.com/OneXray/OneXray)
- [AsteriskNG](https://github.com/Asterisk4Magisk/AsteriskNG)
- [NetProxy-Magisk](https://github.com/Fanju6/NetProxy-Magisk)
- 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)
- [Streisand](https://apps.apple.com/app/streisand/id6450534064)
@@ -145,14 +143,10 @@
- [AnyPortal](https://github.com/AnyPortal/AnyPortal)
- [v2rayN](https://github.com/2dust/v2rayN)
- [GenyConnect](https://github.com/genyleap/GenyConnect)
- [OneXray](https://github.com/OneXray/OneXray)
- HarmonyOS
- [Hey](https://github.com/popsiclelmlm/Hey)
## Others that support VLESS, XTLS, REALITY, XUDP, PLUX...
- iOS & macOS arm64 & tvOS
- [Anywhere](https://github.com/NodePassProject/Anywhere)
- [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118)
- [Loon](https://apps.apple.com/us/app/loon/id1373567447)
- [Egern](https://apps.apple.com/us/app/egern/id1616105820)
@@ -165,7 +159,6 @@
- [xtls-sdk](https://github.com/remnawave/xtls-sdk)
- [xtlsapi](https://github.com/hiddify/xtlsapi)
- [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-api](https://github.com/XVGuardian/xray-api)
- [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).
- 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
### 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
```
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:
```bash
+5 -19
View File
@@ -3,7 +3,6 @@ package commander
import (
"context"
"net"
"strings"
"sync"
"github.com/xtls/xray-core/common"
@@ -11,7 +10,6 @@ import (
"github.com/xtls/xray-core/common/signal/done"
core "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/outbound"
"github.com/xtls/xray-core/transport/internet"
"google.golang.org/grpc"
)
@@ -69,32 +67,20 @@ func (c *Commander) Start() error {
}
c.Unlock()
listen := func(listener net.Listener) {
var listen = func(listener net.Listener) {
if err := c.server.Serve(listener); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start grpc server")
}
}
if len(c.listen) > 0 {
var addr net.Addr
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 {
if l, err := net.Listen("tcp", c.listen); err != nil {
errors.LogErrorInner(context.Background(), err, "API server failed to listen on ", c.listen)
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
}
+5 -5
View File
@@ -162,7 +162,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
p := d.policy.ForLevel(user.Level)
if p.Stats.UserUplink {
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
inboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: inboundLink.Writer,
@@ -171,7 +171,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
}
if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
outboundLink.Writer = &SizeStatWriter{
Counter: c,
Writer: outboundLink.Writer,
@@ -200,13 +200,13 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
p := policyManager.ForLevel(user.Level)
if p.Stats.UserUplink {
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
link.Reader.(*buf.TimeoutWrapperReader).Counter = c
}
}
if p.Stats.UserDownlink {
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
link.Writer = &SizeStatWriter{
Counter: c,
Writer: link.Writer,
@@ -223,7 +223,7 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
func trackOnlineIP(ctx context.Context, sm stats.Manager, email, ip string) {
name := "user>>>" + email + ">>>online"
if om, _ := sm.GetOrRegisterOnlineMap(name); om != nil {
if om, _ := stats.GetOrRegisterOnlineMap(sm, name); om != nil {
om.AddIP(ip)
context.AfterFunc(ctx, func() { om.RemoveIP(ip) })
}
+3 -4
View File
@@ -139,8 +139,7 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) {
if lenAfter == 0 {
if c.highWatermark >= minSizeForEmptyRebuild {
errors.LogDebug(
context.Background(), c.name,
errors.LogDebug(context.Background(), c.name,
" rebuilding empty cache map to reclaim memory.",
" size_before_cleanup=", lenBefore,
" peak_size_before_rebuild=", c.highWatermark,
@@ -154,8 +153,7 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) {
if reductionFromPeak := c.highWatermark - lenAfter; reductionFromPeak > shrinkAbsoluteThreshold &&
float64(reductionFromPeak) > float64(c.highWatermark)*shrinkRatioThreshold {
errors.LogDebug(
context.Background(), c.name,
errors.LogDebug(context.Background(), c.name,
" shrinking cache map to reclaim memory.",
" new_size=", lenAfter,
" peak_size_before_shrink=", c.highWatermark,
@@ -167,6 +165,7 @@ func (c *CacheController) writeAndShrink(expiredKeys []string) {
c.highWatermark = lenAfter
go c.migrate()
}
}
type migrationEntry struct {
+92 -31
View File
@@ -5,16 +5,18 @@ import (
"context"
go_errors "errors"
"fmt"
"os"
"runtime"
"sort"
"strings"
"sync"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/utils"
"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)
}
defaultTag := config.Tag
var defaultTag = config.Tag
if len(config.Tag) == 0 {
defaultTag = generateRandomTag()
}
@@ -139,7 +141,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
serveExpiredTTL = *ns.ServeExpiredTTL
}
tag := defaultTag
var tag = defaultTag
if len(ns.Tag) > 0 {
tag = ns.Tag
}
@@ -156,12 +158,9 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
clients = append(clients, client)
}
var domainMatcher geodata.DomainMatcher
if len(effectiveRules) > 0 {
domainMatcher, err = geodata.DomainReg.BuildDomainMatcher(effectiveRules)
if err != nil {
return nil, err
}
domainMatcher, err := geodata.DomainReg.BuildDomainMatcher(effectiveRules)
if err != nil {
return nil, err
}
// If there is no DNS client in config, add a `localhost` DNS client
@@ -221,7 +220,7 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, er
}
if s.checkSystem {
supportIPv4, supportIPv6 := utils.CheckRoutes()
supportIPv4, supportIPv6 := checkRoutes()
option.IPv4Enable = option.IPv4Enable && supportIPv4
option.IPv6Enable = option.IPv6Enable && supportIPv6
} else {
@@ -272,27 +271,25 @@ func (s *DNS) sortClients(domain string) []*Client {
// Priority domain matching
hasMatch := false
if s.domainMatcher != nil {
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
sort.Slice(matchSlice, func(i, j int) bool {
return matchSlice[i] < matchSlice[j]
})
for _, match := range matchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
if clientUsed[info.clientIdx] {
continue
}
clientUsed[info.clientIdx] = true
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
hasMatch = true
if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
matchSlice := s.domainMatcher.Match(strings.ToLower(domain))
sort.Slice(matchSlice, func(i, j int) bool {
return matchSlice[i] < matchSlice[j]
})
for _, match := range matchSlice {
info := s.matcherInfos[match]
client := s.clients[info.clientIdx]
domainRule := info.domainRule
domainRules = append(domainRules, fmt.Sprintf("%s(DNS idx:%d)", domainRule, info.clientIdx))
if clientUsed[info.clientIdx] {
continue
}
clientUsed[info.clientIdx] = true
clients = append(clients, client)
clientNames = append(clientNames, client.Name())
hasMatch = true
if client.finalQuery {
logDecision(s.ctx, domain, domainRules, clientNames)
return clients
}
}
@@ -537,3 +534,67 @@ func init() {
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{
{
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{
{
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{
{
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{
{
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{
{
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{
{
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{
{
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{
{
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
}
func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) ([]*dnsRequest, error) {
name, err := dnsmessage.NewName(domain)
if err != nil {
return nil, err
}
func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) []*dnsRequest {
qA := dnsmessage.Question{
Name: name,
Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
}
qAAAA := dnsmessage.Question{
Name: name,
Name: dnsmessage.MustNewName(domain),
Type: dnsmessage.TypeAAAA,
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
@@ -198,14 +193,9 @@ func parseResponse(payload []byte) (*IPRecord, error) {
ipRecord := &IPRecord{
ReqID: h.ID,
RCode: h.RCode,
Expire: now.Add(time.Second * dns_feature.DefaultTTL),
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:
for {
@@ -222,7 +212,7 @@ L:
ttl = 1
}
expire := now.Add(time.Duration(ttl) * time.Second)
if ipRecord.Expire.IsZero() || ipRecord.Expire.After(expire) {
if ipRecord.Expire.After(expire) {
ipRecord.Expire = expire
}
+3 -11
View File
@@ -2,7 +2,6 @@ package dns
import (
"math/rand"
"strings"
"testing"
"time"
@@ -25,8 +24,7 @@ func Test_parseResponse(t *testing.T) {
ans = new(dns.Msg)
ans.Id = 1
ans.Answer = append(
ans.Answer,
ans.Answer = append(ans.Answer,
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 A 8.8.8.8")),
@@ -36,8 +34,7 @@ func Test_parseResponse(t *testing.T) {
ans = new(dns.Msg)
ans.Id = 2
ans.Answer = append(
ans.Answer,
ans.Answer = append(ans.Answer,
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 m.test.google.com")),
@@ -134,15 +131,10 @@ func Test_buildReqMsgs(t *testing.T) {
IPv6Enable: false,
FakeEnable: false,
}, 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 {
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)
}
})
+9 -10
View File
@@ -129,16 +129,15 @@ func TestFakeDnsHolderCreateMappingAndRollOver(t *testing.T) {
}
func TestFakeDNSMulti(t *testing.T) {
fakeMulti, err := NewFakeDNSHolderMulti(
&FakeDnsPoolMulti{
Pools: []*FakeDnsPool{{
IpPool: "240.0.0.0/12",
LruSize: 256,
}, {
IpPool: "fddd:c5b4:ff5f:f4f0::/64",
LruSize: 256,
}},
},
fakeMulti, err := NewFakeDNSHolderMulti(&FakeDnsPoolMulti{
Pools: []*FakeDnsPool{{
IpPool: "240.0.0.0/12",
LruSize: 256,
}, {
IpPool: "fddd:c5b4:ff5f:f4f0::/64",
LruSize: 256,
}},
},
)
common.Must(err)
+11 -14
View File
@@ -13,8 +13,8 @@ import (
// StaticHosts represents static domain-ip mapping in DNS server.
type StaticHosts struct {
responses [][]net.Address
matcher geodata.DomainMatcher
reps [][]net.Address
matcher geodata.DomainMatcher
}
// NewStaticHosts creates a new StaticHosts instance.
@@ -45,21 +45,21 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
rep = append(rep, addr)
}
}
// if len(rep) == 0 {
// errors.LogError(context.Background(), "empty value in static hosts, ignore this rule: ", mapping.Domain)
// continue
// }
reps = append(reps, rep)
rules = append(rules, mapping.Domain)
}
if len(rules) == 0 {
return &StaticHosts{}, nil
}
matcher, err := geodata.DomainReg.BuildDomainMatcher(rules)
if err != nil {
return nil, err
}
return &StaticHosts{
responses: reps,
matcher: matcher,
reps: reps,
matcher: matcher,
}, nil
}
@@ -76,8 +76,8 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
ips := make([]net.Address, 0)
found := false
for _, idx := range h.matcher.Match(domain) {
for _, rep := range h.responses[idx] {
for _, ruleIdx := range h.matcher.Match(domain) {
for _, rep := range h.reps[ruleIdx] {
if err, ok := rep.(dns.RCodeError); ok {
if uint16(err) == 0 {
return nil, dns.ErrEmptyResponse
@@ -85,7 +85,7 @@ func (h *StaticHosts) lookupInternal(domain string) ([]net.Address, error) {
return nil, err
}
}
ips = append(ips, h.responses[idx]...)
ips = append(ips, h.reps[ruleIdx]...)
found = true
}
if !found {
@@ -122,8 +122,5 @@ func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) (
// Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts.
func (h *StaticHosts) Lookup(domain string, option dns.IPOption) ([]net.Address, error) {
if h.matcher == nil {
return nil, nil
}
return h.lookup(domain, option, 5)
}
+2 -3
View File
@@ -10,7 +10,6 @@ import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"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/features/dns"
"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 {
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.
func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) {
if c.checkSystem {
supportIPv4, supportIPv6 := utils.CheckRoutes()
supportIPv4, supportIPv6 := checkRoutes()
option.IPv4Enable = option.IPv4Enable && supportIPv4
option.IPv6Enable = option.IPv6Enable && supportIPv6
} else {
+2 -20
View File
@@ -137,32 +137,14 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
if s.Name()+"." == "DOH//"+fqdn {
errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead")
if noResponseErrCh != nil {
err := errors.New("tries to resolve itself!", s.Name())
if option.IPv4Enable {
noResponseErrCh <- err
}
if option.IPv6Enable {
noResponseErrCh <- err
}
noResponseErrCh <- errors.New("tries to resolve itself!", s.Name())
}
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
// 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))))
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
}
reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, int(crypto.RandBetween(100, 300))))
var deadline time.Time
if d, ok := ctx.Deadline(); ok {
+1
View File
@@ -18,6 +18,7 @@ type LocalNameServer struct {
// QueryIP implements Server.
func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, option dns.IPOption) (ips []net.IP, ttl uint32, err error) {
start := time.Now()
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) {
errors.LogInfo(ctx, s.Name(), " querying: ", fqdn)
reqs, err := 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
}
reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
var deadline time.Time
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) {
errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn)
reqs, err := 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
}
reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
var deadline time.Time
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
s.addPendingRequest(&newReq)
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)
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) {
errors.LogInfo(ctx, s.Name(), " querying DNS for: ", fqdn)
reqs, err := 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
}
reqs := buildReqMsgs(fqdn, option, s.newReqID, genEDNS0Options(s.clientIP, 0))
for _, req := range reqs {
udpReq := &udpDnsRequest{
@@ -189,6 +179,8 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<
}
return
}
copyDest := net.UDPDestination(s.address.Address, s.address.Port)
b.UDP = &copyDest
s.udpServer.Dispatch(toDnsContext(ctx, s.address.String()), *s.address, b)
}
}
-198
View File
@@ -1,198 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.5
// source: app/geodata/config.proto
package geodata
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Asset struct {
state protoimpl.MessageState `protogen:"open.v1"`
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
File string `protobuf:"bytes,2,opt,name=file,proto3" json:"file,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Asset) Reset() {
*x = Asset{}
mi := &file_app_geodata_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Asset) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Asset) ProtoMessage() {}
func (x *Asset) ProtoReflect() protoreflect.Message {
mi := &file_app_geodata_config_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Asset.ProtoReflect.Descriptor instead.
func (*Asset) Descriptor() ([]byte, []int) {
return file_app_geodata_config_proto_rawDescGZIP(), []int{0}
}
func (x *Asset) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *Asset) GetFile() string {
if x != nil {
return x.File
}
return ""
}
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
Cron string `protobuf:"bytes,1,opt,name=cron,proto3" json:"cron,omitempty"`
Outbound string `protobuf:"bytes,2,opt,name=outbound,proto3" json:"outbound,omitempty"`
Assets []*Asset `protobuf:"bytes,3,rep,name=assets,proto3" json:"assets,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_app_geodata_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_app_geodata_config_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_app_geodata_config_proto_rawDescGZIP(), []int{1}
}
func (x *Config) GetCron() string {
if x != nil {
return x.Cron
}
return ""
}
func (x *Config) GetOutbound() string {
if x != nil {
return x.Outbound
}
return ""
}
func (x *Config) GetAssets() []*Asset {
if x != nil {
return x.Assets
}
return nil
}
var File_app_geodata_config_proto protoreflect.FileDescriptor
const file_app_geodata_config_proto_rawDesc = "" +
"\n" +
"\x18app/geodata/config.proto\x12\x10xray.app.geodata\"-\n" +
"\x05Asset\x12\x10\n" +
"\x03url\x18\x01 \x01(\tR\x03url\x12\x12\n" +
"\x04file\x18\x02 \x01(\tR\x04file\"i\n" +
"\x06Config\x12\x12\n" +
"\x04cron\x18\x01 \x01(\tR\x04cron\x12\x1a\n" +
"\boutbound\x18\x02 \x01(\tR\boutbound\x12/\n" +
"\x06assets\x18\x03 \x03(\v2\x17.xray.app.geodata.AssetR\x06assetsBR\n" +
"\x14com.xray.app.geodataP\x01Z%github.com/xtls/xray-core/app/geodata\xaa\x02\x10Xray.App.Geodatab\x06proto3"
var (
file_app_geodata_config_proto_rawDescOnce sync.Once
file_app_geodata_config_proto_rawDescData []byte
)
func file_app_geodata_config_proto_rawDescGZIP() []byte {
file_app_geodata_config_proto_rawDescOnce.Do(func() {
file_app_geodata_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_geodata_config_proto_rawDesc), len(file_app_geodata_config_proto_rawDesc)))
})
return file_app_geodata_config_proto_rawDescData
}
var file_app_geodata_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_app_geodata_config_proto_goTypes = []any{
(*Asset)(nil), // 0: xray.app.geodata.Asset
(*Config)(nil), // 1: xray.app.geodata.Config
}
var file_app_geodata_config_proto_depIdxs = []int32{
0, // 0: xray.app.geodata.Config.assets:type_name -> xray.app.geodata.Asset
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_app_geodata_config_proto_init() }
func file_app_geodata_config_proto_init() {
if File_app_geodata_config_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_geodata_config_proto_rawDesc), len(file_app_geodata_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_geodata_config_proto_goTypes,
DependencyIndexes: file_app_geodata_config_proto_depIdxs,
MessageInfos: file_app_geodata_config_proto_msgTypes,
}.Build()
File_app_geodata_config_proto = out.File
file_app_geodata_config_proto_goTypes = nil
file_app_geodata_config_proto_depIdxs = nil
}
-21
View File
@@ -1,21 +0,0 @@
syntax = "proto3";
package xray.app.geodata;
option csharp_namespace = "Xray.App.Geodata";
option go_package = "github.com/xtls/xray-core/app/geodata";
option java_package = "com.xray.app.geodata";
option java_multiple_files = true;
message Asset {
string url = 1;
string file = 2;
}
message Config {
string cron = 1;
string outbound = 2;
repeated Asset assets = 3;
}
-346
View File
@@ -1,346 +0,0 @@
package geodata
import (
"context"
"crypto/tls"
go_errors "errors"
"io"
"net/http"
"os"
"path/filepath"
"time"
utls "github.com/refraction-networking/utls"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/platform/filesystem"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet/tagged"
"golang.org/x/net/http2"
)
const idleTimeout = 30 * time.Second
type stage struct {
target string
temp string
}
type downloader struct {
ctx context.Context
httpClient *http.Client
httpsClient *http.Client
}
type idleConn struct {
net.Conn
}
func (c *idleConn) Read(b []byte) (int, error) {
t := time.AfterFunc(idleTimeout, func() {
_ = c.Close()
})
n, err := c.Conn.Read(b)
if !t.Stop() {
_ = c.Close()
return n, errors.New("connection idle timeout")
}
return n, err
}
func (c *idleConn) Write(b []byte) (int, error) {
return c.Conn.Write(b)
}
func newDownloader(ctx context.Context, dispatcher routing.Dispatcher, outbound string) *downloader {
return &downloader{
ctx: ctx,
httpClient: newClient(ctx, dispatcher, outbound, false),
httpsClient: newClient(ctx, dispatcher, outbound, true),
}
}
func newClient(baseCtx context.Context, dispatcher routing.Dispatcher, outbound string, isHTTPS bool) *http.Client {
dial := func(ctx context.Context, network, address string) (net.Conn, error) {
var conn net.Conn
err := task.Run(ctx, func() error {
if tagged.Dialer == nil {
return errors.New("tagged dialer is not initialized")
}
dest, err := net.ParseDestination(network + ":" + address)
if err != nil {
return errors.New("cannot understand address").Base(err)
}
c, err := tagged.Dialer(baseCtx, dispatcher, dest, outbound)
if err != nil {
return errors.New("cannot dial remote address ", dest).Base(err)
}
conn = c
return nil
})
if err != nil {
return nil, errors.New("cannot finish connection").Base(err)
}
return &idleConn{
Conn: conn,
}, nil
}
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 {
return nil, err
}
host, _, _ := net.SplitHostPort(address)
tlsConn := utls.UClient(conn, &utls.Config{ServerName: host}, utls.HelloChrome_Auto)
handshakeCtx, cancel := context.WithTimeout(ctx, idleTimeout)
defer cancel()
if err := tlsConn.HandshakeContext(handshakeCtx); err != nil {
conn.Close()
return nil, err
}
return tlsConn, nil
},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
} else {
return &http.Client{
Transport: &http.Transport{
Proxy: nil,
DisableKeepAlives: true,
DialContext: dial,
ResponseHeaderTimeout: idleTimeout,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.URL.Scheme != "https" {
return errors.New("redirected to non-https URL: ", req.URL.String())
}
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
},
}
}
}
func (d *downloader) download(assets []*Asset) ([]stage, error) {
staged := make([]stage, 0, len(assets))
for _, asset := range assets {
stage, err := d.downloadOne(asset)
if err != nil {
clean(staged)
return nil, err
}
staged = append(staged, stage)
}
return staged, nil
}
func (d *downloader) downloadOne(asset *Asset) (stage, error) {
target, err := filesystem.ResolveAsset(asset.File)
if err != nil {
return stage{}, err
}
errors.LogInfo(d.ctx, "downloading geodata asset from ", asset.Url, " to ", target)
temp, err := tempFile(target, ".tmp")
if err != nil {
return stage{}, err
}
tempName := temp.Name()
keepTemp := false
defer func() {
if !keepTemp {
os.Remove(tempName)
}
}()
if err := d.fetch(asset.Url, temp); err != nil {
temp.Close()
return stage{}, err
}
if err := temp.Chmod(0o644); err != nil {
temp.Close()
return stage{}, err
}
if err := temp.Close(); err != nil {
return stage{}, err
}
keepTemp = true
return stage{
target: target,
temp: tempName,
}, nil
}
func (d *downloader) fetch(rawURL string, writer io.Writer) error {
req, err := http.NewRequestWithContext(d.ctx, http.MethodGet, rawURL, nil)
if err != nil {
return err
}
utils.TryDefaultHeadersWith(req.Header, "nav")
var client *http.Client
if req.URL.Scheme == "https" {
client = d.httpsClient
} else {
client = d.httpClient
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
io.Copy(io.Discard, resp.Body)
return errors.New("unexpected status code: ", resp.StatusCode)
}
n, err := io.Copy(writer, resp.Body)
if err != nil {
return err
}
if n == 0 {
return errors.New("empty response body")
}
return nil
}
func clean(assets []stage) {
for _, asset := range assets {
if asset.temp != "" {
os.Remove(asset.temp)
}
}
}
type tx struct {
swaps []swap
}
type swap struct {
target string
backup string
hadOriginal bool
}
func swapAll(assets []stage) (*tx, error) {
t := &tx{}
for _, asset := range assets {
s, err := swapOne(asset)
if err != nil {
return nil, errors.Combine(err, t.rollback())
}
t.swaps = append(t.swaps, s)
}
return t, nil
}
func swapOne(asset stage) (swap, error) {
backup, err := backupFile(asset.target)
if err != nil {
return swap{}, err
}
s := swap{
target: asset.target,
backup: backup,
}
if err := os.Rename(asset.target, backup); err != nil {
if !go_errors.Is(err, os.ErrNotExist) {
return swap{}, err
}
if err := os.Remove(backup); err != nil && !go_errors.Is(err, os.ErrNotExist) {
return swap{}, err
}
} else {
s.hadOriginal = true
}
if err := os.Rename(asset.temp, asset.target); err != nil {
if s.hadOriginal {
if restoreErr := os.Rename(backup, asset.target); restoreErr != nil {
return swap{}, errors.Combine(err, restoreErr)
}
}
return swap{}, err
}
return s, nil
}
func (t *tx) rollback() error {
var errs []error
for i := len(t.swaps) - 1; i >= 0; i-- {
if err := t.swaps[i].rollback(); err != nil {
errs = append(errs, err)
}
}
return errors.Combine(errs...)
}
func (s swap) rollback() error {
var errs []error
if err := os.Remove(s.target); err != nil && !go_errors.Is(err, os.ErrNotExist) {
errs = append(errs, err)
}
if s.hadOriginal {
if err := os.Rename(s.backup, s.target); err != nil {
errs = append(errs, err)
}
} else if err := os.Remove(s.backup); err != nil && !go_errors.Is(err, os.ErrNotExist) {
errs = append(errs, err)
}
return errors.Combine(errs...)
}
func (t *tx) commit() error {
var errs []error
for _, swap := range t.swaps {
if err := os.Remove(swap.backup); err != nil && !go_errors.Is(err, os.ErrNotExist) {
errs = append(errs, err)
}
}
return errors.Combine(errs...)
}
func tempFile(target string, suffix string) (*os.File, error) {
dir := filepath.Dir(target)
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
return os.CreateTemp(dir, "."+filepath.Base(target)+".*"+suffix)
}
func backupFile(target string) (string, error) {
file, err := tempFile(target, ".bak")
if err != nil {
return "", err
}
name := file.Name()
if err := file.Close(); err != nil {
os.Remove(name)
return "", err
}
if err := os.Remove(name); err != nil {
return "", err
}
return name, nil
}
-134
View File
@@ -1,134 +0,0 @@
package geodata
import (
"context"
"sync"
"github.com/robfig/cron/v3"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
commongeodata "github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing"
)
type Instance struct {
assets []*Asset
downloader *downloader
tasker *cron.Cron
mu sync.Mutex
running bool
}
func New(ctx context.Context, config *Config) (*Instance, error) {
if config.Cron == "" {
return &Instance{}, nil
}
g := &Instance{
assets: config.Assets,
}
if len(g.assets) > 0 {
var dispatcher routing.Dispatcher
if err := core.RequireFeatures(ctx, func(d routing.Dispatcher) {
dispatcher = d
}); err != nil {
return nil, errors.New("failed to get dispatcher for geodata downloader").Base(err)
}
g.downloader = newDownloader(ctx, dispatcher, config.Outbound)
}
g.tasker = cron.New(
cron.WithChain(cron.SkipIfStillRunning(cron.DiscardLogger)),
cron.WithLogger(cron.DiscardLogger),
)
if _, err := g.tasker.AddFunc(config.Cron, g.execute); err != nil {
return nil, errors.New("invalid geodata cron").Base(err)
}
errors.LogInfo(ctx, "scheduled geodata reload with cron: ", config.Cron)
return g, nil
}
func (g *Instance) execute() {
var err error
if g.downloader != nil {
err = g.reloadWithUpdate()
} else {
err = reload()
}
if err != nil {
errors.LogErrorInner(context.Background(), err, "scheduled geodata reload failed")
}
}
func (g *Instance) reloadWithUpdate() error {
staged, err := g.downloader.download(g.assets)
if err != nil {
return err
}
defer clean(staged)
tx, err := swapAll(staged)
if err != nil {
return err
}
if err := reload(); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload geodata after downloading assets, rolling back")
rollbackErr := tx.rollback()
return errors.Combine(err, rollbackErr)
}
return tx.commit()
}
func reload() error {
return errors.Combine(commongeodata.IPReg.Reload(), commongeodata.DomainReg.Reload())
}
func (g *Instance) Type() interface{} {
return (*Instance)(nil)
}
func (g *Instance) Start() error {
g.mu.Lock()
defer g.mu.Unlock()
if g.running {
return nil
}
if g.tasker != nil {
g.tasker.Start()
}
g.running = true
return nil
}
func (g *Instance) Close() error {
g.mu.Lock()
defer g.mu.Unlock()
if !g.running {
return nil
}
if g.tasker != nil {
<-g.tasker.Stop().Done()
}
g.running = false
return nil
}
func init() {
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, cfg interface{}) (interface{}, error) {
return New(ctx, cfg.(*Config))
}))
}
+59 -150
View File
@@ -2,18 +2,15 @@ package metrics
import (
"context"
"encoding/json"
stderrors "errors"
"expvar"
stdnet "net"
"net/http"
"net/http/pprof"
_ "net/http/pprof"
"strings"
"github.com/xtls/xray-core/app/observatory"
"github.com/xtls/xray-core/common"
"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/core"
"github.com/xtls/xray-core/features/extension"
@@ -24,17 +21,15 @@ import (
type MetricsHandler struct {
ohm outbound.Manager
statsManager feature_stats.Manager
ctx context.Context
observatory extension.Observatory
tag string
listen string
tcpListener xnet.Listener
listener *OutboundListener
tcpListener net.Listener
}
// NewMetricsHandler creates a new MetricsHandler based on the given config.
func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, error) {
c := &MetricsHandler{
ctx: ctx,
tag: config.Tag,
listen: config.Listen,
}
@@ -42,6 +37,46 @@ func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, er
c.statsManager = sm
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
}
@@ -50,172 +85,46 @@ func (p *MetricsHandler) Type() interface{} {
}
func (p *MetricsHandler) Start() error {
handler := p.httpHandler()
// direct listen a port if listen is set
if p.listen != "" {
TCPlistener, err := xnet.Listen("tcp", p.listen)
TCPlistener, err := net.Listen("tcp", p.listen)
if err != nil {
return err
}
p.tcpListener = TCPlistener
errors.LogInfo(context.Background(), "Metrics server listening on ", p.listen)
go p.serve(TCPlistener, handler)
}
if p.tag == "" {
if p.tcpListener == nil {
return errors.New("metrics must have a tag or listen address")
}
return nil
go func() {
if err := http.Serve(TCPlistener, http.DefaultServeMux); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}()
}
listener := &OutboundListener{
buffer: make(chan xnet.Conn, 4),
buffer: make(chan net.Conn, 4),
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 {
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,
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 {
var errs []error
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
return nil
}
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 (
"context"
"sync"
"github.com/xtls/xray-core/app/observatory"
@@ -71,6 +72,7 @@ func (o *Observer) Start() error {
o.hp.StartScheduler(func() ([]string, error) {
hs, ok := o.ohm.(outbound.HandlerSelector)
if !ok {
return nil, errors.New("outbound.Manager is not a HandlerSelector")
}
+24 -42
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/xtls/xray-core/common/dice"
@@ -25,12 +24,11 @@ type HealthPingSettings struct {
// HealthPing is the health checker for balancers
type HealthPing struct {
ctx context.Context
cancelCtx context.CancelFunc
cancelPending atomic.Pointer[context.CancelFunc]
dispatcher routing.Dispatcher
access sync.Mutex
ticker *time.Ticker
ctx context.Context
dispatcher routing.Dispatcher
access sync.Mutex
ticker *time.Ticker
tickerClose chan struct{}
Settings *HealthPingSettings
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"
}
if settings.Interval == 0 {
settings.Interval = 1 * time.Minute
} else if settings.Interval < 10*time.Second {
settings.Interval = time.Duration(1) * time.Minute
} else if settings.Interval < 10 {
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 {
settings.SamplingCount = 10
@@ -75,12 +73,10 @@ func NewHealthPing(ctx context.Context, dispatcher routing.Dispatcher, config *H
if settings.Timeout <= 0 {
// results are saved after all health pings finish,
// 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{
ctx: ctx,
cancelCtx: cancel,
dispatcher: dispatcher,
Settings: settings,
Results: nil,
@@ -94,9 +90,9 @@ func (h *HealthPing) StartScheduler(selector func() ([]string, error)) {
}
interval := h.Settings.Interval * time.Duration(h.Settings.SamplingCount)
ticker := time.NewTicker(interval)
tickerClose := make(chan struct{})
h.ticker = ticker
// init run to get a fast check result
h.tickerClose = tickerClose
go func() {
tags, err := selector()
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)
return
}
subCtx, cancel := context.WithCancel(h.ctx)
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.doCheck(tags, interval, h.Settings.SamplingCount)
h.Cleanup(tags)
}()
select {
case <-ticker.C:
continue
case <-h.ctx.Done():
case <-tickerClose:
return
}
}
@@ -141,7 +130,8 @@ func (h *HealthPing) StopScheduler() {
}
h.ticker.Stop()
h.ticker = nil
h.cancelCtx()
close(h.tickerClose)
h.tickerClose = nil
}
// Check implements the HealthChecker
@@ -150,7 +140,7 @@ func (h *HealthPing) Check(tags []string) error {
return nil
}
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
}
@@ -161,14 +151,13 @@ type rtt struct {
// doCheck performs the 'rounds' amount checks in given 'duration'. You should make
// sure all tags are valid for current balancer
// cancel ctx will stop all pending checks
func (h *HealthPing) doCheck(ctx context.Context, tags []string, duration time.Duration, rounds int) {
func (h *HealthPing) doCheck(tags []string, duration time.Duration, rounds int) {
count := len(tags) * rounds
if count == 0 {
return
}
ch := make(chan *rtt, count)
timers := make([]*time.Timer, 0, count)
for _, tag := range tags {
handler := tag
client := newPingClient(
@@ -183,7 +172,7 @@ func (h *HealthPing) doCheck(ctx context.Context, tags []string, duration time.D
if duration > 0 {
delay = time.Duration(dice.RollInt63n(int64(duration)))
}
timers = append(timers, time.AfterFunc(delay, func() {
time.AfterFunc(delay, func() {
errors.LogDebug(h.ctx, "checking ", handler)
delay, err := client.MeasureDelay(h.Settings.HttpMethod)
if err == nil {
@@ -211,21 +200,14 @@ func (h *HealthPing) doCheck(ctx context.Context, tags []string, duration time.D
handler: handler,
value: rttFailed,
}
}))
})
}
}
for i := 0; i < count; i++ {
select {
case rtt := <-ch:
if rtt.value > 0 {
// should not put results when network is down
h.PutResult(rtt.handler, rtt.value)
}
case <-ctx.Done():
for _, timer := range timers {
timer.Stop()
}
return
rtt := <-ch
if rtt.value > 0 {
// should not put results when network is down
h.PutResult(rtt.handler, rtt.value)
}
}
}
+2 -2
View File
@@ -59,7 +59,7 @@ func (h *HealthPingRTTS) Put(d time.Duration) {
if h.rtts == nil {
h.rtts = make([]*pingRTT, h.cap)
for i := 0; i < h.cap; i++ {
h.rtts[i] = &pingRTT{value: rttUntested}
h.rtts[i] = &pingRTT{}
}
h.idx = -1
}
@@ -88,7 +88,7 @@ func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
validRTTs := make([]time.Duration, 0)
for _, rtt := range h.rtts {
switch {
case rtt.value == rttUntested || time.Since(rtt.time) > h.validity:
case rtt.value == 0 || time.Since(rtt.time) > h.validity:
continue
case rtt.value == rttFailed:
stats.Fail++
+1 -1
View File
@@ -186,7 +186,7 @@ func (o *Observer) probe(outbound string) ProbeResult {
return 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)
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 {
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)
for _, u := range users {
result = append(result, protocol.ToProtoUser(u))
+16 -24
View File
@@ -26,7 +26,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.InboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := statsManager.GetOrRegisterCounter(name)
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
uplinkCounter = c
}
@@ -34,7 +34,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.InboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := statsManager.GetOrRegisterCounter(name)
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
downlinkCounter = c
}
@@ -57,23 +57,16 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
if err != nil {
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})
newCtx = session.ContextWithContent(newCtx, &session.Content{SniffingRequest: sniffingRequest})
newCtx = session.ContextWithStreamSettings(newCtx, mss)
rawProxy, err := common.CreateObject(newCtx, proxyConfig)
// Set tag and sniffing config in context before creating proxy
// This allows proxies like TUN to access these settings
ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: tag})
if receiverConfig.SniffingSettings != nil {
ctx = session.ContextWithContent(ctx, &session.Content{
SniffingRequest: sniffingRequest,
})
}
rawProxy, err := common.CreateObject(ctx, proxyConfig)
if err != nil {
return nil, err
}
@@ -99,6 +92,11 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
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 mss.SocketSettings == nil {
mss.SocketSettings = &internet.SocketConfig{}
@@ -172,12 +170,6 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
// Start implements common.Runnable.
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 {
if err := worker.Start(); err != nil {
return err
+3 -3
View File
@@ -16,10 +16,10 @@ import (
// Manager manages all inbound handlers.
type Manager struct {
access sync.RWMutex
access sync.RWMutex
untaggedHandlers []inbound.Handler
taggedHandlers map[string]inbound.Handler
running bool
taggedHandlers map[string]inbound.Handler
running bool
}
// 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/stats"
"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/hysteria"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tcp"
"github.com/xtls/xray-core/transport/internet/udp"
@@ -134,8 +134,10 @@ func (w *tcpWorker) Proxy() proxy.Inbound {
func (w *tcpWorker) Start() error {
ctx := context.Background()
if v, ok := w.proxy.(*hysteria_proxy.Server); ok {
ctx = hysteria.ContextWithValidator(ctx, v.HysteriaInboundValidator())
type HysteriaInboundValidator interface{ HysteriaInboundValidator() *account.Validator }
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) {
+11 -6
View File
@@ -6,6 +6,7 @@ import (
goerrors "errors"
"io"
"math/big"
"os"
"github.com/xtls/xray-core/common/dice"
@@ -39,7 +40,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.OutboundUplink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
c, _ := statsManager.GetOrRegisterCounter(name)
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
uplinkCounter = c
}
@@ -47,7 +48,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
if len(tag) > 0 && policy.ForSystem().Stats.OutboundDownlink {
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
c, _ := statsManager.GetOrRegisterCounter(name)
c, _ := stats.GetOrRegisterCounter(statsManager, name)
if c != nil {
downlinkCounter = c
}
@@ -108,9 +109,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
ctx = session.ContextWithFullHandler(ctx, h)
newCtx := session.ContextWithStreamSettings(ctx, h.streamSettings)
rawProxyHandler, err := common.CreateObject(newCtx, proxyConfig)
rawProxyHandler, err := common.CreateObject(ctx, proxyConfig)
if err != nil {
return nil, err
}
@@ -307,6 +306,11 @@ func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connecti
ob := outbounds[len(outbounds)-1]
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)
@@ -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())
}
}
// case addr.Family().IsDomain():
//case addr.Family().IsDomain():
default:
ob.Gateway = addr
@@ -392,6 +396,7 @@ func (h *Handler) ProxySettings() *serial.TypedMessage {
}
func ParseRandomIP(addr net.Address, prefix string) net.Address {
_, ipnet, _ := net.ParseCIDR(addr.IP().String() + "/" + prefix)
ones, bits := ipnet.Mask.Size()
+8 -7
View File
@@ -22,8 +22,8 @@ import (
)
func TestInterfaces(t *testing.T) {
_ = outbound.Handler(new(Handler))
_ = outbound.Manager(new(Manager))
_ = (outbound.Handler)(new(Handler))
_ = (outbound.Manager)(new(Manager))
}
const xrayKey core.XrayKey = 1
@@ -43,12 +43,12 @@ func TestOutboundWithoutStatCounter(t *testing.T) {
}
v, _ := core.New(config)
v.AddFeature(outbound.Manager(new(Manager)))
v.AddFeature((outbound.Manager)(new(Manager)))
ctx := context.WithValue(context.Background(), xrayKey, v)
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
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))
_, ok := conn.(*stat.CounterConnection)
@@ -73,12 +73,12 @@ func TestOutboundWithStatCounter(t *testing.T) {
}
v, _ := core.New(config)
v.AddFeature(outbound.Manager(new(Manager)))
v.AddFeature((outbound.Manager)(new(Manager)))
ctx := context.WithValue(context.Background(), xrayKey, v)
ctx = session.ContextWithOutbounds(ctx, []*session.Outbound{{}})
h, _ := NewHandler(ctx, &core.OutboundHandlerConfig{
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))
_, ok := conn.(*stat.CounterConnection)
@@ -88,6 +88,7 @@ func TestOutboundWithStatCounter(t *testing.T) {
}
func TestTagsCache(t *testing.T) {
test_duration := 10 * time.Second
threads_num := 50
delay := 10 * time.Millisecond
@@ -117,7 +118,7 @@ func TestTagsCache(t *testing.T) {
tag := fmt.Sprintf("%s%d", tags_prefix, idx)
cfg := &core.OutboundHandlerConfig{
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 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.
func (m *Manager) Select(selectors []string) []string {
key := strings.Join(selectors, ",")
if cache, ok := m.tagsCache.Load(key); ok {
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 nil, errors.New("unsupported router implementation")
}
func (s *routingServer) RemoveRule(ctx context.Context, request *RemoveRuleRequest) (*RemoveRuleResponse, error) {
+1 -8
View File
@@ -12,7 +12,6 @@ import (
"github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/routing/dns"
)
type Condition interface {
@@ -357,13 +356,7 @@ func (m *ProcessNameMatcher) Apply(ctx routing.Context) bool {
var dstIP string
var dstPort uint16 = 0
// do not use resolved IP because Android process lookup needs original dst ip
resolvableContext, ok := ctx.(*dns.ResolvableContext)
if ok && len(resolvableContext.Context.GetTargetIPs()) > 0 {
dstIP = resolvableContext.Context.GetTargetIPs()[0].String()
dstPort = uint16(resolvableContext.Context.GetTargetPort())
} else if len(ctx.GetTargetIPs()) > 0 {
if len(ctx.GetTargetIPs()) > 0 {
dstIP = ctx.GetTargetIPs()[0].String()
dstPort = uint16(ctx.GetTargetPort())
}
+2
View File
@@ -110,6 +110,7 @@ func (r *Router) PickRoute(ctx routing.Context) (routing.Route, error) {
// AddRule implements routing.Router.
func (r *Router) AddRule(config *serial.TypedMessage, shouldAppend bool) error {
inst, err := config.GetInstance()
if err != nil {
return err
@@ -226,6 +227,7 @@ func (r *Router) RemoveRule(tag string) error {
return nil
}
return errors.New("empty tag name!")
}
// ListRule implements routing.Router
+6 -22
View File
@@ -3,7 +3,6 @@ package router
import (
"context"
"math"
"slices"
"sort"
"time"
@@ -78,7 +77,7 @@ func (s *LeastLoadStrategy) PickOutbound(candidates []string) string {
}
func (s *LeastLoadStrategy) pickOutbounds(candidates []string) []*node {
qualified := s.getNodes(candidates)
qualified := s.getNodes(candidates, time.Duration(s.settings.MaxRTT))
selects := s.selectLeastLoad(qualified)
return selects
}
@@ -139,7 +138,7 @@ func (s *LeastLoadStrategy) selectLeastLoad(nodes []*node) []*node {
return nodes[:count]
}
func (s *LeastLoadStrategy) getNodes(candidates []string) []*node {
func (s *LeastLoadStrategy) getNodes(candidates []string, maxRTT time.Duration) []*node {
if s.observer == nil {
errors.LogError(s.ctx, "observer is nil")
return make([]*node, 0)
@@ -152,10 +151,12 @@ func (s *LeastLoadStrategy) getNodes(candidates []string) []*node {
results := observeResult.(*observatory.ObservationResult)
outboundlist := outboundList(candidates)
var ret []*node
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{
Tag: v.OutboundTag,
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.CountAll = int(v.HealthPing.All)
record.CountFail = int(v.HealthPing.Fail)
}
}
ret = append(ret, record)
}
}
@@ -181,23 +182,6 @@ func (s *LeastLoadStrategy) getNodes(candidates []string) []*node {
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) {
sort.Slice(nodes, func(i, j int) bool {
left := nodes[i]
-5
View File
@@ -85,7 +85,6 @@ func TestSelectLeastExpected(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns))
}
}
func TestSelectLeastExpected2(t *testing.T) {
strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{
@@ -103,7 +102,6 @@ func TestSelectLeastExpected2(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns))
}
}
func TestSelectLeastExpectedAndBaselines(t *testing.T) {
strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{
@@ -124,7 +122,6 @@ func TestSelectLeastExpectedAndBaselines(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns))
}
}
func TestSelectLeastExpectedAndBaselines2(t *testing.T) {
strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{
@@ -145,7 +142,6 @@ func TestSelectLeastExpectedAndBaselines2(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns))
}
}
func TestSelectLeastLoadBaselines(t *testing.T) {
strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{
@@ -164,7 +160,6 @@ func TestSelectLeastLoadBaselines(t *testing.T) {
t.Errorf("expected: %v, actual: %v", expected, len(ns))
}
}
func TestSelectLeastLoadBaselinesNoQualified(t *testing.T) {
strategy := &LeastLoadStrategy{
settings: &StrategyLeastLoadConfig{
+49 -3
View File
@@ -7,15 +7,61 @@ import (
"io"
"net"
"net/http"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/routing"
routing_session "github.com/xtls/xray-core/features/routing/session"
)
// 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 }
type event struct {
@@ -50,7 +96,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
return nil, nil
}
httpURL, socketPath := utils.SplitHTTPUnixURL(cfg.Url)
httpURL, socketPath := parseURL(cfg.Url)
h := &WebhookNotifier{
url: httpURL,
deduplication: cfg.Deduplication,
@@ -61,7 +107,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
}
if socketPath != "" {
dialAddr := utils.ResolveSocketPath(socketPath)
dialAddr := resolveSocketPath(socketPath)
h.client.Transport = &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
-48
View File
@@ -48,20 +48,6 @@ func (m *Manager) RegisterCounter(name string) (stats.Counter, error) {
return c, nil
}
// GetOrRegisterCounter implements stats.Manager.
func (m *Manager) GetOrRegisterCounter(name string) (stats.Counter, error) {
m.access.Lock()
defer m.access.Unlock()
if c, found := m.counters[name]; found {
return c, nil
}
errors.LogDebug(context.Background(), "create new counter ", name)
c := new(Counter)
m.counters[name] = c
return c, nil
}
// UnregisterCounter implements stats.Manager.
func (m *Manager) UnregisterCounter(name string) error {
m.access.Lock()
@@ -111,20 +97,6 @@ func (m *Manager) RegisterOnlineMap(name string) (stats.OnlineMap, error) {
return om, nil
}
// GetOrRegisterOnlineMap implements stats.Manager.
func (m *Manager) GetOrRegisterOnlineMap(name string) (stats.OnlineMap, error) {
m.access.Lock()
defer m.access.Unlock()
if om, found := m.onlineMaps[name]; found {
return om, nil
}
errors.LogDebug(context.Background(), "create new OnlineMap ", name)
om := NewOnlineMap()
m.onlineMaps[name] = om
return om, nil
}
// UnregisterOnlineMap implements stats.Manager.
func (m *Manager) UnregisterOnlineMap(name string) error {
m.access.Lock()
@@ -177,26 +149,6 @@ func (m *Manager) RegisterChannel(name string) (stats.Channel, error) {
return c, nil
}
// GetOrRegisterChannel implements stats.Manager.
func (m *Manager) GetOrRegisterChannel(name string) (stats.Channel, error) {
m.access.Lock()
defer m.access.Unlock()
if c, found := m.channels[name]; found {
return c, nil
}
errors.LogDebug(context.Background(), "create new channel ", name)
c := NewChannel(&ChannelConfig{BufferSize: 64, Blocking: false})
if m.running {
// Start before publishing so no goroutine can observe an unstarted channel.
if err := c.Start(); err != nil {
return nil, err
}
}
m.channels[name] = c
return c, nil
}
// UnregisterChannel implements stats.Manager.
func (m *Manager) UnregisterChannel(name string) error {
m.access.Lock()
+1 -1
View File
@@ -11,7 +11,7 @@ import (
)
func TestInterface(t *testing.T) {
_ = stats.Manager(new(Manager))
_ = (stats.Manager)(new(Manager))
}
func TestStatsChannelRunnable(t *testing.T) {
+2 -3
View File
@@ -2,11 +2,10 @@ package version
import (
"context"
"strconv"
"strings"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"strconv"
"strings"
)
type Version struct {
+1 -1
View File
@@ -122,7 +122,7 @@ func NewReader(reader io.Reader) Reader {
}
_, isFile := reader.(*os.File)
if !isFile && useReadV() {
if !isFile && useReadv {
if sc, ok := reader.(syscall.Conn); ok {
rawConn, err := sc.SyscallConn()
if err != nil {
+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.
func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer {
n := len(dest)
if n > 0 && !dest[n-1].IsFull() {
nBytes, _ := dest[n-1].Write(src)
if n > 0 && !(dest)[n-1].IsFull() {
nBytes, _ := (dest)[n-1].Write(src)
src = src[nBytes:]
}
+6 -6
View File
@@ -121,11 +121,11 @@ func TestPacketReader_ReadMultiBuffer(t *testing.T) {
}
func TestReaderInterface(t *testing.T) {
_ = io.Reader(new(ReadVReader))
_ = Reader(new(ReadVReader))
_ = (io.Reader)(new(ReadVReader))
_ = (Reader)(new(ReadVReader))
_ = Reader(new(BufferedReader))
_ = io.Reader(new(BufferedReader))
_ = io.ByteReader(new(BufferedReader))
_ = io.WriterTo(new(BufferedReader))
_ = (Reader)(new(BufferedReader))
_ = (io.Reader)(new(BufferedReader))
_ = (io.ByteReader)(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 {
iovecs = append(iovecs, syscall.Iovec{
Base: &b.v[0],
Base: &(b.v[0]),
})
iovecs[idx].SetLen(int(Size))
}
+7 -19
View File
@@ -5,7 +5,6 @@ package buf
import (
"io"
"sync/atomic"
"syscall"
"github.com/xtls/xray-core/common/platform"
@@ -144,24 +143,13 @@ func (r *ReadVReader) ReadMultiBuffer() (MultiBuffer, error) {
return mb, nil
}
var useReadv atomic.Bool
func useReadV() bool {
return useReadv.Load()
}
func reloadEnvSettings() error {
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue })
enabled := false
switch value {
case defaultFlagValue, "auto", "enable":
enabled = true
}
useReadv.Store(enabled)
return nil
}
var useReadv bool
func init() {
platform.RegisterEnvReload(reloadEnvSettings)
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue })
switch value {
case defaultFlagValue, "auto", "enable":
useReadv = true
}
}
+1 -3
View File
@@ -10,9 +10,7 @@ import (
"github.com/xtls/xray-core/features/stats"
)
func useReadV() bool {
return false
}
const useReadv = false
func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader {
panic("not implemented")
-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() }
+14 -67
View File
@@ -8,7 +8,6 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata/strmatcher"
"github.com/xtls/xray-core/common/utils"
)
type DomainMatcher interface {
@@ -24,54 +23,10 @@ type DomainMatcherFactory interface {
BuildMatcher(rules []*DomainRule) (DomainMatcher, error)
}
type MphDomainMatcherFactory struct {
sync.Mutex
shared *utils.WeakCacheMap[string, strmatcher.MphValueMatcher]
}
func buildDomainRulesKey(rules []*DomainRule) string {
var sb strings.Builder
cache := false
for _, r := range rules {
switch v := r.Value.(type) {
case *DomainRule_Custom:
sb.WriteString(v.Custom.Type.String())
sb.WriteString(":")
sb.WriteString(v.Custom.Value)
sb.WriteString(",")
case *DomainRule_Geosite:
cache = true
sb.WriteString(v.Geosite.File)
sb.WriteString(":")
sb.WriteString(v.Geosite.Code)
sb.WriteString("@")
sb.WriteString(v.Geosite.Attrs)
sb.WriteString(",")
default:
panic("unknown domain rule type")
}
}
if !cache {
return ""
}
return sb.String()
}
type MphDomainMatcherFactory struct{}
// BuildMatcher implements DomainMatcherFactory.
func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
key := buildDomainRulesKey(rules)
if key != "" {
f.Lock()
defer f.Unlock()
if g, ok := f.shared.Load(key); ok {
errors.LogDebug(context.Background(), "geodata mph domain matcher cache HIT for ", len(rules), " rules")
return g, nil
}
errors.LogDebug(context.Background(), "geodata mph domain matcher cache MISS for ", len(rules), " rules")
}
g := strmatcher.NewMphValueMatcher()
for i, r := range rules {
switch v := r.Value.(type) {
@@ -102,30 +57,25 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
if err := g.Build(); err != nil {
return nil, err
}
if key != "" {
f.shared.Store(key, g)
}
return g, nil
}
type CompactDomainMatcherFactory struct {
sync.Mutex
shared *utils.WeakCacheMap[string, strmatcher.LinearAnyMatcher]
shared map[string]strmatcher.MatcherGroup // TODO: cleanup
}
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherGroup, error) {
key := rule.File + ":" + rule.Code + "@" + rule.Attrs
f.Lock()
defer f.Unlock()
if s, ok := f.shared.Load(key); ok {
errors.LogDebug(context.Background(), "geodata geosite matcher cache HIT ", key)
return s, nil
if m := f.shared[key]; m != nil {
return m, nil
}
errors.LogDebug(context.Background(), "geodata geosite matcher cache MISS ", key)
s := strmatcher.NewLinearAnyMatcher()
g := strmatcher.NewLinearValueMatcher()
domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
if err != nil {
return nil, err
@@ -137,19 +87,16 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
continue
}
s.Add(m)
g.Add(m, 0)
}
f.shared.Store(key, s)
return s, err
f.shared[key] = g
return g, err
}
// BuildMatcher implements DomainMatcherFactory.
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
if len(rules) == 0 {
return nil, errors.New("empty domain rule list")
}
compact := &CompactDomainMatcher{
matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
matchers: make([]strmatcher.MatcherGroup, 0, len(rules)),
values: make([]uint32, 0, len(rules)),
}
for i, r := range rules {
@@ -179,7 +126,7 @@ func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainM
type CompactDomainMatcher struct {
custom strmatcher.ValueMatcher
matchers []strmatcher.MatcherSet
matchers []strmatcher.MatcherGroup
values []uint32
}
@@ -220,7 +167,7 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
case Domain_Regex:
return strmatcher.Regex.New(d.Value)
case Domain_Domain:
return strmatcher.Domain.New(strings.ToLower(d.Value))
return strmatcher.Domain.New(d.Value)
case Domain_Full:
return strmatcher.Full.New(strings.ToLower(d.Value))
default:
@@ -231,8 +178,8 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
func newDomainMatcherFactory() DomainMatcherFactory {
switch runtime.GOOS {
case "ios", "android":
return &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
return &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
default:
return &MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}
return &MphDomainMatcherFactory{}
}
}
+6 -8
View File
@@ -7,11 +7,10 @@ import (
"testing"
"github.com/xtls/xray-core/common/geodata/strmatcher"
"github.com/xtls/xray-core/common/utils"
)
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
@@ -32,7 +31,7 @@ func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
factory := &CompactDomainMatcherFactory{shared: make(map[string]strmatcher.MatcherGroup)}
matcher, err := factory.BuildMatcher([]*DomainRule{
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
@@ -51,11 +50,10 @@ func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
}
func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
matcher, err := (&MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}).
BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
})
matcher, err := (&MphDomainMatcherFactory{}).BuildMatcher([]*DomainRule{
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
})
if err != nil {
t.Fatalf("BuildMatcher() failed: %v", err)
}
+3 -88
View File
@@ -1,102 +1,17 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/common/uuid"
)
type DomainRegistry struct {
mu sync.Mutex
factory DomainMatcherFactory
matchers *utils.WeakCacheMap[uuid.UUID, DynamicDomainMatcher]
factory DomainMatcherFactory
}
func (r *DomainRegistry) BuildDomainMatcher(rules []*DomainRule) (DomainMatcher, error) {
r.mu.Lock()
defer r.mu.Unlock()
m, err := r.factory.BuildMatcher(rules)
if err != nil {
return nil, err
}
d := NewDynamicDomainMatcher(rules, m)
r.matchers.Store(uuid.New(), d)
return d, nil
}
func (r *DomainRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
var matchers []*DynamicDomainMatcher
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()
type reloadEntry struct {
dynamic *DynamicDomainMatcher
matcher DomainMatcher
}
reloaded := make([]reloadEntry, len(matchers))
for i, d := range matchers {
m, err := factory.BuildMatcher(d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoSite data for domain matcher ", i)
return err
}
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
}
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.factory = factory
errors.LogInfo(context.Background(), "reloaded GeoSite data for ", len(matchers), " domain matcher(s)")
return nil
return r.factory.BuildMatcher(rules)
}
func newDomainRegistry() *DomainRegistry {
return &DomainRegistry{
factory: newDomainMatcherFactory(),
matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicDomainMatcher](),
factory: newDomainMatcherFactory(),
}
}
var DomainReg = newDomainRegistry()
type domainMatcherState struct {
matcher DomainMatcher
}
type DynamicDomainMatcher struct {
rules []*DomainRule
state atomic.Pointer[domainMatcherState]
}
// Match implements DomainMatcher.
func (d *DynamicDomainMatcher) Match(input string) []uint32 {
return d.state.Load().matcher.Match(input)
}
// MatchAny implements DomainMatcher.
func (d *DynamicDomainMatcher) MatchAny(input string) bool {
return d.state.Load().matcher.MatchAny(input)
}
func (d *DynamicDomainMatcher) Reload(newMatcher DomainMatcher) {
d.state.Store(&domainMatcherState{matcher: newMatcher})
}
func NewDynamicDomainMatcher(rules []*DomainRule, matcher DomainMatcher) *DynamicDomainMatcher {
d := &DynamicDomainMatcher{rules: rules}
d.Reload(matcher)
return d
}
+5 -19
View File
@@ -11,7 +11,6 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
"go4.org/netipx"
)
@@ -807,7 +806,7 @@ func (mm *HeuristicMultiIPMatcher) SetReverse(reverse bool) {
type IPSetFactory struct {
sync.Mutex
shared *utils.WeakCacheMap[string, IPSet]
shared map[string]*IPSet // TODO: cleanup
}
func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, error) {
@@ -816,11 +815,9 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
f.Lock()
defer f.Unlock()
if ipset, ok := f.shared.Load(key); ok {
errors.LogDebug(context.Background(), "geodata geoip matcher cache HIT ", key)
if ipset := f.shared[key]; ipset != nil {
return ipset, nil
}
errors.LogDebug(context.Background(), "geodata geoip matcher cache MISS ", key)
ipset, err := f.createFrom(func(add func(*CIDR)) error {
for _, r := range rules {
@@ -836,7 +833,7 @@ func (f *IPSetFactory) GetOrCreateFromGeoIPRules(rules []*GeoIPRule) (*IPSet, er
return nil
})
if err == nil {
f.shared.Store(key, ipset)
f.shared[key] = ipset
}
return ipset, err
}
@@ -918,31 +915,24 @@ func (f *IPSetFactory) createFrom(yield func(func(*CIDR)) error) (*IPSet, error)
return nil, errors.New("failed to build IPv6 set").Base(err)
}
var has4, has6 bool
var max4, max6 int
for _, p := range ipv4.Prefixes() {
has4 = true
if b := p.Bits(); b > max4 {
max4 = b
}
}
for _, p := range ipv6.Prefixes() {
has6 = true
if b := p.Bits(); b > max6 {
max6 = b
}
}
if !has4 {
if max4 == 0 {
max4 = 0xff
} else if max4 == 0 {
max4 = 0xfe
}
if !has6 {
if max6 == 0 {
max6 = 0xff
} else if max6 == 0 {
max6 = 0xfe
}
return &IPSet{ipv4: ipv4, ipv6: ipv6, max4: uint8(max4), max6: uint8(max6)}, nil
@@ -1017,7 +1007,3 @@ func buildOptimizedIPMatcher(f *IPSetFactory, rules []*IPRule) (IPMatcher, error
return &HeuristicMultiIPMatcher{matchers: subs}, nil
}
}
func newIPSetFactory() *IPSetFactory {
return &IPSetFactory{shared: utils.NewWeakCacheMap[string, IPSet]()}
}
+3 -87
View File
@@ -97,90 +97,6 @@ func TestIPMatcher(t *testing.T) {
}
}
func TestIPMatcherFullCIDR4(t *testing.T) {
matcher := buildIPMatcher(
"0.0.0.0/0",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: true,
},
{
Input: "0.0.0.0",
Output: true,
},
{
Input: "255.255.255.255",
Output: true,
},
{
Input: "2001:cdba::3257:9652",
Output: false,
},
{
Input: "::0",
Output: false,
},
{
Input: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
Output: false,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherFullCIDR6(t *testing.T) {
matcher := buildIPMatcher(
"::0/0",
)
testCases := []struct {
Input string
Output bool
}{
{
Input: "192.168.1.1",
Output: false,
},
{
Input: "0.0.0.0",
Output: false,
},
{
Input: "255.255.255.255",
Output: false,
},
{
Input: "2001:cdba::3257:9652",
Output: true,
},
{
Input: "::0",
Output: true,
},
{
Input: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
Output: true,
},
}
for _, test := range testCases {
if v := matcher.Match(xnet.ParseAddress(test.Input).IP()); v != test.Output {
t.Error("unexpected output: ", v, " for test case ", test)
}
}
}
func TestIPMatcherRegression(t *testing.T) {
matcher := buildIPMatcher(
"98.108.20.0/22",
@@ -315,7 +231,7 @@ func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
}
if !matcher.AnyMatch([]net.IP{
{},
net.IP{},
ip("1.1.1.1"),
ip("8.8.8.8"),
}) {
@@ -345,7 +261,7 @@ func TestIPMatcherAnyMatchAndMatches(t *testing.T) {
if matcher.Matches([]net.IP{
ip("8.8.8.8"),
{},
net.IP{},
}) {
t.Fatal("expect Matches to be false when any IP is invalid")
}
@@ -362,7 +278,7 @@ func TestIPMatcherFilterIPs(t *testing.T) {
}
matched, unmatched := matcher.FilterIPs([]net.IP{
{},
net.IP{},
ip("8.8.8.8"),
ip("91.108.255.254"),
ip("1.1.1.1"),
+3 -129
View File
@@ -1,143 +1,17 @@
package geodata
import (
"context"
"sync"
"sync/atomic"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/common/uuid"
)
type IPRegistry struct {
mu sync.Mutex
factory *IPSetFactory
matchers *utils.WeakCacheMap[uuid.UUID, DynamicIPMatcher]
ipsetFactory *IPSetFactory
}
func (r *IPRegistry) BuildIPMatcher(rules []*IPRule) (IPMatcher, error) {
r.mu.Lock()
defer r.mu.Unlock()
m, err := buildOptimizedIPMatcher(r.factory, rules)
if err != nil {
return nil, err
}
d := NewDynamicIPMatcher(rules, m)
r.matchers.Store(uuid.New(), d)
return d, nil
}
func (r *IPRegistry) Reload() error {
r.mu.Lock()
defer r.mu.Unlock()
var matchers []*DynamicIPMatcher
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()
type reloadEntry struct {
dynamic *DynamicIPMatcher
matcher IPMatcher
}
reloaded := make([]reloadEntry, len(matchers))
for i, d := range matchers {
m, err := buildOptimizedIPMatcher(factory, d.rules)
if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to reload GeoIP data for IP matcher ", i)
return err
}
reloaded[i] = reloadEntry{dynamic: d, matcher: m}
}
for _, entry := range reloaded {
entry.dynamic.Reload(entry.matcher)
}
r.factory = factory
errors.LogInfo(context.Background(), "reloaded GeoIP data for ", len(matchers), " IP matcher(s)")
return nil
return buildOptimizedIPMatcher(r.ipsetFactory, rules)
}
func newIPRegistry() *IPRegistry {
return &IPRegistry{
factory: newIPSetFactory(),
matchers: utils.NewWeakCacheMap[uuid.UUID, DynamicIPMatcher](),
ipsetFactory: &IPSetFactory{shared: make(map[string]*IPSet)},
}
}
var IPReg = newIPRegistry()
type ipMatcherState struct {
matcher IPMatcher
}
type DynamicIPMatcher struct {
rules []*IPRule
state atomic.Pointer[ipMatcherState]
mu sync.Mutex
reverse bool
reverseSet bool
}
// Match implements IPMatcher.
func (d *DynamicIPMatcher) Match(ip net.IP) bool {
return d.state.Load().matcher.Match(ip)
}
// AnyMatch implements IPMatcher.
func (d *DynamicIPMatcher) AnyMatch(ips []net.IP) bool {
return d.state.Load().matcher.AnyMatch(ips)
}
// Matches implements IPMatcher.
func (d *DynamicIPMatcher) Matches(ips []net.IP) bool {
return d.state.Load().matcher.Matches(ips)
}
// FilterIPs implements IPMatcher.
func (d *DynamicIPMatcher) FilterIPs(ips []net.IP) (matched []net.IP, unmatched []net.IP) {
return d.state.Load().matcher.FilterIPs(ips)
}
// ToggleReverse implements IPMatcher.
func (d *DynamicIPMatcher) ToggleReverse() {
d.mu.Lock()
defer d.mu.Unlock()
d.reverse = !d.reverse
d.state.Load().matcher.ToggleReverse()
}
// SetReverse implements IPMatcher.
func (d *DynamicIPMatcher) SetReverse(reverse bool) {
d.mu.Lock()
defer d.mu.Unlock()
d.reverse = reverse
d.reverseSet = true
d.state.Load().matcher.SetReverse(reverse)
}
func (d *DynamicIPMatcher) Reload(newMatcher IPMatcher) {
d.mu.Lock()
defer d.mu.Unlock()
if d.reverseSet {
newMatcher.SetReverse(d.reverse)
} else if d.reverse {
newMatcher.ToggleReverse()
}
d.state.Store(&ipMatcherState{matcher: newMatcher})
}
func NewDynamicIPMatcher(rules []*IPRule, matcher IPMatcher) *DynamicIPMatcher {
d := &DynamicIPMatcher{rules: rules}
d.Reload(matcher)
return d
}
+2 -2
View File
@@ -138,7 +138,7 @@ func ParseDomainRule(r string, defaultType Domain_Type) (*DomainRule, error) {
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} {
for _, ext := range [...]string{"ext:", "ext-domain:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
@@ -167,7 +167,7 @@ func ParseDomainRules(rules []string, defaultType Domain_Type) ([]*DomainRule, e
}
prefix := 0
for _, ext := range [...]string{"ext:", "ext-domain:", "ext-site:"} {
for _, ext := range [...]string{"ext:", "ext-domain:"} {
if strings.HasPrefix(r, ext) {
prefix = len(ext)
break
@@ -1,53 +0,0 @@
package strmatcher
// LinearAnyMatcher is an implementation of AnyMatcher.
type LinearAnyMatcher struct {
full *FullMatcherSet
domain *DomainMatcherSet
substr *SubstrMatcherSet
regex *SimpleMatcherSet
}
func NewLinearAnyMatcher() *LinearAnyMatcher {
return new(LinearAnyMatcher)
}
// Add implements AnyMatcher.Add.
func (s *LinearAnyMatcher) Add(matcher Matcher) {
switch matcher := matcher.(type) {
case FullMatcher:
if s.full == nil {
s.full = NewFullMatcherSet()
}
s.full.AddFullMatcher(matcher)
case DomainMatcher:
if s.domain == nil {
s.domain = NewDomainMatcherSet()
}
s.domain.AddDomainMatcher(matcher)
case SubstrMatcher:
if s.substr == nil {
s.substr = new(SubstrMatcherSet)
}
s.substr.AddSubstrMatcher(matcher)
default:
if s.regex == nil {
s.regex = new(SimpleMatcherSet)
}
s.regex.AddMatcher(matcher)
}
}
// MatchAny implements AnyMatcher.MatchAny.
func (s *LinearAnyMatcher) MatchAny(input string) bool {
if s.full != nil && s.full.MatchAny(input) {
return true
}
if s.domain != nil && s.domain.MatchAny(input) {
return true
}
if s.substr != nil && s.substr.MatchAny(input) {
return true
}
return s.regex != nil && s.regex.MatchAny(input)
}
+4 -62
View File
@@ -100,6 +100,10 @@ func (t Type) New(pattern string) (Matcher, error) {
case Substr:
return SubstrMatcher(pattern), nil
case Domain:
pattern, err := ToDomain(pattern)
if err != nil {
return nil, err
}
return DomainMatcher(pattern), nil
case Regex: // 1. regex matching is case-sensitive
regex, err := regexp.Compile(pattern)
@@ -284,65 +288,3 @@ func CompositeMatchesReverse(matches [][]uint32) []uint32 {
return result
}
}
// MatcherSetForAll is an interface indicating a MatcherSet could accept all types of matchers.
type MatcherSetForAll interface {
AddMatcher(matcher Matcher)
}
// MatcherSetForFull is an interface indicating a MatcherSet could accept FullMatchers.
type MatcherSetForFull interface {
AddFullMatcher(matcher FullMatcher)
}
// MatcherSetForDomain is an interface indicating a MatcherSet could accept DomainMatchers.
type MatcherSetForDomain interface {
AddDomainMatcher(matcher DomainMatcher)
}
// MatcherSetForSubstr is an interface indicating a MatcherSet could accept SubstrMatchers.
type MatcherSetForSubstr interface {
AddSubstrMatcher(matcher SubstrMatcher)
}
// MatcherSetForRegex is an interface indicating a MatcherSet could accept RegexMatchers.
type MatcherSetForRegex interface {
AddRegexMatcher(matcher *RegexMatcher)
}
// AddMatcherToSet is a helper function to try to add a Matcher to any kind of MatcherSet.
// It returns error if the MatcherSet does not accept the provided Matcher's type.
// This function is provided to help writing code to test a MatcherSet.
func AddMatcherToSet(s MatcherSet, matcher Matcher) error {
if s, ok := s.(IndexMatcher); ok {
s.Add(matcher)
return nil
}
if s, ok := s.(MatcherSetForAll); ok {
s.AddMatcher(matcher)
return nil
}
switch matcher := matcher.(type) {
case FullMatcher:
if s, ok := s.(MatcherSetForFull); ok {
s.AddFullMatcher(matcher)
return nil
}
case DomainMatcher:
if s, ok := s.(MatcherSetForDomain); ok {
s.AddDomainMatcher(matcher)
return nil
}
case SubstrMatcher:
if s, ok := s.(MatcherSetForSubstr); ok {
s.AddSubstrMatcher(matcher)
return nil
}
case *RegexMatcher:
if s, ok := s.(MatcherSetForRegex); ok {
s.AddRegexMatcher(matcher)
return nil
}
}
return errors.New("cannot add matcher to matcher set")
}
@@ -1,79 +0,0 @@
package strmatcher
type trieNode2 struct {
matched bool
children map[string]*trieNode2
}
// DomainMatcherSet is an implementation of MatcherSet.
// It uses trie to optimize both memory consumption and lookup speed. Trie node is domain label based.
type DomainMatcherSet struct {
root *trieNode2
}
func NewDomainMatcherSet() *DomainMatcherSet {
return &DomainMatcherSet{
root: new(trieNode2),
}
}
// AddDomainMatcher implements MatcherSetForDomain.AddDomainMatcher.
func (s *DomainMatcherSet) AddDomainMatcher(matcher DomainMatcher) {
node := s.root
pattern := matcher.Pattern()
for i := len(pattern); i > 0; {
var part string
for j := i - 1; ; j-- {
if pattern[j] == '.' {
part = pattern[j+1 : i]
i = j
break
}
if j == 0 {
part = pattern[j:i]
i = j
break
}
}
if node.children == nil {
node.children = make(map[string]*trieNode2)
}
next := node.children[part]
if next == nil {
next = new(trieNode2)
node.children[part] = next
}
node = next
}
node.matched = true
}
// MatchAny implements MatcherSet.MatchAny.
func (s *DomainMatcherSet) MatchAny(input string) bool {
node := s.root
for i := len(input); i > 0; {
for j := i - 1; ; j-- {
if input[j] == '.' {
node = node.children[input[j+1:i]]
i = j
break
}
if j == 0 {
node = node.children[input[j:i]]
i = j
break
}
}
if node == nil {
return false
}
if node.matched {
return true
}
if node.children == nil {
return false
}
}
return false
}
@@ -1,95 +0,0 @@
package strmatcher_test
import (
"reflect"
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestDomainMatcherSet(t *testing.T) {
patterns := []struct {
Pattern string
}{
{
Pattern: "example.com",
},
{
Pattern: "google.com",
},
{
Pattern: "x.a.com",
},
{
Pattern: "a.b.com",
},
{
Pattern: "c.a.b.com",
},
{
Pattern: "x.y.com",
},
{
Pattern: "x.y.com",
},
}
testCases := []struct {
Domain string
Result bool
}{
{
Domain: "x.example.com",
Result: true,
},
{
Domain: "y.com",
Result: false,
},
{
Domain: "a.b.com",
Result: true,
},
{
Domain: "c.a.b.com",
Result: true,
},
{
Domain: "c.a..b.com",
Result: false,
},
{
Domain: ".com",
Result: false,
},
{
Domain: "com",
Result: false,
},
{
Domain: "",
Result: false,
},
{
Domain: "x.y.com",
Result: true,
},
}
s := NewDomainMatcherSet()
for _, pattern := range patterns {
AddMatcherToSet(s, DomainMatcher(pattern.Pattern))
}
for _, testCase := range testCases {
r := s.MatchAny(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) {
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
}
}
}
func TestEmptyDomainMatcherSet(t *testing.T) {
s := NewDomainMatcherSet()
r := s.MatchAny("example.com")
if r {
t.Error("Expect false, but ", r)
}
}
@@ -1,24 +0,0 @@
package strmatcher
// FullMatcherSet is an implementation of MatcherSet.
// It uses a hash table to facilitate exact match lookup.
type FullMatcherSet struct {
matchers map[string]struct{}
}
func NewFullMatcherSet() *FullMatcherSet {
return &FullMatcherSet{
matchers: make(map[string]struct{}),
}
}
// AddFullMatcher implements MatcherSetForFull.AddFullMatcher.
func (s *FullMatcherSet) AddFullMatcher(matcher FullMatcher) {
s.matchers[matcher.Pattern()] = struct{}{}
}
// MatchAny implements MatcherSet.Any.
func (s *FullMatcherSet) MatchAny(input string) bool {
_, found := s.matchers[input]
return found
}
@@ -1,65 +0,0 @@
package strmatcher_test
import (
"reflect"
"testing"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestFullMatcherSet(t *testing.T) {
patterns := []struct {
Pattern string
}{
{
Pattern: "example.com",
},
{
Pattern: "google.com",
},
{
Pattern: "x.a.com",
},
{
Pattern: "x.y.com",
},
{
Pattern: "x.y.com",
},
}
testCases := []struct {
Domain string
Result bool
}{
{
Domain: "example.com",
Result: true,
},
{
Domain: "y.com",
Result: false,
},
{
Domain: "x.y.com",
Result: true,
},
}
s := NewFullMatcherSet()
for _, pattern := range patterns {
AddMatcherToSet(s, FullMatcher(pattern.Pattern))
}
for _, testCase := range testCases {
r := s.MatchAny(testCase.Domain)
if !reflect.DeepEqual(r, testCase.Result) {
t.Error("Failed to match domain: ", testCase.Domain, ", expect ", testCase.Result, ", but got ", r)
}
}
}
func TestEmptyFullMatcherSet(t *testing.T) {
s := NewFullMatcherSet()
r := s.MatchAny("example.com")
if r {
t.Error("Expect false, but ", r)
}
}
@@ -1,22 +0,0 @@
package strmatcher
// SimpleMatcherSet is an implementation of MatcherSet.
// It simply stores all matchers in an array and sequentially matches them.
type SimpleMatcherSet struct {
matchers []Matcher
}
// AddMatcher implements MatcherSetForAll.AddMatcher.
func (s *SimpleMatcherSet) AddMatcher(matcher Matcher) {
s.matchers = append(s.matchers, matcher)
}
// MatchAny implements MatcherSet.MatchAny.
func (s *SimpleMatcherSet) MatchAny(input string) bool {
for _, m := range s.matchers {
if m.Match(input) {
return true
}
}
return false
}
@@ -1,69 +0,0 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestSimpleMatcherSet(t *testing.T) {
patterns := []struct {
pattern string
mType Type
}{
{
pattern: "example.com",
mType: Domain,
},
{
pattern: "example.com",
mType: Full,
},
{
pattern: "example.com",
mType: Regex,
},
}
cases := []struct {
input string
output bool
}{
{
input: "www.example.com",
output: true,
},
{
input: "example.com",
output: true,
},
{
input: "www.e3ample.com",
output: false,
},
{
input: "xample.com",
output: false,
},
{
input: "xexample.com",
output: true,
},
{
input: "examplexcom",
output: true,
},
}
matcherSet := &SimpleMatcherSet{}
for _, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToSet(matcherSet, matcher))
}
for _, test := range cases {
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}
@@ -1,24 +0,0 @@
package strmatcher
import "strings"
// SubstrMatcherSet is implementation of MatcherSet,
// It is simply implmeneted to comply with the priority specification of Substr matchers.
type SubstrMatcherSet struct {
patterns []string
}
// AddSubstrMatcher implements MatcherSetForSubstr.AddSubstrMatcher.
func (s *SubstrMatcherSet) AddSubstrMatcher(matcher SubstrMatcher) {
s.patterns = append(s.patterns, matcher.Pattern())
}
// MatchAny implements MatcherSet.MatchAny.
func (s *SubstrMatcherSet) MatchAny(input string) bool {
for _, pattern := range s.patterns {
if strings.Contains(input, pattern) {
return true
}
}
return false
}
@@ -1,77 +0,0 @@
package strmatcher_test
import (
"reflect"
"testing"
"github.com/xtls/xray-core/common"
. "github.com/xtls/xray-core/common/geodata/strmatcher"
)
func TestSubstrMatcherSet(t *testing.T) {
patterns := []struct {
pattern string
mType Type
}{
{
pattern: "apis",
mType: Substr,
},
{
pattern: "google",
mType: Substr,
},
{
pattern: "apis",
mType: Substr,
},
}
cases := []struct {
input string
output bool
}{
{
input: "google.com",
output: true,
},
{
input: "apis.com",
output: true,
},
{
input: "googleapis.com",
output: true,
},
{
input: "fonts.googleapis.com",
output: true,
},
{
input: "apis.googleapis.com",
output: true,
},
{
input: "baidu.com",
output: false,
},
{
input: "goog",
output: false,
},
{
input: "api",
output: false,
},
}
matcherSet := &SubstrMatcherSet{}
for _, entry := range patterns {
matcher, err := entry.mType.New(entry.pattern)
common.Must(err)
common.Must(AddMatcherToSet(matcherSet, matcher))
}
for _, test := range cases {
if r := matcherSet.MatchAny(test.input); !reflect.DeepEqual(r, test.output) {
t.Error("unexpected output: ", r, " for test case ", test)
}
}
}
+1 -19
View File
@@ -15,7 +15,7 @@ const (
)
// Matcher is the interface to determine a string matches a pattern.
// - This is a basic matcher to represent a certain kind of match semantic (full, substr, domain or regex).
// - This is a basic matcher to represent a certain kind of match semantic(full, substr, domain or regex).
type Matcher interface {
// Type returns the matcher's type.
Type() Type
@@ -101,21 +101,3 @@ type ValueMatcher interface {
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// MatcherSet is an advanced type of matcher to accept a bunch of basic Matchers (of certain type, not all matcher types).
// For example:
// - FullMatcherSet accepts FullMatcher and uses a hash table to facilitate lookup.
// - DomainMatcherSet accepts DomainMatcher and uses a trie to optimize both memory consumption and lookup speed.
type MatcherSet interface {
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
// AnyMatcher is a lightweight matcher for callers that only need existence checks.
type AnyMatcher interface {
// Add adds a new Matcher to AnyMatcher.
Add(matcher Matcher)
// MatchAny returns true as soon as one matching matcher is found.
MatchAny(input string) bool
}
+1
View File
@@ -98,6 +98,7 @@ func (l *generalLogger) run() {
}
func (l *generalLogger) Handle(msg Message) {
select {
case l.buffer <- msg:
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
-40
View File
@@ -1,40 +0,0 @@
package platform
import (
"errors"
"sync"
)
var envReloadRegistry = struct {
sync.RWMutex
handlers []func() error
}{}
// RegisterEnvReload registers an environment reload handler and runs it once
// immediately so package defaults keep the same behavior as init-time reads.
func RegisterEnvReload(handler func() error) {
if handler == nil {
return
}
envReloadRegistry.Lock()
envReloadRegistry.handlers = append(envReloadRegistry.handlers, handler)
envReloadRegistry.Unlock()
if err := handler(); err != nil {
panic(err)
}
}
// ReloadEnvSettings refreshes all registered environment-backed package state.
func ReloadEnvSettings() error {
envReloadRegistry.RLock()
handlers := append([]func() error{}, envReloadRegistry.handlers...)
envReloadRegistry.RUnlock()
var errs []error
for _, handler := range handlers {
if err := handler(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}
+2 -40
View File
@@ -1,7 +1,6 @@
package filesystem
import (
"errors"
"io"
"os"
"path/filepath"
@@ -27,48 +26,11 @@ func ReadFile(path string) ([]byte, error) {
}
func ReadAsset(file string) ([]byte, error) {
path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return ReadFile(path)
return ReadFile(platform.GetAssetLocation(file))
}
func OpenAsset(file string) (io.ReadCloser, error) {
path, _, err := getAssetFileLocation(file)
if err != nil {
return nil, err
}
return NewFileReader(path)
}
func StatAsset(file string) (os.FileInfo, error) {
_, info, err := getAssetFileLocation(file)
return info, err
}
func ResolveAsset(file string) (string, error) {
path, _, err := getAssetFileLocation(file)
return path, err
}
func getAssetFileLocation(file string) (string, os.FileInfo, error) {
if !filepath.IsLocal(file) || file == "." {
return "", nil, errors.New("asset path must stay in asset directory")
}
local, err := filepath.Localize(file)
if err != nil {
return "", nil, err
}
path := platform.GetAssetLocation(local)
info, err := os.Stat(path)
if err != nil {
return "", nil, err
}
if !info.Mode().IsRegular() {
return "", nil, errors.New("asset is not a regular file")
}
return path, info, nil
return NewFileReader(platform.GetAssetLocation(file))
}
func ReadCert(file string) ([]byte, error) {
-32
View File
@@ -1,32 +0,0 @@
package filesystem_test
import (
"path/filepath"
"testing"
. "github.com/xtls/xray-core/common/platform/filesystem"
)
func TestStatAssetRejectsInvalidPath(t *testing.T) {
for _, file := range []string{
"",
".",
"..",
"../geoip.dat",
"nested/..",
"nested/../geoip.dat",
"nested//geoip.dat",
"/geoip.dat",
"/tmp/geoip.dat",
`C:\geoip.dat`,
`C:geoip.dat`,
`\\server\share\geoip.dat`,
`nested\geoip.dat`,
`nested\..\geoip.dat`,
filepath.Join(t.TempDir(), "geoip.dat"),
} {
if _, err := StatAsset(file); err == nil {
t.Fatalf("expected error for %q", file)
}
}
}
-1
View File
@@ -17,7 +17,6 @@ const (
UseFreedomSplice = "xray.buf.splice"
UseVmessPadding = "xray.vmess.padding"
UseCone = "xray.cone.disabled"
UseStrictJSON = "xray.json.strict"
BufferSize = "xray.ray.buffer.size"
BrowserDialerAddress = "xray.browser.dialer"
+2 -3
View File
@@ -36,10 +36,9 @@ func TestNormalizeEnvName(t *testing.T) {
}
func TestEnvFlag(t *testing.T) {
v := EnvFlag{
if v := (EnvFlag{
Name: "xxxxx.y",
}.GetValueAsInt(10)
if v != 10 {
}.GetValueAsInt(10)); v != 10 {
t.Error("env value: ", v)
}
}
+10 -2
View File
@@ -28,6 +28,8 @@ const (
SecurityType_AUTO SecurityType = 2
SecurityType_AES128_GCM SecurityType = 3
SecurityType_CHACHA20_POLY1305 SecurityType = 4
SecurityType_NONE SecurityType = 5 // [DEPRECATED 2023-06]
SecurityType_ZERO SecurityType = 6
)
// Enum value maps for SecurityType.
@@ -37,12 +39,16 @@ var (
2: "AUTO",
3: "AES128_GCM",
4: "CHACHA20_POLY1305",
5: "NONE",
6: "ZERO",
}
SecurityType_value = map[string]int32{
"UNKNOWN": 0,
"AUTO": 2,
"AES128_GCM": 3,
"CHACHA20_POLY1305": 4,
"NONE": 5,
"ZERO": 6,
}
)
@@ -123,13 +129,15 @@ const file_common_protocol_headers_proto_rawDesc = "" +
"\n" +
"\x1dcommon/protocol/headers.proto\x12\x14xray.common.protocol\"H\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" +
"\aUNKNOWN\x10\x00\x12\b\n" +
"\x04AUTO\x10\x02\x12\x0e\n" +
"\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"
var (
+2
View File
@@ -11,6 +11,8 @@ enum SecurityType {
AUTO = 2;
AES128_GCM = 3;
CHACHA20_POLY1305 = 4;
NONE = 5; // [DEPRECATED 2023-06]
ZERO = 6;
}
message SecurityConfig {
+10 -26
View File
@@ -1,41 +1,25 @@
package http
import (
"context"
"net/http"
"strconv"
"strings"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
// ApplyTrustedXForwardedFor returns remoteAddr overridden by X-Forwarded-For only when a configured trusted header is present.
func ApplyTrustedXForwardedFor(header http.Header, trusted []string, remoteAddr net.Addr) net.Addr {
value := header.Get("X-Forwarded-For")
if value == "" {
return remoteAddr
// ParseXForwardedFor parses X-Forwarded-For header in http headers, and return the IP list in it.
func ParseXForwardedFor(header http.Header) []net.Address {
xff := header.Get("X-Forwarded-For")
if xff == "" {
return nil
}
for _, t := range trusted {
if len(header.Values(t)) > 0 {
if idx := strings.IndexByte(value, ','); idx >= 0 {
value = value[:idx]
}
if addr := net.ParseAddress(value); addr.Family().IsIP() {
return &net.TCPAddr{
IP: addr.IP(),
Port: 0,
}
}
return remoteAddr
}
list := strings.Split(xff, ",")
addrs := make([]net.Address, 0, len(list))
for _, proxy := range list {
addrs = append(addrs, net.ParseAddress(proxy))
}
if len(trusted) == 0 {
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
return addrs
}
// RemoveHopByHopHeaders removes hop by hop headers in http header list.
+8 -33
View File
@@ -2,48 +2,23 @@ package http_test
import (
"bufio"
gonet "net"
"net/http"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net"
. "github.com/xtls/xray-core/common/protocol/http"
)
func TestApplyTrustedXForwardedFor(t *testing.T) {
remoteAddr := &gonet.TCPAddr{IP: gonet.ParseIP("127.0.0.1"), Port: 12345}
t.Run("ignore X-Forwarded-For without trusted header", func(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
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 TestParseXForwardedFor(t *testing.T) {
header := http.Header{}
header.Add("X-Forwarded-For", "129.78.138.66, 129.78.64.103")
addrs := ParseXForwardedFor(header)
if r := cmp.Diff(addrs, []net.Address{net.ParseAddress("129.78.138.66"), net.ParseAddress("129.78.64.103")}); r != "" {
t.Error(r)
}
}
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.
func (id *ID) Equals(another *ID) bool {
return id.uuid.Equals(&another.uuid)
return id.uuid.Equals(&(another.uuid))
}
func (id *ID) Bytes() []byte {
+1 -3
View File
@@ -143,9 +143,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
if err != nil {
return nil, err
}
if len(b) < hdrLen+4+block.BlockSize() {
return nil, errNotQuic
}
cache.Clear()
mask := cache.Extend(int32(block.BlockSize()))
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+len(mask)])
+1 -1
View File
@@ -229,7 +229,7 @@ func TestSniffQUICComplex(t *testing.T) {
t.Errorf("SniffQUIC() error = %v, wantErr %v", err, tt.wantErr)
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)
return
}
+2 -2
View File
@@ -5,8 +5,8 @@ import (
)
type ServerSpec struct {
Destination net.Destination
User *MemoryUser
Destination net.Destination
User *MemoryUser
}
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{} {
if r, ok := marshalKnownType(v, ignoreNullValue, insertTypeInfo); ok {
return r
}
+3 -1
View File
@@ -27,6 +27,7 @@ func TestMashalAccount(t *testing.T) {
j, ok := MarshalToJson(user, false)
if !ok || strings.Contains(j, "_TypedMessage_") {
t.Error("marshal account failed")
}
@@ -78,12 +79,13 @@ func TestMashalStruct(t *testing.T) {
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")
}
}
func TestMarshalConfigJson(t *testing.T) {
buf := bytes.NewBufferString(getConfig())
config, err := iserial.DecodeJSONConfig(buf)
if err != nil {
+29
View File
@@ -4,6 +4,7 @@ import (
"testing"
. "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
)
func TestGetInstance(t *testing.T) {
@@ -22,3 +23,31 @@ func TestConvertingNilMessage(t *testing.T) {
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
mitmAlpn11Key ctx.SessionKey = 11 // 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 {
@@ -194,11 +192,3 @@ func MitmServerNameFromContext(ctx context.Context) string {
}
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)
}

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