Compare commits

..

No commits in common. "main" and "v25.1.1" have entirely different histories.

155 changed files with 2768 additions and 4422 deletions

View File

@ -1,5 +1,11 @@
name: Build and Release for Windows 7 name: Build and Release for Windows 7
# NOTE: This Github Actions file depends on the Makefile.
# Building the correct package requires the correct binaries generated by the Makefile. To
# ensure the correct output, the Makefile must accept the appropriate input and compile the
# correct file with the correct name. If you need to modify this file, please ensure it won't
# disrupt the Makefile.
on: on:
workflow_dispatch: workflow_dispatch:
release: release:
@ -9,38 +15,51 @@ on:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
jobs: jobs:
check-assets: prepare:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Restore Geodat Cache - name: Restore Cache
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
with: with:
path: resources path: resources
key: xray-geodat- key: xray-geodat-
- name: Check Assets Existence - name: Update Geodat
id: check-assets id: update
run: | uses: nick-fields/retry@v3
[ -d 'resources' ] || mkdir resources with:
LIST=('geoip.dat' 'geosite.dat') timeout_minutes: 60
for FILE_NAME in "${LIST[@]}" retry_wait_seconds: 60
do max_attempts: 60
echo -e "Checking ${FILE_NAME}..." command: |
if [ -s "./resources/${FILE_NAME}" ]; then [ -d 'resources' ] || mkdir resources
echo -e "${FILE_NAME} exists." LIST=('geoip geoip geoip' 'domain-list-community dlc geosite')
else for i in "${LIST[@]}"
echo -e "${FILE_NAME} does not exist." do
echo "missing=true" >> $GITHUB_OUTPUT INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3}'))
break FILE_NAME="${INFO[2]}.dat"
fi echo -e "Verifying HASH key..."
done HASH="$(curl -sL "https://raw.githubusercontent.com/v2fly/${INFO[0]}/release/${INFO[1]}.dat.sha256sum" | awk -F ' ' '{print $1}')"
if [ -s "./resources/${FILE_NAME}" ] && [ "$(sha256sum "./resources/${FILE_NAME}" | awk -F ' ' '{print $1}')" == "${HASH}" ]; then
continue
else
echo -e "Downloading https://raw.githubusercontent.com/v2fly/${INFO[0]}/release/${INFO[1]}.dat..."
curl -L "https://raw.githubusercontent.com/v2fly/${INFO[0]}/release/${INFO[1]}.dat" -o ./resources/${FILE_NAME}
echo -e "Verifying HASH key..."
[ "$(sha256sum "./resources/${FILE_NAME}" | awk -F ' ' '{print $1}')" == "${HASH}" ] || { echo -e "The HASH key of ${FILE_NAME} does not match cloud one."; exit 1; }
echo "unhit=true" >> $GITHUB_OUTPUT
fi
done
- name: Sleep for 90 seconds if Assets Missing - name: Save Cache
if: steps.check-assets.outputs.missing == 'true' uses: actions/cache/save@v4
run: sleep 90 if: ${{ steps.update.outputs.unhit }}
with:
path: resources
key: xray-geodat-${{ github.sha }}-${{ github.run_number }}
build: build:
needs: check-assets needs: prepare
permissions: permissions:
contents: write contents: write
strategy: strategy:
@ -62,9 +81,6 @@ jobs:
GOARCH: ${{ matrix.goarch }} GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0 CGO_ENABLED: 0
steps: steps:
- name: Checkout codebase
uses: actions/checkout@v4
- name: Show workflow information - name: Show workflow information
run: | run: |
_NAME=${{ matrix.assetname }} _NAME=${{ matrix.assetname }}
@ -74,17 +90,18 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version-file: go.mod go-version: stable
check-latest: true check-latest: true
- name: Setup patched builder - name: Setup patched builder
run: | run: |
GOSDK=$(go env GOROOT) GOSDK=$(go env GOROOT)
curl -O -L https://github.com/XTLS/go-win7/releases/latest/download/go-for-win7-linux-amd64.zip
rm -r $GOSDK/* rm -r $GOSDK/*
cd $GOSDK
curl -O -L -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://github.com/XTLS/go-win7/releases/latest/download/go-for-win7-linux-amd64.zip
unzip ./go-for-win7-linux-amd64.zip -d $GOSDK unzip ./go-for-win7-linux-amd64.zip -d $GOSDK
rm ./go-for-win7-linux-amd64.zip
- name: Checkout codebase
uses: actions/checkout@v4
- name: Get project dependencies - name: Get project dependencies
run: go mod download run: go mod download
@ -92,15 +109,10 @@ jobs:
- name: Build Xray - name: Build Xray
run: | run: |
mkdir -p build_assets mkdir -p build_assets
COMMID=$(git describe --always --dirty) make
echo 'Building Xray for Windows 7...' find . -maxdepth 1 -type f -regex './\(wxray\|xray\).exe' -exec mv {} ./build_assets/ \;
go build -o build_assets/xray.exe -trimpath -buildvcs=false -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
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
# 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 -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
- name: Restore Geodat Cache - name: Restore Cache
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
with: with:
path: resources path: resources

View File

@ -1,5 +1,11 @@
name: Build and Release name: Build and Release
# NOTE: This Github Actions file depends on the Makefile.
# Building the correct package requires the correct binaries generated by the Makefile. To
# ensure the correct output, the Makefile must accept the appropriate input and compile the
# correct file with the correct name. If you need to modify this file, please ensure it won't
# disrupt the Makefile.
on: on:
workflow_dispatch: workflow_dispatch:
release: release:
@ -9,53 +15,51 @@ on:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
jobs: jobs:
check-assets: prepare:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Restore Geodat Cache - name: Restore Cache
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
with: with:
path: resources path: resources
key: xray-geodat- key: xray-geodat-
- name: Check Assets Existence - name: Update Geodat
id: check-assets id: update
run: | uses: nick-fields/retry@v3
[ -d 'resources' ] || mkdir resources
LIST=('geoip.dat' 'geosite.dat')
for FILE_NAME in "${LIST[@]}"
do
echo -e "Checking ${FILE_NAME}..."
if [ -s "./resources/${FILE_NAME}" ]; then
echo -e "${FILE_NAME} exists."
else
echo -e "${FILE_NAME} does not exist."
echo "missing=true" >> $GITHUB_OUTPUT
break
fi
done
- name: Trigger Asset Update Workflow if Assets Missing
if: steps.check-assets.outputs.missing == 'true'
uses: actions/github-script@v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} timeout_minutes: 60
script: | retry_wait_seconds: 60
const { owner, repo } = context.repo; max_attempts: 60
await github.rest.actions.createWorkflowDispatch({ command: |
owner, [ -d 'resources' ] || mkdir resources
repo, LIST=('geoip geoip geoip' 'domain-list-community dlc geosite')
workflow_id: 'scheduled-assets-update.yml', for i in "${LIST[@]}"
ref: context.ref do
}); INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3}'))
console.log('Triggered scheduled-assets-update.yml due to missing assets on branch:', context.ref); FILE_NAME="${INFO[2]}.dat"
echo -e "Verifying HASH key..."
HASH="$(curl -sL "https://raw.githubusercontent.com/v2fly/${INFO[0]}/release/${INFO[1]}.dat.sha256sum" | awk -F ' ' '{print $1}')"
if [ -s "./resources/${FILE_NAME}" ] && [ "$(sha256sum "./resources/${FILE_NAME}" | awk -F ' ' '{print $1}')" == "${HASH}" ]; then
continue
else
echo -e "Downloading https://raw.githubusercontent.com/v2fly/${INFO[0]}/release/${INFO[1]}.dat..."
curl -L "https://raw.githubusercontent.com/v2fly/${INFO[0]}/release/${INFO[1]}.dat" -o ./resources/${FILE_NAME}
echo -e "Verifying HASH key..."
[ "$(sha256sum "./resources/${FILE_NAME}" | awk -F ' ' '{print $1}')" == "${HASH}" ] || { echo -e "The HASH key of ${FILE_NAME} does not match cloud one."; exit 1; }
echo "unhit=true" >> $GITHUB_OUTPUT
fi
done
- name: Sleep for 90 seconds if Assets Missing - name: Save Cache
if: steps.check-assets.outputs.missing == 'true' uses: actions/cache/save@v4
run: sleep 90 if: ${{ steps.update.outputs.unhit }}
with:
path: resources
key: xray-geodat-${{ github.sha }}-${{ github.run_number }}
build: build:
needs: check-assets needs: prepare
permissions: permissions:
contents: write contents: write
strategy: strategy:
@ -88,11 +92,6 @@ jobs:
- goos: android - goos: android
goarch: arm64 goarch: arm64
# END Android ARM 8 # END Android ARM 8
# BEGIN Android AMD64
- goos: android
goarch: amd64
patch-assetname: android-amd64
# END Android AMD64
# Windows ARM # Windows ARM
- goos: windows - goos: windows
goarch: arm64 goarch: arm64
@ -155,19 +154,6 @@ jobs:
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up NDK
if: matrix.goos == 'android'
run: |
wget -qO android-ndk.zip https://dl.google.com/android/repository/android-ndk-r28b-linux.zip
unzip android-ndk.zip
rm android-ndk.zip
declare -A arches=(
["amd64"]="x86_64-linux-android24-clang"
["arm64"]="aarch64-linux-android24-clang"
)
echo CC="$(realpath android-ndk-*/toolchains/llvm/prebuilt/linux-x86_64/bin)/${arches[${{ matrix.goarch }}]}" >> $GITHUB_ENV
echo CGO_ENABLED=1 >> $GITHUB_ENV
- name: Show workflow information - name: Show workflow information
run: | run: |
_NAME=${{ matrix.patch-assetname }} _NAME=${{ matrix.patch-assetname }}
@ -187,24 +173,10 @@ jobs:
- name: Build Xray - name: Build Xray
run: | run: |
mkdir -p build_assets mkdir -p build_assets
COMMID=$(git describe --always --dirty) make
if [[ ${GOOS} == 'windows' ]]; then find . -maxdepth 1 -type f -regex './\(wxray\|xray\|xray_softfloat\)\(\|.exe\)' -exec mv {} ./build_assets/ \;
echo 'Building Xray for Windows...'
go build -o build_assets/xray.exe -trimpath -buildvcs=false -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
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
# 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 -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
else
echo 'Building Xray...'
go build -o build_assets/xray -trimpath -buildvcs=false -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
if [[ ${GOARCH} == 'mips' || ${GOARCH} == 'mipsle' ]]; then
echo 'Building soft-float Xray for MIPS/MIPSLE 32-bit...'
GOMIPS=softfloat go build -o build_assets/xray_softfloat -trimpath -buildvcs=false -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
fi
fi
- name: Restore Geodat Cache - name: Restore Cache
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
with: with:
path: resources path: resources

View File

@ -1,65 +0,0 @@
name: Scheduled assets update
# NOTE: This Github Actions is required by other actions, for preparing other packaging assets in a
# routine manner, for example: GeoIP/GeoSite.
# Currently updating:
# - Geodat (GeoIP/Geosite)
on:
workflow_dispatch:
schedule:
# Update GeoData on every day (22:30 UTC)
- cron: "30 22 * * *"
push:
# Prevent triggering update request storm
paths:
- ".github/workflows/scheduled-assets-update.yml"
pull_request:
# Prevent triggering update request storm
paths:
- ".github/workflows/scheduled-assets-update.yml"
jobs:
geodat:
if: github.event.schedule == '30 22 * * *' || github.event_name == 'push'|| github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v4
with:
path: resources
key: xray-geodat-
- name: Update Geodat
id: update
uses: nick-fields/retry@v3
with:
timeout_minutes: 60
retry_wait_seconds: 60
max_attempts: 60
command: |
[ -d 'resources' ] || mkdir resources
LIST=('Loyalsoldier v2ray-rules-dat geoip geoip' 'Loyalsoldier v2ray-rules-dat geosite geosite')
for i in "${LIST[@]}"
do
INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3,$4}'))
FILE_NAME="${INFO[3]}.dat"
echo -e "Verifying HASH key..."
HASH="$(curl -sL -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://raw.githubusercontent.com/${INFO[0]}/${INFO[1]}/release/${INFO[2]}.dat.sha256sum" | awk -F ' ' '{print $1}')"
if [ -s "./resources/${FILE_NAME}" ] && [ "$(sha256sum "./resources/${FILE_NAME}" | awk -F ' ' '{print $1}')" == "${HASH}" ]; then
continue
else
echo -e "Downloading https://raw.githubusercontent.com/${INFO[0]}/${INFO[1]}/release/${INFO[2]}.dat..."
curl -L -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://raw.githubusercontent.com/${INFO[0]}/${INFO[1]}/release/${INFO[2]}.dat" -o ./resources/${FILE_NAME}
echo -e "Verifying HASH key..."
[ "$(sha256sum "./resources/${FILE_NAME}" | awk -F ' ' '{print $1}')" == "${HASH}" ] || { echo -e "The HASH key of ${FILE_NAME} does not match cloud one."; exit 1; }
echo "unhit=true" >> $GITHUB_OUTPUT
fi
done
- name: Save Geodat Cache
uses: actions/cache/save@v4
if: ${{ steps.update.outputs.unhit }}
with:
path: resources
key: xray-geodat-${{ github.sha }}-${{ github.run_number }}

View File

@ -6,36 +6,7 @@ on:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
jobs: jobs:
check-assets:
runs-on: ubuntu-latest
steps:
- name: Restore Geodat Cache
uses: actions/cache/restore@v4
with:
path: resources
key: xray-geodat-
- name: Check Assets Existence
id: check-assets
run: |
[ -d 'resources' ] || mkdir resources
LIST=('geoip.dat' 'geosite.dat')
for FILE_NAME in "${LIST[@]}"
do
echo -e "Checking ${FILE_NAME}..."
if [ -s "./resources/${FILE_NAME}" ]; then
echo -e "${FILE_NAME} exists."
else
echo -e "${FILE_NAME} does not exist."
echo "missing=true" >> $GITHUB_OUTPUT
break
fi
done
- name: Sleep for 90 seconds if Assets Missing
if: steps.check-assets.outputs.missing == 'true'
run: sleep 90
test: test:
needs: check-assets
permissions: permissions:
contents: read contents: read
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -51,7 +22,7 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- name: Restore Geodat Cache - name: Restore Cache
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
with: with:
path: resources path: resources

17
.gitignore vendored
View File

@ -14,18 +14,10 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
# macOS specific files
*.DS_Store *.DS_Store
.idea
# IDE specific files
.idea/
.vscode/
# Archive files
*.zip *.zip
*.tar.gz *.tar.gz
# Binaries
xray xray
xray_softfloat xray_softfloat
mockgen mockgen
@ -34,13 +26,8 @@ vprotogen
errorgen errorgen
!common/errors/errorgen/ !common/errors/errorgen/
*.dat *.dat
.vscode
# Build assets
/build_assets /build_assets
# Output from dlv test # Output from dlv test
**/debug.* **/debug.*
# Certificates
*.crt
*.key

37
Makefile Normal file
View File

@ -0,0 +1,37 @@
NAME = xray
VERSION=$(shell git describe --always --dirty)
# NOTE: This MAKEFILE can be used to build Xray-core locally and in Automatic workflows. It is \
provided for convenience in automatic building and functions as a part of it.
# NOTE: If you need to modify this file, please be aware that:\
- This file is not the main Makefile; it only accepts environment variables and builds the \
binary.\
- Automatic building expects the correct binaries to be built by this Makefile. If you \
intend to propose a change to this Makefile, carefully review the file below and ensure \
that the change will not accidentally break the automatic building:\
.github/workflows/release.yml \
Otherwise it is recommended to contact the project maintainers.
LDFLAGS = -X github.com/xtls/xray-core/core.build=$(VERSION) -s -w -buildid=
PARAMS = -trimpath -ldflags "$(LDFLAGS)" -v
MAIN = ./main
PREFIX ?= $(shell go env GOPATH)
ifeq ($(GOOS),windows)
OUTPUT = $(NAME).exe
ADDITION = go build -o w$(NAME).exe -trimpath -ldflags "-H windowsgui $(LDFLAGS)" -v $(MAIN)
else
OUTPUT = $(NAME)
endif
ifeq ($(shell echo "$(GOARCH)" | grep -Eq "(mips|mipsle)" && echo true),true) #
ADDITION = GOMIPS=softfloat go build -o $(NAME)_softfloat -trimpath -ldflags "$(LDFLAGS)" -v $(MAIN)
endif
.PHONY: clean build
build:
go build -o $(OUTPUT) $(PARAMS) $(MAIN)
$(ADDITION)
clean:
go clean -v -i $(PWD)
rm -f xray xray.exe wxray.exe xray_softfloat

View File

@ -24,9 +24,7 @@
[Project X Channel](https://t.me/projectXtls) [Project X Channel](https://t.me/projectXtls)
[Project VLESS](https://t.me/projectVless) (Русский) [Project VLESS](https://t.me/projectVless) (non-Chinese)
[Project XHTTP](https://t.me/projectXhttp) (Persian)
## Installation ## Installation
@ -38,7 +36,6 @@
- [teddysun/xray](https://hub.docker.com/r/teddysun/xray) - [teddysun/xray](https://hub.docker.com/r/teddysun/xray)
- [wulabing/xray_docker](https://github.com/wulabing/xray_docker) - [wulabing/xray_docker](https://github.com/wulabing/xray_docker)
- Web Panel - **WARNING: Please DO NOT USE plain HTTP panels like 3X-UI**, as they are believed to be bribed by Iran GFW for supporting plain HTTP by default and refused to change (https://github.com/XTLS/Xray-core/pull/3884#issuecomment-2439595331), which has already put many users' data security in danger in the past few years. **If you are already using 3X-UI, please switch to the following panels, which are verified to support HTTPS and SSH port forwarding only:** - Web Panel - **WARNING: Please DO NOT USE plain HTTP panels like 3X-UI**, as they are believed to be bribed by Iran GFW for supporting plain HTTP by default and refused to change (https://github.com/XTLS/Xray-core/pull/3884#issuecomment-2439595331), which has already put many users' data security in danger in the past few years. **If you are already using 3X-UI, please switch to the following panels, which are verified to support HTTPS and SSH port forwarding only:**
- [Remnawave](https://github.com/remnawave/panel)
- [Marzban](https://github.com/Gozargah/Marzban) - [Marzban](https://github.com/Gozargah/Marzban)
- [Xray-UI](https://github.com/qist/xray-ui) - [Xray-UI](https://github.com/qist/xray-ui)
- [Hiddify](https://github.com/hiddify/Hiddify-Manager) - [Hiddify](https://github.com/hiddify/Hiddify-Manager)
@ -75,8 +72,6 @@
- [PassWall](https://github.com/xiaorouji/openwrt-passwall), [PassWall 2](https://github.com/xiaorouji/openwrt-passwall2) - [PassWall](https://github.com/xiaorouji/openwrt-passwall), [PassWall 2](https://github.com/xiaorouji/openwrt-passwall2)
- [ShadowSocksR Plus+](https://github.com/fw876/helloworld) - [ShadowSocksR Plus+](https://github.com/fw876/helloworld)
- [luci-app-xray](https://github.com/yichya/luci-app-xray) ([openwrt-xray](https://github.com/yichya/openwrt-xray)) - [luci-app-xray](https://github.com/yichya/luci-app-xray) ([openwrt-xray](https://github.com/yichya/openwrt-xray))
- Asuswrt-Merlin
- [XRAYUI](https://github.com/DanielLavrushin/asuswrt-merlin-xrayui)
- Windows - Windows
- [v2rayN](https://github.com/2dust/v2rayN) - [v2rayN](https://github.com/2dust/v2rayN)
- [Furious](https://github.com/LorenEteval/Furious) - [Furious](https://github.com/LorenEteval/Furious)
@ -86,7 +81,6 @@
- [X-flutter](https://github.com/XTLS/X-flutter) - [X-flutter](https://github.com/XTLS/X-flutter)
- [SaeedDev94/Xray](https://github.com/SaeedDev94/Xray) - [SaeedDev94/Xray](https://github.com/SaeedDev94/Xray)
- iOS & macOS arm64 - iOS & macOS arm64
- [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215)
- [FoXray](https://apps.apple.com/app/foxray/id6448898396) - [FoXray](https://apps.apple.com/app/foxray/id6448898396)
- [Streisand](https://apps.apple.com/app/streisand/id6450534064) - [Streisand](https://apps.apple.com/app/streisand/id6450534064)
- macOS arm64 & x64 - macOS arm64 & x64
@ -102,10 +96,8 @@
- iOS & macOS arm64 - iOS & macOS arm64
- [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118) - [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118)
- [Loon](https://apps.apple.com/us/app/loon/id1373567447)
- Xray Tools - Xray Tools
- [xray-knife](https://github.com/lilendian0x00/xray-knife) - [xray-knife](https://github.com/lilendian0x00/xray-knife)
- [xray-checker](https://github.com/kutovoys/xray-checker)
- Xray Wrapper - Xray Wrapper
- [XTLS/libXray](https://github.com/XTLS/libXray) - [XTLS/libXray](https://github.com/XTLS/libXray)
- [xtlsapi](https://github.com/hiddify/xtlsapi) - [xtlsapi](https://github.com/hiddify/xtlsapi)
@ -115,9 +107,10 @@
- [XrayR](https://github.com/XrayR-project/XrayR) - [XrayR](https://github.com/XrayR-project/XrayR)
- [XrayR-release](https://github.com/XrayR-project/XrayR-release) - [XrayR-release](https://github.com/XrayR-project/XrayR-release)
- [XrayR-V2Board](https://github.com/missuo/XrayR-V2Board) - [XrayR-V2Board](https://github.com/missuo/XrayR-V2Board)
- Cores - [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta)
- [mihomo](https://github.com/MetaCubeX/mihomo) - [clashN](https://github.com/2dust/clashN)
- [sing-box](https://github.com/SagerNet/sing-box) - [Clash Meta for Android](https://github.com/MetaCubeX/ClashMetaForAndroid)
- [sing-box](https://github.com/SagerNet/sing-box)
## Contributing ## Contributing
@ -128,27 +121,25 @@
- [Xray-core v1.0.0](https://github.com/XTLS/Xray-core/releases/tag/v1.0.0) was forked from [v2fly-core 9a03cc5](https://github.com/v2fly/v2ray-core/commit/9a03cc5c98d04cc28320fcee26dbc236b3291256), and we have made & accumulated a huge number of enhancements over time, check [the release notes for each version](https://github.com/XTLS/Xray-core/releases). - [Xray-core v1.0.0](https://github.com/XTLS/Xray-core/releases/tag/v1.0.0) was forked from [v2fly-core 9a03cc5](https://github.com/v2fly/v2ray-core/commit/9a03cc5c98d04cc28320fcee26dbc236b3291256), and we have made & accumulated a huge number of enhancements over time, check [the release notes for each version](https://github.com/XTLS/Xray-core/releases).
- For third-party projects used in [Xray-core](https://github.com/XTLS/Xray-core), check your local or [the latest go.mod](https://github.com/XTLS/Xray-core/blob/main/go.mod). - For third-party projects used in [Xray-core](https://github.com/XTLS/Xray-core), check your local or [the latest go.mod](https://github.com/XTLS/Xray-core/blob/main/go.mod).
## One-line Compilation ## Compilation
### Windows (PowerShell) ### Windows (PowerShell)
```powershell ```powershell
$env:CGO_ENABLED=0 $env:CGO_ENABLED=0
go build -o xray.exe -trimpath -buildvcs=false -ldflags="-s -w -buildid=" -v ./main go build -o xray.exe -trimpath -ldflags "-s -w -buildid=" ./main
``` ```
### Linux / macOS ### Linux / macOS
```bash ```bash
CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -ldflags="-s -w -buildid=" -v ./main CGO_ENABLED=0 go build -o xray -trimpath -ldflags "-s -w -buildid=" ./main
``` ```
### Reproducible Releases ### Reproducible Releases
Make sure that you are using the same Go version, and remember to set the git commit id (7 bytes):
```bash ```bash
CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid=" -v ./main make
``` ```
## Stargazers over time ## Stargazers over time

View File

@ -33,21 +33,23 @@ type cachedReader struct {
cache buf.MultiBuffer cache buf.MultiBuffer
} }
func (r *cachedReader) Cache(b *buf.Buffer, deadline time.Duration) error { func (r *cachedReader) Cache(b *buf.Buffer) {
mb, err := r.reader.ReadMultiBufferTimeout(deadline) mb, _ := r.reader.ReadMultiBufferTimeout(time.Millisecond * 100)
if err != nil {
return err
}
r.Lock() r.Lock()
if !mb.IsEmpty() { if !mb.IsEmpty() {
r.cache, _ = buf.MergeMulti(r.cache, mb) r.cache, _ = buf.MergeMulti(r.cache, mb)
} }
b.Clear() cacheLen := r.cache.Len()
rawBytes := b.Extend(min(r.cache.Len(), b.Cap())) if cacheLen <= b.Cap() {
b.Clear()
} else {
b.Release()
*b = *buf.NewWithSize(cacheLen)
}
rawBytes := b.Extend(cacheLen)
n := r.cache.Copy(rawBytes) n := r.cache.Copy(rawBytes)
b.Resize(0, int32(n)) b.Resize(0, int32(n))
r.Unlock() r.Unlock()
return nil
} }
func (r *cachedReader) readInternal() buf.MultiBuffer { func (r *cachedReader) readInternal() buf.MultiBuffer {
@ -96,6 +98,7 @@ type DefaultDispatcher struct {
router routing.Router router routing.Router
policy policy.Manager policy policy.Manager
stats stats.Manager stats stats.Manager
dns dns.Client
fdns dns.FakeDNSEngine fdns dns.FakeDNSEngine
} }
@ -106,7 +109,7 @@ func init() {
core.OptionalFeatures(ctx, func(fdns dns.FakeDNSEngine) { core.OptionalFeatures(ctx, func(fdns dns.FakeDNSEngine) {
d.fdns = fdns d.fdns = fdns
}) })
return d.Init(config.(*Config), om, router, pm, sm) return d.Init(config.(*Config), om, router, pm, sm, dc)
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
@ -115,11 +118,12 @@ func init() {
} }
// Init initializes DefaultDispatcher. // Init initializes DefaultDispatcher.
func (d *DefaultDispatcher) Init(config *Config, om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager) error { func (d *DefaultDispatcher) Init(config *Config, om outbound.Manager, router routing.Router, pm policy.Manager, sm stats.Manager, dns dns.Client) error {
d.ohm = om d.ohm = om
d.router = router d.router = router
d.policy = pm d.policy = pm
d.stats = sm d.stats = sm
d.dns = dns
return nil return nil
} }
@ -351,7 +355,7 @@ func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.De
} }
func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, network net.Network) (SniffResult, error) { func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, network net.Network) (SniffResult, error) {
payload := buf.NewWithSize(32767) payload := buf.New()
defer payload.Release() defer payload.Release()
sniffer := NewSniffer(ctx) sniffer := NewSniffer(ctx)
@ -363,36 +367,26 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, netw
} }
contentResult, contentErr := func() (SniffResult, error) { contentResult, contentErr := func() (SniffResult, error) {
cacheDeadline := 200 * time.Millisecond
totalAttempt := 0 totalAttempt := 0
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() return nil, ctx.Err()
default: default:
cachingStartingTimeStamp := time.Now() totalAttempt++
err := cReader.Cache(payload, cacheDeadline) if totalAttempt > 2 {
if err != nil { return nil, errSniffingTimeout
return nil, err
} }
cachingTimeElapsed := time.Since(cachingStartingTimeStamp)
cacheDeadline -= cachingTimeElapsed
cReader.Cache(payload)
if !payload.IsEmpty() { if !payload.IsEmpty() {
result, err := sniffer.Sniff(ctx, payload.Bytes(), network) result, err := sniffer.Sniff(ctx, payload.Bytes(), network)
switch err { if err != common.ErrNoClue {
case common.ErrNoClue: // No Clue: protocol not matches, and sniffer cannot determine whether there will be a match or not
totalAttempt++
case protocol.ErrProtoNeedMoreData: // Protocol Need More Data: protocol matches, but need more data to complete sniffing
// in this case, do not add totalAttempt(allow to read until timeout)
default:
return result, err return result, err
} }
} else {
totalAttempt++
} }
if totalAttempt >= 2 || cacheDeadline <= 0 { if payload.IsFull() {
return nil, errSniffingTimeout return nil, errUnknownContent
} }
} }
} }
@ -408,6 +402,18 @@ func sniffer(ctx context.Context, cReader *cachedReader, metadataOnly bool, netw
func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination) { func (d *DefaultDispatcher) routedDispatch(ctx context.Context, link *transport.Link, destination net.Destination) {
outbounds := session.OutboundsFromContext(ctx) outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1] ob := outbounds[len(outbounds)-1]
if hosts, ok := d.dns.(dns.HostsLookup); ok && destination.Address.Family().IsDomain() {
proxied := hosts.LookupHosts(ob.Target.String())
if proxied != nil {
ro := ob.RouteTarget == destination
destination.Address = *proxied
if ro {
ob.RouteTarget = destination
} else {
ob.Target = destination
}
}
}
var handler outbound.Handler var handler outbound.Handler

View File

@ -6,7 +6,6 @@ import (
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/protocol/bittorrent" "github.com/xtls/xray-core/common/protocol/bittorrent"
"github.com/xtls/xray-core/common/protocol/http" "github.com/xtls/xray-core/common/protocol/http"
"github.com/xtls/xray-core/common/protocol/quic" "github.com/xtls/xray-core/common/protocol/quic"
@ -59,17 +58,14 @@ var errUnknownContent = errors.New("unknown content")
func (s *Sniffer) Sniff(c context.Context, payload []byte, network net.Network) (SniffResult, error) { func (s *Sniffer) Sniff(c context.Context, payload []byte, network net.Network) (SniffResult, error) {
var pendingSniffer []protocolSnifferWithMetadata var pendingSniffer []protocolSnifferWithMetadata
for _, si := range s.sniffer { for _, si := range s.sniffer {
protocolSniffer := si.protocolSniffer s := si.protocolSniffer
if si.metadataSniffer || si.network != network { if si.metadataSniffer || si.network != network {
continue continue
} }
result, err := protocolSniffer(c, payload) result, err := s(c, payload)
if err == common.ErrNoClue { if err == common.ErrNoClue {
pendingSniffer = append(pendingSniffer, si) pendingSniffer = append(pendingSniffer, si)
continue continue
} else if err == protocol.ErrProtoNeedMoreData { // Sniffer protocol matched, but need more data to complete sniffing
s.sniffer = []protocolSnifferWithMetadata{si}
return nil, err
} }
if err == nil && result != nil { if err == nil && result != nil {

View File

@ -1,188 +0,0 @@
package dns
import (
"context"
go_errors "errors"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/signal/pubsub"
"github.com/xtls/xray-core/common/task"
dns_feature "github.com/xtls/xray-core/features/dns"
"golang.org/x/net/dns/dnsmessage"
"sync"
"time"
)
type CacheController struct {
sync.RWMutex
ips map[string]*record
pub *pubsub.Service
cacheCleanup *task.Periodic
name string
disableCache bool
}
func NewCacheController(name string, disableCache bool) *CacheController {
c := &CacheController{
name: name,
disableCache: disableCache,
ips: make(map[string]*record),
pub: pubsub.NewService(),
}
c.cacheCleanup = &task.Periodic{
Interval: time.Minute,
Execute: c.CacheCleanup,
}
return c
}
// CacheCleanup clears expired items from cache
func (c *CacheController) CacheCleanup() error {
now := time.Now()
c.Lock()
defer c.Unlock()
if len(c.ips) == 0 {
return errors.New("nothing to do. stopping...")
}
for domain, record := range c.ips {
if record.A != nil && record.A.Expire.Before(now) {
record.A = nil
}
if record.AAAA != nil && record.AAAA.Expire.Before(now) {
record.AAAA = nil
}
if record.A == nil && record.AAAA == nil {
errors.LogDebug(context.Background(), c.name, "cache cleanup ", domain)
delete(c.ips, domain)
} else {
c.ips[domain] = record
}
}
if len(c.ips) == 0 {
c.ips = make(map[string]*record)
}
return nil
}
func (c *CacheController) updateIP(req *dnsRequest, ipRec *IPRecord) {
elapsed := time.Since(req.start)
c.Lock()
rec, found := c.ips[req.domain]
if !found {
rec = &record{}
}
switch req.reqType {
case dnsmessage.TypeA:
rec.A = ipRec
case dnsmessage.TypeAAAA:
rec.AAAA = ipRec
}
errors.LogInfo(context.Background(), c.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed)
c.ips[req.domain] = rec
switch req.reqType {
case dnsmessage.TypeA:
c.pub.Publish(req.domain+"4", nil)
if !c.disableCache {
_, _, err := rec.AAAA.getIPs()
if !go_errors.Is(err, errRecordNotFound) {
c.pub.Publish(req.domain+"6", nil)
}
}
case dnsmessage.TypeAAAA:
c.pub.Publish(req.domain+"6", nil)
if !c.disableCache {
_, _, err := rec.A.getIPs()
if !go_errors.Is(err, errRecordNotFound) {
c.pub.Publish(req.domain+"4", nil)
}
}
}
c.Unlock()
common.Must(c.cacheCleanup.Start())
}
func (c *CacheController) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) {
c.RLock()
record, found := c.ips[domain]
c.RUnlock()
if !found {
return nil, 0, errRecordNotFound
}
var errs []error
var allIPs []net.IP
var rTTL uint32 = dns_feature.DefaultTTL
mergeReq := option.IPv4Enable && option.IPv6Enable
if option.IPv4Enable {
ips, ttl, err := record.A.getIPs()
if !mergeReq || go_errors.Is(err, errRecordNotFound) {
return ips, ttl, err
}
if ttl < rTTL {
rTTL = ttl
}
if len(ips) > 0 {
allIPs = append(allIPs, ips...)
} else {
errs = append(errs, err)
}
}
if option.IPv6Enable {
ips, ttl, err := record.AAAA.getIPs()
if !mergeReq || go_errors.Is(err, errRecordNotFound) {
return ips, ttl, err
}
if ttl < rTTL {
rTTL = ttl
}
if len(ips) > 0 {
allIPs = append(allIPs, ips...)
} else {
errs = append(errs, err)
}
}
if len(allIPs) > 0 {
return allIPs, rTTL, nil
}
if go_errors.Is(errs[0], errs[1]) {
return nil, rTTL, errs[0]
}
return nil, rTTL, errors.Combine(errs...)
}
func (c *CacheController) registerSubscribers(domain string, option dns_feature.IPOption) (sub4 *pubsub.Subscriber, sub6 *pubsub.Subscriber) {
// ipv4 and ipv6 belong to different subscription groups
if option.IPv4Enable {
sub4 = c.pub.Subscribe(domain + "4")
}
if option.IPv6Enable {
sub6 = c.pub.Subscribe(domain + "6")
}
return
}
func closeSubscribers(sub4 *pubsub.Subscriber, sub6 *pubsub.Subscriber) {
if sub4 != nil {
sub4.Close()
}
if sub6 != nil {
sub6.Close()
}
}

View File

@ -128,16 +128,13 @@ type NameServer struct {
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
Address *net.Endpoint `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` Address *net.Endpoint `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
ClientIp []byte `protobuf:"bytes,5,opt,name=client_ip,json=clientIp,proto3" json:"client_ip,omitempty"` ClientIp []byte `protobuf:"bytes,5,opt,name=client_ip,json=clientIp,proto3" json:"client_ip,omitempty"`
SkipFallback bool `protobuf:"varint,6,opt,name=skipFallback,proto3" json:"skipFallback,omitempty"` SkipFallback bool `protobuf:"varint,6,opt,name=skipFallback,proto3" json:"skipFallback,omitempty"`
PrioritizedDomain []*NameServer_PriorityDomain `protobuf:"bytes,2,rep,name=prioritized_domain,json=prioritizedDomain,proto3" json:"prioritized_domain,omitempty"` PrioritizedDomain []*NameServer_PriorityDomain `protobuf:"bytes,2,rep,name=prioritized_domain,json=prioritizedDomain,proto3" json:"prioritized_domain,omitempty"`
Geoip []*router.GeoIP `protobuf:"bytes,3,rep,name=geoip,proto3" json:"geoip,omitempty"` Geoip []*router.GeoIP `protobuf:"bytes,3,rep,name=geoip,proto3" json:"geoip,omitempty"`
OriginalRules []*NameServer_OriginalRule `protobuf:"bytes,4,rep,name=original_rules,json=originalRules,proto3" json:"original_rules,omitempty"` OriginalRules []*NameServer_OriginalRule `protobuf:"bytes,4,rep,name=original_rules,json=originalRules,proto3" json:"original_rules,omitempty"`
QueryStrategy QueryStrategy `protobuf:"varint,7,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"` QueryStrategy QueryStrategy `protobuf:"varint,7,opt,name=query_strategy,json=queryStrategy,proto3,enum=xray.app.dns.QueryStrategy" json:"query_strategy,omitempty"`
AllowUnexpectedIPs bool `protobuf:"varint,8,opt,name=allowUnexpectedIPs,proto3" json:"allowUnexpectedIPs,omitempty"`
Tag string `protobuf:"bytes,9,opt,name=tag,proto3" json:"tag,omitempty"`
TimeoutMs uint64 `protobuf:"varint,10,opt,name=timeoutMs,proto3" json:"timeoutMs,omitempty"`
} }
func (x *NameServer) Reset() { func (x *NameServer) Reset() {
@ -219,27 +216,6 @@ func (x *NameServer) GetQueryStrategy() QueryStrategy {
return QueryStrategy_USE_IP return QueryStrategy_USE_IP
} }
func (x *NameServer) GetAllowUnexpectedIPs() bool {
if x != nil {
return x.AllowUnexpectedIPs
}
return false
}
func (x *NameServer) GetTag() string {
if x != nil {
return x.Tag
}
return ""
}
func (x *NameServer) GetTimeoutMs() uint64 {
if x != nil {
return x.TimeoutMs
}
return 0
}
type Config struct { type Config struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -532,7 +508,7 @@ var file_app_dns_config_proto_rawDesc = []byte{
0x2e, 0x64, 0x6e, 0x73, 0x1a, 0x1c, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2e, 0x64, 0x6e, 0x73, 0x1a, 0x1c, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74,
0x2f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x2f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x1a, 0x17, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63, 0x74, 0x6f, 0x1a, 0x17, 0x61, 0x70, 0x70, 0x2f, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x2f, 0x63,
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x92, 0x05, 0x0a, 0x0a, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb2, 0x04, 0x0a, 0x0a,
0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x61, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x61, 0x64,
0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x78, 0x72,
0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x45, 0x6e, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x45, 0x6e,
@ -558,13 +534,7 @@ var file_app_dns_config_proto_rawDesc = []byte{
0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x78,
0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x51, 0x75, 0x65, 0x72,
0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x79, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79,
0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x2e, 0x0a, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x1a, 0x5e, 0x0a, 0x0e, 0x50, 0x72, 0x69, 0x6f,
0x77, 0x55, 0x6e, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x49, 0x50, 0x73, 0x18, 0x08,
0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x55, 0x6e, 0x65, 0x78, 0x70,
0x65, 0x63, 0x74, 0x65, 0x64, 0x49, 0x50, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18,
0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69,
0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74,
0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4d, 0x73, 0x1a, 0x5e, 0x0a, 0x0e, 0x50, 0x72, 0x69, 0x6f,
0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79, 0x72, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x04, 0x74, 0x79,
0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61, 0x61, 0x70, 0x70, 0x2e, 0x64, 0x6e, 0x73, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x4d, 0x61,

View File

@ -28,9 +28,6 @@ message NameServer {
repeated xray.app.router.GeoIP geoip = 3; repeated xray.app.router.GeoIP geoip = 3;
repeated OriginalRule original_rules = 4; repeated OriginalRule original_rules = 4;
QueryStrategy query_strategy = 7; QueryStrategy query_strategy = 7;
bool allowUnexpectedIPs = 8;
string tag = 9;
uint64 timeoutMs = 10;
} }
enum DomainMatchingType { enum DomainMatchingType {

View File

@ -3,12 +3,11 @@ package dns
import ( import (
"context" "context"
go_errors "errors"
"fmt" "fmt"
"sort"
"strings" "strings"
"sync" "sync"
"github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
@ -20,6 +19,8 @@ import (
// DNS is a DNS rely server. // DNS is a DNS rely server.
type DNS struct { type DNS struct {
sync.Mutex sync.Mutex
tag string
disableCache bool
disableFallback bool disableFallback bool
disableFallbackIfMatch bool disableFallbackIfMatch bool
ipOption *dns.IPOption ipOption *dns.IPOption
@ -38,6 +39,13 @@ type DomainMatcherInfo struct {
// New creates a new DNS server with given configuration. // New creates a new DNS server with given configuration.
func New(ctx context.Context, config *Config) (*DNS, error) { func New(ctx context.Context, config *Config) (*DNS, error) {
var tag string
if len(config.Tag) > 0 {
tag = config.Tag
} else {
tag = generateRandomTag()
}
var clientIP net.IP var clientIP net.IP
switch len(config.ClientIp) { switch len(config.ClientIp) {
case 0, net.IPv4len, net.IPv6len: case 0, net.IPv4len, net.IPv6len:
@ -46,28 +54,26 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
return nil, errors.New("unexpected client IP length ", len(config.ClientIp)) return nil, errors.New("unexpected client IP length ", len(config.ClientIp))
} }
var ipOption dns.IPOption var ipOption *dns.IPOption
switch config.QueryStrategy { switch config.QueryStrategy {
case QueryStrategy_USE_IP: case QueryStrategy_USE_IP:
ipOption = dns.IPOption{ ipOption = &dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
} }
case QueryStrategy_USE_IP4: case QueryStrategy_USE_IP4:
ipOption = dns.IPOption{ ipOption = &dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: false, IPv6Enable: false,
FakeEnable: false, FakeEnable: false,
} }
case QueryStrategy_USE_IP6: case QueryStrategy_USE_IP6:
ipOption = dns.IPOption{ ipOption = &dns.IPOption{
IPv4Enable: false, IPv4Enable: false,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
} }
default:
return nil, errors.New("unexpected query strategy ", config.QueryStrategy)
} }
hosts, err := NewStaticHosts(config.StaticHosts) hosts, err := NewStaticHosts(config.StaticHosts)
@ -75,14 +81,8 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
return nil, errors.New("failed to create hosts").Base(err) return nil, errors.New("failed to create hosts").Base(err)
} }
var clients []*Client clients := []*Client{}
domainRuleCount := 0 domainRuleCount := 0
var defaultTag = config.Tag
if len(config.Tag) == 0 {
defaultTag = generateRandomTag()
}
for _, ns := range config.NameServer { for _, ns := range config.NameServer {
domainRuleCount += len(ns.PrioritizedDomain) domainRuleCount += len(ns.PrioritizedDomain)
} }
@ -90,6 +90,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
// MatcherInfos is ensured to cover the maximum index domainMatcher could return, where matcher's index starts from 1 // MatcherInfos is ensured to cover the maximum index domainMatcher could return, where matcher's index starts from 1
matcherInfos := make([]*DomainMatcherInfo, domainRuleCount+1) matcherInfos := make([]*DomainMatcherInfo, domainRuleCount+1)
domainMatcher := &strmatcher.MatcherGroup{} domainMatcher := &strmatcher.MatcherGroup{}
geoipContainer := router.GeoIPMatcherContainer{}
for _, ns := range config.NameServer { for _, ns := range config.NameServer {
clientIdx := len(clients) clientIdx := len(clients)
@ -107,18 +108,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
case net.IPv4len, net.IPv6len: case net.IPv4len, net.IPv6len:
myClientIP = net.IP(ns.ClientIp) myClientIP = net.IP(ns.ClientIp)
} }
client, err := NewClient(ctx, ns, myClientIP, geoipContainer, &matcherInfos, updateDomain)
disableCache := config.DisableCache
var tag = defaultTag
if len(ns.Tag) > 0 {
tag = ns.Tag
}
clientIPOption := ResolveIpOptionOverride(ns.QueryStrategy, ipOption)
if !clientIPOption.IPv4Enable && !clientIPOption.IPv6Enable {
return nil, errors.New("no QueryStrategy available for ", ns.Address)
}
client, err := NewClient(ctx, ns, myClientIP, disableCache, tag, clientIPOption, &matcherInfos, updateDomain)
if err != nil { if err != nil {
return nil, errors.New("failed to create client").Base(err) return nil, errors.New("failed to create client").Base(err)
} }
@ -127,16 +117,18 @@ func New(ctx context.Context, config *Config) (*DNS, error) {
// If there is no DNS client in config, add a `localhost` DNS client // If there is no DNS client in config, add a `localhost` DNS client
if len(clients) == 0 { if len(clients) == 0 {
clients = append(clients, NewLocalDNSClient(ipOption)) clients = append(clients, NewLocalDNSClient())
} }
return &DNS{ return &DNS{
tag: tag,
hosts: hosts, hosts: hosts,
ipOption: &ipOption, ipOption: ipOption,
clients: clients, clients: clients,
ctx: ctx, ctx: ctx,
domainMatcher: domainMatcher, domainMatcher: domainMatcher,
matcherInfos: matcherInfos, matcherInfos: matcherInfos,
disableCache: config.DisableCache,
disableFallback: config.DisableFallback, disableFallback: config.DisableFallback,
disableFallbackIfMatch: config.DisableFallbackIfMatch, disableFallbackIfMatch: config.DisableFallbackIfMatch,
}, nil }, nil
@ -160,87 +152,94 @@ func (s *DNS) Close() error {
// IsOwnLink implements proxy.dns.ownLinkVerifier // IsOwnLink implements proxy.dns.ownLinkVerifier
func (s *DNS) IsOwnLink(ctx context.Context) bool { func (s *DNS) IsOwnLink(ctx context.Context) bool {
inbound := session.InboundFromContext(ctx) inbound := session.InboundFromContext(ctx)
if inbound == nil { return inbound != nil && inbound.Tag == s.tag
return false
}
for _, client := range s.clients {
if client.tag == inbound.Tag {
return true
}
}
return false
} }
// LookupIP implements dns.Client. // LookupIP implements dns.Client.
func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, error) { func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) {
// Normalize the FQDN form query
domain = strings.TrimSuffix(domain, ".")
if domain == "" { if domain == "" {
return nil, 0, errors.New("empty domain name") return nil, errors.New("empty domain name")
} }
option.IPv4Enable = option.IPv4Enable && s.ipOption.IPv4Enable option.IPv4Enable = option.IPv4Enable && s.ipOption.IPv4Enable
option.IPv6Enable = option.IPv6Enable && s.ipOption.IPv6Enable option.IPv6Enable = option.IPv6Enable && s.ipOption.IPv6Enable
if !option.IPv4Enable && !option.IPv6Enable { if !option.IPv4Enable && !option.IPv6Enable {
return nil, 0, dns.ErrEmptyResponse return nil, dns.ErrEmptyResponse
} }
// Normalize the FQDN form query
domain = strings.TrimSuffix(domain, ".")
// Static host lookup // Static host lookup
switch addrs := s.hosts.Lookup(domain, option); { switch addrs := s.hosts.Lookup(domain, option); {
case addrs == nil: // Domain not recorded in static host case addrs == nil: // Domain not recorded in static host
break break
case len(addrs) == 0: // Domain recorded, but no valid IP returned (e.g. IPv4 address with only IPv6 enabled) case len(addrs) == 0: // Domain recorded, but no valid IP returned (e.g. IPv4 address with only IPv6 enabled)
return nil, 0, dns.ErrEmptyResponse return nil, dns.ErrEmptyResponse
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Domain replacement case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Domain replacement
errors.LogInfo(s.ctx, "domain replaced: ", domain, " -> ", addrs[0].Domain()) errors.LogInfo(s.ctx, "domain replaced: ", domain, " -> ", addrs[0].Domain())
domain = addrs[0].Domain() domain = addrs[0].Domain()
default: // Successfully found ip records in static host default: // Successfully found ip records in static host
errors.LogInfo(s.ctx, "returning ", len(addrs), " IP(s) for domain ", domain, " -> ", addrs) errors.LogInfo(s.ctx, "returning ", len(addrs), " IP(s) for domain ", domain, " -> ", addrs)
ips, err := toNetIP(addrs) return toNetIP(addrs)
if err != nil {
return nil, 0, err
}
return ips, 10, nil // Hosts ttl is 10
} }
// Name servers lookup // Name servers lookup
var errs []error errs := []error{}
ctx := session.ContextWithInbound(s.ctx, &session.Inbound{Tag: s.tag})
for _, client := range s.sortClients(domain) { for _, client := range s.sortClients(domain) {
if !option.FakeEnable && strings.EqualFold(client.Name(), "FakeDNS") { if !option.FakeEnable && strings.EqualFold(client.Name(), "FakeDNS") {
errors.LogDebug(s.ctx, "skip DNS resolution for domain ", domain, " at server ", client.Name()) errors.LogDebug(s.ctx, "skip DNS resolution for domain ", domain, " at server ", client.Name())
continue continue
} }
ips, err := client.QueryIP(ctx, domain, option, s.disableCache)
ips, ttl, err := client.QueryIP(s.ctx, domain, option)
if len(ips) > 0 { if len(ips) > 0 {
if ttl == 0 { return ips, nil
ttl = 1
}
return ips, ttl, nil
} }
if err != nil {
errors.LogInfoInner(s.ctx, err, "failed to lookup ip for domain ", domain, " at server ", client.Name()) errors.LogInfoInner(s.ctx, err, "failed to lookup ip for domain ", domain, " at server ", client.Name())
if err == nil { errs = append(errs, err)
err = dns.ErrEmptyResponse }
// 5 for RcodeRefused in miekg/dns, hardcode to reduce binary size
if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch && err != dns.ErrEmptyResponse && dns.RCodeFromError(err) != 5 {
return nil, err
} }
errs = append(errs, err)
} }
if len(errs) > 0 { return nil, errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...))
allErrs := errors.Combine(errs...) }
err0 := errs[0]
if errors.AllEqual(err0, allErrs) { // LookupHosts implements dns.HostsLookup.
if go_errors.Is(err0, dns.ErrEmptyResponse) { func (s *DNS) LookupHosts(domain string) *net.Address {
return nil, 0, dns.ErrEmptyResponse domain = strings.TrimSuffix(domain, ".")
} if domain == "" {
return nil, 0, errors.New("returning nil for domain ", domain).Base(err0) return nil
}
return nil, 0, errors.New("returning nil for domain ", domain).Base(allErrs)
} }
return nil, 0, dns.ErrEmptyResponse // Normalize the FQDN form query
addrs := s.hosts.Lookup(domain, *s.ipOption)
if len(addrs) > 0 {
errors.LogInfo(s.ctx, "domain replaced: ", domain, " -> ", addrs[0].String())
return &addrs[0]
}
return nil
}
// GetIPOption implements ClientWithIPOption.
func (s *DNS) GetIPOption() *dns.IPOption {
return s.ipOption
}
// SetQueryOption implements ClientWithIPOption.
func (s *DNS) SetQueryOption(isIPv4Enable, isIPv6Enable bool) {
s.ipOption.IPv4Enable = isIPv4Enable
s.ipOption.IPv6Enable = isIPv6Enable
}
// SetFakeDNSOption implements ClientWithIPOption.
func (s *DNS) SetFakeDNSOption(isFakeEnable bool) {
s.ipOption.FakeEnable = isFakeEnable
} }
func (s *DNS) sortClients(domain string) []*Client { func (s *DNS) sortClients(domain string) []*Client {
@ -251,11 +250,7 @@ func (s *DNS) sortClients(domain string) []*Client {
// Priority domain matching // Priority domain matching
hasMatch := false hasMatch := false
MatchSlice := s.domainMatcher.Match(domain) for _, match := range s.domainMatcher.Match(domain) {
sort.Slice(MatchSlice, func(i, j int) bool {
return MatchSlice[i] < MatchSlice[j]
})
for _, match := range MatchSlice {
info := s.matcherInfos[match] info := s.matcherInfos[match]
client := s.clients[info.clientIdx] client := s.clients[info.clientIdx]
domainRule := client.domains[info.domainRuleIdx] domainRule := client.domains[info.domainRuleIdx]

View File

@ -76,9 +76,6 @@ func (*staticHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
case q.Name == "notexist.google.com." && q.Qtype == dns.TypeAAAA: case q.Name == "notexist.google.com." && q.Qtype == dns.TypeAAAA:
ans.MsgHdr.Rcode = dns.RcodeNameError ans.MsgHdr.Rcode = dns.RcodeNameError
case q.Name == "notexist.google.com." && q.Qtype == dns.TypeA:
ans.MsgHdr.Rcode = dns.RcodeNameError
case q.Name == "hostname." && q.Qtype == dns.TypeA: case q.Name == "hostname." && q.Qtype == dns.TypeA:
rr, _ := dns.NewRR("hostname. IN A 127.0.0.1") rr, _ := dns.NewRR("hostname. IN A 127.0.0.1")
ans.Answer = append(ans.Answer, rr) ans.Answer = append(ans.Answer, rr)
@ -120,6 +117,7 @@ func TestUDPServerSubnet(t *testing.T) {
Handler: &staticHandler{}, Handler: &staticHandler{},
UDPSize: 1200, UDPSize: 1200,
} }
go dnsServer.ListenAndServe() go dnsServer.ListenAndServe()
time.Sleep(time.Second) time.Sleep(time.Second)
@ -157,7 +155,7 @@ func TestUDPServerSubnet(t *testing.T) {
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ ips, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -218,7 +216,7 @@ func TestUDPServer(t *testing.T) {
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
{ {
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ ips, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -233,7 +231,7 @@ func TestUDPServer(t *testing.T) {
} }
{ {
ips, _, err := client.LookupIP("facebook.com", feature_dns.IPOption{ ips, err := client.LookupIP("facebook.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -248,7 +246,7 @@ func TestUDPServer(t *testing.T) {
} }
{ {
_, _, err := client.LookupIP("notexist.google.com", feature_dns.IPOption{ _, err := client.LookupIP("notexist.google.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -262,7 +260,7 @@ func TestUDPServer(t *testing.T) {
} }
{ {
ips, _, err := client.LookupIP("ipv4only.google.com", feature_dns.IPOption{ ips, err := client.LookupIP("ipv4only.google.com", feature_dns.IPOption{
IPv4Enable: false, IPv4Enable: false,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -278,7 +276,7 @@ func TestUDPServer(t *testing.T) {
dnsServer.Shutdown() dnsServer.Shutdown()
{ {
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ ips, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -359,7 +357,7 @@ func TestPrioritizedDomain(t *testing.T) {
startTime := time.Now() startTime := time.Now()
{ {
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ ips, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -425,7 +423,7 @@ func TestUDPServerIPv6(t *testing.T) {
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
{ {
ips, _, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{ ips, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{
IPv4Enable: false, IPv4Enable: false,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -494,7 +492,7 @@ func TestStaticHostDomain(t *testing.T) {
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
{ {
ips, _, err := client.LookupIP("example.com", feature_dns.IPOption{ ips, err := client.LookupIP("example.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -605,7 +603,7 @@ func TestIPMatch(t *testing.T) {
startTime := time.Now() startTime := time.Now()
{ {
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ ips, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -728,7 +726,7 @@ func TestLocalDomain(t *testing.T) {
startTime := time.Now() startTime := time.Now()
{ // Will match dotless: { // Will match dotless:
ips, _, err := client.LookupIP("hostname", feature_dns.IPOption{ ips, err := client.LookupIP("hostname", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -743,7 +741,7 @@ func TestLocalDomain(t *testing.T) {
} }
{ // Will match domain:local { // Will match domain:local
ips, _, err := client.LookupIP("hostname.local", feature_dns.IPOption{ ips, err := client.LookupIP("hostname.local", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -758,7 +756,7 @@ func TestLocalDomain(t *testing.T) {
} }
{ // Will match static ip { // Will match static ip
ips, _, err := client.LookupIP("hostnamestatic", feature_dns.IPOption{ ips, err := client.LookupIP("hostnamestatic", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -773,7 +771,7 @@ func TestLocalDomain(t *testing.T) {
} }
{ // Will match domain replacing { // Will match domain replacing
ips, _, err := client.LookupIP("hostnamealias", feature_dns.IPOption{ ips, err := client.LookupIP("hostnamealias", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -787,8 +785,8 @@ func TestLocalDomain(t *testing.T) {
} }
} }
{ // Will match dotless:localhost, but not expectedIPs: 127.0.0.2, 127.0.0.3, then matches at dotless: { // Will match dotless:localhost, but not expectIPs: 127.0.0.2, 127.0.0.3, then matches at dotless:
ips, _, err := client.LookupIP("localhost", feature_dns.IPOption{ ips, err := client.LookupIP("localhost", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -802,8 +800,8 @@ func TestLocalDomain(t *testing.T) {
} }
} }
{ // Will match dotless:localhost, and expectedIPs: 127.0.0.2, 127.0.0.3 { // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
ips, _, err := client.LookupIP("localhost-a", feature_dns.IPOption{ ips, err := client.LookupIP("localhost-a", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -817,8 +815,8 @@ func TestLocalDomain(t *testing.T) {
} }
} }
{ // Will match dotless:localhost, and expectedIPs: 127.0.0.2, 127.0.0.3 { // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
ips, _, err := client.LookupIP("localhost-b", feature_dns.IPOption{ ips, err := client.LookupIP("localhost-b", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -833,7 +831,7 @@ func TestLocalDomain(t *testing.T) {
} }
{ // Will match dotless: { // Will match dotless:
ips, _, err := client.LookupIP("Mijia Cloud", feature_dns.IPOption{ ips, err := client.LookupIP("Mijia Cloud", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -999,7 +997,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
startTime := time.Now() startTime := time.Now()
{ // Will match server 1,2 and server 1 returns expected ip { // Will match server 1,2 and server 1 returns expected ip
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{ ips, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -1014,7 +1012,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
} }
{ // Will match server 1,2 and server 1 returns unexpected ip, then server 2 returns expected one { // Will match server 1,2 and server 1 returns unexpected ip, then server 2 returns expected one
ips, _, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{ ips, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: false, IPv6Enable: false,
FakeEnable: false, FakeEnable: false,
@ -1029,7 +1027,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
} }
{ // Will match server 3,1,2 and server 3 returns expected one { // Will match server 3,1,2 and server 3 returns expected one
ips, _, err := client.LookupIP("api.google.com", feature_dns.IPOption{ ips, err := client.LookupIP("api.google.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
@ -1044,7 +1042,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
} }
{ // Will match server 4,3,1,2 and server 4 returns expected one { // Will match server 4,3,1,2 and server 4 returns expected one
ips, _, err := client.LookupIP("v2.api.google.com", feature_dns.IPOption{ ips, err := client.LookupIP("v2.api.google.com", feature_dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,

View File

@ -31,31 +31,30 @@ type record struct {
// IPRecord is a cacheable item for a resolved domain // IPRecord is a cacheable item for a resolved domain
type IPRecord struct { type IPRecord struct {
ReqID uint16 ReqID uint16
IP []net.IP IP []net.Address
Expire time.Time Expire time.Time
RCode dnsmessage.RCode RCode dnsmessage.RCode
RawHeader *dnsmessage.Header
} }
func (r *IPRecord) getIPs() ([]net.IP, uint32, error) { func (r *IPRecord) getIPs() ([]net.Address, error) {
if r == nil { if r == nil || r.Expire.Before(time.Now()) {
return nil, 0, errRecordNotFound return nil, errRecordNotFound
} }
untilExpire := time.Until(r.Expire)
if untilExpire <= 0 {
return nil, 0, errRecordNotFound
}
ttl := uint32(untilExpire/time.Second) + uint32(1)
if r.RCode != dnsmessage.RCodeSuccess { if r.RCode != dnsmessage.RCodeSuccess {
return nil, ttl, dns_feature.RCodeError(r.RCode) return nil, dns_feature.RCodeError(r.RCode)
}
if len(r.IP) == 0 {
return nil, ttl, dns_feature.ErrEmptyResponse
} }
return r.IP, nil
}
return r.IP, ttl, nil func isNewer(baseRec *IPRecord, newRec *IPRecord) bool {
if newRec == nil {
return false
}
if baseRec == nil {
return true
}
return baseRec.Expire.Before(newRec.Expire)
} }
var errRecordNotFound = errors.New("record not found") var errRecordNotFound = errors.New("record not found")
@ -68,59 +67,49 @@ type dnsRequest struct {
msg *dnsmessage.Message msg *dnsmessage.Message
} }
func genEDNS0Options(clientIP net.IP, padding int) *dnsmessage.Resource { func genEDNS0Options(clientIP net.IP) *dnsmessage.Resource {
if len(clientIP) == 0 && padding == 0 { if len(clientIP) == 0 {
return nil return nil
} }
const EDNS0SUBNET = 0x8 var netmask int
const EDNS0PADDING = 0xc var family uint16
if len(clientIP) == 4 {
family = 1
netmask = 24 // 24 for IPV4, 96 for IPv6
} else {
family = 2
netmask = 96
}
b := make([]byte, 4)
binary.BigEndian.PutUint16(b[0:], family)
b[2] = byte(netmask)
b[3] = 0
switch family {
case 1:
ip := clientIP.To4().Mask(net.CIDRMask(netmask, net.IPv4len*8))
needLength := (netmask + 8 - 1) / 8 // division rounding up
b = append(b, ip[:needLength]...)
case 2:
ip := clientIP.Mask(net.CIDRMask(netmask, net.IPv6len*8))
needLength := (netmask + 8 - 1) / 8 // division rounding up
b = append(b, ip[:needLength]...)
}
const EDNS0SUBNET = 0x08
opt := new(dnsmessage.Resource) opt := new(dnsmessage.Resource)
common.Must(opt.Header.SetEDNS0(1350, 0xfe00, true)) common.Must(opt.Header.SetEDNS0(1350, 0xfe00, true))
body := dnsmessage.OPTResource{}
opt.Body = &body
if len(clientIP) != 0 { opt.Body = &dnsmessage.OPTResource{
var netmask int Options: []dnsmessage.Option{
var family uint16 {
if len(clientIP) == 4 {
family = 1
netmask = 24 // 24 for IPV4, 96 for IPv6
} else {
family = 2
netmask = 96
}
b := make([]byte, 4)
binary.BigEndian.PutUint16(b[0:], family)
b[2] = byte(netmask)
b[3] = 0
switch family {
case 1:
ip := clientIP.To4().Mask(net.CIDRMask(netmask, net.IPv4len*8))
needLength := (netmask + 8 - 1) / 8 // division rounding up
b = append(b, ip[:needLength]...)
case 2:
ip := clientIP.Mask(net.CIDRMask(netmask, net.IPv6len*8))
needLength := (netmask + 8 - 1) / 8 // division rounding up
b = append(b, ip[:needLength]...)
}
body.Options = append(body.Options,
dnsmessage.Option{
Code: EDNS0SUBNET, Code: EDNS0SUBNET,
Data: b, Data: b,
}) },
} },
if padding != 0 {
body.Options = append(body.Options,
dnsmessage.Option{
Code: EDNS0PADDING,
Data: make([]byte, padding),
})
} }
return opt return opt
@ -190,10 +179,9 @@ func parseResponse(payload []byte) (*IPRecord, error) {
now := time.Now() now := time.Now()
ipRecord := &IPRecord{ ipRecord := &IPRecord{
ReqID: h.ID, ReqID: h.ID,
RCode: h.RCode, RCode: h.RCode,
Expire: now.Add(time.Second * dns_feature.DefaultTTL), Expire: now.Add(time.Second * 600),
RawHeader: &h,
} }
L: L:
@ -208,7 +196,7 @@ L:
ttl := ah.TTL ttl := ah.TTL
if ttl == 0 { if ttl == 0 {
ttl = 1 ttl = 600
} }
expire := now.Add(time.Duration(ttl) * time.Second) expire := now.Add(time.Duration(ttl) * time.Second)
if ipRecord.Expire.After(expire) { if ipRecord.Expire.After(expire) {
@ -222,17 +210,14 @@ L:
errors.LogInfoInner(context.Background(), err, "failed to parse A record for domain: ", ah.Name) errors.LogInfoInner(context.Background(), err, "failed to parse A record for domain: ", ah.Name)
break L break L
} }
ipRecord.IP = append(ipRecord.IP, net.IPAddress(ans.A[:]).IP()) ipRecord.IP = append(ipRecord.IP, net.IPAddress(ans.A[:]))
case dnsmessage.TypeAAAA: case dnsmessage.TypeAAAA:
ans, err := parser.AAAAResource() ans, err := parser.AAAAResource()
if err != nil { if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to parse AAAA record for domain: ", ah.Name) errors.LogInfoInner(context.Background(), err, "failed to parse AAAA record for domain: ", ah.Name)
break L break L
} }
newIP := net.IPAddress(ans.AAAA[:]).IP() ipRecord.IP = append(ipRecord.IP, net.IPAddress(ans.AAAA[:]))
if len(newIP) == net.IPv6len {
ipRecord.IP = append(ipRecord.IP, newIP)
}
default: default:
if err := parser.SkipAnswer(); err != nil { if err := parser.SkipAnswer(); err != nil {
errors.LogInfoInner(context.Background(), err, "failed to skip answer") errors.LogInfoInner(context.Background(), err, "failed to skip answer")

View File

@ -51,7 +51,7 @@ func Test_parseResponse(t *testing.T) {
}{ }{
{ {
"empty", "empty",
&IPRecord{0, []net.IP(nil), time.Time{}, dnsmessage.RCodeSuccess, nil}, &IPRecord{0, []net.Address(nil), time.Time{}, dnsmessage.RCodeSuccess},
false, false,
}, },
{ {
@ -63,16 +63,15 @@ func Test_parseResponse(t *testing.T) {
"a record", "a record",
&IPRecord{ &IPRecord{
1, 1,
[]net.IP{net.ParseIP("8.8.8.8"), net.ParseIP("8.8.4.4")}, []net.Address{net.ParseAddress("8.8.8.8"), net.ParseAddress("8.8.4.4")},
time.Time{}, time.Time{},
dnsmessage.RCodeSuccess, dnsmessage.RCodeSuccess,
nil,
}, },
false, false,
}, },
{ {
"aaaa record", "aaaa record",
&IPRecord{2, []net.IP{net.ParseIP("2001::123:8888"), net.ParseIP("2001::123:8844")}, time.Time{}, dnsmessage.RCodeSuccess, nil}, &IPRecord{2, []net.Address{net.ParseAddress("2001::123:8888"), net.ParseAddress("2001::123:8844")}, time.Time{}, dnsmessage.RCodeSuccess},
false, false,
}, },
} }
@ -85,9 +84,8 @@ func Test_parseResponse(t *testing.T) {
} }
if got != nil { if got != nil {
// reset the time and RawHeader // reset the time
got.Expire = time.Time{} got.Expire = time.Time{}
got.RawHeader = nil
} }
if cmp.Diff(got, tt.want) != "" { if cmp.Diff(got, tt.want) != "" {
t.Error(cmp.Diff(got, tt.want)) t.Error(cmp.Diff(got, tt.want))
@ -156,7 +154,7 @@ func Test_genEDNS0Options(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if got := genEDNS0Options(tt.args.clientIP, 0); got == nil { if got := genEDNS0Options(tt.args.clientIP); got == nil {
t.Errorf("genEDNS0Options() = %v, want %v", got, tt.want) t.Errorf("genEDNS0Options() = %v, want %v", got, tt.want)
} }
}) })

View File

@ -2,6 +2,7 @@ package dns
import ( import (
"context" "context"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/strmatcher" "github.com/xtls/xray-core/common/strmatcher"
@ -40,6 +41,8 @@ func NewStaticHosts(hosts []*Config_HostMapping) (*StaticHosts, error) {
} }
ips = append(ips, addr) ips = append(ips, addr)
} }
default:
return nil, errors.New("neither IP address nor proxied domain specified for domain: ", mapping.Domain).AtWarning()
} }
sh.ips[id] = ips sh.ips[id] = ips
@ -59,14 +62,9 @@ func filterIP(ips []net.Address, option dns.IPOption) []net.Address {
} }
func (h *StaticHosts) lookupInternal(domain string) []net.Address { func (h *StaticHosts) lookupInternal(domain string) []net.Address {
ips := make([]net.Address, 0) var ips []net.Address
found := false
for _, id := range h.matchers.Match(domain) { for _, id := range h.matchers.Match(domain) {
ips = append(ips, h.ips[id]...) ips = append(ips, h.ips[id]...)
found = true
}
if !found {
return nil
} }
return ips return ips
} }
@ -74,7 +72,7 @@ func (h *StaticHosts) lookupInternal(domain string) []net.Address {
func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) []net.Address { func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) []net.Address {
switch addrs := h.lookupInternal(domain); { switch addrs := h.lookupInternal(domain); {
case len(addrs) == 0: // Not recorded in static hosts, return nil case len(addrs) == 0: // Not recorded in static hosts, return nil
return addrs return nil
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain
errors.LogDebug(context.Background(), "found replaced domain: ", domain, " -> ", addrs[0].Domain(), ". Try to unwrap it") errors.LogDebug(context.Background(), "found replaced domain: ", domain, " -> ", addrs[0].Domain(), ". Try to unwrap it")
if maxDepth > 0 { if maxDepth > 0 {

View File

@ -9,7 +9,6 @@ import (
"github.com/xtls/xray-core/app/router" "github.com/xtls/xray-core/app/router"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/strmatcher" "github.com/xtls/xray-core/common/strmatcher"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
@ -21,23 +20,22 @@ type Server interface {
// Name of the Client. // Name of the Client.
Name() string Name() string
// QueryIP sends IP queries to its configured server. // QueryIP sends IP queries to its configured server.
QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns.IPOption, disableCache bool) ([]net.IP, error)
} }
// Client is the interface for DNS client. // Client is the interface for DNS client.
type Client struct { type Client struct {
server Server server Server
skipFallback bool clientIP net.IP
domains []string skipFallback bool
expectedIPs []*router.GeoIPMatcher domains []string
allowUnexpectedIPs bool expectIPs []*router.GeoIPMatcher
tag string
timeoutMs time.Duration
ipOption *dns.IPOption
} }
var errExpectedIPNonMatch = errors.New("expectIPs not match")
// NewServer creates a name server object according to the network destination url. // NewServer creates a name server object according to the network destination url.
func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dispatcher, disableCache bool, clientIP net.IP) (Server, error) { func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dispatcher, queryStrategy QueryStrategy) (Server, error) {
if address := dest.Address; address.Family().IsDomain() { if address := dest.Address; address.Family().IsDomain() {
u, err := url.Parse(address.Domain()) u, err := url.Parse(address.Domain())
if err != nil { if err != nil {
@ -46,28 +44,21 @@ func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dis
switch { switch {
case strings.EqualFold(u.String(), "localhost"): case strings.EqualFold(u.String(), "localhost"):
return NewLocalNameServer(), nil return NewLocalNameServer(), nil
case strings.EqualFold(u.Scheme, "https"): // DNS-over-HTTPS Remote mode case strings.EqualFold(u.Scheme, "https"): // DOH Remote mode
return NewDoHNameServer(u, dispatcher, false, disableCache, clientIP), nil return NewDoHNameServer(u, dispatcher, queryStrategy)
case strings.EqualFold(u.Scheme, "h2c"): // DNS-over-HTTPS h2c Remote mode case strings.EqualFold(u.Scheme, "https+local"): // DOH Local mode
return NewDoHNameServer(u, dispatcher, true, disableCache, clientIP), nil return NewDoHLocalNameServer(u, queryStrategy), nil
case strings.EqualFold(u.Scheme, "https+local"): // DNS-over-HTTPS Local mode
return NewDoHNameServer(u, nil, false, disableCache, clientIP), nil
case strings.EqualFold(u.Scheme, "h2c+local"): // DNS-over-HTTPS h2c Local mode
return NewDoHNameServer(u, nil, true, disableCache, clientIP), nil
case strings.EqualFold(u.Scheme, "quic+local"): // DNS-over-QUIC Local mode case strings.EqualFold(u.Scheme, "quic+local"): // DNS-over-QUIC Local mode
return NewQUICNameServer(u, disableCache, clientIP) return NewQUICNameServer(u, queryStrategy)
case strings.EqualFold(u.Scheme, "tcp"): // DNS-over-TCP Remote mode case strings.EqualFold(u.Scheme, "tcp"): // DNS-over-TCP Remote mode
return NewTCPNameServer(u, dispatcher, disableCache, clientIP) return NewTCPNameServer(u, dispatcher, queryStrategy)
case strings.EqualFold(u.Scheme, "tcp+local"): // DNS-over-TCP Local mode case strings.EqualFold(u.Scheme, "tcp+local"): // DNS-over-TCP Local mode
return NewTCPLocalNameServer(u, disableCache, clientIP) return NewTCPLocalNameServer(u, queryStrategy)
case strings.EqualFold(u.String(), "fakedns"): case strings.EqualFold(u.String(), "fakedns"):
var fd dns.FakeDNSEngine var fd dns.FakeDNSEngine
err = core.RequireFeatures(ctx, func(fdns dns.FakeDNSEngine) { core.RequireFeatures(ctx, func(fdns dns.FakeDNSEngine) {
fd = fdns fd = fdns
}) })
if err != nil {
return nil, err
}
return NewFakeDNSServer(fd), nil return NewFakeDNSServer(fd), nil
} }
} }
@ -75,7 +66,7 @@ func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dis
dest.Network = net.Network_UDP dest.Network = net.Network_UDP
} }
if dest.Network == net.Network_UDP { // UDP classic DNS mode if dest.Network == net.Network_UDP { // UDP classic DNS mode
return NewClassicNameServer(dest, dispatcher, disableCache, clientIP), nil return NewClassicNameServer(dest, dispatcher, queryStrategy), nil
} }
return nil, errors.New("No available name server could be created from ", dest).AtWarning() return nil, errors.New("No available name server could be created from ", dest).AtWarning()
} }
@ -85,9 +76,7 @@ func NewClient(
ctx context.Context, ctx context.Context,
ns *NameServer, ns *NameServer,
clientIP net.IP, clientIP net.IP,
disableCache bool, container router.GeoIPMatcherContainer,
tag string,
ipOption dns.IPOption,
matcherInfos *[]*DomainMatcherInfo, matcherInfos *[]*DomainMatcherInfo,
updateDomainRule func(strmatcher.Matcher, int, []*DomainMatcherInfo) error, updateDomainRule func(strmatcher.Matcher, int, []*DomainMatcherInfo) error,
) (*Client, error) { ) (*Client, error) {
@ -95,7 +84,7 @@ func NewClient(
err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error { err := core.RequireFeatures(ctx, func(dispatcher routing.Dispatcher) error {
// Create a new server for each client for now // Create a new server for each client for now
server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, clientIP) server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, ns.GetQueryStrategy())
if err != nil { if err != nil {
return errors.New("failed to create nameserver").Base(err).AtWarning() return errors.New("failed to create nameserver").Base(err).AtWarning()
} }
@ -152,7 +141,7 @@ func NewClient(
// Establish expected IPs // Establish expected IPs
var matchers []*router.GeoIPMatcher var matchers []*router.GeoIPMatcher
for _, geoip := range ns.Geoip { for _, geoip := range ns.Geoip {
matcher, err := router.GlobalGeoIPContainer.Add(geoip) matcher, err := container.Add(geoip)
if err != nil { if err != nil {
return errors.New("failed to create ip matcher").Base(err).AtWarning() return errors.New("failed to create ip matcher").Base(err).AtWarning()
} }
@ -164,23 +153,15 @@ func NewClient(
case *net.IPOrDomain_Domain: case *net.IPOrDomain_Domain:
errors.LogInfo(ctx, "DNS: client ", ns.Address.Address.GetDomain(), " uses clientIP ", clientIP.String()) errors.LogInfo(ctx, "DNS: client ", ns.Address.Address.GetDomain(), " uses clientIP ", clientIP.String())
case *net.IPOrDomain_Ip: case *net.IPOrDomain_Ip:
errors.LogInfo(ctx, "DNS: client ", net.IP(ns.Address.Address.GetIp()), " uses clientIP ", clientIP.String()) errors.LogInfo(ctx, "DNS: client ", ns.Address.Address.GetIp(), " uses clientIP ", clientIP.String())
} }
} }
var timeoutMs = 4000 * time.Millisecond
if ns.TimeoutMs > 0 {
timeoutMs = time.Duration(ns.TimeoutMs) * time.Millisecond
}
client.server = server client.server = server
client.clientIP = clientIP
client.skipFallback = ns.SkipFallback client.skipFallback = ns.SkipFallback
client.domains = rules client.domains = rules
client.expectedIPs = matchers client.expectIPs = matchers
client.allowUnexpectedIPs = ns.AllowUnexpectedIPs
client.tag = tag
client.timeoutMs = timeoutMs
client.ipOption = &ipOption
return nil return nil
}) })
return client, err return client, err
@ -192,53 +173,36 @@ func (c *Client) Name() string {
} }
// QueryIP sends DNS query to the name server with the client's IP. // QueryIP sends DNS query to the name server with the client's IP.
func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, uint32, error) { func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption, disableCache bool) ([]net.IP, error) {
option.IPv4Enable = option.IPv4Enable && c.ipOption.IPv4Enable ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
option.IPv6Enable = option.IPv6Enable && c.ipOption.IPv6Enable ips, err := c.server.QueryIP(ctx, domain, c.clientIP, option, disableCache)
if !option.IPv4Enable && !option.IPv6Enable {
return nil, 0, dns.ErrEmptyResponse
}
ctx, cancel := context.WithTimeout(ctx, c.timeoutMs)
ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: c.tag})
ips, ttl, err := c.server.QueryIP(ctx, domain, option)
cancel() cancel()
if err != nil { if err != nil {
return nil, 0, err return ips, err
} }
return c.MatchExpectedIPs(domain, ips)
if len(ips) == 0 {
return nil, 0, dns.ErrEmptyResponse
}
if len(c.expectedIPs) > 0 {
newIps := c.MatchExpectedIPs(domain, ips)
if len(newIps) == 0 {
if !c.allowUnexpectedIPs {
return nil, 0, dns.ErrEmptyResponse
}
} else {
ips = newIps
}
}
return ips, ttl, nil
} }
// MatchExpectedIPs matches queried domain IPs with expected IPs and returns matched ones. // MatchExpectedIPs matches queried domain IPs with expected IPs and returns matched ones.
func (c *Client) MatchExpectedIPs(domain string, ips []net.IP) []net.IP { func (c *Client) MatchExpectedIPs(domain string, ips []net.IP) ([]net.IP, error) {
var newIps []net.IP if len(c.expectIPs) == 0 {
return ips, nil
}
newIps := []net.IP{}
for _, ip := range ips { for _, ip := range ips {
for _, matcher := range c.expectedIPs { for _, matcher := range c.expectIPs {
if matcher.Match(ip) { if matcher.Match(ip) {
newIps = append(newIps, ip) newIps = append(newIps, ip)
break break
} }
} }
} }
errors.LogDebug(context.Background(), "domain ", domain, " expectedIPs ", newIps, " matched at server ", c.Name()) if len(newIps) == 0 {
return newIps return nil, errExpectedIPNonMatch
}
errors.LogDebug(context.Background(), "domain ", domain, " expectIPs ", newIps, " matched at server ", c.Name())
return newIps, nil
} }
func ResolveIpOptionOverride(queryStrategy QueryStrategy, ipOption dns.IPOption) dns.IPOption { func ResolveIpOptionOverride(queryStrategy QueryStrategy, ipOption dns.IPOption) dns.IPOption {

View File

@ -3,136 +3,235 @@ package dns
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/tls"
go_errors "errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "sync"
"time" "time"
utls "github.com/refraction-networking/utls"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net/cnc" "github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/protocol/dns" "github.com/xtls/xray-core/common/protocol/dns"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal/pubsub"
"github.com/xtls/xray-core/common/task"
dns_feature "github.com/xtls/xray-core/features/dns" dns_feature "github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"golang.org/x/net/http2" "golang.org/x/net/dns/dnsmessage"
) )
// DoHNameServer implemented DNS over HTTPS (RFC8484) Wire Format, // DoHNameServer implemented DNS over HTTPS (RFC8484) Wire Format,
// which is compatible with traditional dns over udp(RFC1035), // which is compatible with traditional dns over udp(RFC1035),
// thus most of the DOH implementation is copied from udpns.go // thus most of the DOH implementation is copied from udpns.go
type DoHNameServer struct { type DoHNameServer struct {
cacheController *CacheController dispatcher routing.Dispatcher
httpClient *http.Client sync.RWMutex
dohURL string ips map[string]*record
clientIP net.IP pub *pubsub.Service
cleanup *task.Periodic
httpClient *http.Client
dohURL string
name string
queryStrategy QueryStrategy
} }
// NewDoHNameServer creates DOH/DOHL client object for remote/local resolving. // NewDoHNameServer creates DOH server object for remote resolving.
func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, h2c bool, disableCache bool, clientIP net.IP) *DoHNameServer { func NewDoHNameServer(url *url.URL, dispatcher routing.Dispatcher, queryStrategy QueryStrategy) (*DoHNameServer, error) {
url.Scheme = "https" errors.LogInfo(context.Background(), "DNS: created Remote DOH client for ", url.String())
mode := "DOH" s := baseDOHNameServer(url, "DOH", queryStrategy)
if dispatcher == nil {
mode = "DOHL" s.dispatcher = dispatcher
} tr := &http.Transport{
errors.LogInfo(context.Background(), "DNS: created ", mode, " client for ", url.String(), ", with h2c ", h2c) MaxIdleConns: 30,
s := &DoHNameServer{ IdleConnTimeout: 90 * time.Second,
cacheController: NewCacheController(mode+"//"+url.Host, disableCache), TLSHandshakeTimeout: 30 * time.Second,
dohURL: url.String(), ForceAttemptHTTP2: true,
clientIP: clientIP, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr)
if err != nil {
return nil, err
}
link, err := s.dispatcher.Dispatch(toDnsContext(ctx, s.dohURL), dest)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
if err != nil {
return nil, err
}
cc := common.ChainedClosable{}
if cw, ok := link.Writer.(common.Closable); ok {
cc = append(cc, cw)
}
if cr, ok := link.Reader.(common.Closable); ok {
cc = append(cc, cr)
}
return cnc.NewConnection(
cnc.ConnectionInputMulti(link.Writer),
cnc.ConnectionOutputMulti(link.Reader),
cnc.ConnectionOnClose(cc),
), nil
},
} }
s.httpClient = &http.Client{ s.httpClient = &http.Client{
Transport: &http2.Transport{ Timeout: time.Second * 180,
IdleConnTimeout: net.ConnIdleTimeout, Transport: tr,
ReadIdleTimeout: net.ChromeH2KeepAlivePeriod, }
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr) return s, nil
if err != nil { }
return nil, err
} // NewDoHLocalNameServer creates DOH client object for local resolving
var conn net.Conn func NewDoHLocalNameServer(url *url.URL, queryStrategy QueryStrategy) *DoHNameServer {
if dispatcher != nil { url.Scheme = "https"
dnsCtx := toDnsContext(ctx, s.dohURL) s := baseDOHNameServer(url, "DOHL", queryStrategy)
if h2c { tr := &http.Transport{
dnsCtx = session.ContextWithMitmAlpn11(dnsCtx, false) // for insurance IdleConnTimeout: 90 * time.Second,
dnsCtx = session.ContextWithMitmServerName(dnsCtx, url.Hostname()) ForceAttemptHTTP2: true,
} DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
link, err := dispatcher.Dispatch(dnsCtx, dest) dest, err := net.ParseDestination(network + ":" + addr)
select { if err != nil {
case <-ctx.Done(): return nil, err
return nil, ctx.Err() }
default: conn, err := internet.DialSystem(ctx, dest, nil)
} log.Record(&log.AccessMessage{
if err != nil { From: "DNS",
return nil, err To: s.dohURL,
} Status: log.AccessAccepted,
cc := common.ChainedClosable{} Detour: "local",
if cw, ok := link.Writer.(common.Closable); ok { })
cc = append(cc, cw) if err != nil {
} return nil, err
if cr, ok := link.Reader.(common.Closable); ok { }
cc = append(cc, cr) return conn, nil
}
conn = cnc.NewConnection(
cnc.ConnectionInputMulti(link.Writer),
cnc.ConnectionOutputMulti(link.Reader),
cnc.ConnectionOnClose(cc),
)
} else {
log.Record(&log.AccessMessage{
From: "DNS",
To: s.dohURL,
Status: log.AccessAccepted,
Detour: "local",
})
conn, err = internet.DialSystem(ctx, dest, nil)
if err != nil {
return nil, err
}
}
if !h2c {
conn = utls.UClient(conn, &utls.Config{ServerName: url.Hostname()}, utls.HelloChrome_Auto)
if err := conn.(*utls.UConn).HandshakeContext(ctx); err != nil {
return nil, err
}
}
return conn, nil
},
}, },
} }
s.httpClient = &http.Client{
Timeout: time.Second * 180,
Transport: tr,
}
errors.LogInfo(context.Background(), "DNS: created Local DOH client for ", url.String())
return s
}
func baseDOHNameServer(url *url.URL, prefix string, queryStrategy QueryStrategy) *DoHNameServer {
s := &DoHNameServer{
ips: make(map[string]*record),
pub: pubsub.NewService(),
name: prefix + "//" + url.Host,
dohURL: url.String(),
queryStrategy: queryStrategy,
}
s.cleanup = &task.Periodic{
Interval: time.Minute,
Execute: s.Cleanup,
}
return s return s
} }
// Name implements Server. // Name implements Server.
func (s *DoHNameServer) Name() string { func (s *DoHNameServer) Name() string {
return s.cacheController.name return s.name
}
// Cleanup clears expired items from cache
func (s *DoHNameServer) Cleanup() error {
now := time.Now()
s.Lock()
defer s.Unlock()
if len(s.ips) == 0 {
return errors.New("nothing to do. stopping...")
}
for domain, record := range s.ips {
if record.A != nil && record.A.Expire.Before(now) {
record.A = nil
}
if record.AAAA != nil && record.AAAA.Expire.Before(now) {
record.AAAA = nil
}
if record.A == nil && record.AAAA == nil {
errors.LogDebug(context.Background(), s.name, " cleanup ", domain)
delete(s.ips, domain)
} else {
s.ips[domain] = record
}
}
if len(s.ips) == 0 {
s.ips = make(map[string]*record)
}
return nil
}
func (s *DoHNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) {
elapsed := time.Since(req.start)
s.Lock()
rec, found := s.ips[req.domain]
if !found {
rec = &record{}
}
updated := false
switch req.reqType {
case dnsmessage.TypeA:
if isNewer(rec.A, ipRec) {
rec.A = ipRec
updated = true
}
case dnsmessage.TypeAAAA:
addr := make([]net.Address, 0, len(ipRec.IP))
for _, ip := range ipRec.IP {
if len(ip.IP()) == net.IPv6len {
addr = append(addr, ip)
}
}
ipRec.IP = addr
if isNewer(rec.AAAA, ipRec) {
rec.AAAA = ipRec
updated = true
}
}
errors.LogInfo(context.Background(), s.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed)
if updated {
s.ips[req.domain] = rec
}
switch req.reqType {
case dnsmessage.TypeA:
s.pub.Publish(req.domain+"4", nil)
case dnsmessage.TypeAAAA:
s.pub.Publish(req.domain+"6", nil)
}
s.Unlock()
common.Must(s.cleanup.Start())
} }
func (s *DoHNameServer) newReqID() uint16 { func (s *DoHNameServer) newReqID() uint16 {
return 0 return 0
} }
func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, domain string, option dns_feature.IPOption) { func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) {
errors.LogInfo(ctx, s.Name(), " querying: ", domain) errors.LogInfo(ctx, s.name, " querying: ", domain)
if s.Name()+"." == "DOH//"+domain { if s.name+"." == "DOH//"+domain {
errors.LogError(ctx, s.Name(), " tries to resolve itself! Use IP or set \"hosts\" instead.") errors.LogError(ctx, s.name, " tries to resolve itself! Use IP or set \"hosts\" instead.")
noResponseErrCh <- errors.New("tries to resolve itself!", s.Name())
return return
} }
// As we don't want our traffic pattern looks like DoH, we use Random-Length Padding instead of Block-Length Padding recommended in RFC 8467 reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP))
// 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 := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP, int(crypto.RandBetween(100, 300))))
var deadline time.Time var deadline time.Time
if d, ok := ctx.Deadline(); ok { if d, ok := ctx.Deadline(); ok {
@ -167,22 +266,19 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
b, err := dns.PackMessage(r.msg) b, err := dns.PackMessage(r.msg)
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to pack dns query for ", domain) errors.LogErrorInner(ctx, err, "failed to pack dns query for ", domain)
noResponseErrCh <- err
return return
} }
resp, err := s.dohHTTPSContext(dnsCtx, b.Bytes()) resp, err := s.dohHTTPSContext(dnsCtx, b.Bytes())
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to retrieve response for ", domain) errors.LogErrorInner(ctx, err, "failed to retrieve response for ", domain)
noResponseErrCh <- err
return return
} }
rec, err := parseResponse(resp) rec, err := parseResponse(resp)
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to handle DOH response for ", domain) errors.LogErrorInner(ctx, err, "failed to handle DOH response for ", domain)
noResponseErrCh <- err
return return
} }
s.cacheController.updateIP(r, rec) s.updateIP(r, rec)
}(req) }(req)
} }
} }
@ -197,8 +293,6 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte,
req.Header.Add("Accept", "application/dns-message") req.Header.Add("Accept", "application/dns-message")
req.Header.Add("Content-Type", "application/dns-message") req.Header.Add("Content-Type", "application/dns-message")
req.Header.Set("X-Padding", strings.Repeat("X", int(crypto.RandBetween(100, 1000))))
hc := s.httpClient hc := s.httpClient
resp, err := hc.Do(req.WithContext(ctx)) resp, err := hc.Do(req.WithContext(ctx))
@ -215,50 +309,107 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte,
return io.ReadAll(resp.Body) return io.ReadAll(resp.Body)
} }
// QueryIP implements Server. func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) {
func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { // nolint: dupl s.RLock()
fqdn := Fqdn(domain) record, found := s.ips[domain]
sub4, sub6 := s.cacheController.registerSubscribers(fqdn, option) s.RUnlock()
defer closeSubscribers(sub4, sub6)
if s.cacheController.disableCache { if !found {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.Name()) return nil, errRecordNotFound
}
var err4 error
var err6 error
var ips []net.Address
var ip6 []net.Address
if option.IPv4Enable {
ips, err4 = record.A.getIPs()
}
if option.IPv6Enable {
ip6, err6 = record.AAAA.getIPs()
ips = append(ips, ip6...)
}
if len(ips) > 0 {
return toNetIP(ips)
}
if err4 != nil {
return nil, err4
}
if err6 != nil {
return nil, err6
}
if (option.IPv4Enable && record.A != nil) || (option.IPv6Enable && record.AAAA != nil) {
return nil, dns_feature.ErrEmptyResponse
}
return nil, errRecordNotFound
}
// QueryIP implements Server.
func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) { // nolint: dupl
fqdn := Fqdn(domain)
option = ResolveIpOptionOverride(s.queryStrategy, option)
if !option.IPv4Enable && !option.IPv6Enable {
return nil, dns_feature.ErrEmptyResponse
}
if disableCache {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.name)
} else { } else {
ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) ips, err := s.findIPsForDomain(fqdn, option)
if !go_errors.Is(err, errRecordNotFound) { if err == nil || err == dns_feature.ErrEmptyResponse {
errors.LogDebugInner(ctx, err, s.Name(), " cache HIT ", domain, " -> ", ips) errors.LogDebugInner(ctx, err, s.name, " cache HIT ", domain, " -> ", ips)
log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err})
return ips, ttl, err return ips, err
} }
} }
noResponseErrCh := make(chan error, 2) // ipv4 and ipv6 belong to different subscription groups
s.sendQuery(ctx, noResponseErrCh, fqdn, option) var sub4, sub6 *pubsub.Subscriber
if option.IPv4Enable {
sub4 = s.pub.Subscribe(fqdn + "4")
defer sub4.Close()
}
if option.IPv6Enable {
sub6 = s.pub.Subscribe(fqdn + "6")
defer sub6.Close()
}
done := make(chan interface{})
go func() {
if sub4 != nil {
select {
case <-sub4.Wait():
case <-ctx.Done():
}
}
if sub6 != nil {
select {
case <-sub6.Wait():
case <-ctx.Done():
}
}
close(done)
}()
s.sendQuery(ctx, fqdn, clientIP, option)
start := time.Now() start := time.Now()
if sub4 != nil { for {
ips, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, err
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, 0, ctx.Err() return nil, ctx.Err()
case err := <-noResponseErrCh: case <-done:
return nil, 0, err
case <-sub4.Wait():
sub4.Close()
} }
} }
if sub6 != nil {
select {
case <-ctx.Done():
return nil, 0, ctx.Err()
case err := <-noResponseErrCh:
return nil, 0, err
case <-sub6.Wait():
sub6.Close()
}
}
ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option)
log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, ttl, err
} }

View File

@ -17,12 +17,12 @@ func TestDOHNameServer(t *testing.T) {
url, err := url.Parse("https+local://1.1.1.1/dns-query") url, err := url.Parse("https+local://1.1.1.1/dns-query")
common.Must(err) common.Must(err)
s := NewDoHNameServer(url, nil, false, false, net.IP(nil)) s := NewDoHLocalNameServer(url, QueryStrategy_USE_IP)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)
if len(ips) == 0 { if len(ips) == 0 {
@ -34,12 +34,12 @@ func TestDOHNameServerWithCache(t *testing.T) {
url, err := url.Parse("https+local://1.1.1.1/dns-query") url, err := url.Parse("https+local://1.1.1.1/dns-query")
common.Must(err) common.Must(err)
s := NewDoHNameServer(url, nil, false, false, net.IP(nil)) s := NewDoHLocalNameServer(url, QueryStrategy_USE_IP)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)
if len(ips) == 0 { if len(ips) == 0 {
@ -47,10 +47,10 @@ func TestDOHNameServerWithCache(t *testing.T) {
} }
ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips2, _, err := s.QueryIP(ctx2, "google.com", dns_feature.IPOption{ ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
}) }, true)
cancel() cancel()
common.Must(err) common.Must(err)
if r := cmp.Diff(ips2, ips); r != "" { if r := cmp.Diff(ips2, ips); r != "" {
@ -62,12 +62,12 @@ func TestDOHNameServerWithIPv4Override(t *testing.T) {
url, err := url.Parse("https+local://1.1.1.1/dns-query") url, err := url.Parse("https+local://1.1.1.1/dns-query")
common.Must(err) common.Must(err)
s := NewDoHNameServer(url, nil, false, false, net.IP(nil)) s := NewDoHLocalNameServer(url, QueryStrategy_USE_IP4)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: false, IPv6Enable: true,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)
if len(ips) == 0 { if len(ips) == 0 {
@ -85,12 +85,12 @@ func TestDOHNameServerWithIPv6Override(t *testing.T) {
url, err := url.Parse("https+local://1.1.1.1/dns-query") url, err := url.Parse("https+local://1.1.1.1/dns-query")
common.Must(err) common.Must(err)
s := NewDoHNameServer(url, nil, false, false, net.IP(nil)) s := NewDoHLocalNameServer(url, QueryStrategy_USE_IP6)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: false, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)
if len(ips) == 0 { if len(ips) == 0 {

View File

@ -20,9 +20,9 @@ func (FakeDNSServer) Name() string {
return "FakeDNS" return "FakeDNS"
} }
func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOption) ([]net.IP, uint32, error) { func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ net.IP, opt dns.IPOption, _ bool) ([]net.IP, error) {
if f.fakeDNSEngine == nil { if f.fakeDNSEngine == nil {
return nil, 0, errors.New("Unable to locate a fake DNS Engine").AtError() return nil, errors.New("Unable to locate a fake DNS Engine").AtError()
} }
var ips []net.Address var ips []net.Address
@ -34,13 +34,13 @@ func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOp
netIP, err := toNetIP(ips) netIP, err := toNetIP(ips)
if err != nil { if err != nil {
return nil, 0, errors.New("Unable to convert IP to net ip").Base(err).AtError() return nil, errors.New("Unable to convert IP to net ip").Base(err).AtError()
} }
errors.LogInfo(ctx, f.Name(), " got answer: ", domain, " -> ", ips) errors.LogInfo(ctx, f.Name(), " got answer: ", domain, " -> ", ips)
if len(netIP) > 0 { if len(netIP) > 0 {
return netIP, 1, nil // fakeIP ttl is 1 return netIP, nil
} }
return nil, 0, dns.ErrEmptyResponse return nil, dns.ErrEmptyResponse
} }

View File

@ -2,6 +2,7 @@ package dns
import ( import (
"context" "context"
"strings"
"time" "time"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@ -16,11 +17,16 @@ type LocalNameServer struct {
client *localdns.Client client *localdns.Client
} }
// QueryIP implements Server. const errEmptyResponse = "No address associated with hostname"
func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, option dns.IPOption) (ips []net.IP, ttl uint32, err error) {
// QueryIP implements Server.
func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, _ net.IP, option dns.IPOption, _ bool) (ips []net.IP, err error) {
start := time.Now() start := time.Now()
ips, ttl, err = s.client.LookupIP(domain, option) ips, err = s.client.LookupIP(domain, option)
if err != nil && strings.HasSuffix(err.Error(), errEmptyResponse) {
err = dns.ErrEmptyResponse
}
if len(ips) > 0 { if len(ips) > 0 {
errors.LogInfo(ctx, "Localhost got answer: ", domain, " -> ", ips) errors.LogInfo(ctx, "Localhost got answer: ", domain, " -> ", ips)
@ -44,6 +50,6 @@ func NewLocalNameServer() *LocalNameServer {
} }
// NewLocalDNSClient creates localdns client object for directly lookup in system DNS. // NewLocalDNSClient creates localdns client object for directly lookup in system DNS.
func NewLocalDNSClient(ipOption dns.IPOption) *Client { func NewLocalDNSClient() *Client {
return &Client{server: NewLocalNameServer(), ipOption: &ipOption} return &Client{server: NewLocalNameServer()}
} }

View File

@ -7,17 +7,18 @@ import (
. "github.com/xtls/xray-core/app/dns" . "github.com/xtls/xray-core/app/dns"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/features/dns" "github.com/xtls/xray-core/features/dns"
) )
func TestLocalNameServer(t *testing.T) { func TestLocalNameServer(t *testing.T) {
s := NewLocalNameServer() s := NewLocalNameServer()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP{}, dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)
if len(ips) == 0 { if len(ips) == 0 {

View File

@ -4,20 +4,23 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/binary" "encoding/binary"
go_errors "errors"
"net/url" "net/url"
"sync" "sync"
"time" "time"
"github.com/quic-go/quic-go" "github.com/xtls/quic-go"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol/dns" "github.com/xtls/xray-core/common/protocol/dns"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal/pubsub"
"github.com/xtls/xray-core/common/task"
dns_feature "github.com/xtls/xray-core/features/dns" dns_feature "github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/transport/internet/tls" "github.com/xtls/xray-core/transport/internet/tls"
"golang.org/x/net/dns/dnsmessage"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@ -30,14 +33,17 @@ const handshakeTimeout = time.Second * 8
// QUICNameServer implemented DNS over QUIC // QUICNameServer implemented DNS over QUIC
type QUICNameServer struct { type QUICNameServer struct {
sync.RWMutex sync.RWMutex
cacheController *CacheController ips map[string]*record
destination *net.Destination pub *pubsub.Service
connection quic.Connection cleanup *task.Periodic
clientIP net.IP name string
destination *net.Destination
connection quic.Connection
queryStrategy QueryStrategy
} }
// NewQUICNameServer creates DNS-over-QUIC client object for local resolving // NewQUICNameServer creates DNS-over-QUIC client object for local resolving
func NewQUICNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*QUICNameServer, error) { func NewQUICNameServer(url *url.URL, queryStrategy QueryStrategy) (*QUICNameServer, error) {
errors.LogInfo(context.Background(), "DNS: created Local DNS-over-QUIC client for ", url.String()) errors.LogInfo(context.Background(), "DNS: created Local DNS-over-QUIC client for ", url.String())
var err error var err error
@ -51,9 +57,15 @@ func NewQUICNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*QUICN
dest := net.UDPDestination(net.ParseAddress(url.Hostname()), port) dest := net.UDPDestination(net.ParseAddress(url.Hostname()), port)
s := &QUICNameServer{ s := &QUICNameServer{
cacheController: NewCacheController(url.String(), disableCache), ips: make(map[string]*record),
destination: &dest, pub: pubsub.NewService(),
clientIP: clientIP, name: url.String(),
destination: &dest,
queryStrategy: queryStrategy,
}
s.cleanup = &task.Periodic{
Interval: time.Minute,
Execute: s.Cleanup,
} }
return s, nil return s, nil
@ -61,17 +73,94 @@ func NewQUICNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*QUICN
// Name returns client name // Name returns client name
func (s *QUICNameServer) Name() string { func (s *QUICNameServer) Name() string {
return s.cacheController.name return s.name
}
// Cleanup clears expired items from cache
func (s *QUICNameServer) Cleanup() error {
now := time.Now()
s.Lock()
defer s.Unlock()
if len(s.ips) == 0 {
return errors.New("nothing to do. stopping...")
}
for domain, record := range s.ips {
if record.A != nil && record.A.Expire.Before(now) {
record.A = nil
}
if record.AAAA != nil && record.AAAA.Expire.Before(now) {
record.AAAA = nil
}
if record.A == nil && record.AAAA == nil {
errors.LogDebug(context.Background(), s.name, " cleanup ", domain)
delete(s.ips, domain)
} else {
s.ips[domain] = record
}
}
if len(s.ips) == 0 {
s.ips = make(map[string]*record)
}
return nil
}
func (s *QUICNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) {
elapsed := time.Since(req.start)
s.Lock()
rec, found := s.ips[req.domain]
if !found {
rec = &record{}
}
updated := false
switch req.reqType {
case dnsmessage.TypeA:
if isNewer(rec.A, ipRec) {
rec.A = ipRec
updated = true
}
case dnsmessage.TypeAAAA:
addr := make([]net.Address, 0)
for _, ip := range ipRec.IP {
if len(ip.IP()) == net.IPv6len {
addr = append(addr, ip)
}
}
ipRec.IP = addr
if isNewer(rec.AAAA, ipRec) {
rec.AAAA = ipRec
updated = true
}
}
errors.LogInfo(context.Background(), s.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed)
if updated {
s.ips[req.domain] = rec
}
switch req.reqType {
case dnsmessage.TypeA:
s.pub.Publish(req.domain+"4", nil)
case dnsmessage.TypeAAAA:
s.pub.Publish(req.domain+"6", nil)
}
s.Unlock()
common.Must(s.cleanup.Start())
} }
func (s *QUICNameServer) newReqID() uint16 { func (s *QUICNameServer) newReqID() uint16 {
return 0 return 0
} }
func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, domain string, option dns_feature.IPOption) { func (s *QUICNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) {
errors.LogInfo(ctx, s.Name(), " querying: ", domain) errors.LogInfo(ctx, s.name, " querying: ", domain)
reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP))
var deadline time.Time var deadline time.Time
if d, ok := ctx.Deadline(); ok { if d, ok := ctx.Deadline(); ok {
@ -103,36 +192,23 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e
b, err := dns.PackMessage(r.msg) b, err := dns.PackMessage(r.msg)
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to pack dns query") errors.LogErrorInner(ctx, err, "failed to pack dns query")
noResponseErrCh <- err
return return
} }
dnsReqBuf := buf.New() dnsReqBuf := buf.New()
err = binary.Write(dnsReqBuf, binary.BigEndian, uint16(b.Len())) binary.Write(dnsReqBuf, binary.BigEndian, uint16(b.Len()))
if err != nil { dnsReqBuf.Write(b.Bytes())
errors.LogErrorInner(ctx, err, "binary write failed")
noResponseErrCh <- err
return
}
_, err = dnsReqBuf.Write(b.Bytes())
if err != nil {
errors.LogErrorInner(ctx, err, "buffer write failed")
noResponseErrCh <- err
return
}
b.Release() b.Release()
conn, err := s.openStream(dnsCtx) conn, err := s.openStream(dnsCtx)
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to open quic connection") errors.LogErrorInner(ctx, err, "failed to open quic connection")
noResponseErrCh <- err
return return
} }
_, err = conn.Write(dnsReqBuf.Bytes()) _, err = conn.Write(dnsReqBuf.Bytes())
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to send query") errors.LogErrorInner(ctx, err, "failed to send query")
noResponseErrCh <- err
return return
} }
@ -143,81 +219,134 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- e
n, err := respBuf.ReadFullFrom(conn, 2) n, err := respBuf.ReadFullFrom(conn, 2)
if err != nil && n == 0 { if err != nil && n == 0 {
errors.LogErrorInner(ctx, err, "failed to read response length") errors.LogErrorInner(ctx, err, "failed to read response length")
noResponseErrCh <- err
return return
} }
var length int16 var length int16
err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length) err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length)
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to parse response length") errors.LogErrorInner(ctx, err, "failed to parse response length")
noResponseErrCh <- err
return return
} }
respBuf.Clear() respBuf.Clear()
n, err = respBuf.ReadFullFrom(conn, int32(length)) n, err = respBuf.ReadFullFrom(conn, int32(length))
if err != nil && n == 0 { if err != nil && n == 0 {
errors.LogErrorInner(ctx, err, "failed to read response length") errors.LogErrorInner(ctx, err, "failed to read response length")
noResponseErrCh <- err
return return
} }
rec, err := parseResponse(respBuf.Bytes()) rec, err := parseResponse(respBuf.Bytes())
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to handle response") errors.LogErrorInner(ctx, err, "failed to handle response")
noResponseErrCh <- err
return return
} }
s.cacheController.updateIP(r, rec) s.updateIP(r, rec)
}(req) }(req)
} }
} }
// QueryIP is called from dns.Server->queryIPTimeout func (s *QUICNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) {
func (s *QUICNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { s.RLock()
fqdn := Fqdn(domain) record, found := s.ips[domain]
sub4, sub6 := s.cacheController.registerSubscribers(fqdn, option) s.RUnlock()
defer closeSubscribers(sub4, sub6)
if s.cacheController.disableCache { if !found {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.Name()) return nil, errRecordNotFound
}
var err4 error
var err6 error
var ips []net.Address
var ip6 []net.Address
if option.IPv4Enable {
ips, err4 = record.A.getIPs()
}
if option.IPv6Enable {
ip6, err6 = record.AAAA.getIPs()
ips = append(ips, ip6...)
}
if len(ips) > 0 {
return toNetIP(ips)
}
if err4 != nil {
return nil, err4
}
if err6 != nil {
return nil, err6
}
if (option.IPv4Enable && record.A != nil) || (option.IPv6Enable && record.AAAA != nil) {
return nil, dns_feature.ErrEmptyResponse
}
return nil, errRecordNotFound
}
// QueryIP is called from dns.Server->queryIPTimeout
func (s *QUICNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) {
fqdn := Fqdn(domain)
option = ResolveIpOptionOverride(s.queryStrategy, option)
if !option.IPv4Enable && !option.IPv6Enable {
return nil, dns_feature.ErrEmptyResponse
}
if disableCache {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.name)
} else { } else {
ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) ips, err := s.findIPsForDomain(fqdn, option)
if !go_errors.Is(err, errRecordNotFound) { if err == nil || err == dns_feature.ErrEmptyResponse {
errors.LogDebugInner(ctx, err, s.Name(), " cache HIT ", domain, " -> ", ips) errors.LogDebugInner(ctx, err, s.name, " cache HIT ", domain, " -> ", ips)
log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err})
return ips, ttl, err return ips, err
} }
} }
noResponseErrCh := make(chan error, 2) // ipv4 and ipv6 belong to different subscription groups
s.sendQuery(ctx, noResponseErrCh, fqdn, option) var sub4, sub6 *pubsub.Subscriber
if option.IPv4Enable {
sub4 = s.pub.Subscribe(fqdn + "4")
defer sub4.Close()
}
if option.IPv6Enable {
sub6 = s.pub.Subscribe(fqdn + "6")
defer sub6.Close()
}
done := make(chan interface{})
go func() {
if sub4 != nil {
select {
case <-sub4.Wait():
case <-ctx.Done():
}
}
if sub6 != nil {
select {
case <-sub6.Wait():
case <-ctx.Done():
}
}
close(done)
}()
s.sendQuery(ctx, fqdn, clientIP, option)
start := time.Now() start := time.Now()
if sub4 != nil { for {
ips, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, err
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, 0, ctx.Err() return nil, ctx.Err()
case err := <-noResponseErrCh: case <-done:
return nil, 0, err
case <-sub4.Wait():
sub4.Close()
} }
} }
if sub6 != nil {
select {
case <-ctx.Done():
return nil, 0, ctx.Err()
case err := <-noResponseErrCh:
return nil, 0, err
case <-sub6.Wait():
sub6.Close()
}
}
ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option)
log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, ttl, err
} }
func isActive(s quic.Connection) bool { func isActive(s quic.Connection) bool {

View File

@ -16,23 +16,24 @@ import (
func TestQUICNameServer(t *testing.T) { func TestQUICNameServer(t *testing.T) {
url, err := url.Parse("quic://dns.adguard-dns.com") url, err := url.Parse("quic://dns.adguard-dns.com")
common.Must(err) common.Must(err)
s, err := NewQUICNameServer(url, false, net.IP(nil)) s, err := NewQUICNameServer(url, QueryStrategy_USE_IP)
common.Must(err) common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)
if len(ips) == 0 { if len(ips) == 0 {
t.Error("expect some ips, but got 0") t.Error("expect some ips, but got 0")
} }
ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips2, _, err := s.QueryIP(ctx2, "google.com", dns.IPOption{ ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
}) }, true)
cancel() cancel()
common.Must(err) common.Must(err)
if r := cmp.Diff(ips2, ips); r != "" { if r := cmp.Diff(ips2, ips); r != "" {
@ -43,13 +44,13 @@ func TestQUICNameServer(t *testing.T) {
func TestQUICNameServerWithIPv4Override(t *testing.T) { func TestQUICNameServerWithIPv4Override(t *testing.T) {
url, err := url.Parse("quic://dns.adguard-dns.com") url, err := url.Parse("quic://dns.adguard-dns.com")
common.Must(err) common.Must(err)
s, err := NewQUICNameServer(url, false, net.IP(nil)) s, err := NewQUICNameServer(url, QueryStrategy_USE_IP4)
common.Must(err) common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: false, IPv6Enable: true,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)
if len(ips) == 0 { if len(ips) == 0 {
@ -66,13 +67,13 @@ func TestQUICNameServerWithIPv4Override(t *testing.T) {
func TestQUICNameServerWithIPv6Override(t *testing.T) { func TestQUICNameServerWithIPv6Override(t *testing.T) {
url, err := url.Parse("quic://dns.adguard-dns.com") url, err := url.Parse("quic://dns.adguard-dns.com")
common.Must(err) common.Must(err)
s, err := NewQUICNameServer(url, false, net.IP(nil)) s, err := NewQUICNameServer(url, QueryStrategy_USE_IP6)
common.Must(err) common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
ips, _, err := s.QueryIP(ctx, "google.com", dns.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns.IPOption{
IPv4Enable: false, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)
if len(ips) == 0 { if len(ips) == 0 {

View File

@ -4,11 +4,12 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/binary" "encoding/binary"
go_errors "errors"
"net/url" "net/url"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/log"
@ -16,28 +17,34 @@ import (
"github.com/xtls/xray-core/common/net/cnc" "github.com/xtls/xray-core/common/net/cnc"
"github.com/xtls/xray-core/common/protocol/dns" "github.com/xtls/xray-core/common/protocol/dns"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal/pubsub"
"github.com/xtls/xray-core/common/task"
dns_feature "github.com/xtls/xray-core/features/dns" dns_feature "github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"golang.org/x/net/dns/dnsmessage"
) )
// TCPNameServer implemented DNS over TCP (RFC7766). // TCPNameServer implemented DNS over TCP (RFC7766).
type TCPNameServer struct { type TCPNameServer struct {
cacheController *CacheController sync.RWMutex
destination *net.Destination name string
reqID uint32 destination *net.Destination
dial func(context.Context) (net.Conn, error) ips map[string]*record
clientIP net.IP pub *pubsub.Service
cleanup *task.Periodic
reqID uint32
dial func(context.Context) (net.Conn, error)
queryStrategy QueryStrategy
} }
// NewTCPNameServer creates DNS over TCP server object for remote resolving. // NewTCPNameServer creates DNS over TCP server object for remote resolving.
func NewTCPNameServer( func NewTCPNameServer(
url *url.URL, url *url.URL,
dispatcher routing.Dispatcher, dispatcher routing.Dispatcher,
disableCache bool, queryStrategy QueryStrategy,
clientIP net.IP,
) (*TCPNameServer, error) { ) (*TCPNameServer, error) {
s, err := baseTCPNameServer(url, "TCP", disableCache, clientIP) s, err := baseTCPNameServer(url, "TCP", queryStrategy)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -58,8 +65,8 @@ func NewTCPNameServer(
} }
// NewTCPLocalNameServer creates DNS over TCP client object for local resolving // NewTCPLocalNameServer creates DNS over TCP client object for local resolving
func NewTCPLocalNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*TCPNameServer, error) { func NewTCPLocalNameServer(url *url.URL, queryStrategy QueryStrategy) (*TCPNameServer, error) {
s, err := baseTCPNameServer(url, "TCPL", disableCache, clientIP) s, err := baseTCPNameServer(url, "TCPL", queryStrategy)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -71,7 +78,7 @@ func NewTCPLocalNameServer(url *url.URL, disableCache bool, clientIP net.IP) (*T
return s, nil return s, nil
} }
func baseTCPNameServer(url *url.URL, prefix string, disableCache bool, clientIP net.IP) (*TCPNameServer, error) { func baseTCPNameServer(url *url.URL, prefix string, queryStrategy QueryStrategy) (*TCPNameServer, error) {
port := net.Port(53) port := net.Port(53)
if url.Port() != "" { if url.Port() != "" {
var err error var err error
@ -82,9 +89,15 @@ func baseTCPNameServer(url *url.URL, prefix string, disableCache bool, clientIP
dest := net.TCPDestination(net.ParseAddress(url.Hostname()), port) dest := net.TCPDestination(net.ParseAddress(url.Hostname()), port)
s := &TCPNameServer{ s := &TCPNameServer{
cacheController: NewCacheController(prefix+"//"+dest.NetAddr(), disableCache), destination: &dest,
destination: &dest, ips: make(map[string]*record),
clientIP: clientIP, pub: pubsub.NewService(),
name: prefix + "//" + dest.NetAddr(),
queryStrategy: queryStrategy,
}
s.cleanup = &task.Periodic{
Interval: time.Minute,
Execute: s.Cleanup,
} }
return s, nil return s, nil
@ -92,17 +105,94 @@ func baseTCPNameServer(url *url.URL, prefix string, disableCache bool, clientIP
// Name implements Server. // Name implements Server.
func (s *TCPNameServer) Name() string { func (s *TCPNameServer) Name() string {
return s.cacheController.name return s.name
}
// Cleanup clears expired items from cache
func (s *TCPNameServer) Cleanup() error {
now := time.Now()
s.Lock()
defer s.Unlock()
if len(s.ips) == 0 {
return errors.New("nothing to do. stopping...")
}
for domain, record := range s.ips {
if record.A != nil && record.A.Expire.Before(now) {
record.A = nil
}
if record.AAAA != nil && record.AAAA.Expire.Before(now) {
record.AAAA = nil
}
if record.A == nil && record.AAAA == nil {
errors.LogDebug(context.Background(), s.name, " cleanup ", domain)
delete(s.ips, domain)
} else {
s.ips[domain] = record
}
}
if len(s.ips) == 0 {
s.ips = make(map[string]*record)
}
return nil
}
func (s *TCPNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) {
elapsed := time.Since(req.start)
s.Lock()
rec, found := s.ips[req.domain]
if !found {
rec = &record{}
}
updated := false
switch req.reqType {
case dnsmessage.TypeA:
if isNewer(rec.A, ipRec) {
rec.A = ipRec
updated = true
}
case dnsmessage.TypeAAAA:
addr := make([]net.Address, 0)
for _, ip := range ipRec.IP {
if len(ip.IP()) == net.IPv6len {
addr = append(addr, ip)
}
}
ipRec.IP = addr
if isNewer(rec.AAAA, ipRec) {
rec.AAAA = ipRec
updated = true
}
}
errors.LogInfo(context.Background(), s.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed)
if updated {
s.ips[req.domain] = rec
}
switch req.reqType {
case dnsmessage.TypeA:
s.pub.Publish(req.domain+"4", nil)
case dnsmessage.TypeAAAA:
s.pub.Publish(req.domain+"6", nil)
}
s.Unlock()
common.Must(s.cleanup.Start())
} }
func (s *TCPNameServer) newReqID() uint16 { func (s *TCPNameServer) newReqID() uint16 {
return uint16(atomic.AddUint32(&s.reqID, 1)) return uint16(atomic.AddUint32(&s.reqID, 1))
} }
func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- error, domain string, option dns_feature.IPOption) { func (s *TCPNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) {
errors.LogDebug(ctx, s.Name(), " querying DNS for: ", domain) errors.LogDebug(ctx, s.name, " querying DNS for: ", domain)
reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP))
var deadline time.Time var deadline time.Time
if d, ok := ctx.Deadline(); ok { if d, ok := ctx.Deadline(); ok {
@ -131,36 +221,23 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
b, err := dns.PackMessage(r.msg) b, err := dns.PackMessage(r.msg)
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to pack dns query") errors.LogErrorInner(ctx, err, "failed to pack dns query")
noResponseErrCh <- err
return return
} }
conn, err := s.dial(dnsCtx) conn, err := s.dial(dnsCtx)
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to dial namesever") errors.LogErrorInner(ctx, err, "failed to dial namesever")
noResponseErrCh <- err
return return
} }
defer conn.Close() defer conn.Close()
dnsReqBuf := buf.New() dnsReqBuf := buf.New()
err = binary.Write(dnsReqBuf, binary.BigEndian, uint16(b.Len())) binary.Write(dnsReqBuf, binary.BigEndian, uint16(b.Len()))
if err != nil { dnsReqBuf.Write(b.Bytes())
errors.LogErrorInner(ctx, err, "binary write failed")
noResponseErrCh <- err
return
}
_, err = dnsReqBuf.Write(b.Bytes())
if err != nil {
errors.LogErrorInner(ctx, err, "buffer write failed")
noResponseErrCh <- err
return
}
b.Release() b.Release()
_, err = conn.Write(dnsReqBuf.Bytes()) _, err = conn.Write(dnsReqBuf.Bytes())
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to send query") errors.LogErrorInner(ctx, err, "failed to send query")
noResponseErrCh <- err
return return
} }
dnsReqBuf.Release() dnsReqBuf.Release()
@ -170,80 +247,129 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, noResponseErrCh chan<- er
n, err := respBuf.ReadFullFrom(conn, 2) n, err := respBuf.ReadFullFrom(conn, 2)
if err != nil && n == 0 { if err != nil && n == 0 {
errors.LogErrorInner(ctx, err, "failed to read response length") errors.LogErrorInner(ctx, err, "failed to read response length")
noResponseErrCh <- err
return return
} }
var length int16 var length int16
err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length) err = binary.Read(bytes.NewReader(respBuf.Bytes()), binary.BigEndian, &length)
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to parse response length") errors.LogErrorInner(ctx, err, "failed to parse response length")
noResponseErrCh <- err
return return
} }
respBuf.Clear() respBuf.Clear()
n, err = respBuf.ReadFullFrom(conn, int32(length)) n, err = respBuf.ReadFullFrom(conn, int32(length))
if err != nil && n == 0 { if err != nil && n == 0 {
errors.LogErrorInner(ctx, err, "failed to read response length") errors.LogErrorInner(ctx, err, "failed to read response length")
noResponseErrCh <- err
return return
} }
rec, err := parseResponse(respBuf.Bytes()) rec, err := parseResponse(respBuf.Bytes())
if err != nil { if err != nil {
errors.LogErrorInner(ctx, err, "failed to parse DNS over TCP response") errors.LogErrorInner(ctx, err, "failed to parse DNS over TCP response")
noResponseErrCh <- err
return return
} }
s.cacheController.updateIP(r, rec) s.updateIP(r, rec)
}(req) }(req)
} }
} }
// QueryIP implements Server. func (s *TCPNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) {
func (s *TCPNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { s.RLock()
fqdn := Fqdn(domain) record, found := s.ips[domain]
sub4, sub6 := s.cacheController.registerSubscribers(fqdn, option) s.RUnlock()
defer closeSubscribers(sub4, sub6)
if s.cacheController.disableCache { if !found {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.Name()) return nil, errRecordNotFound
}
var err4 error
var err6 error
var ips []net.Address
var ip6 []net.Address
if option.IPv4Enable {
ips, err4 = record.A.getIPs()
}
if option.IPv6Enable {
ip6, err6 = record.AAAA.getIPs()
ips = append(ips, ip6...)
}
if len(ips) > 0 {
return toNetIP(ips)
}
if err4 != nil {
return nil, err4
}
if err6 != nil {
return nil, err6
}
return nil, dns_feature.ErrEmptyResponse
}
// QueryIP implements Server.
func (s *TCPNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) {
fqdn := Fqdn(domain)
option = ResolveIpOptionOverride(s.queryStrategy, option)
if !option.IPv4Enable && !option.IPv6Enable {
return nil, dns_feature.ErrEmptyResponse
}
if disableCache {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.name)
} else { } else {
ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) ips, err := s.findIPsForDomain(fqdn, option)
if !go_errors.Is(err, errRecordNotFound) { if err == nil || err == dns_feature.ErrEmptyResponse {
errors.LogDebugInner(ctx, err, s.Name(), " cache HIT ", domain, " -> ", ips) errors.LogDebugInner(ctx, err, s.name, " cache HIT ", domain, " -> ", ips)
log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err})
return ips, ttl, err return ips, err
} }
} }
noResponseErrCh := make(chan error, 2) // ipv4 and ipv6 belong to different subscription groups
s.sendQuery(ctx, noResponseErrCh, fqdn, option) var sub4, sub6 *pubsub.Subscriber
if option.IPv4Enable {
sub4 = s.pub.Subscribe(fqdn + "4")
defer sub4.Close()
}
if option.IPv6Enable {
sub6 = s.pub.Subscribe(fqdn + "6")
defer sub6.Close()
}
done := make(chan interface{})
go func() {
if sub4 != nil {
select {
case <-sub4.Wait():
case <-ctx.Done():
}
}
if sub6 != nil {
select {
case <-sub6.Wait():
case <-ctx.Done():
}
}
close(done)
}()
s.sendQuery(ctx, fqdn, clientIP, option)
start := time.Now() start := time.Now()
if sub4 != nil { for {
ips, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, err
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, 0, ctx.Err() return nil, ctx.Err()
case err := <-noResponseErrCh: case <-done:
return nil, 0, err
case <-sub4.Wait():
sub4.Close()
} }
} }
if sub6 != nil {
select {
case <-ctx.Done():
return nil, 0, ctx.Err()
case err := <-noResponseErrCh:
return nil, 0, err
case <-sub6.Wait():
sub6.Close()
}
}
ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option)
log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, ttl, err
} }

View File

@ -16,13 +16,13 @@ import (
func TestTCPLocalNameServer(t *testing.T) { func TestTCPLocalNameServer(t *testing.T) {
url, err := url.Parse("tcp+local://8.8.8.8") url, err := url.Parse("tcp+local://8.8.8.8")
common.Must(err) common.Must(err)
s, err := NewTCPLocalNameServer(url, false, net.IP(nil)) s, err := NewTCPLocalNameServer(url, QueryStrategy_USE_IP)
common.Must(err) common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)
if len(ips) == 0 { if len(ips) == 0 {
@ -33,13 +33,13 @@ func TestTCPLocalNameServer(t *testing.T) {
func TestTCPLocalNameServerWithCache(t *testing.T) { func TestTCPLocalNameServerWithCache(t *testing.T) {
url, err := url.Parse("tcp+local://8.8.8.8") url, err := url.Parse("tcp+local://8.8.8.8")
common.Must(err) common.Must(err)
s, err := NewTCPLocalNameServer(url, false, net.IP(nil)) s, err := NewTCPLocalNameServer(url, QueryStrategy_USE_IP)
common.Must(err) common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)
if len(ips) == 0 { if len(ips) == 0 {
@ -47,10 +47,10 @@ func TestTCPLocalNameServerWithCache(t *testing.T) {
} }
ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips2, _, err := s.QueryIP(ctx2, "google.com", dns_feature.IPOption{ ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
}) }, true)
cancel() cancel()
common.Must(err) common.Must(err)
if r := cmp.Diff(ips2, ips); r != "" { if r := cmp.Diff(ips2, ips); r != "" {
@ -61,13 +61,13 @@ func TestTCPLocalNameServerWithCache(t *testing.T) {
func TestTCPLocalNameServerWithIPv4Override(t *testing.T) { func TestTCPLocalNameServerWithIPv4Override(t *testing.T) {
url, err := url.Parse("tcp+local://8.8.8.8") url, err := url.Parse("tcp+local://8.8.8.8")
common.Must(err) common.Must(err)
s, err := NewTCPLocalNameServer(url, false, net.IP(nil)) s, err := NewTCPLocalNameServer(url, QueryStrategy_USE_IP4)
common.Must(err) common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: false, IPv6Enable: true,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)
@ -85,13 +85,13 @@ func TestTCPLocalNameServerWithIPv4Override(t *testing.T) {
func TestTCPLocalNameServerWithIPv6Override(t *testing.T) { func TestTCPLocalNameServerWithIPv6Override(t *testing.T) {
url, err := url.Parse("tcp+local://8.8.8.8") url, err := url.Parse("tcp+local://8.8.8.8")
common.Must(err) common.Must(err)
s, err := NewTCPLocalNameServer(url, false, net.IP(nil)) s, err := NewTCPLocalNameServer(url, QueryStrategy_USE_IP6)
common.Must(err) common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, _, err := s.QueryIP(ctx, "google.com", dns_feature.IPOption{ ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: false, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
}) }, false)
cancel() cancel()
common.Must(err) common.Must(err)

View File

@ -2,7 +2,6 @@ package dns
import ( import (
"context" "context"
go_errors "errors"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -14,6 +13,7 @@ import (
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol/dns" "github.com/xtls/xray-core/common/protocol/dns"
udp_proto "github.com/xtls/xray-core/common/protocol/udp" udp_proto "github.com/xtls/xray-core/common/protocol/udp"
"github.com/xtls/xray-core/common/signal/pubsub"
"github.com/xtls/xray-core/common/task" "github.com/xtls/xray-core/common/task"
dns_feature "github.com/xtls/xray-core/features/dns" dns_feature "github.com/xtls/xray-core/features/dns"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
@ -24,36 +24,35 @@ import (
// ClassicNameServer implemented traditional UDP DNS. // ClassicNameServer implemented traditional UDP DNS.
type ClassicNameServer struct { type ClassicNameServer struct {
sync.RWMutex sync.RWMutex
cacheController *CacheController name string
address *net.Destination address *net.Destination
requests map[uint16]*udpDnsRequest ips map[string]*record
udpServer *udp.Dispatcher requests map[uint16]*dnsRequest
requestsCleanup *task.Periodic pub *pubsub.Service
reqID uint32 udpServer *udp.Dispatcher
clientIP net.IP cleanup *task.Periodic
} reqID uint32
queryStrategy QueryStrategy
type udpDnsRequest struct {
dnsRequest
ctx context.Context
} }
// NewClassicNameServer creates udp server object for remote resolving. // NewClassicNameServer creates udp server object for remote resolving.
func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher, disableCache bool, clientIP net.IP) *ClassicNameServer { func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher, queryStrategy QueryStrategy) *ClassicNameServer {
// default to 53 if unspecific // default to 53 if unspecific
if address.Port == 0 { if address.Port == 0 {
address.Port = net.Port(53) address.Port = net.Port(53)
} }
s := &ClassicNameServer{ s := &ClassicNameServer{
cacheController: NewCacheController(strings.ToUpper(address.String()), disableCache), address: &address,
address: &address, ips: make(map[string]*record),
requests: make(map[uint16]*udpDnsRequest), requests: make(map[uint16]*dnsRequest),
clientIP: clientIP, pub: pubsub.NewService(),
name: strings.ToUpper(address.String()),
queryStrategy: queryStrategy,
} }
s.requestsCleanup = &task.Periodic{ s.cleanup = &task.Periodic{
Interval: time.Minute, Interval: time.Minute,
Execute: s.RequestsCleanup, Execute: s.Cleanup,
} }
s.udpServer = udp.NewDispatcher(dispatcher, s.HandleResponse) s.udpServer = udp.NewDispatcher(dispatcher, s.HandleResponse)
errors.LogInfo(context.Background(), "DNS: created UDP client initialized for ", address.NetAddr()) errors.LogInfo(context.Background(), "DNS: created UDP client initialized for ", address.NetAddr())
@ -62,17 +61,37 @@ func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher
// Name implements Server. // Name implements Server.
func (s *ClassicNameServer) Name() string { func (s *ClassicNameServer) Name() string {
return s.cacheController.name return s.name
} }
// RequestsCleanup clears expired items from cache // Cleanup clears expired items from cache
func (s *ClassicNameServer) RequestsCleanup() error { func (s *ClassicNameServer) Cleanup() error {
now := time.Now() now := time.Now()
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
if len(s.requests) == 0 { if len(s.ips) == 0 && len(s.requests) == 0 {
return errors.New(s.Name(), " nothing to do. stopping...") return errors.New(s.name, " nothing to do. stopping...")
}
for domain, record := range s.ips {
if record.A != nil && record.A.Expire.Before(now) {
record.A = nil
}
if record.AAAA != nil && record.AAAA.Expire.Before(now) {
record.AAAA = nil
}
if record.A == nil && record.AAAA == nil {
errors.LogDebug(context.Background(), s.name, " cleanup ", domain)
delete(s.ips, domain)
} else {
s.ips[domain] = record
}
}
if len(s.ips) == 0 {
s.ips = make(map[string]*record)
} }
for id, req := range s.requests { for id, req := range s.requests {
@ -82,7 +101,7 @@ func (s *ClassicNameServer) RequestsCleanup() error {
} }
if len(s.requests) == 0 { if len(s.requests) == 0 {
s.requests = make(map[uint16]*udpDnsRequest) s.requests = make(map[uint16]*dnsRequest)
} }
return nil return nil
@ -92,7 +111,7 @@ func (s *ClassicNameServer) RequestsCleanup() error {
func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_proto.Packet) { func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_proto.Packet) {
ipRec, err := parseResponse(packet.Payload.Bytes()) ipRec, err := parseResponse(packet.Payload.Bytes())
if err != nil { if err != nil {
errors.LogError(ctx, s.Name(), " fail to parse responded DNS udp") errors.LogError(ctx, s.name, " fail to parse responded DNS udp")
return return
} }
@ -105,107 +124,179 @@ func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_prot
} }
s.Unlock() s.Unlock()
if !ok { if !ok {
errors.LogError(ctx, s.Name(), " cannot find the pending request") errors.LogError(ctx, s.name, " cannot find the pending request")
return return
} }
// if truncated, retry with EDNS0 option(udp payload size: 1350) var rec record
if ipRec.RawHeader.Truncated { switch req.reqType {
// if already has EDNS0 option, no need to retry case dnsmessage.TypeA:
if len(req.msg.Additionals) == 0 { rec.A = ipRec
// copy necessary meta data from original request case dnsmessage.TypeAAAA:
// and add EDNS0 option rec.AAAA = ipRec
opt := new(dnsmessage.Resource)
common.Must(opt.Header.SetEDNS0(1350, 0xfe00, true))
opt.Body = &dnsmessage.OPTResource{}
newMsg := *req.msg
newReq := *req
newMsg.Additionals = append(newMsg.Additionals, *opt)
newMsg.ID = s.newReqID()
newReq.msg = &newMsg
s.addPendingRequest(&newReq)
b, _ := dns.PackMessage(newReq.msg)
s.udpServer.Dispatch(toDnsContext(newReq.ctx, s.address.String()), *s.address, b)
return
}
} }
s.cacheController.updateIP(&req.dnsRequest, ipRec) elapsed := time.Since(req.start)
errors.LogInfo(ctx, s.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed)
if len(req.domain) > 0 && (rec.A != nil || rec.AAAA != nil) {
s.updateIP(req.domain, &rec)
}
}
func (s *ClassicNameServer) updateIP(domain string, newRec *record) {
s.Lock()
rec, found := s.ips[domain]
if !found {
rec = &record{}
}
updated := false
if isNewer(rec.A, newRec.A) {
rec.A = newRec.A
updated = true
}
if isNewer(rec.AAAA, newRec.AAAA) {
rec.AAAA = newRec.AAAA
updated = true
}
if updated {
errors.LogDebug(context.Background(), s.name, " updating IP records for domain:", domain)
s.ips[domain] = rec
}
if newRec.A != nil {
s.pub.Publish(domain+"4", nil)
}
if newRec.AAAA != nil {
s.pub.Publish(domain+"6", nil)
}
s.Unlock()
common.Must(s.cleanup.Start())
} }
func (s *ClassicNameServer) newReqID() uint16 { func (s *ClassicNameServer) newReqID() uint16 {
return uint16(atomic.AddUint32(&s.reqID, 1)) return uint16(atomic.AddUint32(&s.reqID, 1))
} }
func (s *ClassicNameServer) addPendingRequest(req *udpDnsRequest) { func (s *ClassicNameServer) addPendingRequest(req *dnsRequest) {
s.Lock() s.Lock()
defer s.Unlock()
id := req.msg.ID id := req.msg.ID
req.expire = time.Now().Add(time.Second * 8) req.expire = time.Now().Add(time.Second * 8)
s.requests[id] = req s.requests[id] = req
s.Unlock()
common.Must(s.requestsCleanup.Start())
} }
func (s *ClassicNameServer) sendQuery(ctx context.Context, _ chan<- error, domain string, option dns_feature.IPOption) { func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) {
errors.LogDebug(ctx, s.Name(), " querying DNS for: ", domain) errors.LogDebug(ctx, s.name, " querying DNS for: ", domain)
reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(s.clientIP, 0)) reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP))
for _, req := range reqs { for _, req := range reqs {
udpReq := &udpDnsRequest{ s.addPendingRequest(req)
dnsRequest: *req,
ctx: ctx,
}
s.addPendingRequest(udpReq)
b, _ := dns.PackMessage(req.msg) b, _ := dns.PackMessage(req.msg)
s.udpServer.Dispatch(toDnsContext(ctx, s.address.String()), *s.address, b) s.udpServer.Dispatch(toDnsContext(ctx, s.address.String()), *s.address, b)
} }
} }
// QueryIP implements Server. func (s *ClassicNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) {
func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) { s.RLock()
fqdn := Fqdn(domain) record, found := s.ips[domain]
sub4, sub6 := s.cacheController.registerSubscribers(fqdn, option) s.RUnlock()
defer closeSubscribers(sub4, sub6)
if s.cacheController.disableCache { if !found {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.Name()) return nil, errRecordNotFound
}
var err4 error
var err6 error
var ips []net.Address
var ip6 []net.Address
if option.IPv4Enable {
ips, err4 = record.A.getIPs()
}
if option.IPv6Enable {
ip6, err6 = record.AAAA.getIPs()
ips = append(ips, ip6...)
}
if len(ips) > 0 {
return toNetIP(ips)
}
if err4 != nil {
return nil, err4
}
if err6 != nil {
return nil, err6
}
return nil, dns_feature.ErrEmptyResponse
}
// QueryIP implements Server.
func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) {
fqdn := Fqdn(domain)
option = ResolveIpOptionOverride(s.queryStrategy, option)
if !option.IPv4Enable && !option.IPv6Enable {
return nil, dns_feature.ErrEmptyResponse
}
if disableCache {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.name)
} else { } else {
ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option) ips, err := s.findIPsForDomain(fqdn, option)
if !go_errors.Is(err, errRecordNotFound) { if err == nil || err == dns_feature.ErrEmptyResponse {
errors.LogDebugInner(ctx, err, s.Name(), " cache HIT ", domain, " -> ", ips) errors.LogDebugInner(ctx, err, s.name, " cache HIT ", domain, " -> ", ips)
log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err}) log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err})
return ips, ttl, err return ips, err
} }
} }
noResponseErrCh := make(chan error, 2) // ipv4 and ipv6 belong to different subscription groups
s.sendQuery(ctx, noResponseErrCh, fqdn, option) var sub4, sub6 *pubsub.Subscriber
if option.IPv4Enable {
sub4 = s.pub.Subscribe(fqdn + "4")
defer sub4.Close()
}
if option.IPv6Enable {
sub6 = s.pub.Subscribe(fqdn + "6")
defer sub6.Close()
}
done := make(chan interface{})
go func() {
if sub4 != nil {
select {
case <-sub4.Wait():
case <-ctx.Done():
}
}
if sub6 != nil {
select {
case <-sub6.Wait():
case <-ctx.Done():
}
}
close(done)
}()
s.sendQuery(ctx, fqdn, clientIP, option)
start := time.Now() start := time.Now()
if sub4 != nil { for {
ips, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, err
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, 0, ctx.Err() return nil, ctx.Err()
case err := <-noResponseErrCh: case <-done:
return nil, 0, err
case <-sub4.Wait():
sub4.Close()
} }
} }
if sub6 != nil {
select {
case <-ctx.Done():
return nil, 0, ctx.Err()
case err := <-noResponseErrCh:
return nil, 0, err
case <-sub6.Wait():
sub6.Close()
}
}
ips, ttl, err := s.cacheController.findIPsForDomain(fqdn, option)
log.Record(&log.DNSLog{Server: s.Name(), Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, ttl, err
} }

View File

@ -27,8 +27,7 @@ type Config struct {
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
// Tag of the outbound handler that handles metrics http connections. // Tag of the outbound handler that handles metrics http connections.
Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"` Tag string `protobuf:"bytes,1,opt,name=tag,proto3" json:"tag,omitempty"`
Listen string `protobuf:"bytes,2,opt,name=listen,proto3" json:"listen,omitempty"`
} }
func (x *Config) Reset() { func (x *Config) Reset() {
@ -68,28 +67,20 @@ func (x *Config) GetTag() string {
return "" return ""
} }
func (x *Config) GetListen() string {
if x != nil {
return x.Listen
}
return ""
}
var File_app_metrics_config_proto protoreflect.FileDescriptor var File_app_metrics_config_proto protoreflect.FileDescriptor
var file_app_metrics_config_proto_rawDesc = []byte{ var file_app_metrics_config_proto_rawDesc = []byte{
0x0a, 0x18, 0x61, 0x70, 0x70, 0x2f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x2f, 0x63, 0x6f, 0x0a, 0x18, 0x61, 0x70, 0x70, 0x2f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x2f, 0x63, 0x6f,
0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61, 0x79, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61, 0x79,
0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x22, 0x32, 0x0a, 0x06, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x22, 0x1a, 0x0a, 0x06,
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x69, 0x73, 0x74, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x42, 0x52, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e,
0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73,
0x42, 0x52, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x50, 0x01, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78,
0x2e, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x50, 0x01, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x70, 0x2f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0xaa, 0x02, 0x10, 0x58, 0x72, 0x61, 0x79,
0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x62, 0x06, 0x70, 0x72,
0x73, 0xaa, 0x02, 0x10, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x4d, 0x65, 0x74, 0x6f, 0x74, 0x6f, 0x33,
0x72, 0x69, 0x63, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (

View File

@ -10,5 +10,4 @@ option java_multiple_files = true;
message Config { message Config {
// Tag of the outbound handler that handles metrics http connections. // Tag of the outbound handler that handles metrics http connections.
string tag = 1; string tag = 1;
string listen = 2;
} }

View File

@ -24,15 +24,12 @@ type MetricsHandler struct {
statsManager feature_stats.Manager statsManager feature_stats.Manager
observatory extension.Observatory observatory extension.Observatory
tag string tag string
listen string
tcpListener net.Listener
} }
// NewMetricsHandler creates a new MetricsHandler based on the given config. // NewMetricsHandler creates a new MetricsHandler based on the given config.
func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, error) { func NewMetricsHandler(ctx context.Context, config *Config) (*MetricsHandler, error) {
c := &MetricsHandler{ c := &MetricsHandler{
tag: config.Tag, tag: config.Tag,
listen: config.Listen,
} }
common.Must(core.RequireFeatures(ctx, func(om outbound.Manager, sm feature_stats.Manager) { common.Must(core.RequireFeatures(ctx, func(om outbound.Manager, sm feature_stats.Manager) {
c.statsManager = sm c.statsManager = sm
@ -90,23 +87,6 @@ func (p *MetricsHandler) Type() interface{} {
} }
func (p *MetricsHandler) Start() error { func (p *MetricsHandler) Start() error {
// direct listen a port if listen is set
if 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 func() {
if err := http.Serve(TCPlistener, http.DefaultServeMux); err != nil {
errors.LogErrorInner(context.Background(), err, "failed to start metrics server")
}
}()
}
listener := &OutboundListener{ listener := &OutboundListener{
buffer: make(chan net.Conn, 4), buffer: make(chan net.Conn, 4),
done: done.New(), done: done.New(),

View File

@ -66,10 +66,10 @@ func NewHealthPing(ctx context.Context, dispatcher routing.Dispatcher, config *H
settings.Timeout = time.Duration(5) * time.Second settings.Timeout = time.Duration(5) * time.Second
} }
return &HealthPing{ return &HealthPing{
ctx: ctx, ctx: ctx,
dispatcher: dispatcher, dispatcher: dispatcher,
Settings: settings, Settings: settings,
Results: nil, Results: nil,
} }
} }

View File

@ -32,7 +32,7 @@ type Observer struct {
finished *done.Instance finished *done.Instance
ohm outbound.Manager ohm outbound.Manager
dispatcher routing.Dispatcher dispatcher routing.Dispatcher
} }
@ -226,9 +226,9 @@ func New(ctx context.Context, config *Config) (*Observer, error) {
return nil, errors.New("Cannot get depended features").Base(err) return nil, errors.New("Cannot get depended features").Base(err)
} }
return &Observer{ return &Observer{
config: config, config: config,
ctx: ctx, ctx: ctx,
ohm: outboundManager, ohm: outboundManager,
dispatcher: dispatcher, dispatcher: dispatcher,
}, nil }, nil
} }

View File

@ -23,7 +23,7 @@ type DynamicInboundHandler struct {
receiverConfig *proxyman.ReceiverConfig receiverConfig *proxyman.ReceiverConfig
streamSettings *internet.MemoryStreamConfig streamSettings *internet.MemoryStreamConfig
portMutex sync.Mutex portMutex sync.Mutex
portsInUse map[net.Port]struct{} portsInUse map[net.Port]bool
workerMutex sync.RWMutex workerMutex sync.RWMutex
worker []worker worker []worker
lastRefresh time.Time lastRefresh time.Time
@ -39,7 +39,7 @@ func NewDynamicInboundHandler(ctx context.Context, tag string, receiverConfig *p
tag: tag, tag: tag,
proxyConfig: proxyConfig, proxyConfig: proxyConfig,
receiverConfig: receiverConfig, receiverConfig: receiverConfig,
portsInUse: make(map[net.Port]struct{}), portsInUse: make(map[net.Port]bool),
mux: mux.NewServer(ctx), mux: mux.NewServer(ctx),
v: v, v: v,
ctx: ctx, ctx: ctx,
@ -84,7 +84,7 @@ func (h *DynamicInboundHandler) allocatePort() net.Port {
port := net.Port(allPorts[r]) port := net.Port(allPorts[r])
_, used := h.portsInUse[port] _, used := h.portsInUse[port]
if !used { if !used {
h.portsInUse[port] = struct{}{} h.portsInUse[port] = true
return port return port
} }
} }

View File

@ -324,7 +324,6 @@ func (w *udpWorker) callback(b *buf.Buffer, source net.Destination, originalDest
if w.sniffingConfig != nil { if w.sniffingConfig != nil {
content.SniffingRequest.Enabled = w.sniffingConfig.Enabled content.SniffingRequest.Enabled = w.sniffingConfig.Enabled
content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded
content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly
} }
@ -465,7 +464,8 @@ func (w *dsWorker) callback(conn stat.Connection) {
} }
} }
ctx = session.ContextWithInbound(ctx, &session.Inbound{ ctx = session.ContextWithInbound(ctx, &session.Inbound{
Source: net.DestinationFromAddr(conn.RemoteAddr()), // Unix have no source addr, so we use gateway as source for log.
Source: net.UnixDestination(w.address),
Gateway: net.UnixDestination(w.address), Gateway: net.UnixDestination(w.address),
Tag: w.tag, Tag: w.tag,
Conn: conn, Conn: conn,

View File

@ -241,9 +241,7 @@ func (h *Handler) DestIpAddress() net.IP {
// Dial implements internet.Dialer. // Dial implements internet.Dialer.
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) { func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
if h.senderSettings != nil { if h.senderSettings != nil {
if h.senderSettings.ProxySettings.HasTag() { if h.senderSettings.ProxySettings.HasTag() {
tag := h.senderSettings.ProxySettings.Tag tag := h.senderSettings.ProxySettings.Tag
handler := h.outboundManager.GetHandler(tag) handler := h.outboundManager.GetHandler(tag)
if handler != nil { if handler != nil {
@ -272,40 +270,13 @@ func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connecti
} }
if h.senderSettings.Via != nil { if h.senderSettings.Via != nil {
outbounds := session.OutboundsFromContext(ctx) outbounds := session.OutboundsFromContext(ctx)
ob := outbounds[len(outbounds)-1] ob := outbounds[len(outbounds)-1]
addr := h.senderSettings.Via.AsAddress() if h.senderSettings.ViaCidr == "" {
var domain string ob.Gateway = h.senderSettings.Via.AsAddress()
if addr.Family().IsDomain() { } else { //Get a random address.
domain = addr.Domain() ob.Gateway = ParseRandomIPv6(h.senderSettings.Via.AsAddress(), h.senderSettings.ViaCidr)
} }
switch {
case h.senderSettings.ViaCidr != "":
ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr)
case domain == "origin":
if inbound := session.InboundFromContext(ctx); inbound != nil {
origin, _, err := net.SplitHostPort(inbound.Conn.LocalAddr().String())
if err == nil {
ob.Gateway = net.ParseAddress(origin)
}
}
case domain == "srcip":
if inbound := session.InboundFromContext(ctx); inbound != nil {
srcip, _, err := net.SplitHostPort(inbound.Conn.RemoteAddr().String())
if err == nil {
ob.Gateway = net.ParseAddress(srcip)
}
}
//case addr.Family().IsDomain():
default:
ob.Gateway = addr
}
} }
} }
@ -345,25 +316,23 @@ func (h *Handler) Start() error {
// Close implements common.Closable. // Close implements common.Closable.
func (h *Handler) Close() error { func (h *Handler) Close() error {
common.Close(h.mux) common.Close(h.mux)
common.Close(h.proxy)
return nil return nil
} }
func ParseRandomIP(addr net.Address, prefix string) net.Address { func ParseRandomIPv6(address net.Address, prefix string) net.Address {
_, network, _ := gonet.ParseCIDR(address.IP().String() + "/" + prefix)
_, ipnet, _ := gonet.ParseCIDR(addr.IP().String() + "/" + prefix) maskSize, totalBits := network.Mask.Size()
subnetSize := big.NewInt(1).Lsh(big.NewInt(1), uint(totalBits-maskSize))
ones, bits := ipnet.Mask.Size() // random
subnetSize := new(big.Int).Lsh(big.NewInt(1), uint(bits-ones)) randomBigInt, _ := rand.Int(rand.Reader, subnetSize)
rnd, _ := rand.Int(rand.Reader, subnetSize) startIPBigInt := big.NewInt(0).SetBytes(network.IP.To16())
randomIPBigInt := big.NewInt(0).Add(startIPBigInt, randomBigInt)
startInt := new(big.Int).SetBytes(ipnet.IP) randomIPBytes := randomIPBigInt.Bytes()
rndInt := new(big.Int).Add(startInt, rnd) randomIPBytes = append(make([]byte, 16-len(randomIPBytes)), randomIPBytes...)
rndBytes := rndInt.Bytes() return net.ParseAddress(gonet.IP(randomIPBytes).String())
padded := make([]byte, len(ipnet.IP))
copy(padded[len(padded)-len(rndBytes):], rndBytes)
return net.ParseAddress(gonet.IP(padded).String())
} }

View File

@ -119,7 +119,7 @@ type MultiGeoIPMatcher struct {
func NewMultiGeoIPMatcher(geoips []*GeoIP, onSource bool) (*MultiGeoIPMatcher, error) { func NewMultiGeoIPMatcher(geoips []*GeoIP, onSource bool) (*MultiGeoIPMatcher, error) {
var matchers []*GeoIPMatcher var matchers []*GeoIPMatcher
for _, geoip := range geoips { for _, geoip := range geoips {
matcher, err := GlobalGeoIPContainer.Add(geoip) matcher, err := globalGeoIPContainer.Add(geoip)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -115,4 +115,4 @@ func (c *GeoIPMatcherContainer) Add(geoip *GeoIP) (*GeoIPMatcher, error) {
return m, nil return m, nil
} }
var GlobalGeoIPContainer GeoIPMatcherContainer var globalGeoIPContainer GeoIPMatcherContainer

View File

@ -177,7 +177,7 @@ func TestIPOnDemand(t *testing.T) {
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
}).Return([]net.IP{{192, 168, 0, 1}}, uint32(600), nil).AnyTimes() }).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
r := new(Router) r := new(Router)
common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil)) common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil))
@ -222,7 +222,7 @@ func TestIPIfNonMatchDomain(t *testing.T) {
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,
}).Return([]net.IP{{192, 168, 0, 1}}, uint32(600), nil).AnyTimes() }).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
r := new(Router) r := new(Router)
common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil)) common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil))

View File

@ -60,24 +60,6 @@ func (s *statsServer) GetStatsOnline(ctx context.Context, request *GetStatsReque
}, nil }, nil
} }
func (s *statsServer) GetStatsOnlineIpList(ctx context.Context, request *GetStatsRequest) (*GetStatsOnlineIpListResponse, error) {
c := s.stats.GetOnlineMap(request.Name)
if c == nil {
return nil, errors.New(request.Name, " not found.")
}
ips := make(map[string]int64)
for ip, t := range c.IpTimeMap() {
ips[ip] = t.Unix()
}
return &GetStatsOnlineIpListResponse{
Name: request.Name,
Ips: ips,
}, nil
}
func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) { func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) {
matcher, err := strmatcher.Substr.New(request.Pattern) matcher, err := strmatcher.Substr.New(request.Pattern)
if err != nil { if err != nil {

View File

@ -424,59 +424,6 @@ func (x *SysStatsResponse) GetUptime() uint32 {
return 0 return 0
} }
type GetStatsOnlineIpListResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Ips map[string]int64 `protobuf:"bytes,2,rep,name=ips,proto3" json:"ips,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
}
func (x *GetStatsOnlineIpListResponse) Reset() {
*x = GetStatsOnlineIpListResponse{}
mi := &file_app_stats_command_command_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetStatsOnlineIpListResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetStatsOnlineIpListResponse) ProtoMessage() {}
func (x *GetStatsOnlineIpListResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetStatsOnlineIpListResponse.ProtoReflect.Descriptor instead.
func (*GetStatsOnlineIpListResponse) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{7}
}
func (x *GetStatsOnlineIpListResponse) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *GetStatsOnlineIpListResponse) GetIps() map[string]int64 {
if x != nil {
return x.Ips
}
return nil
}
type Config struct { type Config struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -485,7 +432,7 @@ type Config struct {
func (x *Config) Reset() { func (x *Config) Reset() {
*x = Config{} *x = Config{}
mi := &file_app_stats_command_command_proto_msgTypes[8] mi := &file_app_stats_command_command_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -497,7 +444,7 @@ func (x *Config) String() string {
func (*Config) ProtoMessage() {} func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message { func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_app_stats_command_command_proto_msgTypes[8] mi := &file_app_stats_command_command_proto_msgTypes[7]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -510,7 +457,7 @@ func (x *Config) ProtoReflect() protoreflect.Message {
// Deprecated: Use Config.ProtoReflect.Descriptor instead. // Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) { func (*Config) Descriptor() ([]byte, []int) {
return file_app_stats_command_command_proto_rawDescGZIP(), []int{8} return file_app_stats_command_command_proto_rawDescGZIP(), []int{7}
} }
var File_app_stats_command_command_proto protoreflect.FileDescriptor var File_app_stats_command_command_proto protoreflect.FileDescriptor
@ -559,60 +506,40 @@ var file_app_stats_command_command_proto_rawDesc = []byte{
0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x50, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x50,
0x61, 0x75, 0x73, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x55, 0x61, 0x75, 0x73, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x55,
0x70, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x55, 0x70, 0x74, 0x70, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x55, 0x70, 0x74,
0x69, 0x6d, 0x65, 0x22, 0xbb, 0x01, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x69, 0x6d, 0x65, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0xa1, 0x03,
0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x49, 0x70, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x0a, 0x0c, 0x53, 0x74, 0x61, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61,
0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4f, 0x0a, 0x03, 0x69, 0x70, 0x73, 0x18,
0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47,
0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x49, 0x70, 0x4c,
0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x49, 0x70, 0x73, 0x45,
0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x69, 0x70, 0x73, 0x1a, 0x36, 0x0a, 0x08, 0x49, 0x70, 0x73,
0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
0x01, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0x9a, 0x04, 0x0a, 0x0c,
0x53, 0x74, 0x61, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x08,
0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61,
0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74,
0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a,
0x0e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x12,
0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73,
0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74,
0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a, 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61,
0x74, 0x73, 0x12, 0x29, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74,
0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75, 0x65, 0x72,
0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e,
0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63,
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x62, 0x0a, 0x0b, 0x47,
0x65, 0x74, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61,
0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
0x61, 0x6e, 0x64, 0x2e, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73,
0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74,
0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
0x77, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e,
0x65, 0x49, 0x70, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x65, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61,
0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74,
0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61,
0x1a, 0x34, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x49, 0x70, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a, 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x64, 0x0a, 0x1a, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x61, 0x74, 0x73, 0x12, 0x29, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e,
0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75,
0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x2a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73,
0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2f, 0x63, 0x6f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74,
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x16, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x62, 0x0a,
0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, 0x0b, 0x47, 0x65, 0x74, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f,
0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53,
0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x00, 0x42, 0x64, 0x0a, 0x1a, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50,
0x01, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74,
0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70,
0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02,
0x16, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e,
0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@ -627,38 +554,33 @@ func file_app_stats_command_command_proto_rawDescGZIP() []byte {
return file_app_stats_command_command_proto_rawDescData return file_app_stats_command_command_proto_rawDescData
} }
var file_app_stats_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_app_stats_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_app_stats_command_command_proto_goTypes = []any{ var file_app_stats_command_command_proto_goTypes = []any{
(*GetStatsRequest)(nil), // 0: xray.app.stats.command.GetStatsRequest (*GetStatsRequest)(nil), // 0: xray.app.stats.command.GetStatsRequest
(*Stat)(nil), // 1: xray.app.stats.command.Stat (*Stat)(nil), // 1: xray.app.stats.command.Stat
(*GetStatsResponse)(nil), // 2: xray.app.stats.command.GetStatsResponse (*GetStatsResponse)(nil), // 2: xray.app.stats.command.GetStatsResponse
(*QueryStatsRequest)(nil), // 3: xray.app.stats.command.QueryStatsRequest (*QueryStatsRequest)(nil), // 3: xray.app.stats.command.QueryStatsRequest
(*QueryStatsResponse)(nil), // 4: xray.app.stats.command.QueryStatsResponse (*QueryStatsResponse)(nil), // 4: xray.app.stats.command.QueryStatsResponse
(*SysStatsRequest)(nil), // 5: xray.app.stats.command.SysStatsRequest (*SysStatsRequest)(nil), // 5: xray.app.stats.command.SysStatsRequest
(*SysStatsResponse)(nil), // 6: xray.app.stats.command.SysStatsResponse (*SysStatsResponse)(nil), // 6: xray.app.stats.command.SysStatsResponse
(*GetStatsOnlineIpListResponse)(nil), // 7: xray.app.stats.command.GetStatsOnlineIpListResponse (*Config)(nil), // 7: xray.app.stats.command.Config
(*Config)(nil), // 8: xray.app.stats.command.Config
nil, // 9: xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry
} }
var file_app_stats_command_command_proto_depIdxs = []int32{ var file_app_stats_command_command_proto_depIdxs = []int32{
1, // 0: xray.app.stats.command.GetStatsResponse.stat:type_name -> xray.app.stats.command.Stat 1, // 0: xray.app.stats.command.GetStatsResponse.stat:type_name -> xray.app.stats.command.Stat
1, // 1: xray.app.stats.command.QueryStatsResponse.stat:type_name -> xray.app.stats.command.Stat 1, // 1: xray.app.stats.command.QueryStatsResponse.stat:type_name -> xray.app.stats.command.Stat
9, // 2: xray.app.stats.command.GetStatsOnlineIpListResponse.ips:type_name -> xray.app.stats.command.GetStatsOnlineIpListResponse.IpsEntry 0, // 2: xray.app.stats.command.StatsService.GetStats:input_type -> xray.app.stats.command.GetStatsRequest
0, // 3: xray.app.stats.command.StatsService.GetStats:input_type -> xray.app.stats.command.GetStatsRequest 0, // 3: xray.app.stats.command.StatsService.GetStatsOnline:input_type -> xray.app.stats.command.GetStatsRequest
0, // 4: xray.app.stats.command.StatsService.GetStatsOnline:input_type -> xray.app.stats.command.GetStatsRequest 3, // 4: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest
3, // 5: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest 5, // 5: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest
5, // 6: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest 2, // 6: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse
0, // 7: xray.app.stats.command.StatsService.GetStatsOnlineIpList:input_type -> xray.app.stats.command.GetStatsRequest 2, // 7: xray.app.stats.command.StatsService.GetStatsOnline:output_type -> xray.app.stats.command.GetStatsResponse
2, // 8: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse 4, // 8: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse
2, // 9: xray.app.stats.command.StatsService.GetStatsOnline:output_type -> xray.app.stats.command.GetStatsResponse 6, // 9: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse
4, // 10: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse 6, // [6:10] is the sub-list for method output_type
6, // 11: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse 2, // [2:6] is the sub-list for method input_type
7, // 12: xray.app.stats.command.StatsService.GetStatsOnlineIpList:output_type -> xray.app.stats.command.GetStatsOnlineIpListResponse 2, // [2:2] is the sub-list for extension type_name
8, // [8:13] is the sub-list for method output_type 2, // [2:2] is the sub-list for extension extendee
3, // [3:8] is the sub-list for method input_type 0, // [0:2] is the sub-list for field type_name
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
} }
func init() { file_app_stats_command_command_proto_init() } func init() { file_app_stats_command_command_proto_init() }
@ -672,7 +594,7 @@ func file_app_stats_command_command_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_stats_command_command_proto_rawDesc, RawDescriptor: file_app_stats_command_command_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 10, NumMessages: 8,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@ -46,17 +46,11 @@ message SysStatsResponse {
uint32 Uptime = 10; uint32 Uptime = 10;
} }
message GetStatsOnlineIpListResponse {
string name = 1;
map<string, int64> ips = 2;
}
service StatsService { service StatsService {
rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {} rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {}
rpc GetStatsOnline(GetStatsRequest) returns (GetStatsResponse) {} rpc GetStatsOnline(GetStatsRequest) returns (GetStatsResponse) {}
rpc QueryStats(QueryStatsRequest) returns (QueryStatsResponse) {} rpc QueryStats(QueryStatsRequest) returns (QueryStatsResponse) {}
rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {} rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {}
rpc GetStatsOnlineIpList(GetStatsRequest) returns (GetStatsOnlineIpListResponse) {}
} }
message Config {} message Config {}

View File

@ -19,11 +19,10 @@ import (
const _ = grpc.SupportPackageIsVersion9 const _ = grpc.SupportPackageIsVersion9
const ( const (
StatsService_GetStats_FullMethodName = "/xray.app.stats.command.StatsService/GetStats" StatsService_GetStats_FullMethodName = "/xray.app.stats.command.StatsService/GetStats"
StatsService_GetStatsOnline_FullMethodName = "/xray.app.stats.command.StatsService/GetStatsOnline" StatsService_GetStatsOnline_FullMethodName = "/xray.app.stats.command.StatsService/GetStatsOnline"
StatsService_QueryStats_FullMethodName = "/xray.app.stats.command.StatsService/QueryStats" StatsService_QueryStats_FullMethodName = "/xray.app.stats.command.StatsService/QueryStats"
StatsService_GetSysStats_FullMethodName = "/xray.app.stats.command.StatsService/GetSysStats" StatsService_GetSysStats_FullMethodName = "/xray.app.stats.command.StatsService/GetSysStats"
StatsService_GetStatsOnlineIpList_FullMethodName = "/xray.app.stats.command.StatsService/GetStatsOnlineIpList"
) )
// StatsServiceClient is the client API for StatsService service. // StatsServiceClient is the client API for StatsService service.
@ -34,7 +33,6 @@ type StatsServiceClient interface {
GetStatsOnline(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error) GetStatsOnline(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error)
QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error) QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error)
GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error) GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error)
GetStatsOnlineIpList(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsOnlineIpListResponse, error)
} }
type statsServiceClient struct { type statsServiceClient struct {
@ -85,16 +83,6 @@ func (c *statsServiceClient) GetSysStats(ctx context.Context, in *SysStatsReques
return out, nil return out, nil
} }
func (c *statsServiceClient) GetStatsOnlineIpList(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsOnlineIpListResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetStatsOnlineIpListResponse)
err := c.cc.Invoke(ctx, StatsService_GetStatsOnlineIpList_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// StatsServiceServer is the server API for StatsService service. // StatsServiceServer is the server API for StatsService service.
// All implementations must embed UnimplementedStatsServiceServer // All implementations must embed UnimplementedStatsServiceServer
// for forward compatibility. // for forward compatibility.
@ -103,7 +91,6 @@ type StatsServiceServer interface {
GetStatsOnline(context.Context, *GetStatsRequest) (*GetStatsResponse, error) GetStatsOnline(context.Context, *GetStatsRequest) (*GetStatsResponse, error)
QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error) QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error)
GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error)
GetStatsOnlineIpList(context.Context, *GetStatsRequest) (*GetStatsOnlineIpListResponse, error)
mustEmbedUnimplementedStatsServiceServer() mustEmbedUnimplementedStatsServiceServer()
} }
@ -126,9 +113,6 @@ func (UnimplementedStatsServiceServer) QueryStats(context.Context, *QueryStatsRe
func (UnimplementedStatsServiceServer) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) { func (UnimplementedStatsServiceServer) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetSysStats not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetSysStats not implemented")
} }
func (UnimplementedStatsServiceServer) GetStatsOnlineIpList(context.Context, *GetStatsRequest) (*GetStatsOnlineIpListResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetStatsOnlineIpList not implemented")
}
func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {} func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {}
func (UnimplementedStatsServiceServer) testEmbeddedByValue() {} func (UnimplementedStatsServiceServer) testEmbeddedByValue() {}
@ -222,24 +206,6 @@ func _StatsService_GetSysStats_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _StatsService_GetStatsOnlineIpList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StatsServiceServer).GetStatsOnlineIpList(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StatsService_GetStatsOnlineIpList_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StatsServiceServer).GetStatsOnlineIpList(ctx, req.(*GetStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
// StatsService_ServiceDesc is the grpc.ServiceDesc for StatsService service. // StatsService_ServiceDesc is the grpc.ServiceDesc for StatsService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@ -263,10 +229,6 @@ var StatsService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetSysStats", MethodName: "GetSysStats",
Handler: _StatsService_GetSysStats_Handler, Handler: _StatsService_GetSysStats_Handler,
}, },
{
MethodName: "GetStatsOnlineIpList",
Handler: _StatsService_GetStatsOnlineIpList_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "app/stats/command/command.proto", Metadata: "app/stats/command/command.proto",

View File

@ -40,11 +40,11 @@ func (c *OnlineMap) AddIP(ip string) {
if ip == "127.0.0.1" { if ip == "127.0.0.1" {
return return
} }
c.access.Lock()
if _, ok := list[ip]; !ok { if _, ok := list[ip]; !ok {
c.access.Lock()
list[ip] = time.Now() list[ip] = time.Now()
c.access.Unlock()
} }
c.access.Unlock()
if time.Since(c.lastCleanup) > c.cleanupPeriod { if time.Since(c.lastCleanup) > c.cleanupPeriod {
list = c.RemoveExpiredIPs(list) list = c.RemoveExpiredIPs(list)
c.lastCleanup = time.Now() c.lastCleanup = time.Now()
@ -78,13 +78,3 @@ func (c *OnlineMap) RemoveExpiredIPs(list map[string]time.Time) map[string]time.
} }
return list return list
} }
func (c *OnlineMap) IpTimeMap() map[string]time.Time {
list := c.ipList
if time.Since(c.lastCleanup) > c.cleanupPeriod {
list = c.RemoveExpiredIPs(list)
c.lastCleanup = time.Now()
}
return c.ipList
}

View File

@ -13,19 +13,8 @@ const (
Size = 8192 Size = 8192
) )
var zero = [Size * 10]byte{0}
var pool = bytespool.GetPool(Size) var pool = bytespool.GetPool(Size)
// ownership represents the data owner of the buffer.
type ownership uint8
const (
managed ownership = iota
unmanaged
bytespools
)
// Buffer is a recyclable allocation of a byte array. Buffer.Release() recycles // Buffer is a recyclable allocation of a byte array. Buffer.Release() recycles
// the buffer into an internal buffer pool, in order to recreate a buffer more // the buffer into an internal buffer pool, in order to recreate a buffer more
// quickly. // quickly.
@ -33,11 +22,11 @@ type Buffer struct {
v []byte v []byte
start int32 start int32
end int32 end int32
ownership ownership unmanaged bool
UDP *net.Destination UDP *net.Destination
} }
// New creates a Buffer with 0 length and 8K capacity, managed. // New creates a Buffer with 0 length and 8K capacity.
func New() *Buffer { func New() *Buffer {
buf := pool.Get().([]byte) buf := pool.Get().([]byte)
if cap(buf) >= Size { if cap(buf) >= Size {
@ -51,7 +40,7 @@ func New() *Buffer {
} }
} }
// NewExisted creates a standard size Buffer with an existed bytearray, managed. // NewExisted creates a managed, standard size Buffer with an existed bytearray
func NewExisted(b []byte) *Buffer { func NewExisted(b []byte) *Buffer {
if cap(b) < Size { if cap(b) < Size {
panic("Invalid buffer") panic("Invalid buffer")
@ -68,16 +57,16 @@ func NewExisted(b []byte) *Buffer {
} }
} }
// FromBytes creates a Buffer with an existed bytearray, unmanaged. // FromBytes creates a Buffer with an existed bytearray
func FromBytes(b []byte) *Buffer { func FromBytes(b []byte) *Buffer {
return &Buffer{ return &Buffer{
v: b, v: b,
end: int32(len(b)), end: int32(len(b)),
ownership: unmanaged, unmanaged: true,
} }
} }
// StackNew creates a new Buffer object on stack, managed. // StackNew creates a new Buffer object on stack.
// This method is for buffers that is released in the same function. // This method is for buffers that is released in the same function.
func StackNew() Buffer { func StackNew() Buffer {
buf := pool.Get().([]byte) buf := pool.Get().([]byte)
@ -92,17 +81,9 @@ func StackNew() Buffer {
} }
} }
// NewWithSize creates a Buffer with 0 length and capacity with at least the given size, bytespool's.
func NewWithSize(size int32) *Buffer {
return &Buffer{
v: bytespool.Alloc(size),
ownership: bytespools,
}
}
// Release recycles the buffer into an internal buffer pool. // Release recycles the buffer into an internal buffer pool.
func (b *Buffer) Release() { func (b *Buffer) Release() {
if b == nil || b.v == nil || b.ownership == unmanaged { if b == nil || b.v == nil || b.unmanaged {
return return
} }
@ -110,13 +91,8 @@ func (b *Buffer) Release() {
b.v = nil b.v = nil
b.Clear() b.Clear()
switch b.ownership { if cap(p) == Size {
case managed: pool.Put(p)
if cap(p) == Size {
pool.Put(p)
}
case bytespools:
bytespool.Free(p)
} }
b.UDP = nil b.UDP = nil
} }
@ -152,7 +128,6 @@ func (b *Buffer) Extend(n int32) []byte {
} }
ext := b.v[b.end:end] ext := b.v[b.end:end]
b.end = end b.end = end
copy(ext, zero[:])
return ext return ext
} }
@ -201,7 +176,6 @@ func (b *Buffer) Check() {
// Resize cuts the buffer at the given position. // Resize cuts the buffer at the given position.
func (b *Buffer) Resize(from, to int32) { func (b *Buffer) Resize(from, to int32) {
oldEnd := b.end
if from < 0 { if from < 0 {
from += b.Len() from += b.Len()
} }
@ -214,9 +188,6 @@ func (b *Buffer) Resize(from, to int32) {
b.end = b.start + to b.end = b.start + to
b.start += from b.start += from
b.Check() b.Check()
if b.end > oldEnd {
copy(b.v[oldEnd:b.end], zero[:])
}
} }
// Advance cuts the buffer at the given position. // Advance cuts the buffer at the given position.
@ -244,6 +215,13 @@ func (b *Buffer) Cap() int32 {
return int32(len(b.v)) return int32(len(b.v))
} }
// NewWithSize creates a Buffer with 0 length and capacity with at least the given size.
func NewWithSize(size int32) *Buffer {
return &Buffer{
v: bytespool.Alloc(size),
}
}
// IsEmpty returns true if the buffer is empty. // IsEmpty returns true if the buffer is empty.
func (b *Buffer) IsEmpty() bool { func (b *Buffer) IsEmpty() bool {
return b.Len() == 0 return b.Len() == 0

View File

@ -1,15 +1,2 @@
// Package crypto provides common crypto libraries for Xray. // Package crypto provides common crypto libraries for Xray.
package crypto // import "github.com/xtls/xray-core/common/crypto" package crypto // import "github.com/xtls/xray-core/common/crypto"
import (
"crypto/rand"
"math/big"
)
func RandBetween(from int64, to int64) int64 {
if from == to {
return from
}
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
return from + bigInt.Int64()
}

View File

@ -1,7 +1,6 @@
package errors package errors
import ( import (
"errors"
"strings" "strings"
) )
@ -37,12 +36,12 @@ func AllEqual(expected error, actual error) bool {
return false return false
} }
for _, err := range errs { for _, err := range errs {
if !errors.Is(err, expected) { if err != expected {
return false return false
} }
} }
return true return true
default: default:
return errors.Is(errs, expected) return errs == expected
} }
} }

View File

@ -146,7 +146,7 @@ func (w *fileLogWriter) Close() error {
func CreateStdoutLogWriter() WriterCreator { func CreateStdoutLogWriter() WriterCreator {
return func() Writer { return func() Writer {
return &consoleLogWriter{ return &consoleLogWriter{
logger: log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lmicroseconds), logger: log.New(os.Stdout, "", log.Ldate|log.Ltime),
} }
} }
} }
@ -155,7 +155,7 @@ func CreateStdoutLogWriter() WriterCreator {
func CreateStderrLogWriter() WriterCreator { func CreateStderrLogWriter() WriterCreator {
return func() Writer { return func() Writer {
return &consoleLogWriter{ return &consoleLogWriter{
logger: log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lmicroseconds), logger: log.New(os.Stderr, "", log.Ldate|log.Ltime),
} }
} }
} }
@ -174,7 +174,7 @@ func CreateFileLogWriter(path string) (WriterCreator, error) {
} }
return &fileLogWriter{ return &fileLogWriter{
file: file, file: file,
logger: log.New(file, "", log.Ldate|log.Ltime|log.Lmicroseconds), logger: log.New(file, "", log.Ldate|log.Ltime),
} }
}, nil }, nil
} }

View File

@ -120,7 +120,7 @@ func (w *ServerWorker) handleStatusKeepAlive(meta *FrameMetadata, reader *buf.Bu
func (w *ServerWorker) handleStatusNew(ctx context.Context, meta *FrameMetadata, reader *buf.BufferedReader) error { func (w *ServerWorker) handleStatusNew(ctx context.Context, meta *FrameMetadata, reader *buf.BufferedReader) error {
// deep-clone outbounds because it is going to be mutated concurrently // deep-clone outbounds because it is going to be mutated concurrently
// (Target and OriginalTarget) // (Target and OriginalTarget)
ctx = session.ContextCloneOutboundsAndContent(ctx) ctx = session.ContextCloneOutbounds(ctx)
errors.LogInfo(ctx, "received request for ", meta.Target) errors.LogInfo(ctx, "received request for ", meta.Target)
{ {
msg := &log.AccessMessage{ msg := &log.AccessMessage{

View File

@ -89,10 +89,12 @@ func UnixDestination(address Address) Destination {
// NetAddr returns the network address in this Destination in string form. // NetAddr returns the network address in this Destination in string form.
func (d Destination) NetAddr() string { func (d Destination) NetAddr() string {
addr := "" addr := ""
if d.Network == Network_TCP || d.Network == Network_UDP { if d.Address != nil {
addr = d.Address.String() + ":" + d.Port.String() if d.Network == Network_TCP || d.Network == Network_UDP {
} else if d.Network == Network_UNIX { addr = d.Address.String() + ":" + d.Port.String()
addr = d.Address.String() } else if d.Network == Network_UNIX {
addr = d.Address.String()
}
} }
return addr return addr
} }

View File

@ -1,14 +1,2 @@
// Package net is a drop-in replacement to Golang's net package, with some more functionalities. // Package net is a drop-in replacement to Golang's net package, with some more functionalities.
package net // import "github.com/xtls/xray-core/common/net" package net // import "github.com/xtls/xray-core/common/net"
import "time"
// defines the maximum time an idle TCP session can survive in the tunnel, so
// it should be consistent across HTTP versions and with other transports.
const ConnIdleTimeout = 300 * time.Second
// consistent with quic-go
const QuicgoH3KeepAlivePeriod = 10 * time.Second
// consistent with chrome
const ChromeH2KeepAlivePeriod = 45 * time.Second

View File

@ -76,9 +76,8 @@ type (
) )
var ( var (
ResolveTCPAddr = net.ResolveTCPAddr
ResolveUDPAddr = net.ResolveUDPAddr
ResolveUnixAddr = net.ResolveUnixAddr ResolveUnixAddr = net.ResolveUnixAddr
ResolveUDPAddr = net.ResolveUDPAddr
) )
type Resolver = net.Resolver type Resolver = net.Resolver

View File

@ -3,7 +3,6 @@ package filesystem
import ( import (
"io" "io"
"os" "os"
"path/filepath"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/common/platform"
@ -29,13 +28,6 @@ func ReadAsset(file string) ([]byte, error) {
return ReadFile(platform.GetAssetLocation(file)) return ReadFile(platform.GetAssetLocation(file))
} }
func ReadCert(file string) ([]byte, error) {
if filepath.IsAbs(file) {
return ReadFile(file)
}
return ReadFile(platform.GetCertLocation(file))
}
func CopyFile(dst string, src string) error { func CopyFile(dst string, src string) error {
bytes, err := ReadFile(src) bytes, err := ReadFile(src)
if err != nil { if err != nil {

View File

@ -21,7 +21,7 @@ func GetToolLocation(file string) string {
return filepath.Join(toolPath, file) return filepath.Join(toolPath, file)
} }
// GetAssetLocation searches for `file` in the env dir, the executable dir, and certain locations // GetAssetLocation searches for `file` in certain locations
func GetAssetLocation(file string) string { func GetAssetLocation(file string) string {
assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir)
defPath := filepath.Join(assetPath, file) defPath := filepath.Join(assetPath, file)
@ -42,9 +42,3 @@ func GetAssetLocation(file string) string {
// asset not found, let the caller throw out the error // asset not found, let the caller throw out the error
return defPath return defPath
} }
// GetCertLocation searches for `file` in the env dir and the executable dir
func GetCertLocation(file string) string {
certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir)
return filepath.Join(certPath, file)
}

View File

@ -13,7 +13,6 @@ const (
ConfdirLocation = "xray.location.confdir" ConfdirLocation = "xray.location.confdir"
ToolLocation = "xray.location.tool" ToolLocation = "xray.location.tool"
AssetLocation = "xray.location.asset" AssetLocation = "xray.location.asset"
CertLocation = "xray.location.cert"
UseReadV = "xray.buf.readv" UseReadV = "xray.buf.readv"
UseFreedomSplice = "xray.buf.splice" UseFreedomSplice = "xray.buf.splice"

View File

@ -19,14 +19,8 @@ func GetToolLocation(file string) string {
return filepath.Join(toolPath, file+".exe") return filepath.Join(toolPath, file+".exe")
} }
// GetAssetLocation searches for `file` in the env dir and the executable dir // GetAssetLocation searches for `file` in the executable dir
func GetAssetLocation(file string) string { func GetAssetLocation(file string) string {
assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir) assetPath := NewEnvFlag(AssetLocation).GetValue(getExecutableDir)
return filepath.Join(assetPath, file) return filepath.Join(assetPath, file)
} }
// GetCertLocation searches for `file` in the env dir and the executable dir
func GetCertLocation(file string) string {
certPath := NewEnvFlag(CertLocation).GetValue(getExecutableDir)
return filepath.Join(certPath, file)
}

View File

@ -63,7 +63,7 @@ func SniffHTTP(b []byte, c context.Context) (*SniffHeader, error) {
ShouldSniffAttr := true ShouldSniffAttr := true
// If content.Attributes have information, that means it comes from HTTP inbound PlainHTTP mode. // If content.Attributes have information, that means it comes from HTTP inbound PlainHTTP mode.
// It will set attributes, so skip it. // It will set attributes, so skip it.
if content == nil || len(content.Attributes) != 0 { if content == nil || content.AttributeLen() != 0 {
ShouldSniffAttr = false ShouldSniffAttr = false
} }
if err := beginWithHTTPMethod(b); err != nil { if err := beginWithHTTPMethod(b); err != nil {

View File

@ -1,7 +1 @@
package protocol // import "github.com/xtls/xray-core/common/protocol" package protocol // import "github.com/xtls/xray-core/common/protocol"
import (
"errors"
)
var ErrProtoNeedMoreData = errors.New("protocol matches, but need more data to complete sniffing")

View File

@ -1,17 +1,18 @@
package quic package quic
import ( import (
"context"
"crypto" "crypto"
"crypto/aes" "crypto/aes"
"crypto/tls" "crypto/tls"
"encoding/binary" "encoding/binary"
"io" "io"
"github.com/quic-go/quic-go/quicvarint" "github.com/xtls/quic-go/quicvarint"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/bytespool"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
ptls "github.com/xtls/xray-core/common/protocol/tls" ptls "github.com/xtls/xray-core/common/protocol/tls"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
) )
@ -46,17 +47,22 @@ var (
errNotQuicInitial = errors.New("not initial packet") errNotQuicInitial = errors.New("not initial packet")
) )
func SniffQUIC(b []byte) (*SniffHeader, error) { func SniffQUIC(b []byte) (resultReturn *SniffHeader, errorReturn error) {
if len(b) == 0 { // In extremely rare cases, this sniffer may cause slice error
return nil, common.ErrNoClue // and we set recover() here to prevent crash.
} // TODO: Thoroughly fix this panic
defer func() {
if r := recover(); r != nil {
errors.LogError(context.Background(), "Failed to sniff QUIC: ", r)
resultReturn = nil
errorReturn = common.ErrNoClue
}
}()
// Crypto data separated across packets // Crypto data separated across packets
cryptoLen := int32(0) cryptoLen := 0
cryptoDataBuf := buf.NewWithSize(32767) cryptoData := bytespool.Alloc(int32(len(b)))
defer cryptoDataBuf.Release() defer bytespool.Free(cryptoData)
cache := buf.New()
defer cache.Release()
// Parse QUIC packets // Parse QUIC packets
for len(b) > 0 { for len(b) > 0 {
@ -99,25 +105,19 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, errNotQuic return nil, errNotQuic
} }
if isQuicInitial { // Only initial packets have token, see https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.2 tokenLen, err := quicvarint.Read(buffer)
tokenLen, err := quicvarint.Read(buffer) if err != nil || tokenLen > uint64(len(b)) {
if err != nil || tokenLen > uint64(len(b)) { return nil, errNotQuic
return nil, errNotQuic }
}
if _, err = buffer.ReadBytes(int32(tokenLen)); err != nil { if _, err = buffer.ReadBytes(int32(tokenLen)); err != nil {
return nil, errNotQuic return nil, errNotQuic
}
} }
packetLen, err := quicvarint.Read(buffer) packetLen, err := quicvarint.Read(buffer)
if err != nil { if err != nil {
return nil, errNotQuic return nil, errNotQuic
} }
// packetLen is impossible to be shorter than this
if packetLen < 4 {
return nil, errNotQuic
}
hdrLen := len(b) - int(buffer.Len()) hdrLen := len(b) - int(buffer.Len())
if len(b) < hdrLen+int(packetLen) { if len(b) < hdrLen+int(packetLen) {
@ -130,6 +130,9 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
continue continue
} }
origPNBytes := make([]byte, 4)
copy(origPNBytes, b[hdrLen:hdrLen+4])
var salt []byte var salt []byte
if versionNumber == version1 { if versionNumber == version1 {
salt = quicSalt salt = quicSalt
@ -144,34 +147,44 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
return nil, err return nil, err
} }
cache.Clear() cache := buf.New()
defer cache.Release()
mask := cache.Extend(int32(block.BlockSize())) mask := cache.Extend(int32(block.BlockSize()))
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+len(mask)]) block.Encrypt(mask, b[hdrLen+4:hdrLen+4+16])
b[0] ^= mask[0] & 0xf b[0] ^= mask[0] & 0xf
packetNumberLength := int(b[0]&0x3 + 1) for i := range b[hdrLen : hdrLen+4] {
for i := range packetNumberLength {
b[hdrLen+i] ^= mask[i+1] b[hdrLen+i] ^= mask[i+1]
} }
packetNumberLength := b[0]&0x3 + 1
if packetNumberLength != 1 {
return nil, errNotQuicInitial
}
var packetNumber uint32
{
n, err := buffer.ReadByte()
if err != nil {
return nil, err
}
packetNumber = uint32(n)
}
extHdrLen := hdrLen + int(packetNumberLength)
copy(b[extHdrLen:hdrLen+4], origPNBytes[packetNumberLength:])
data := b[extHdrLen : int(packetLen)+hdrLen]
key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16) key := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic key", 16)
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12) iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
cipher := AEADAESGCMTLS13(key, iv) cipher := AEADAESGCMTLS13(key, iv)
nonce := cache.Extend(int32(cipher.NonceSize())) nonce := cache.Extend(int32(cipher.NonceSize()))
_, err = buffer.Read(nonce[len(nonce)-packetNumberLength:]) binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(packetNumber))
if err != nil {
return nil, err
}
extHdrLen := hdrLen + packetNumberLength
data := b[extHdrLen : int(packetLen)+hdrLen]
decrypted, err := cipher.Open(b[extHdrLen:extHdrLen], nonce, data, b[:extHdrLen]) decrypted, err := cipher.Open(b[extHdrLen:extHdrLen], nonce, data, b[:extHdrLen])
if err != nil { if err != nil {
return nil, err return nil, err
} }
buffer = buf.FromBytes(decrypted) buffer = buf.FromBytes(decrypted)
for !buffer.IsEmpty() { for i := 0; !buffer.IsEmpty(); i++ {
frameType, _ := buffer.ReadByte() frameType := byte(0x0) // Default to PADDING frame
for frameType == 0x0 && !buffer.IsEmpty() { for frameType == 0x0 && !buffer.IsEmpty() {
frameType, _ = buffer.ReadByte() frameType, _ = buffer.ReadByte()
} }
@ -220,15 +233,16 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
if err != nil || length > uint64(buffer.Len()) { if err != nil || length > uint64(buffer.Len()) {
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
currentCryptoLen := int32(offset + length) if cryptoLen < int(offset+length) {
if cryptoLen < currentCryptoLen { cryptoLen = int(offset + length)
if cryptoDataBuf.Cap() < currentCryptoLen { if len(cryptoData) < cryptoLen {
return nil, io.ErrShortBuffer newCryptoData := bytespool.Alloc(int32(cryptoLen))
copy(newCryptoData, cryptoData)
bytespool.Free(cryptoData)
cryptoData = newCryptoData
} }
cryptoDataBuf.Extend(currentCryptoLen - cryptoLen)
cryptoLen = currentCryptoLen
} }
if _, err := buffer.Read(cryptoDataBuf.BytesRange(int32(offset), currentCryptoLen)); err != nil { // Field: Crypto Data if _, err := buffer.Read(cryptoData[offset : offset+length]); err != nil { // Field: Crypto Data
return nil, io.ErrUnexpectedEOF return nil, io.ErrUnexpectedEOF
} }
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
@ -253,7 +267,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
} }
tlsHdr := &ptls.SniffHeader{} tlsHdr := &ptls.SniffHeader{}
err = ptls.ReadClientHello(cryptoDataBuf.BytesRange(0, cryptoLen), tlsHdr) err = ptls.ReadClientHello(cryptoData[:cryptoLen], tlsHdr)
if err != nil { if err != nil {
// The crypto data may have not been fully recovered in current packets, // The crypto data may have not been fully recovered in current packets,
// So we continue to sniff rest packets. // So we continue to sniff rest packets.
@ -262,8 +276,7 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
} }
return &SniffHeader{domain: tlsHdr.Domain()}, nil return &SniffHeader{domain: tlsHdr.Domain()}, nil
} }
// All payload is parsed as valid QUIC packets, but we need more packets for crypto data to read client hello. return nil, common.ErrNoClue
return nil, protocol.ErrProtoNeedMoreData
} }
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte { func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {

File diff suppressed because one or more lines are too long

1
common/protocol/tls/cert/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.pem

View File

@ -78,9 +78,9 @@ func printJSON(certificate *Certificate) {
func printFile(certificate *Certificate, name string) error { func printFile(certificate *Certificate, name string) error {
certPEM, keyPEM := certificate.ToPEM() certPEM, keyPEM := certificate.ToPEM()
return task.Run(context.Background(), func() error { return task.Run(context.Background(), func() error {
return writeFile(certPEM, name+".crt") return writeFile(certPEM, name+"_cert.pem")
}, func() error { }, func() error {
return writeFile(keyPEM, name+".key") return writeFile(keyPEM, name+"_key.pem")
}) })
} }

View File

@ -3,9 +3,9 @@ package tls
import ( import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"strings"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/protocol"
) )
type SniffHeader struct { type SniffHeader struct {
@ -59,6 +59,9 @@ func ReadClientHello(data []byte, h *SniffHeader) error {
} }
data = data[1+compressionMethodsLen:] data = data[1+compressionMethodsLen:]
if len(data) == 0 {
return errNotClientHello
}
if len(data) < 2 { if len(data) < 2 {
return errNotClientHello return errNotClientHello
} }
@ -101,21 +104,13 @@ func ReadClientHello(data []byte, h *SniffHeader) error {
return errNotClientHello return errNotClientHello
} }
if nameType == 0 { if nameType == 0 {
// QUIC separated across packets serverName := string(d[:nameLen])
// May cause the serverName to be incomplete
b := byte(0)
for _, b = range d[:nameLen] {
if b <= ' ' {
return protocol.ErrProtoNeedMoreData
}
}
// An SNI value may not include a // An SNI value may not include a
// trailing dot. See // trailing dot. See
// https://tools.ietf.org/html/rfc6066#section-3. // https://tools.ietf.org/html/rfc6066#section-3.
if b == '.' { if strings.HasSuffix(serverName, ".") {
return errNotClientHello return errNotClientHello
} }
serverName := string(d[:nameLen])
h.domain = serverName h.domain = serverName
return nil return nil
} }

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"slices"
"strings" "strings"
cnet "github.com/xtls/xray-core/common/net" cnet "github.com/xtls/xray-core/common/net"
@ -31,9 +32,6 @@ func JSONMarshalWithoutEscape(t interface{}) ([]byte, error) {
} }
func marshalTypedMessage(v *cserial.TypedMessage, ignoreNullValue bool, insertTypeInfo bool) interface{} { func marshalTypedMessage(v *cserial.TypedMessage, ignoreNullValue bool, insertTypeInfo bool) interface{} {
if v == nil {
return nil
}
tmsg, err := v.GetInstance() tmsg, err := v.GetInstance()
if err != nil { if err != nil {
return nil return nil
@ -58,9 +56,7 @@ func marshalSlice(v reflect.Value, ignoreNullValue bool, insertTypeInfo bool) in
} }
func isNullValue(f reflect.StructField, rv reflect.Value) bool { func isNullValue(f reflect.StructField, rv reflect.Value) bool {
if rv.Kind() == reflect.Struct { if rv.Kind() == reflect.String && rv.Len() == 0 {
return false
} else if rv.Kind() == reflect.String && rv.Len() == 0 {
return true return true
} else if !isValueKind(rv.Kind()) && rv.IsNil() { } else if !isValueKind(rv.Kind()) && rv.IsNil() {
return true return true
@ -186,12 +182,6 @@ func marshalKnownType(v interface{}, ignoreNullValue bool, insertTypeInfo bool)
case *conf.PortList: case *conf.PortList:
cpl := v.(*conf.PortList) cpl := v.(*conf.PortList)
return serializePortList(cpl.Build()) return serializePortList(cpl.Build())
case conf.Int32Range:
i32rng := v.(conf.Int32Range)
if i32rng.Left == i32rng.Right {
return i32rng.Left, true
}
return i32rng.String(), true
case cnet.Address: case cnet.Address:
if addr := v.(cnet.Address); addr != nil { if addr := v.(cnet.Address); addr != nil {
return addr.String(), true return addr.String(), true
@ -202,29 +192,28 @@ func marshalKnownType(v interface{}, ignoreNullValue bool, insertTypeInfo bool)
} }
} }
var valueKinds = []reflect.Kind{
reflect.Bool,
reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64,
reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64,
reflect.Uintptr,
reflect.Float32,
reflect.Float64,
reflect.Complex64,
reflect.Complex128,
reflect.String,
}
func isValueKind(kind reflect.Kind) bool { func isValueKind(kind reflect.Kind) bool {
switch kind { return slices.Contains(valueKinds, kind)
case reflect.Bool,
reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64,
reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64,
reflect.Uintptr,
reflect.Float32,
reflect.Float64,
reflect.Complex64,
reflect.Complex128,
reflect.String:
return true
default:
return false
}
} }
func marshalInterface(v interface{}, ignoreNullValue bool, insertTypeInfo bool) interface{} { func marshalInterface(v interface{}, ignoreNullValue bool, insertTypeInfo bool) interface{} {

View File

@ -116,129 +116,98 @@ func TestMarshalConfigJson(t *testing.T) {
"system", "system",
"inboundDownlink", "inboundDownlink",
"outboundUplink", "outboundUplink",
"XHTTP_IN",
"\"host\": \"bing.com\"",
"scMaxEachPostBytes",
"\"from\": 100",
"\"to\": 1000",
"\"from\": 1000000",
"\"to\": 1000000",
} }
for _, kw := range keywords { for _, kw := range keywords {
if !strings.Contains(tc, kw) { if !strings.Contains(tc, kw) {
t.Log("config.json:", tc) t.Error("marshaled config error")
t.Error("keyword not found:", kw)
break
} }
} }
} }
func getConfig() string { func getConfig() string {
return `{ return `{
"log": { "log": {
"loglevel": "debug" "loglevel": "debug"
}, },
"stats": {}, "stats": {},
"policy": { "policy": {
"levels": { "levels": {
"0": { "0": {
"statsUserUplink": true, "statsUserUplink": true,
"statsUserDownlink": true "statsUserDownlink": true
} }
}, },
"system": { "system": {
"statsInboundUplink": true, "statsInboundUplink": true,
"statsInboundDownlink": true, "statsInboundDownlink": true,
"statsOutboundUplink": true, "statsOutboundUplink": true,
"statsOutboundDownlink": true "statsOutboundDownlink": true
} }
}, },
"inbounds": [ "inbounds": [
{ {
"tag": "agentin", "tag": "agentin",
"protocol": "http", "protocol": "http",
"port": 18080, "port": 8080,
"listen": "127.0.0.1", "listen": "127.0.0.1",
"settings": {} "settings": {}
}, },
{ {
"listen": "127.0.0.1", "listen": "127.0.0.1",
"port": 10085, "port": 10085,
"protocol": "dokodemo-door", "protocol": "dokodemo-door",
"settings": { "settings": {
"address": "127.0.0.1" "address": "127.0.0.1"
}, },
"tag": "api-in" "tag": "api-in"
} }
], ],
"api": { "api": {
"tag": "api", "tag": "api",
"services": [ "services": [
"HandlerService", "HandlerService",
"StatsService" "StatsService"
] ]
}, },
"routing": { "routing": {
"rules": [ "rules": [
{ {
"inboundTag": [ "inboundTag": [
"api-in" "api-in"
], ],
"outboundTag": "api", "outboundTag": "api",
"type": "field" "type": "field"
} }
], ],
"domainStrategy": "AsIs" "domainStrategy": "AsIs"
}, },
"outbounds": [ "outbounds": [
{ {
"protocol": "vless", "protocol": "vless",
"settings": { "settings": {
"vnext": [ "vnext": [
{ {
"address": "1.2.3.4", "address": "1.2.3.4",
"port": 1234, "port": 1234,
"users": [ "users": [
{ {
"id": "4784f9b8-a879-4fec-9718-ebddefa47750", "id": "4784f9b8-a879-4fec-9718-ebddefa47750",
"encryption": "none" "encryption": "none"
} }
] ]
} }
] ]
}, },
"tag": "XHTTP_IN", "tag": "agentout",
"streamSettings": { "streamSettings": {
"network": "xhttp", "network": "ws",
"xhttpSettings": { "security": "none",
"host": "bing.com", "wsSettings": {
"path": "/xhttp_client_upload", "path": "/?ed=2048",
"mode": "auto", "host": "bing.com"
"extra": { }
"noSSEHeader": false, }
"scMaxEachPostBytes": 1000000, }
"scMaxBufferedPosts": 30, ]
"xPaddingBytes": "100-1000" }`
}
},
"sockopt": {
"tcpFastOpen": true,
"acceptProxyProtocol": false,
"tcpcongestion": "bbr",
"tcpMptcp": true
}
},
"sniffing": {
"enabled": true,
"destOverride": [
"http",
"tls",
"quic"
],
"metadataOnly": false,
"routeOnly": true
}
}
]
}`
} }

View File

@ -23,8 +23,6 @@ const (
timeoutOnlyKey ctx.SessionKey = 8 timeoutOnlyKey ctx.SessionKey = 8
allowedNetworkKey ctx.SessionKey = 9 allowedNetworkKey ctx.SessionKey = 9
handlerSessionKey ctx.SessionKey = 10 handlerSessionKey ctx.SessionKey = 10
mitmAlpn11Key ctx.SessionKey = 11
mitmServerNameKey ctx.SessionKey = 12
) )
func ContextWithInbound(ctx context.Context, inbound *Inbound) context.Context { func ContextWithInbound(ctx context.Context, inbound *Inbound) context.Context {
@ -42,7 +40,7 @@ func ContextWithOutbounds(ctx context.Context, outbounds []*Outbound) context.Co
return context.WithValue(ctx, outboundSessionKey, outbounds) return context.WithValue(ctx, outboundSessionKey, outbounds)
} }
func ContextCloneOutboundsAndContent(ctx context.Context) context.Context { func ContextCloneOutbounds(ctx context.Context) context.Context {
outbounds := OutboundsFromContext(ctx) outbounds := OutboundsFromContext(ctx)
newOutbounds := make([]*Outbound, len(outbounds)) newOutbounds := make([]*Outbound, len(outbounds))
for i, ob := range outbounds { for i, ob := range outbounds {
@ -55,15 +53,7 @@ func ContextCloneOutboundsAndContent(ctx context.Context) context.Context {
newOutbounds[i] = &v newOutbounds[i] = &v
} }
content := ContentFromContext(ctx) return ContextWithOutbounds(ctx, newOutbounds)
newContent := Content{}
if content != nil {
newContent = *content
if content.Attributes != nil {
panic("content.Attributes != nil")
}
}
return ContextWithContent(ContextWithOutbounds(ctx, newOutbounds), &newContent)
} }
func OutboundsFromContext(ctx context.Context) []*Outbound { func OutboundsFromContext(ctx context.Context) []*Outbound {
@ -172,25 +162,3 @@ func AllowedNetworkFromContext(ctx context.Context) net.Network {
} }
return net.Network_Unknown return net.Network_Unknown
} }
func ContextWithMitmAlpn11(ctx context.Context, alpn11 bool) context.Context {
return context.WithValue(ctx, mitmAlpn11Key, alpn11)
}
func MitmAlpn11FromContext(ctx context.Context) bool {
if val, ok := ctx.Value(mitmAlpn11Key).(bool); ok {
return val
}
return false
}
func ContextWithMitmServerName(ctx context.Context, serverName string) context.Context {
return context.WithValue(ctx, mitmServerNameKey, serverName)
}
func MitmServerNameFromContext(ctx context.Context) string {
if val, ok := ctx.Value(mitmServerNameKey).(string); ok {
return val
}
return ""
}

View File

@ -4,6 +4,7 @@ package session // import "github.com/xtls/xray-core/common/session"
import ( import (
"context" "context"
"math/rand" "math/rand"
"sync"
c "github.com/xtls/xray-core/common/ctx" c "github.com/xtls/xray-core/common/ctx"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@ -74,8 +75,8 @@ type Outbound struct {
// SniffingRequest controls the behavior of content sniffing. // SniffingRequest controls the behavior of content sniffing.
type SniffingRequest struct { type SniffingRequest struct {
ExcludeForDomain []string // read-only once set ExcludeForDomain []string
OverrideDestinationForProtocol []string // read-only once set OverrideDestinationForProtocol []string
Enabled bool Enabled bool
MetadataOnly bool MetadataOnly bool
RouteOnly bool RouteOnly bool
@ -91,6 +92,10 @@ type Content struct {
Attributes map[string]string Attributes map[string]string
SkipDNSResolve bool SkipDNSResolve bool
mu sync.Mutex
isLocked bool
} }
// Sockopt is the settings for socket connection. // Sockopt is the settings for socket connection.
@ -99,8 +104,22 @@ type Sockopt struct {
Mark int32 Mark int32
} }
// Some how when using mux, there will be a same ctx between different requests
// This will cause problem as it's designed for single request, like concurrent map writes
// Add a Mutex as a temp solution
// SetAttribute attaches additional string attributes to content. // SetAttribute attaches additional string attributes to content.
func (c *Content) SetAttribute(name string, value string) { func (c *Content) SetAttribute(name string, value string) {
if c.isLocked {
errors.LogError(context.Background(), "Multiple goroutines are tring to access one routing content, tring to write ", name, ":", value)
}
c.mu.Lock()
c.isLocked = true
defer func() {
c.isLocked = false
c.mu.Unlock()
}()
if c.Attributes == nil { if c.Attributes == nil {
c.Attributes = make(map[string]string) c.Attributes = make(map[string]string)
} }
@ -109,8 +128,24 @@ func (c *Content) SetAttribute(name string, value string) {
// Attribute retrieves additional string attributes from content. // Attribute retrieves additional string attributes from content.
func (c *Content) Attribute(name string) string { func (c *Content) Attribute(name string) string {
c.mu.Lock()
c.isLocked = true
defer func() {
c.isLocked = false
c.mu.Unlock()
}()
if c.Attributes == nil { if c.Attributes == nil {
return "" return ""
} }
return c.Attributes[name] return c.Attributes[name]
} }
func (c *Content) AttributeLen() int {
c.mu.Lock()
c.isLocked = true
defer func() {
c.isLocked = false
c.mu.Unlock()
}()
return len(c.Attributes)
}

View File

@ -18,8 +18,8 @@ import (
var ( var (
Version_x byte = 25 Version_x byte = 25
Version_y byte = 5 Version_y byte = 1
Version_z byte = 16 Version_z byte = 1
) )
var ( var (

View File

@ -21,7 +21,11 @@ type Client interface {
features.Feature features.Feature
// LookupIP returns IP address for the given domain. IPs may contain IPv4 and/or IPv6 addresses. // LookupIP returns IP address for the given domain. IPs may contain IPv4 and/or IPv6 addresses.
LookupIP(domain string, option IPOption) ([]net.IP, uint32, error) LookupIP(domain string, option IPOption) ([]net.IP, error)
}
type HostsLookup interface {
LookupHosts(domain string) *net.Address
} }
// ClientType returns the type of Client interface. Can be used for implementing common.HasType. // ClientType returns the type of Client interface. Can be used for implementing common.HasType.
@ -34,8 +38,6 @@ func ClientType() interface{} {
// ErrEmptyResponse indicates that DNS query succeeded but no answer was returned. // ErrEmptyResponse indicates that DNS query succeeded but no answer was returned.
var ErrEmptyResponse = errors.New("empty response") var ErrEmptyResponse = errors.New("empty response")
const DefaultTTL = 300
type RCodeError uint16 type RCodeError uint16
func (e RCodeError) Error() string { func (e RCodeError) Error() string {

View File

@ -20,44 +20,41 @@ func (*Client) Start() error { return nil }
func (*Client) Close() error { return nil } func (*Client) Close() error { return nil }
// LookupIP implements Client. // LookupIP implements Client.
func (*Client) LookupIP(host string, option dns.IPOption) ([]net.IP, uint32, error) { func (*Client) LookupIP(host string, option dns.IPOption) ([]net.IP, error) {
ips, err := net.LookupIP(host) ips, err := net.LookupIP(host)
if err != nil { if err != nil {
return nil, 0, err return nil, err
} }
parsedIPs := make([]net.IP, 0, len(ips)) parsedIPs := make([]net.IP, 0, len(ips))
ipv4 := make([]net.IP, 0, len(ips)) ipv4 := make([]net.IP, 0, len(ips))
ipv6 := make([]net.IP, 0, len(ips)) ipv6 := make([]net.IP, 0, len(ips))
for _, ip := range ips { for _, ip := range ips {
parsed := net.IPAddress(ip) parsed := net.IPAddress(ip)
if parsed == nil { if parsed != nil {
continue parsedIPs = append(parsedIPs, parsed.IP())
} }
parsedIP := parsed.IP() if len(ip) == net.IPv4len {
parsedIPs = append(parsedIPs, parsedIP) ipv4 = append(ipv4, ip)
}
if len(parsedIP) == net.IPv4len { if len(ip) == net.IPv6len {
ipv4 = append(ipv4, parsedIP) ipv6 = append(ipv6, ip)
} else {
ipv6 = append(ipv6, parsedIP)
} }
} }
switch { switch {
case option.IPv4Enable && option.IPv6Enable: case option.IPv4Enable && option.IPv6Enable:
if len(parsedIPs) > 0 { if len(parsedIPs) > 0 {
return parsedIPs, dns.DefaultTTL, nil return parsedIPs, nil
} }
case option.IPv4Enable: case option.IPv4Enable:
if len(ipv4) > 0 { if len(ipv4) > 0 {
return ipv4, dns.DefaultTTL, nil return ipv4, nil
} }
case option.IPv6Enable: case option.IPv6Enable:
if len(ipv6) > 0 { if len(ipv6) > 0 {
return ipv6, dns.DefaultTTL, nil return ipv6, nil
} }
} }
return nil, 0, dns.ErrEmptyResponse return nil, dns.ErrEmptyResponse
} }
// New create a new dns.Client that queries localhost for DNS. // New create a new dns.Client that queries localhost for DNS.

View File

@ -25,7 +25,7 @@ type Handler interface {
// xray:api:stable // xray:api:stable
type Manager interface { type Manager interface {
features.Feature features.Feature
// GetHandler returns an InboundHandler for the given tag. // GetHandlers returns an InboundHandler for the given tag.
GetHandler(ctx context.Context, tag string) (Handler, error) GetHandler(ctx context.Context, tag string) (Handler, error)
// AddHandler adds the given handler into this Manager. // AddHandler adds the given handler into this Manager.
AddHandler(ctx context.Context, handler Handler) error AddHandler(ctx context.Context, handler Handler) error

View File

@ -11,7 +11,7 @@ type Context interface {
// GetInboundTag returns the tag of the inbound the connection was from. // GetInboundTag returns the tag of the inbound the connection was from.
GetInboundTag() string GetInboundTag() string
// GetSourceIPs returns the source IPs bound to the connection. // GetSourcesIPs returns the source IPs bound to the connection.
GetSourceIPs() []net.IP GetSourceIPs() []net.IP
// GetSourcePort returns the source port of the connection. // GetSourcePort returns the source port of the connection.

View File

@ -23,7 +23,7 @@ func (ctx *ResolvableContext) GetTargetIPs() []net.IP {
} }
if domain := ctx.GetTargetDomain(); len(domain) != 0 { if domain := ctx.GetTargetDomain(); len(domain) != 0 {
ips, _, err := ctx.dnsClient.LookupIP(domain, dns.IPOption{ ips, err := ctx.dnsClient.LookupIP(domain, dns.IPOption{
IPv4Enable: true, IPv4Enable: true,
IPv6Enable: true, IPv6Enable: true,
FakeEnable: false, FakeEnable: false,

View File

@ -2,7 +2,6 @@ package stats
import ( import (
"context" "context"
"time"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@ -31,8 +30,6 @@ type OnlineMap interface {
AddIP(string) AddIP(string)
// List is the current OnlineMap ip list. // List is the current OnlineMap ip list.
List() []string List() []string
// IpTimeMap return client ips and their last access time.
IpTimeMap() map[string]time.Time
} }
// Channel is the interface for stats channel. // Channel is the interface for stats channel.

49
go.mod
View File

@ -1,37 +1,37 @@
module github.com/xtls/xray-core module github.com/xtls/xray-core
go 1.24 go 1.23
require ( require (
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0
github.com/cloudflare/circl v1.6.1 github.com/cloudflare/circl v1.5.0
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1 github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/miekg/dns v1.1.66 github.com/miekg/dns v1.1.62
github.com/pelletier/go-toml v1.9.5 github.com/pelletier/go-toml v1.9.5
github.com/pires/go-proxyproto v0.8.1 github.com/pires/go-proxyproto v0.8.0
github.com/quic-go/quic-go v0.51.0 github.com/refraction-networking/utls v1.6.7
github.com/refraction-networking/utls v1.7.3
github.com/sagernet/sing v0.5.1 github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks v0.2.7
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e
github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netlink v1.3.0
github.com/xtls/reality v0.0.0-20250516070713-4df2ec9a5b47 github.com/xtls/quic-go v0.48.2
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.38.0 golang.org/x/crypto v0.31.0
golang.org/x/net v0.40.0 golang.org/x/net v0.33.0
golang.org/x/sync v0.14.0 golang.org/x/sync v0.10.0
golang.org/x/sys v0.33.0 golang.org/x/sys v0.28.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
google.golang.org/grpc v1.72.1 google.golang.org/grpc v1.69.2
google.golang.org/protobuf v1.36.6 google.golang.org/protobuf v1.36.1
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489
h12.io/socks v1.0.3 h12.io/socks v1.0.3
lukechampine.com/blake3 v1.4.1 lukechampine.com/blake3 v1.3.0
) )
require ( require (
@ -47,14 +47,15 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.4 // indirect
go.uber.org/mock v0.5.0 // indirect go.uber.org/mock v0.4.0 // indirect
golang.org/x/mod v0.24.0 // indirect golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
golang.org/x/text v0.25.0 // indirect golang.org/x/mod v0.18.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.32.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.22.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

116
go.sum
View File

@ -2,8 +2,8 @@ github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJS
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM= github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -24,8 +24,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g= github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g=
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -38,8 +38,8 @@ github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0N
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
@ -48,16 +48,14 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/refraction-networking/utls v1.7.3 h1:L0WRhHY7Oq1T0zkdzVZMR6zWZv+sXbHB9zcuvsAEqCo=
github.com/refraction-networking/utls v1.7.3/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
@ -72,45 +70,47 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtls/reality v0.0.0-20250516070713-4df2ec9a5b47 h1:9aJWkgWBwZ83l3j7+hBh3SurvRKuNfCgsSner5n6BcM= github.com/xtls/quic-go v0.48.2 h1:59Gs+E9qtc9s0uniXYDA649gNEZlMWcNpFLyp9jfkuE=
github.com/xtls/reality v0.0.0-20250516070713-4df2ec9a5b47/go.mod h1:bJdU3ExzfUlY40Xxfibq3THW9IHiE8mHu/tEzud5JWM= github.com/xtls/quic-go v0.48.2/go.mod h1:rcyY5J0JT+1d5pa5Y+FbCsXM7Zu79jE87ZSFOBfiH7Q=
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d h1:+B97uD9uHLgAAulhigmys4BVwZZypzK7gPN3WtpgRJg=
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -119,21 +119,21 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -141,12 +141,12 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -156,9 +156,9 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h1:sfK5nHuG7lRFZ2FdTT3RimOqWBg8IrVm+/Vko1FVOsk= gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo= h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck= h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=

View File

@ -8,13 +8,11 @@ import (
type Duration int64 type Duration int64
// MarshalJSON implements encoding/json.Marshaler.MarshalJSON
func (d *Duration) MarshalJSON() ([]byte, error) { func (d *Duration) MarshalJSON() ([]byte, error) {
dr := time.Duration(*d) dr := time.Duration(*d)
return json.Marshal(dr.String()) return json.Marshal(dr.String())
} }
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (d *Duration) UnmarshalJSON(b []byte) error { func (d *Duration) UnmarshalJSON(b []byte) error {
var v interface{} var v interface{}
if err := json.Unmarshal(b, &v); err != nil { if err := json.Unmarshal(b, &v); err != nil {

View File

@ -23,7 +23,6 @@ func (v StringList) Len() int {
return len(v) return len(v)
} }
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (v *StringList) UnmarshalJSON(data []byte) error { func (v *StringList) UnmarshalJSON(data []byte) error {
var strarray []string var strarray []string
if err := json.Unmarshal(data, &strarray); err == nil { if err := json.Unmarshal(data, &strarray); err == nil {
@ -44,12 +43,10 @@ type Address struct {
net.Address net.Address
} }
// MarshalJSON implements encoding/json.Marshaler.MarshalJSON func (v Address) MarshalJSON() ([]byte, error) {
func (v *Address) MarshalJSON() ([]byte, error) {
return json.Marshal(v.Address.String()) return json.Marshal(v.Address.String())
} }
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (v *Address) UnmarshalJSON(data []byte) error { func (v *Address) UnmarshalJSON(data []byte) error {
var rawStr string var rawStr string
if err := json.Unmarshal(data, &rawStr); err != nil { if err := json.Unmarshal(data, &rawStr); err != nil {
@ -84,7 +81,6 @@ func (v Network) Build() net.Network {
type NetworkList []Network type NetworkList []Network
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (v *NetworkList) UnmarshalJSON(data []byte) error { func (v *NetworkList) UnmarshalJSON(data []byte) error {
var strarray []Network var strarray []Network
if err := json.Unmarshal(data, &strarray); err == nil { if err := json.Unmarshal(data, &strarray); err == nil {
@ -173,19 +169,6 @@ func (v *PortRange) Build() *net.PortRange {
} }
} }
// MarshalJSON implements encoding/json.Marshaler.MarshalJSON
func (v *PortRange) MarshalJSON() ([]byte, error) {
return json.Marshal(v.String())
}
func (port *PortRange) String() string {
if port.From == port.To {
return strconv.Itoa(int(port.From))
} else {
return fmt.Sprintf("%d-%d", port.From, port.To)
}
}
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON // UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (v *PortRange) UnmarshalJSON(data []byte) error { func (v *PortRange) UnmarshalJSON(data []byte) error {
port, err := parseIntPort(data) port, err := parseIntPort(data)
@ -220,25 +203,6 @@ func (list *PortList) Build() *net.PortList {
return portList return portList
} }
// MarshalJSON implements encoding/json.Marshaler.MarshalJSON
func (v *PortList) MarshalJSON() ([]byte, error) {
portStr := v.String()
port, err := strconv.Atoi(portStr)
if err == nil {
return json.Marshal(port)
} else {
return json.Marshal(portStr)
}
}
func (v PortList) String() string {
ports := []string{}
for _, port := range v.Range {
ports = append(ports, port.String())
}
return strings.Join(ports, ",")
}
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON // UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (list *PortList) UnmarshalJSON(data []byte) error { func (list *PortList) UnmarshalJSON(data []byte) error {
var listStr string var listStr string
@ -295,8 +259,7 @@ type Int32Range struct {
To int32 To int32
} }
// MarshalJSON implements encoding/json.Marshaler.MarshalJSON func (v Int32Range) MarshalJSON() ([]byte, error) {
func (v *Int32Range) MarshalJSON() ([]byte, error) {
return json.Marshal(v.String()) return json.Marshal(v.String())
} }
@ -308,7 +271,6 @@ func (v Int32Range) String() string {
} }
} }
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (v *Int32Range) UnmarshalJSON(data []byte) error { func (v *Int32Range) UnmarshalJSON(data []byte) error {
defer v.ensureOrder() defer v.ensureOrder()
var str string var str string

View File

@ -12,20 +12,15 @@ import (
) )
type NameServerConfig struct { type NameServerConfig struct {
Address *Address `json:"address"` Address *Address
ClientIP *Address `json:"clientIp"` ClientIP *Address
Port uint16 `json:"port"` Port uint16
SkipFallback bool `json:"skipFallback"` SkipFallback bool
Domains []string `json:"domains"` Domains []string
ExpectedIPs StringList `json:"expectedIPs"` ExpectIPs StringList
ExpectIPs StringList `json:"expectIPs"` QueryStrategy string
QueryStrategy string `json:"queryStrategy"`
AllowUnexpectedIPs bool `json:"allowUnexpectedIps"`
Tag string `json:"tag"`
TimeoutMs uint64 `json:"timeoutMs"`
} }
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (c *NameServerConfig) UnmarshalJSON(data []byte) error { func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
var address Address var address Address
if err := json.Unmarshal(data, &address); err == nil { if err := json.Unmarshal(data, &address); err == nil {
@ -34,17 +29,13 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
} }
var advanced struct { var advanced struct {
Address *Address `json:"address"` Address *Address `json:"address"`
ClientIP *Address `json:"clientIp"` ClientIP *Address `json:"clientIp"`
Port uint16 `json:"port"` Port uint16 `json:"port"`
SkipFallback bool `json:"skipFallback"` SkipFallback bool `json:"skipFallback"`
Domains []string `json:"domains"` Domains []string `json:"domains"`
ExpectedIPs StringList `json:"expectedIPs"` ExpectIPs StringList `json:"expectIps"`
ExpectIPs StringList `json:"expectIPs"` QueryStrategy string `json:"queryStrategy"`
QueryStrategy string `json:"queryStrategy"`
AllowUnexpectedIPs bool `json:"allowUnexpectedIps"`
Tag string `json:"tag"`
TimeoutMs uint64 `json:"timeoutMs"`
} }
if err := json.Unmarshal(data, &advanced); err == nil { if err := json.Unmarshal(data, &advanced); err == nil {
c.Address = advanced.Address c.Address = advanced.Address
@ -52,12 +43,8 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
c.Port = advanced.Port c.Port = advanced.Port
c.SkipFallback = advanced.SkipFallback c.SkipFallback = advanced.SkipFallback
c.Domains = advanced.Domains c.Domains = advanced.Domains
c.ExpectedIPs = advanced.ExpectedIPs
c.ExpectIPs = advanced.ExpectIPs c.ExpectIPs = advanced.ExpectIPs
c.QueryStrategy = advanced.QueryStrategy c.QueryStrategy = advanced.QueryStrategy
c.AllowUnexpectedIPs = advanced.AllowUnexpectedIPs
c.Tag = advanced.Tag
c.TimeoutMs = advanced.TimeoutMs
return nil return nil
} }
@ -105,13 +92,9 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) {
}) })
} }
var expectedIPs = c.ExpectedIPs geoipList, err := ToCidrList(c.ExpectIPs)
if len(expectedIPs) == 0 {
expectedIPs = c.ExpectIPs
}
geoipList, err := ToCidrList(expectedIPs)
if err != nil { if err != nil {
return nil, errors.New("invalid IP rule: ", expectedIPs).Base(err) return nil, errors.New("invalid IP rule: ", c.ExpectIPs).Base(err)
} }
var myClientIP []byte var myClientIP []byte
@ -128,15 +111,12 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) {
Address: c.Address.Build(), Address: c.Address.Build(),
Port: uint32(c.Port), Port: uint32(c.Port),
}, },
ClientIp: myClientIP, ClientIp: myClientIP,
SkipFallback: c.SkipFallback, SkipFallback: c.SkipFallback,
PrioritizedDomain: domains, PrioritizedDomain: domains,
Geoip: geoipList, Geoip: geoipList,
OriginalRules: originalRules, OriginalRules: originalRules,
QueryStrategy: resolveQueryStrategy(c.QueryStrategy), QueryStrategy: resolveQueryStrategy(c.QueryStrategy),
AllowUnexpectedIPs: c.AllowUnexpectedIPs,
Tag: c.Tag,
TimeoutMs: c.TimeoutMs,
}, nil }, nil
} }
@ -164,18 +144,6 @@ type HostAddress struct {
addrs []*Address addrs []*Address
} }
// MarshalJSON implements encoding/json.Marshaler.MarshalJSON
func (h *HostAddress) MarshalJSON() ([]byte, error) {
if (h.addr != nil) != (h.addrs != nil) {
if h.addr != nil {
return json.Marshal(h.addr)
} else if h.addrs != nil {
return json.Marshal(h.addrs)
}
}
return nil, errors.New("unexpected config state")
}
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON // UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (h *HostAddress) UnmarshalJSON(data []byte) error { func (h *HostAddress) UnmarshalJSON(data []byte) error {
addr := new(Address) addr := new(Address)
@ -221,11 +189,6 @@ func getHostMapping(ha *HostAddress) *dns.Config_HostMapping {
} }
} }
// MarshalJSON implements encoding/json.Marshaler.MarshalJSON
func (m *HostsWrapper) MarshalJSON() ([]byte, error) {
return json.Marshal(m.Hosts)
}
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON // UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (m *HostsWrapper) UnmarshalJSON(data []byte) error { func (m *HostsWrapper) UnmarshalJSON(data []byte) error {
hosts := make(map[string]*HostAddress) hosts := make(map[string]*HostAddress)

View File

@ -20,18 +20,6 @@ type FakeDNSConfig struct {
pools []*FakeDNSPoolElementConfig pools []*FakeDNSPoolElementConfig
} }
// MarshalJSON implements encoding/json.Marshaler.MarshalJSON
func (f *FakeDNSConfig) MarshalJSON() ([]byte, error) {
if (f.pool != nil) != (f.pools != nil) {
if f.pool != nil {
return json.Marshal(f.pool)
} else if f.pools != nil {
return json.Marshal(f.pools)
}
}
return nil, errors.New("unexpected config state")
}
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON // UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (f *FakeDNSConfig) UnmarshalJSON(data []byte) error { func (f *FakeDNSConfig) UnmarshalJSON(data []byte) error {
var pool FakeDNSPoolElementConfig var pool FakeDNSPoolElementConfig

View File

@ -2,7 +2,6 @@ package conf
import ( import (
"encoding/base64" "encoding/base64"
"encoding/hex"
"net" "net"
"strings" "strings"
@ -153,9 +152,8 @@ func (c *FreedomConfig) Build() (proto.Message, error) {
func ParseNoise(noise *Noise) (*freedom.Noise, error) { func ParseNoise(noise *Noise) (*freedom.Noise, error) {
var err error var err error
NConfig := new(freedom.Noise) NConfig := new(freedom.Noise)
noise.Packet = strings.TrimSpace(noise.Packet)
switch noise.Type { switch strings.ToLower(noise.Type) {
case "rand": case "rand":
min, max, err := ParseRangeString(noise.Packet) min, max, err := ParseRangeString(noise.Packet)
if err != nil { if err != nil {
@ -168,25 +166,18 @@ func ParseNoise(noise *Noise) (*freedom.Noise, error) {
} }
case "str": case "str":
// user input string //user input string
NConfig.Packet = []byte(noise.Packet) NConfig.StrNoise = []byte(strings.TrimSpace(noise.Packet))
case "hex":
// user input hex
NConfig.Packet, err = hex.DecodeString(noise.Packet)
if err != nil {
return nil, errors.New("Invalid hex string").Base(err)
}
case "base64": case "base64":
// user input base64 //user input base64
NConfig.Packet, err = base64.RawURLEncoding.DecodeString(strings.NewReplacer("+", "-", "/", "_", "=", "").Replace(noise.Packet)) NConfig.StrNoise, err = base64.StdEncoding.DecodeString(strings.TrimSpace(noise.Packet))
if err != nil { if err != nil {
return nil, errors.New("Invalid base64 string").Base(err) return nil, errors.New("Invalid base64 string")
} }
default: default:
return nil, errors.New("Invalid packet, only rand/str/hex/base64 are supported") return nil, errors.New("Invalid packet, only rand/str/base64 are supported")
} }
if noise.Delay != nil { if noise.Delay != nil {

View File

@ -6,21 +6,15 @@ import (
) )
type MetricsConfig struct { type MetricsConfig struct {
Tag string `json:"tag"` Tag string `json:"tag"`
Listen string `json:"listen"`
} }
func (c *MetricsConfig) Build() (*metrics.Config, error) { func (c *MetricsConfig) Build() (*metrics.Config, error) {
if c.Listen == "" && c.Tag == "" {
return nil, errors.New("Metrics must have a tag or listen address.")
}
// If the tag is empty but have "listen" set a default "Metrics" for compatibility.
if c.Tag == "" { if c.Tag == "" {
c.Tag = "Metrics" return nil, errors.New("metrics tag can't be empty.")
} }
return &metrics.Config{ return &metrics.Config{
Tag: c.Tag, Tag: c.Tag,
Listen: c.Listen,
}, nil }, nil
} }

View File

@ -231,7 +231,6 @@ type SplitHTTPConfig struct {
ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"` ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"`
ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"` ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"`
ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"` ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"`
ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"`
Xmux XmuxConfig `json:"xmux"` Xmux XmuxConfig `json:"xmux"`
DownloadSettings *StreamConfig `json:"downloadSettings"` DownloadSettings *StreamConfig `json:"downloadSettings"`
Extra json.RawMessage `json:"extra"` Extra json.RawMessage `json:"extra"`
@ -281,10 +280,6 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
} }
} }
if c.XPaddingBytes != (Int32Range{}) && (c.XPaddingBytes.From <= 0 || c.XPaddingBytes.To <= 0) {
return nil, errors.New("xPaddingBytes cannot be disabled")
}
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 { if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency") return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
} }
@ -308,7 +303,6 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes), ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes),
ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs), ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs),
ScMaxBufferedPosts: c.ScMaxBufferedPosts, ScMaxBufferedPosts: c.ScMaxBufferedPosts,
ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs),
Xmux: &splithttp.XmuxConfig{ Xmux: &splithttp.XmuxConfig{
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency), MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
MaxConnections: newRangeConfig(c.Xmux.MaxConnections), MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
@ -334,7 +328,7 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
func readFileOrString(f string, s []string) ([]byte, error) { func readFileOrString(f string, s []string) ([]byte, error) {
if len(f) > 0 { if len(f) > 0 {
return filesystem.ReadCert(f) return filesystem.ReadFile(f)
} }
if len(s) > 0 { if len(s) > 0 {
return []byte(strings.Join(s, "\n")), nil return []byte(strings.Join(s, "\n")), nil
@ -410,8 +404,6 @@ type TLSConfig struct {
PinnedPeerCertificatePublicKeySha256 *[]string `json:"pinnedPeerCertificatePublicKeySha256"` PinnedPeerCertificatePublicKeySha256 *[]string `json:"pinnedPeerCertificatePublicKeySha256"`
CurvePreferences *StringList `json:"curvePreferences"` CurvePreferences *StringList `json:"curvePreferences"`
MasterKeyLog string `json:"masterKeyLog"` MasterKeyLog string `json:"masterKeyLog"`
ServerNameToVerify string `json:"serverNameToVerify"`
VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"`
} }
// Build implements Buildable. // Build implements Buildable.
@ -433,13 +425,6 @@ func (c *TLSConfig) Build() (proto.Message, error) {
if c.ALPN != nil && len(*c.ALPN) > 0 { if c.ALPN != nil && len(*c.ALPN) > 0 {
config.NextProtocol = []string(*c.ALPN) config.NextProtocol = []string(*c.ALPN)
} }
if len(config.NextProtocol) > 1 {
for _, p := range config.NextProtocol {
if tcp.IsFromMitm(p) {
return nil, errors.New(`only one element is allowed in "alpn" when using "fromMitm" in it`)
}
}
}
if c.CurvePreferences != nil && len(*c.CurvePreferences) > 0 { if c.CurvePreferences != nil && len(*c.CurvePreferences) > 0 {
config.CurvePreferences = []string(*c.CurvePreferences) config.CurvePreferences = []string(*c.CurvePreferences)
} }
@ -450,7 +435,7 @@ func (c *TLSConfig) Build() (proto.Message, error) {
config.CipherSuites = c.CipherSuites config.CipherSuites = c.CipherSuites
config.Fingerprint = strings.ToLower(c.Fingerprint) config.Fingerprint = strings.ToLower(c.Fingerprint)
if config.Fingerprint != "unsafe" && tls.GetFingerprint(config.Fingerprint) == nil { if config.Fingerprint != "unsafe" && tls.GetFingerprint(config.Fingerprint) == nil {
return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint) return nil, errors.New(`unknown fingerprint: `, config.Fingerprint)
} }
config.RejectUnknownSni = c.RejectUnknownSNI config.RejectUnknownSni = c.RejectUnknownSNI
@ -478,11 +463,6 @@ func (c *TLSConfig) Build() (proto.Message, error) {
config.MasterKeyLog = c.MasterKeyLog config.MasterKeyLog = c.MasterKeyLog
if c.ServerNameToVerify != "" {
return nil, errors.PrintRemovedFeatureError(`"serverNameToVerify"`, `"verifyPeerCertInNames"`)
}
config.VerifyPeerCertInNames = c.VerifyPeerCertInNames
return config, nil return config, nil
} }
@ -502,7 +482,6 @@ type REALITYConfig struct {
Fingerprint string `json:"fingerprint"` Fingerprint string `json:"fingerprint"`
ServerName string `json:"serverName"` ServerName string `json:"serverName"`
Password string `json:"password"`
PublicKey string `json:"publicKey"` PublicKey string `json:"publicKey"`
ShortId string `json:"shortId"` ShortId string `json:"shortId"`
SpiderX string `json:"spiderX"` SpiderX string `json:"spiderX"`
@ -611,14 +590,11 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
if len(c.ServerNames) != 0 { if len(c.ServerNames) != 0 {
return nil, errors.New(`non-empty "serverNames", please use "serverName" instead`) return nil, errors.New(`non-empty "serverNames", please use "serverName" instead`)
} }
if c.Password != "" {
c.PublicKey = c.Password
}
if c.PublicKey == "" { if c.PublicKey == "" {
return nil, errors.New(`empty "password"`) return nil, errors.New(`empty "publicKey"`)
} }
if config.PublicKey, err = base64.RawURLEncoding.DecodeString(c.PublicKey); err != nil || len(config.PublicKey) != 32 { if config.PublicKey, err = base64.RawURLEncoding.DecodeString(c.PublicKey); err != nil || len(config.PublicKey) != 32 {
return nil, errors.New(`invalid "password": `, c.PublicKey) return nil, errors.New(`invalid "publicKey": `, c.PublicKey)
} }
if len(c.ShortIds) != 0 { if len(c.ShortIds) != 0 {
return nil, errors.New(`non-empty "shortIds", please use "shortId" instead`) return nil, errors.New(`non-empty "shortIds", please use "shortId" instead`)
@ -691,12 +667,10 @@ func (p TransportProtocol) Build() (string, error) {
} }
type CustomSockoptConfig struct { type CustomSockoptConfig struct {
Syetem string `json:"system"` Level string `json:"level"`
Network string `json:"network"` Opt string `json:"opt"`
Level string `json:"level"` Value string `json:"value"`
Opt string `json:"opt"` Type string `json:"type"`
Value string `json:"value"`
Type string `json:"type"`
} }
type SocketConfig struct { type SocketConfig struct {
@ -717,7 +691,6 @@ type SocketConfig struct {
Interface string `json:"interface"` Interface string `json:"interface"`
TcpMptcp bool `json:"tcpMptcp"` TcpMptcp bool `json:"tcpMptcp"`
CustomSockopt []*CustomSockoptConfig `json:"customSockopt"` CustomSockopt []*CustomSockoptConfig `json:"customSockopt"`
AddressPortStrategy string `json:"addressPortStrategy"`
} }
// Build implements Buildable. // Build implements Buildable.
@ -779,36 +752,14 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
for _, copt := range c.CustomSockopt { for _, copt := range c.CustomSockopt {
customSockopt := &internet.CustomSockopt{ customSockopt := &internet.CustomSockopt{
System: copt.Syetem, Level: copt.Level,
Network: copt.Network, Opt: copt.Opt,
Level: copt.Level, Value: copt.Value,
Opt: copt.Opt, Type: copt.Type,
Value: copt.Value,
Type: copt.Type,
} }
customSockopts = append(customSockopts, customSockopt) customSockopts = append(customSockopts, customSockopt)
} }
addressPortStrategy := internet.AddressPortStrategy_None
switch strings.ToLower(c.AddressPortStrategy) {
case "none", "":
addressPortStrategy = internet.AddressPortStrategy_None
case "srvportonly":
addressPortStrategy = internet.AddressPortStrategy_SrvPortOnly
case "srvaddressonly":
addressPortStrategy = internet.AddressPortStrategy_SrvAddressOnly
case "srvportandaddress":
addressPortStrategy = internet.AddressPortStrategy_SrvPortAndAddress
case "txtportonly":
addressPortStrategy = internet.AddressPortStrategy_TxtPortOnly
case "txtaddressonly":
addressPortStrategy = internet.AddressPortStrategy_TxtAddressOnly
case "txtportandaddress":
addressPortStrategy = internet.AddressPortStrategy_TxtPortAndAddress
default:
return nil, errors.New("unsupported address and port strategy: ", c.AddressPortStrategy)
}
return &internet.SocketConfig{ return &internet.SocketConfig{
Mark: c.Mark, Mark: c.Mark,
Tfo: tfo, Tfo: tfo,
@ -827,7 +778,6 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
Interface: c.Interface, Interface: c.Interface,
TcpMptcp: c.TcpMptcp, TcpMptcp: c.TcpMptcp,
CustomSockopt: customSockopts, CustomSockopt: customSockopts,
AddressPortStrategy: addressPortStrategy,
}, nil }, nil
} }

View File

@ -67,7 +67,7 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
var err error var err error
config.SecretKey, err = ParseWireGuardKey(c.SecretKey) config.SecretKey, err = ParseWireGuardKey(c.SecretKey)
if err != nil { if err != nil {
return nil, errors.New("invalid WireGuard secret key: %w", err) return nil, err
} }
if c.Address == nil { if c.Address == nil {
@ -126,10 +126,6 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
func ParseWireGuardKey(str string) (string, error) { func ParseWireGuardKey(str string) (string, error) {
var err error var err error
if str == "" {
return "", errors.New("key must not be empty")
}
if len(str)%2 == 0 { if len(str)%2 == 0 {
_, err = hex.DecodeString(str) _, err = hex.DecodeString(str)
if err == nil { if err == nil {

View File

@ -241,14 +241,14 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
} }
rawConfig, err := inboundConfigLoader.LoadWithID(settings, c.Protocol) rawConfig, err := inboundConfigLoader.LoadWithID(settings, c.Protocol)
if err != nil { if err != nil {
return nil, errors.New("failed to load inbound detour config for protocol ", c.Protocol).Base(err) return nil, errors.New("failed to load inbound detour config.").Base(err)
} }
if dokodemoConfig, ok := rawConfig.(*DokodemoConfig); ok { if dokodemoConfig, ok := rawConfig.(*DokodemoConfig); ok {
receiverSettings.ReceiveOriginalDestination = dokodemoConfig.Redirect receiverSettings.ReceiveOriginalDestination = dokodemoConfig.Redirect
} }
ts, err := rawConfig.(Buildable).Build() ts, err := rawConfig.(Buildable).Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build inbound handler for protocol ", c.Protocol).Base(err) return nil, err
} }
return &core.InboundHandlerConfig{ return &core.InboundHandlerConfig{
@ -292,9 +292,7 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
senderSettings.ViaCidr = strings.Split(*c.SendThrough, "/")[1] senderSettings.ViaCidr = strings.Split(*c.SendThrough, "/")[1]
} else { } else {
if address.Family().IsDomain() { if address.Family().IsDomain() {
if address.Address.Domain() != "origin" { return nil, errors.New("unable to send through: " + address.String())
return nil, errors.New("unable to send through: " + address.String())
}
} }
} }
senderSettings.Via = address.Build() senderSettings.Via = address.Build()
@ -303,7 +301,7 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if c.StreamSetting != nil { if c.StreamSetting != nil {
ss, err := c.StreamSetting.Build() ss, err := c.StreamSetting.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build stream settings for outbound detour").Base(err) return nil, err
} }
senderSettings.StreamSettings = ss senderSettings.StreamSettings = ss
} }
@ -311,7 +309,7 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if c.ProxySettings != nil { if c.ProxySettings != nil {
ps, err := c.ProxySettings.Build() ps, err := c.ProxySettings.Build()
if err != nil { if err != nil {
return nil, errors.New("invalid outbound detour proxy settings").Base(err) return nil, errors.New("invalid outbound detour proxy settings.").Base(err)
} }
if ps.TransportLayerProxy { if ps.TransportLayerProxy {
if senderSettings.StreamSettings != nil { if senderSettings.StreamSettings != nil {
@ -331,7 +329,7 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
if c.MuxSettings != nil { if c.MuxSettings != nil {
ms, err := c.MuxSettings.Build() ms, err := c.MuxSettings.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build Mux config").Base(err) return nil, errors.New("failed to build Mux config.").Base(err)
} }
senderSettings.MultiplexSettings = ms senderSettings.MultiplexSettings = ms
} }
@ -342,11 +340,11 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
} }
rawConfig, err := outboundConfigLoader.LoadWithID(settings, c.Protocol) rawConfig, err := outboundConfigLoader.LoadWithID(settings, c.Protocol)
if err != nil { if err != nil {
return nil, errors.New("failed to load outbound detour config for protocol ", c.Protocol).Base(err) return nil, errors.New("failed to parse to outbound detour config.").Base(err)
} }
ts, err := rawConfig.(Buildable).Build() ts, err := rawConfig.(Buildable).Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err) return nil, err
} }
return &core.OutboundHandlerConfig{ return &core.OutboundHandlerConfig{
@ -490,7 +488,7 @@ func (c *Config) Override(o *Config, fn string) {
// Build implements Buildable. // Build implements Buildable.
func (c *Config) Build() (*core.Config, error) { func (c *Config) Build() (*core.Config, error) {
if err := PostProcessConfigureFile(c); err != nil { if err := PostProcessConfigureFile(c); err != nil {
return nil, errors.New("failed to post-process configuration file").Base(err) return nil, err
} }
config := &core.Config{ config := &core.Config{
@ -504,21 +502,21 @@ func (c *Config) Build() (*core.Config, error) {
if c.API != nil { if c.API != nil {
apiConf, err := c.API.Build() apiConf, err := c.API.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build API configuration").Base(err) return nil, err
} }
config.App = append(config.App, serial.ToTypedMessage(apiConf)) config.App = append(config.App, serial.ToTypedMessage(apiConf))
} }
if c.Metrics != nil { if c.Metrics != nil {
metricsConf, err := c.Metrics.Build() metricsConf, err := c.Metrics.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build metrics configuration").Base(err) return nil, err
} }
config.App = append(config.App, serial.ToTypedMessage(metricsConf)) config.App = append(config.App, serial.ToTypedMessage(metricsConf))
} }
if c.Stats != nil { if c.Stats != nil {
statsConf, err := c.Stats.Build() statsConf, err := c.Stats.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build stats configuration").Base(err) return nil, err
} }
config.App = append(config.App, serial.ToTypedMessage(statsConf)) config.App = append(config.App, serial.ToTypedMessage(statsConf))
} }
@ -536,7 +534,7 @@ func (c *Config) Build() (*core.Config, error) {
if c.RouterConfig != nil { if c.RouterConfig != nil {
routerConfig, err := c.RouterConfig.Build() routerConfig, err := c.RouterConfig.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build routing configuration").Base(err) return nil, err
} }
config.App = append(config.App, serial.ToTypedMessage(routerConfig)) config.App = append(config.App, serial.ToTypedMessage(routerConfig))
} }
@ -544,7 +542,7 @@ func (c *Config) Build() (*core.Config, error) {
if c.DNSConfig != nil { if c.DNSConfig != nil {
dnsApp, err := c.DNSConfig.Build() dnsApp, err := c.DNSConfig.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build DNS configuration").Base(err) return nil, errors.New("failed to parse DNS config").Base(err)
} }
config.App = append(config.App, serial.ToTypedMessage(dnsApp)) config.App = append(config.App, serial.ToTypedMessage(dnsApp))
} }
@ -552,7 +550,7 @@ func (c *Config) Build() (*core.Config, error) {
if c.Policy != nil { if c.Policy != nil {
pc, err := c.Policy.Build() pc, err := c.Policy.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build policy configuration").Base(err) return nil, err
} }
config.App = append(config.App, serial.ToTypedMessage(pc)) config.App = append(config.App, serial.ToTypedMessage(pc))
} }
@ -560,7 +558,7 @@ func (c *Config) Build() (*core.Config, error) {
if c.Reverse != nil { if c.Reverse != nil {
r, err := c.Reverse.Build() r, err := c.Reverse.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build reverse configuration").Base(err) return nil, err
} }
config.App = append(config.App, serial.ToTypedMessage(r)) config.App = append(config.App, serial.ToTypedMessage(r))
} }
@ -568,7 +566,7 @@ func (c *Config) Build() (*core.Config, error) {
if c.FakeDNS != nil { if c.FakeDNS != nil {
r, err := c.FakeDNS.Build() r, err := c.FakeDNS.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build fake DNS configuration").Base(err) return nil, err
} }
config.App = append([]*serial.TypedMessage{serial.ToTypedMessage(r)}, config.App...) config.App = append([]*serial.TypedMessage{serial.ToTypedMessage(r)}, config.App...)
} }
@ -576,7 +574,7 @@ func (c *Config) Build() (*core.Config, error) {
if c.Observatory != nil { if c.Observatory != nil {
r, err := c.Observatory.Build() r, err := c.Observatory.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build observatory configuration").Base(err) return nil, err
} }
config.App = append(config.App, serial.ToTypedMessage(r)) config.App = append(config.App, serial.ToTypedMessage(r))
} }
@ -584,7 +582,7 @@ func (c *Config) Build() (*core.Config, error) {
if c.BurstObservatory != nil { if c.BurstObservatory != nil {
r, err := c.BurstObservatory.Build() r, err := c.BurstObservatory.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build burst observatory configuration").Base(err) return nil, err
} }
config.App = append(config.App, serial.ToTypedMessage(r)) config.App = append(config.App, serial.ToTypedMessage(r))
} }
@ -602,7 +600,7 @@ func (c *Config) Build() (*core.Config, error) {
for _, rawInboundConfig := range inbounds { for _, rawInboundConfig := range inbounds {
ic, err := rawInboundConfig.Build() ic, err := rawInboundConfig.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build inbound config with tag ", rawInboundConfig.Tag).Base(err) return nil, err
} }
config.Inbound = append(config.Inbound, ic) config.Inbound = append(config.Inbound, ic)
} }
@ -616,7 +614,7 @@ func (c *Config) Build() (*core.Config, error) {
for _, rawOutboundConfig := range outbounds { for _, rawOutboundConfig := range outbounds {
oc, err := rawOutboundConfig.Build() oc, err := rawOutboundConfig.Build()
if err != nil { if err != nil {
return nil, errors.New("failed to build outbound config with tag ", rawOutboundConfig.Tag).Base(err) return nil, err
} }
config.Outbound = append(config.Outbound, oc) config.Outbound = append(config.Outbound, oc)
} }

View File

@ -89,11 +89,12 @@ func whichProtoc(suffix, targetedVersion string) (string, error) {
path, err := exec.LookPath(protoc) path, err := exec.LookPath(protoc)
if err != nil { if err != nil {
return "", fmt.Errorf(` errStr := fmt.Sprintf(`
Command "%s" not found. Command "%s" not found.
Make sure that %s is in your system path or current path. Make sure that %s is in your system path or current path.
Download %s v%s or later from https://github.com/protocolbuffers/protobuf/releases Download %s v%s or later from https://github.com/protocolbuffers/protobuf/releases
`, protoc, protoc, protoc, targetedVersion) `, protoc, protoc, protoc, targetedVersion)
return "", fmt.Errorf(errStr)
} }
return path, nil return path, nil
} }

View File

@ -27,6 +27,5 @@ var CmdAPI = &base.Command{
cmdRemoveRules, cmdRemoveRules,
cmdSourceIpBlock, cmdSourceIpBlock,
cmdOnlineStats, cmdOnlineStats,
cmdOnlineStatsIpList,
}, },
} }

View File

@ -13,20 +13,25 @@ import (
var cmdBalancerInfo = &base.Command{ var cmdBalancerInfo = &base.Command{
CustomFlags: true, CustomFlags: true,
UsageLine: "{{.Exec}} api bi [--server=127.0.0.1:8080] [balancer]...", UsageLine: "{{.Exec}} api bi [--server=127.0.0.1:8080] [balancer]...",
Short: "Retrieve balancer information", Short: "balancer information",
Long: ` Long: `
Retrieve information of specified balancers, including health, strategy and selecting. Get information of specified balancers, including health, strategy
If no balancer tag specified, information for all balancers is returned. and selecting. If no balancer tag specified, get information of
all balancers.
> Ensure that "RoutingService" is enabled under "config.api.services" in the server configuration. > Make sure you have "RoutingService" set in "config.api.services"
of server config.
Arguments: Arguments:
-json
Use json output.
-s, -server <server:port> -s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout <seconds> -t, -timeout <seconds>
Timeout in seconds for calling API. Default 3 Timeout seconds to call API. Default 3
Example: Example:

View File

@ -7,27 +7,31 @@ import (
var cmdBalancerOverride = &base.Command{ var cmdBalancerOverride = &base.Command{
CustomFlags: true, CustomFlags: true,
UsageLine: "{{.Exec}} api bo [--server=127.0.0.1:8080] <-b balancer> outboundTag <-r>", UsageLine: "{{.Exec}} api bo [--server=127.0.0.1:8080] <-b balancer> outboundTag",
Short: "Override balancer", Short: "balancer override",
Long: ` Long: `
Override the selection target of a balancer. Override a balancer's selection.
> Ensure that the "RoutingService" is properly configured under "config.api.services" in the server configuration. > Make sure you have "RoutingService" set in "config.api.services"
of server config.
Once the balancer's selection is overridden: Once a balancer's selecting is overridden:
- The balancer's selection result will always be outboundTag - The balancer's selection result will always be outboundTag
Arguments: Arguments:
-s, -server <server:port> -r, -remove
The API server address. Default 127.0.0.1:8080 Remove the overridden
-t, -timeout <seconds>
Timeout in seconds for calling API. Default 3
-r, -remove -r, -remove
Remove the existing override. Remove the override
-s, -server
The API server address. Default 127.0.0.1:8080
-t, -timeout
Timeout seconds to call API. Default 3
Example: Example:

View File

@ -8,28 +8,20 @@ import (
var cmdInboundUser = &base.Command{ var cmdInboundUser = &base.Command{
CustomFlags: true, CustomFlags: true,
UsageLine: "{{.Exec}} api inbounduser [--server=127.0.0.1:8080] -tag=tag [-email=email]", UsageLine: "{{.Exec}} api inbounduser [--server=127.0.0.1:8080] -tag=tag [-email=email]",
Short: "Retrieve inbound user(s)", Short: "Get Inbound User",
Long: ` Long: `
Get User info from an inbound. Get User info from an inbound.
Arguments: Arguments:
-s, -server
-s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout
-t, -timeout <seconds> Timeout seconds to call API. Default 3
Timeout in seconds for calling API. Default 3
-tag -tag
Inbound tag Inbound tag
-email
-email User email. If email is not given, will get all users
The user's email address. If not provided, all users will be retrieved.
Example: Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -tag="tag name" -email="xray@love.com"
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -tag="tag name"
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -tag="tag name" -email="xray@love.com"
`, `,
Run: executeInboundUser, Run: executeInboundUser,
} }

View File

@ -8,24 +8,18 @@ import (
var cmdInboundUserCount = &base.Command{ var cmdInboundUserCount = &base.Command{
CustomFlags: true, CustomFlags: true,
UsageLine: "{{.Exec}} api inboundusercount [--server=127.0.0.1:8080] -tag=tag", UsageLine: "{{.Exec}} api inboundusercount [--server=127.0.0.1:8080] -tag=tag",
Short: "Retrieve inbound user count", Short: "Get Inbound User Count",
Long: ` Long: `
Retrieve the user count for a specified inbound tag. Get User count from an inbound.
Arguments: Arguments:
-s, -server
-s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout
-t, -timeout <seconds> Timeout seconds to call API. Default 3
Timeout in seconds for calling API. Default 3
-tag -tag
Inbound tag Inbound tag
Example: Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -tag="tag name"
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -tag="tag name"
`, `,
Run: executeInboundUserCount, Run: executeInboundUserCount,
} }

View File

@ -15,18 +15,13 @@ var cmdAddInbounds = &base.Command{
Short: "Add inbounds", Short: "Add inbounds",
Long: ` Long: `
Add inbounds to Xray. Add inbounds to Xray.
Arguments: Arguments:
-s, -server
-s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout
-t, -timeout <seconds> Timeout seconds to call API. Default 3
Timeout in seconds for calling API. Default 3
Example: Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
`, `,
Run: executeAddInbounds, Run: executeAddInbounds,
} }

View File

@ -14,18 +14,13 @@ var cmdRemoveInbounds = &base.Command{
Short: "Remove inbounds", Short: "Remove inbounds",
Long: ` Long: `
Remove inbounds from Xray. Remove inbounds from Xray.
Arguments: Arguments:
-s, -server
-s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout
-t, -timeout <seconds> Timeout seconds to call API. Default 3
Timeout in seconds for calling API. Default 3
Example: Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json "tag name"
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json "tag name"
`, `,
Run: executeRemoveInbounds, Run: executeRemoveInbounds,
} }

View File

@ -11,18 +11,11 @@ var cmdRestartLogger = &base.Command{
Short: "Restart the logger", Short: "Restart the logger",
Long: ` Long: `
Restart the logger of Xray. Restart the logger of Xray.
Arguments: Arguments:
-s, -server
-s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout
-t, -timeout <seconds> Timeout seconds to call API. Default 3
Timeout in seconds for calling API. Default 3
Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080
`, `,
Run: executeRestartLogger, Run: executeRestartLogger,
} }

View File

@ -15,18 +15,13 @@ var cmdAddOutbounds = &base.Command{
Short: "Add outbounds", Short: "Add outbounds",
Long: ` Long: `
Add outbounds to Xray. Add outbounds to Xray.
Arguments: Arguments:
-s, -server
-s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout
-t, -timeout <seconds> Timeout seconds to call API. Default 3
Timeout in seconds for calling API. Default 3
Example: Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
`, `,
Run: executeAddOutbounds, Run: executeAddOutbounds,
} }

View File

@ -14,18 +14,13 @@ var cmdRemoveOutbounds = &base.Command{
Short: "Remove outbounds", Short: "Remove outbounds",
Long: ` Long: `
Remove outbounds from Xray. Remove outbounds from Xray.
Arguments: Arguments:
-s, -server
-s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout
-t, -timeout <seconds> Timeout seconds to call API. Default 3
Timeout in seconds for calling API. Default 3
Example: Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json "tag name"
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json "tag name"
`, `,
Run: executeRemoveOutbounds, Run: executeRemoveOutbounds,
} }

View File

@ -16,21 +16,16 @@ var cmdAddRules = &base.Command{
Short: "Add routing rules", Short: "Add routing rules",
Long: ` Long: `
Add routing rules to Xray. Add routing rules to Xray.
Arguments: Arguments:
-s, -server
-s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout
-t, -timeout <seconds>
Timeout seconds to call API. Default 3 Timeout seconds to call API. Default 3
-append -append
Append to the existing configuration instead of replacing it. Default false append or replace config. Default false
Example: Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
`, `,
Run: executeAddRules, Run: executeAddRules,
} }

View File

@ -9,22 +9,17 @@ import (
var cmdRemoveRules = &base.Command{ var cmdRemoveRules = &base.Command{
CustomFlags: true, CustomFlags: true,
UsageLine: "{{.Exec}} api rmrules [--server=127.0.0.1:8080] [ruleTag]...", UsageLine: "{{.Exec}} api rmrules [--server=127.0.0.1:8080] ruleTag1 ruleTag2...",
Short: "Remove routing rules by ruleTag", Short: "Remove routing rules by ruleTag",
Long: ` Long: `
Remove routing rules by ruleTag from Xray. Remove routing rules by ruleTag from Xray.
Arguments: Arguments:
-s, -server
-s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout
-t, -timeout <seconds> Timeout seconds to call API. Default 3
Timeout in seconds for calling API. Default 3
Example: Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 ruleTag1 ruleTag2
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 ruleTag1 ruleTag2
`, `,
Run: executeRemoveRules, Run: executeRemoveRules,
} }

View File

@ -14,34 +14,25 @@ import (
var cmdSourceIpBlock = &base.Command{ var cmdSourceIpBlock = &base.Command{
CustomFlags: true, CustomFlags: true,
UsageLine: "{{.Exec}} api sib [--server=127.0.0.1:8080] -outbound=blocked -inbound=socks 1.2.3.4", UsageLine: "{{.Exec}} api sib [--server=127.0.0.1:8080] -outbound=blocked -inbound=socks 1.2.3.4",
Short: "Block connections by source IP", Short: "Drop connections by source ip",
Long: ` Long: `
Block connections by source IP address. Drop connections by source ip.
Arguments: Arguments:
-s, -server
-s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout
-t, -timeout <seconds> Timeout seconds to call API. Default 3
Timeout in seconds for calling API. Default 3
-outbound -outbound
Specifies the outbound tag. route traffic to specific outbound.
-inbound -inbound
Specifies the inbound tag. target traffig from specific inbound.
-ruletag -ruletag
The ruleTag. Default sourceIpBlock set ruleTag. Default sourceIpBlock
-reset -reset
remove ruletag and apply new source IPs. Default false remove ruletag and apply new source IPs. Default false
Example: Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -outbound=blocked -inbound=socks 1.2.3.4
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -outbound=blocked -inbound=socks 1.2.3.4 -reset
`, `,
Run: executeSourceIpBlock, Run: executeSourceIpBlock,
} }

View File

@ -8,26 +8,19 @@ import (
var cmdGetStats = &base.Command{ var cmdGetStats = &base.Command{
CustomFlags: true, CustomFlags: true,
UsageLine: "{{.Exec}} api stats [--server=127.0.0.1:8080] [-name '']", UsageLine: "{{.Exec}} api stats [--server=127.0.0.1:8080] [-name '']",
Short: "Retrieve statistics", Short: "Get statistics",
Long: ` Long: `
Retrieve the statistics from Xray. Get statistics from Xray.
Arguments: Arguments:
-s, -server
-s, -server <server:port>
The API server address. Default 127.0.0.1:8080 The API server address. Default 127.0.0.1:8080
-t, -timeout
-t, -timeout <seconds> Timeout seconds to call API. Default 3
Timeout in seconds for calling API. Default 3
-name -name
Name of the counter. Name of the stat counter.
-reset -reset
Reset the counter after fetching their values. Default false Reset the counter to fetching its value.
Example: Example:
{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -name "inbound>>>statin>>>traffic>>>downlink" {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -name "inbound>>>statin>>>traffic>>>downlink"
`, `,
Run: executeGetStats, Run: executeGetStats,

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